Android音视频学习: MediaCodec 硬编解码

官方文档 https://developer.android.google.cn/reference/android/media/MediaCodec

MediaCodec 是做硬件(GPU,充分利用GPU 的并行处理能力)编解码的。(通常结合 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack 使用)

codec (即 encoder + decoder ) :编解码器
MediaExtractor:解封装
MediaMuxer: 混合器(封装)(音视频合成)
MediaSync: 音视频同步
MediaCrypto: 加密

原始音频数据 (PCM )和 视频帧压缩编码(aac + h.264 等 )后要封装到一个容器(MP4 等)中进行传播。同理播放器播放前要先解封装,取出里面的音频部分和视频部分,然后解码成硬件可以直接播放和渲染的音频流和视频流。由于音频流和视频流是分别播放和渲染的,所以这里就有音视频同步的问题。

jiagou.png

codec 处理输入数据产生输出数据。它通过输入缓冲集合和输出缓冲集合异步的处理数据。先请求一个空的 input buffer,然后填充上要处理的数据发送给 codec 处理。 codec 处理数据后会把结果写到一个空的 output buffer 中。最后你请求 output buffer 从里面读出处理后的数据就行了。output buffer 用完后释放回 codec 重新使用。

数据类型

codec 处理3种类型的数据, compressed data (待解码的数据 或 编码后的数据)、raw audio data (待编码或解码后的数据)和 raw video data (待编码或解码后的数据)。3种数据类型都可以用 ByteBuffers 处理。还可以用 Surface 来处理 raw video data 来提高性能。因为 Surface 可以直接使用 native video buffers (在 native 层分配的 buffer)而不需要映射或拷贝到 ByteBuffers (ByteBuffers 是分配在 JVM 堆中的缓冲区) 中。

状态

state.png

概念上主要包含 Stopped、Executing、Released 3种状态。Stopped 包含 Configured、Uninitialized、Error 3个子状态。 Executing 包含 Flushed、Running、End of Stream 3个子状态。

codec 实例化以后默认是 Uninitialized 状态,之后需要调用 configure 进入 Configured 状态,再调用 start 进入 Executing 状态。运行状态默认是 Flushed, 这时可以调用 dequeueInputBuffer 拿到一个 input buffer 开始处理数据,进入 Running 状态。当没有输入后需要写一个 end-of-stream marker 的标志(可以放在最后一个 Input buffer 中,也可以用一个单独的空 buffer,空 buffer 的 timestamp 会被忽略)。当 End of stream 后 codec 就不再接受输入了, 但仍然继续产生输出直到输出 buffer 遇到 end-of-stream marker 标志(这个标志是 codec 写的,前提是当没有输入时你必须给 input buffer 写一个 end-of-stream 的标志。这个标志可以作为 codec 处理完毕的标志)。在运行状态可以调用 flush() 方法回到 Flushed 状态。

运行时调用 stop 方法会重新回到 Uninitialized ,这时要重新 configue 、start 才能重新运行。 运行出错时会进入 Error 状态,这时可以调用 reset 方法恢复到 Uninitialized。 codec 不再使用时调用 release 方法释放资源进入 Released 状态。

创建

5.0 之后官方推荐用 MediaCodecList.findDecoderForFormat 传入一个 MediaFormat 来查找你要使用的 codec。 然后调用 MediaCodec.createByCodecName(String) 方法创建 codec。

MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
mediaFormat.setString(MediaFormat.KEY_BIT_RATE, null);
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
String name = mediaCodecList.findEncoderForFormat(mediaFormat);
Log.d(TAG, "name is " + name); // OMX.google.aac.encoder
try {
    mediaCodec = MediaCodec.createByCodecName(name);
} catch (IOException e) {
    e.printStackTrace();
}

也可以调用 MediaCodec.createDecoder/EncoderByType(String) 传入要处理数据的 MIME type 来创建。

try {
    // 5.0 之前可以这样写,aac 编解码一般都支持
    mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
} catch (IOException e) {
    e.printStackTrace();
}

初始化

调用 configue 方法,如果需要异步处理 buffer 可以先调用 setCallback 方法设置回调。

数据处理

输入输出 buffer 是用 buffer-ID 来标识的。调用 start 后通过 dequeueInput/OutputBuffer(…) 方法拿到一个 buffer。异步模式需要在 MediaCodec.Callback.onInput/OutputBufferAvailable(…) 回调中拿到 buffer。
拿到输出 buffer 的数据处理完毕后要调用 releaseOutputBuffer 将 buffer 释放回 codec 中。

输入输出 buffer 用完后都要及时提交到/释放回 codec 。毕竟 codec 的 buffer 数量是有限的,如果占满了,肯定就没法处理了。输入 buffer 被占满后 dequeueInputBuffer 会一直返回 -1, 输出 buffer 占满后 dequeueOutputBuffer 会一直返回 -1

5.0 之后官方推荐以异步方式处理 buffer

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     // 拿到一个输入 buffer -> 填充数据 ->入队交给 codec 处理
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     // 拿出一个 codec 处理完的输出 buffer -> 处理 -> 释放
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }

   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }

   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

同步处理方式(不推荐)

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

同步方式使用 ByteBuffer 数组获取 buffer (已废弃)

MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(…);
   if (inputBufferId >= 0) {
     // fill inputBuffers[inputBufferId] with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     // outputBuffers[outputBufferId] is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     MediaFormat format = codec.getOutputFormat();
   }
 }
codec.stop();
codec.release();

如果需要兼容 4.X 版本,还得用上面的方法。

End-of-stream Handling

当到达输入末尾时,在调用 queueInputBuffer 时要在参数里面写一个 BUFFER_FLAG_END_OF_STREAM 的标志,表示输入完毕了。标志可以写在最后一个 buffer 中,也可以最后再专门提交一个空的 buffer (没有可用数据)。如果用空 buffer ,buffer 的 timestamp 会被忽略。

输入结束后 codec 就不再接受输入了,但会继续产生输出。输出处理完毕后也会在最后一个有用 buffer 或 空buffer 中含有 end-of-stream 的标志。可以用这个标志来标识 codec 处理完毕。通过 MediaCodec.BufferInfo 可以拿到 buffer 的 flag。

发出 end-of-stream 的 buffer 后就不要再提交 Input buffer 了。除非 codec 被 flushed, or stopped and restarted。

Using an Output Surface

codec 的输出也可以直接关联到一个 Surface 上。如视频解码后可以直接渲染到 SurfaceView 上。但这时 output buffers 就不可用了。getOutputBuffer/Image(int) 会返回 null。getOutputBuffers() 也会返回一个全是 null 的数组。

你可以选择是否直接把输出渲染到 Surface 上。

  • Do not render the buffer: Call releaseOutputBuffer(bufferId, false).
  • Render the buffer with the default timestamp: Call releaseOutputBuffer(bufferId, true).
  • Render the buffer with a specific timestamp: Call releaseOutputBuffer(bufferId, timestamp).

Using an Input Surface

也可以用 Surface 作为 codec 的输入,同理这时 input buffer 就不可用了,调用 dequeueInputBuffer 会抛异常。

调用 signalEndOfInputStream() 后 surface 会停止向 codec 发送数据。

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

推荐阅读更多精彩内容