音视频开发进阶指南(第六章)-Android MediaCodec编码为AAC

书中示例源码地址

MediaCodec简介

MediaCodec是Android提供的硬件编解码器,它可以利用设备的硬件来完成编解码,从而大大提高编解码的效率,还可以降低电量的使用。

MediaCodec通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。广义而言,MediaCodec的工作原理就是处理输入数据以产生输出数据。具体来说,MediaCodec在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据:
首先,客户端向输入缓存区写入要编解码的数据并将其提交给编解码器,待编解码器处理完毕后转存到编码器的输出缓存区,同时收回客户端对输入缓存区的所有权;
然后,客户端从输出缓存区读取编码好的数据进行处理,待处理完毕后编解码器收回客户端对输出缓存区的所有权。
不断重复整个过程,直至编码器停止工作或者异常退出。
工作原理图如下:

image.png

MediaCodec的工作状态

在整个编解码过程中,MediaCodec的使用会经历配置、启动、数据处理、停止、释放几个过程。相应的状态可归纳为停止(Stopped)执行(Executing)、以及释放(Released)三个状态,而Stopped状态又可细分为未初始化(Uninitialized)、配置(Configured)、异常( Error),Executing状态也可细分为读写数据(Flushed)、运行(Running)流结束(End-of-Stream)。MediaCodec整个状态结构图如下:

image.png

从上图可知,当MediaCodec被创建后会进入未初始化状态,待设置好配置信息并调用start()启动后,MediaCodec会进入运行状态,并且可进行数据读写操作。如果在这个过程中出现了错误,MediaCodec会进入Stopped状态,可以使用reset方法来重置编解码器,否则MediaCodec所持有的资源最终会被释放。当然,如果MediaCodec正常使用完毕,我们也可以向编解码器发送EOS指令,同时调用stop和release方法终止编解码器的使用。

MediaCodec API 说明

主要API说明:
getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
queueInputBuffer:在指定索引处填充一定范围的输入缓冲区后,将其提交给组件
dequeueInputBuffer:返回要填充有效数据的输入缓冲区的索引
getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
dequeueOutputBuffer:使输出缓冲区出队列,最多阻塞"timeoutUs"微秒。
releaseOutputBuffer:处理完成,释放ByteBuffer数据

MediaCodec的基本使用

  1. 创建并配置一个 MediaCodec 对象
  2. 如果输入缓冲区就绪,读取一个输入块,并复制到输入缓冲区中
  3. 如果输出缓冲区就绪,复制输出缓冲区的数据
  4. 循环2.3步直到完成
  5. 释放 MediaCodec 对象

MediaCodec初始化

MediaCodec主要提供了createEncoderByType(String type)createDecoderByType(String type)两个方法来创建编解码器,它们均需要传入一个MIME类型多媒体格式。常见的MIME类型多媒体格式如下:
● "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
● "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
● "video/avc" - H.264/AVC video
● "video/mp4v-es" - MPEG4 video
● "video/3gpp" - H.263 video
● "audio/3gpp" - AMR narrowband audio
● "audio/amr-wb" - AMR wideband audio
● "audio/mpeg" - MPEG1/2 audio layer III
● "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
● "audio/vorbis" - vorbis audio
● "audio/g711-alaw" - G.711 alaw audio
● "audio/g711-mlaw" - G.711 ulaw audio

MediaCodec还提供了一个createByCodecName (String name)方法,支持使用组件的具体名称来创建编解码器。但是该方法使用起来有些麻烦,且官方是建议最好是配合MediaCodecList使用,因为MediaCodecList记录了所有可用的编解码器。

MediaCodec配置和启动

编解码器配置使用的是MediaCodec的configure方法,该方法首先对MediaFormat存储的数据map进行提取,然后调用本地方法native-configure实现对编解码器的配置工作。
在配置时,configure(format, surface, crypto, flags)方法需要传入format、surface、crypto、flags参数。

  • format为MediaFormat的实例,它使用"key-value"键值对的形式存储多媒体数据格式信息;
  • surface用于指明解码器的数据源来自于该surface;
  • crypto用于指定一个MediaCrypto对象,以便对媒体数据进行安全解密;
  • flags指明配置的是编码器(CONFIGURE_FLAG_ENCODE)。

其中MediaFormat必须配置以下几项,否则运行configure出错:采样率,比特率,通道个数。
下面是编码为AAC的配置

MediaFormat mediaFormat = MediaFormat.createAudioFormat(MINE_TYPE, SAMPLE_RATE, CHANNEL_CONFIG_IN);
MediaFormat mediaFormat = MediaFormat.createAudioFormat(MINE_TYPE, SAMPLE_RATE, CHANNEL_CONFIG_IN);
//指定比特率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128 * 1024);
//指定采样率
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
//指定通道个数
mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNEL_CONFIG_IN);
//指定PROFILE
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectERLC);
//指定缓冲区最大长度
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 10 * 1024);
//应用配置
mMediaCodecEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

当编解码器配置完毕后,就可以调用MediaCodec的start()方法,该方法会调用低层native_start()方法来启动编码器,并调用低层方法ByteBuffer[] getBuffers(input)来开辟一系列输入、输出缓存区。
可以看一下start方法的源码,开辟了一块输入和一块输出缓存区:

public final void start() {
    native_start();
    synchronized(mBufferLock) {
        cacheBuffers(true /* input */);
        cacheBuffers(false /* input */);
    }
}

MediaCodec数据处理

MediaCodec支持两种模式编解码器,即同步synchronous、异步asynchronous

  • 同步模式是指编解码器数据的输入和输出是同步的,编解码器只有处理输出完毕才会再次接收输入数据;
  • 异步编解码器数据的输入和输出是异步的,编解码器不会等待输出数据处理完毕才再次接收输入数据。
    我们主要介绍下同步编解码,因为这种方式我们用得比较多。我们知道当编解码器被启动后,每个编解码器都会拥有一组输入和输出缓存区,但是这些缓存区暂时无法被使用,只有通过MediaCodec的dequeueInputBufferdequeueOutputBuffer方法获取输入输出缓存区授权,通过返回的ID来操作这些缓存区。
    示例如下:
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); //所有的输入缓冲区
 ByteBuffer[]outputBuffers = mediaCodec.getOutputBuffers();//所有的输出缓冲区
//注:一次编码,只使用一个缓冲区,所以需要获取缓冲区的索引
//获取可用的输入缓冲区索引
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
    //写原始数据到输入缓冲区
    ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
    inputBuffer.clear();
    inputBuffer.put(data);
    mediaCodec.queueInputBuffer(inputBufferIndex, 0, len, System.nanoTime(), 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//获取可用输出缓冲区的索引
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex >= 0) { //循环读取完输出缓冲区的数据
    ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
    if (outputAACDelegate != null) {
        int outPacketSize = bufferInfo.size + 7;// 7为ADTS头部的大小
        outputBuffer.position(bufferInfo.offset);
        outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
        byte[] outData = new byte[outPacketSize];
        addADTStoPacket(outData, outPacketSize);//添加ADTS 代码后面会贴上
        outputBuffer.get(outData, 7, bufferInfo.size);//将编码得到的AAC数据 取出到byte[]中 偏移量offset=7
        outputBuffer.position(bufferInfo.offset);
        outputAACDelegate.outputAACPacket(outData); //写入到文件
    }
    mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
    outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}

释放资源

mediaCodec.stop();
mediaCodec.release();

demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容