MediaCodec 入门:生命周期、异步与同步模式、Buffer 与 Codec 交互流程、关键 API 详解

各位同学,欢迎来到《Android 视频硬解码优化实战》的第一章。

今天咱们聊聊 MediaCodec。说实话,这玩意儿是 Android 多媒体开发里绕不开的坎。我刚开始接触它的时候,也被那一堆状态和回调搞得晕头转向。但别怕,今天我会把它的底裤扒干净,让你彻底搞懂。

1. MediaCodec 的生命周期:从出生到销毁

MediaCodec 不是那种 new 出来就能直接用的对象。它有自己的状态机,说白了就是一套严格的“成长流程”。

我个人习惯把它的生命周期分成三个阶段:

  • Stopped 状态:刚创建,或者已经释放。啥也干不了。
  • Executing 状态:正在干活。可以往里塞数据,往外拿数据。
  • Released 状态:彻底销毁,资源归还。

你想想看,一个编解码器从创建到真正开始工作,中间要经历 configurestart 这些步骤。我见过不少新手直接跳过配置,上来就 start,结果 crash 得一塌糊涂。

核心流程:

createByCodecName()configure()start() → 处理数据 → stop()release()

嗯,这里要注意:stop() 之后,你还可以重新 start(),但 release() 之后就不能再用了。这就像你手机用完了可以关机再开,但扔进垃圾桶就真没了。

2. 异步模式 vs 同步模式:你选哪个?

MediaCodec 支持两种工作模式。我当年刚入行时,项目里用的还是同步模式,后来 Android 5.0 之后异步模式才成熟起来。

同步模式(老派做法)

说白了就是“你等我,我等你”。你调用 dequeueInputBuffer(),它可能阻塞,也可能立刻返回。然后你填数据,再调用 queueInputBuffer()。接着去拿输出,调用 dequeueOutputBuffer()

这种模式代码写起来很直观,但容易在主线程里卡死。我建议你开个单独的工作线程去跑。

// 同步模式伪代码
int inputIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inputIndex >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
    // 填充数据...
    codec.queueInputBuffer(inputIndex, offset, size, pts, flags);
}

int outputIndex = codec.dequeueOutputBuffer(info, TIMEOUT_US);
if (outputIndex >= 0) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputIndex);
    // 处理数据...
    codec.releaseOutputBuffer(outputIndex, render);
}

异步模式(现代推荐)

从 Android 5.0 开始,MediaCodec 支持了异步回调。你只需要设置一个 MediaCodec.Callback,然后系统会在合适的时机通知你:

  • onInputBufferAvailable():有空的输入槽了,快来填数据。
  • onOutputBufferAvailable():有解码好的输出数据了,快来拿。
  • onError():出错了,赶紧处理。
  • onOutputFormatChanged():输出格式变了,比如分辨率变化。

我个人更偏爱异步模式。为什么呢?因为代码更干净,不用自己轮询。我曾经在一个直播项目里用同步模式,结果因为超时时间没算好,导致解码线程频繁空转,CPU 占用率飙高。换成异步模式后,问题迎刃而解。

我的建议:新项目直接用异步模式。除非你要兼容 Android 4.x 的老设备,否则别碰同步模式。

3. Buffer 与 Codec 交互流程:数据是怎么流动的?

这是最核心的部分。MediaCodec 内部维护了两组缓冲区:输入缓冲区和输出缓冲区。它们就像两个传送带,一个往里送原材料,一个往外送成品。

咱们来看一张图,我亲手画的:

MediaCodec Buffer 交互流程 输入缓冲区 Input Buffers dequeueInputBuffer() MediaCodec 编解码引擎 queueInputBuffer() 输出缓冲区 Output Buffers dequeueOutputBuffer() 原始数据 解码后数据 典型交互步骤 ① 应用层调用 dequeueInputBuffer() 获取空闲输入槽 ② 填充数据后,调用 queueInputBuffer() 交给编解码器 ③ 编解码器处理完成后,应用层通过 dequeueOutputBuffer() 获取输出 ④ 处理完输出后,调用 releaseOutputBuffer() 归还缓冲区

