抱歉,你查看的文章不存在

MediaCodec进行编解码AAC(文件格式转换)

本文来自eric原创授权发布,eric,音视频开发爱好者,简书地址:https://www.jianshu.com/u/1502591a1753。欢迎大家关注。

AAC,全称Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性价比”。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。 在介绍AAC编解码之前,首先要先学习几个新知识MediaExtractorADTS格式

MediaExtractor

前面在介绍视频编码的时候使用到了MediaCodec,其功能主要是进行音视频的编解码。下面要介绍另外一个类MediaExtractor:负责将指定类型的媒体文件从文件中找到轨道,可以用来分离容器中的视频track和音频track。将得到的原始数据解析成解码器需要的数据。

对象创建和设置源 对象的创建直接new出来即可。然后最要要的是设置数据源。调用setDataSource即可

Sets the data source (file-path or http URL) to use.

这个方法的注释写的比较清楚,可以设置本地文件的位置或者一个http URL。

分离轨道信息

  • getTrackCount()获取轨道数量
  • MediaFormat format = mediaExtractor.getTrackFormat(i);获取对应轨道的信息。通过MediaFormat我们就可以知道每个track的详细信息,如音频/视频、格式等等。
  • selectTrack选择轨道

读取数据

制定轨道后就可以开始读取数据了。

  • readSampleData将数据读取到ByteBuffer 中。返回-1时代表没有更多数据了
  • advance跳到下一个数据包,如果没有下一个就返回false

释放资源

  • 使用完后调用release进行资源释放

ADTS

ADTS是AAC音频文件常见的传输格式。当你编码AAC裸流的时候,会遇到写出来的AAC文件并不能在PC和手机上播放,很大的可能就是AAC文件的每一帧里缺少了ADTS头信息文件的包装拼接。只需要加入头文件ADTS即可。一个AAC原始数据块长度是可变的,对原始帧加上ADTS头进行ADTS的封装,就形成了ADTS帧。

文件格式转换

先来张流程图

第一步 初始化解码器

读取视频文件初始化解码器

/**
* 初始化解码器
*/
private void initMediaDecode() {
    try {
        mediaExtractor = new MediaExtractor();//此类可分离视频文件的音轨和视频轨道
        mediaExtractor.setDataSource(srcPath);//媒体文件的位置
        for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {//遍历媒体轨道 此处我们传入的是音频文件,所以也就只有一条轨道
            MediaFormat format = mediaExtractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("audio")) {//获取音频轨道
                mediaExtractor.selectTrack(i);//选择此音频轨道
                LogUtils.d("mime:" + mime);
                key_bit_rate = format.getInteger(MediaFormat.KEY_BIT_RATE);
                key_channel_count = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                key_sample_rate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                sampleRateType = ADTSUtils.getSampleRateType(key_sample_rate);
                mediaDecode = MediaCodec.createDecoderByType(mime);//创建Decode解码器
                mediaDecode.configure(format, null, null, 0);
                break;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    if (mediaDecode == null) {
        LogUtils.e("create mediaDecode failed");
        return;
    }
    mediaDecode.start();//启动MediaCodec ,等待传入数据
    decodeInputBuffers = mediaDecode.getInputBuffers();//MediaCodec在此ByteBuffer[]中获取输入数据
    decodeOutputBuffers = mediaDecode.getOutputBuffers();//MediaCodec将解码后的数据放到此ByteBuffer[]中 我们可以直接在这里面得到PCM数据
    decodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解码得到的byte[]数据的相关信息
    LogUtils.d("buffers:" + decodeInputBuffers.length);
}

前面已经介绍了MediaExtractor的用法,这里就是解析得到音频轨道,然后创建一个对应解码格式MediaCodec用于解码。MediaCodec的用法在前面视频编码文章中有介绍,这里就不累述。

第二步 初始化编码器

/**
* 初始化AAC编码器
*/
private void initAACMediaEncode() {
    try {
        LogUtils.d(key_bit_rate + " " + key_channel_count + " " + key_sample_rate + " " + sampleRateType);
        MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
                key_sample_rate, key_channel_count);//参数对应-> mime type、采样率、声道数
        encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, key_bit_rate);//比特率
        encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);
        mediaEncode = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    } catch (IOException e) {
        e.printStackTrace();
    }
    if (mediaEncode == null) {
        LogUtils.e("create mediaEncode failed");
        return;
    }
    mediaEncode.start();
    encodeInputBuffers = mediaEncode.getInputBuffers();
    encodeOutputBuffers = mediaEncode.getOutputBuffers();
    encodeBufferInfo = new MediaCodec.BufferInfo();
}