这张图你看懂了吗?其实流程很简单:

  1. 你从输入缓冲区队列里“借”一个空槽。
  2. 把原始视频数据(比如 H.264 的 NAL 单元)塞进去。
  3. 把槽“还”给 MediaCodec,告诉它“可以开工了”。
  4. MediaCodec 解码完成后,把结果放到输出缓冲区。
  5. 你从输出缓冲区拿到解码后的数据(比如 YUV 图像)。
  6. 处理完后,把输出槽也还回去。

嗯,这里有个坑:缓冲区是有限的。如果你借了不还,或者还了不借,都会导致死锁。我曾经在一个项目里忘了调用 releaseOutputBuffer(),结果解码器卡死,画面定格了整整 3 秒才反应过来。

4. 关键 API 详解:你必须要掌握的

咱们挑几个最常用的 API 来细说。这些是我每天写代码都会碰到的。

API 名称 作用 注意事项
createByCodecName() 根据编解码器名字创建实例 名字要从 MediaCodecList 里获取,别硬编码
configure() 配置编码格式、分辨率、码率等 必须在 Stopped 状态下调用,且只能调一次
start() 启动编解码器,进入 Executing 状态 调用后缓冲区才可用
dequeueInputBuffer() 获取一个可用的输入缓冲区索引 超时时间别设太长,建议 10ms
queueInputBuffer() 将填充好的数据提交给编解码器 记得带上时间戳和结束标志
dequeueOutputBuffer() 获取一个解码完成的输出缓冲区 返回 INFO_OUTPUT_FORMAT_CHANGED 时要处理
releaseOutputBuffer() 释放输出缓冲区,可指定是否渲染 如果用于 Surface 渲染,传 true
flush() 清空所有待处理的缓冲区 seek 或切换流时常用,但别乱用
stop() 停止编解码器 会阻塞直到所有缓冲区归还
release() 释放所有资源 调用后对象不可再用

避坑指南:我曾经在 configure() 时传错了 MediaFormat 的 key,比如把 KEY_FRAME_RATE 写成了 KEY_FRAME_RATE(大小写不对),结果编解码器直接罢工。记住,这些 key 都是定义在 MediaFormat 类里的常量,别自己拼字符串。

5. 一个完整的异步模式示例

光说不练假把式。我给你写一个最简单的异步解码骨架:

MediaCodec codec = MediaCodec.createByCodecName("OMX.google.h264.decoder");
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
codec.configure(format, surface, null, 0);
codec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec mc, int index) {
        ByteBuffer inputBuffer = mc.getInputBuffer(index);
        // 从你的数据源读取数据,填入 inputBuffer
        // 然后调用 mc.queueInputBuffer(index, ...)
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec mc, int index, MediaCodec.BufferInfo info) {
        // 如果用于 Surface 渲染,直接 release 即可
        mc.releaseOutputBuffer(index, true);
        // 如果需要读取像素数据,用 getOutputBuffer() 获取 ByteBuffer
    }

    @Override
    public void onError(MediaCodec mc, MediaCodec.CodecException e) {
        // 处理错误,比如重启解码器
    }

    @Override
    public void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
        // 输出格式变了,比如分辨率切换
    }
});
codec.start();

你看,异步模式就是这么清爽。你不需要自己写循环,系统会主动叫你。我个人觉得,这才是现代 Android 开发该有的样子。

总结

今天咱们把 MediaCodec 的底裤扒了个干净。从生命周期到两种工作模式,再到 Buffer 的交互流程,最后撸了一遍关键 API。这些东西你掌握了,后面讲硬解码实战就轻松多了。

记住一句话:MediaCodec 的核心就是“借-还”游戏。你借输入槽,还输入槽;借输出槽,还输出槽。别借了不还,也别还了不借。

好,今天就到这儿。下一章咱们聊聊如何选择合适的解码器,以及那些坑死人不偿命的厂商适配问题。


公众号:蓝海资料掘金营,微信deep3321