这里也是创建一个MediaCodec用于编码,同时设置相关参数,我们保持和源文件的参数一致,也就是MediaExtractor解析得到的码率、声道数、采样率等等。

第三步 分别开启线程编解码

/**
 * 开始转码
 * 音频数据{@link #srcPath}先解码成PCM  
 * PCM数据在编码成MediaFormat.MIMETYPE_AUDIO_AAC音频格式
 * mp3->PCM->aac
 */
public void startAsync() {
    LogUtils.w("start");
    new Thread(new DecodeRunnable()).start();
    new Thread(new EncodeRunnable()).start();
}

先看到解码逻辑

/**
 * 解码{@link #srcPath}音频文件 得到PCM数据块
 *
 * @return 是否解码完所有数据
 */
private void srcAudioFormatToPCM() {
    for (int i = 0; i < decodeInputBuffers.length - 1; i++) {
        int inputIndex = mediaDecode.dequeueInputBuffer(-1);//获取可用的inputBuffer -1代表一直等待,0表示不等待 建议-1,避免丢帧
        if (inputIndex < 0) {
            codeOver = true;
            return;
        }
        ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];//拿到inputBuffer
        inputBuffer.clear();//清空之前传入inputBuffer内的数据
        int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);//MediaExtractor读取数据到inputBuffer中
        if (sampleSize < 0) {//小于0 代表所有数据已读取完成
            codeOver = true;
        } else {
            mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);//通知MediaDecode解码刚刚传入的数据
            mediaExtractor.advance();//MediaExtractor移动到下一取样处
            decodeSize += sampleSize;
            LogUtils.d("read:" + sampleSize);
            if (onProgressListener != null) {
                onProgressListener.progress(decodeSize, fileTotalSize);
            }
        }
    }
    //获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
    //此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
    int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);
    ByteBuffer outputBuffer;
    byte[] chunkPCM;
    while (outputIndex >= 0) {//每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
        outputBuffer = decodeOutputBuffers[outputIndex];//拿到用于存放PCM数据的Buffer
        chunkPCM = new byte[decodeBufferInfo.size];//BufferInfo内定义了此数据块的大小
        outputBuffer.get(chunkPCM);//将Buffer内的数据取出到字节数组中
        outputBuffer.clear();//数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
        putPCMData(chunkPCM);//自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
        mediaDecode.releaseOutputBuffer(outputIndex, false);//此操作一定要做,不然MediaCodec用完所有的Buffer后 将不能向外输出数据
        outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次获取数据,如果没有数据输出则outputIndex=-1 循环结束
    }
}

其实就是基本的MediaCodec操作。使用MediaExtractor.readSampleData读取文件音频数据,然后交给MediaCodec进行解码,最后将得到的PCM数据加入队列中

这里队列我们使用ArrayBlockingQueue,在多线程操作时候,这个容器还是比较好用的

接下来看到编码流程

/**
 * 编码线程
 */
private class EncodeRunnable implements Runnable {
    @Override
    public void run() {
        long t = System.currentTimeMillis();
        while (!codeOver || !queue.isEmpty()) {
            dstAudioFormatFromPCM();
        }
        if (onCompleteListener != null) {
            onCompleteListener.completed();
        }
        LogUtils.w("size:" + fileTotalSize + " decodeSize:" + decodeSize + "time:" + (System.currentTimeMillis() - t));
    }
}

这里判断如果解码未结束或者队列不为空就进入编码流程

/**
 * 编码PCM数据 得到MediaFormat.MIMETYPE_AUDIO_AAC格式的音频文件,并保存到{@link #dstPath}
 */
private void dstAudioFormatFromPCM() {
    int inputIndex;
    ByteBuffer inputBuffer;
    int outputIndex;
    ByteBuffer outputBuffer;
    byte[] chunkAudio;
    int outBitSize;
    int outPacketSize;
    byte[] chunkPCM;
    for (int i = 0; i < encodeInputBuffers.length - 1; i++) {
        chunkPCM = getPCMData();//获取解码器所在线程输出的数据 代码后边会贴上
        if (chunkPCM == null) {
            break;
        }
        inputIndex = mediaEncode.dequeueInputBuffer(-1);//同解码器
        inputBuffer = encodeInputBuffers[inputIndex];//同解码器
        inputBuffer.clear();//同解码器
        inputBuffer.limit(chunkPCM.length);
        inputBuffer.put(chunkPCM);//PCM数据填充给inputBuffer
        mediaEncode.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);//通知编码器 编码
    }
    outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);//同解码器
    while (outputIndex >= 0) {//同解码器
        outBitSize = encodeBufferInfo.size;
        outPacketSize = outBitSize + 7;//7为ADTS头部的大小
        outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
        outputBuffer.position(encodeBufferInfo.offset);
        outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
        chunkAudio = new byte[outPacketSize];
        addADTStoPacket(chunkAudio, outPacketSize);//添加ADTS 代码后面会贴上
        outputBuffer.get(chunkAudio, 7, outBitSize);//将编码得到的AAC数据 取出到byte[]中 偏移量offset=7 你懂得
        outputBuffer.position(encodeBufferInfo.offset);
        try {
            bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 将文件保存到内存卡中 *.aac
            LogUtils.d("write " + chunkAudio.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        mediaEncode.releaseOutputBuffer(outputIndex, false);
        outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);
    }
}

这里也是常规的MediaCodec操作,只是多了一个ADTS封装操作。ADTS前面有介绍,就是多了7个字节。这里直接上代码

第四步 释放资源

/**
 * 释放资源
 */
public void release() {
    try {
        if (bos != null) {
            bos.flush();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (bos != null) {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                bos = null;
            }
        }
    }
    try {
        if (fos != null) {
            fos.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        fos = null;
    }
    if (mediaEncode != null) {
        mediaEncode.stop();
        mediaEncode.release();
        mediaEncode = null;
    }
    if (mediaDecode != null) {
        mediaDecode.stop();
        mediaDecode.release();
        mediaDecode = null;
    }
    if (mediaExtractor != null) {
        mediaExtractor.release();
        mediaExtractor = null;
    }
    if (onCompleteListener != null) {
        onCompleteListener = null;
    }
    if (onProgressListener != null) {
        onProgressListener = null;
    }
    LogUtils.w("release");
}

主要就是I/O流、MediaCodec、MediaExtractor的释放。

到这里整个流程完成。

原文发布于微信公众号 - 何俊林(DriodDeveloper)

原文发表时间:2018-01-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

码农突围

0 篇文章93 人订阅

相关文章

来自专栏difcareer的技术笔记

彻底弄懂dalvik字节码【三】0x01:0x02:0x03:0x04:0x05:

【一】、【二】中从代码的角度分析了dalvik字节码解释执行的过程,这篇文章以一个例子来实际分析一下。

16920
来自专栏GIS讲堂

Arcgis API for Android之GPS定位

先说说写这篇文章的原因吧,在群内讨论的过程中,有人提到了定位的问题,刚好,自己以前在做相关工作的时候做过相关的东西,所以就总结一下,给大家共享出来,由于本人水平...

17030
来自专栏Google Dart

Flutter 构建完整应用手册-列表 顶

显示数据列表是移动应用程序的基本模式。 Flutter包含ListView部件,使列表变得轻而易举!

30820
来自专栏飞扬的花生

在ASP.MVC中使用Ajax

      Asp.net MVC 抛弃了Asp.net WebForm那种高度封装的控件,让我们跟底层的HTML有了更多的亲近。可以更自由、更灵活的去控制HT...

23290
来自专栏移动开发之家

通用RecylerAdapter,内置XRecyclerView,兼容上下拉与动画,高复用,一个Adapter通用所有页面,支持空页面,懒人专属

这是欢迎各位践踏的Github:https://github.com/CarGuo

11150
来自专栏流媒体

MediaCodec进行AAC编解码(文件格式转换)

AAC,全称Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性...

28150
来自专栏程序猿DD

Spring框架中的设计模式(五)

通过以前的4篇文章,我们看到Spring采用了大量的关于创建和结构方面的设计模式。本文将描述属于行为方面的两种设计模式:命令和访问者。 前传: Spring框架...

50770
来自专栏落影的专栏

使用AudioToolbox播放AAC

前言 使用VideoToolbox硬编码H.264 使用VideoToolbox硬解码H.264 使用AudioToolbox编码AAC 在上一篇中,介绍...

44340
来自专栏影子

关于SpringMVC中如何把查询数据全转成String类型

36590
来自专栏技术小黑屋

Android中Handler引起的内存泄露

在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用。通常我们的代码会这样实现。

18920

扫码关注云+社区

领取腾讯云代金券