MediaCodec 入门:生命周期、异步与同步模式、Buffer 与 Codec 交互流程、关键 API 详解
各位同学,欢迎来到《Android 视频硬解码优化实战》的第一章。
今天咱们聊聊 MediaCodec。说实话,这玩意儿是 Android 多媒体开发里绕不开的坎。我刚开始接触它的时候,也被那一堆状态和回调搞得晕头转向。但别怕,今天我会把它的底裤扒干净,让你彻底搞懂。
1. MediaCodec 的生命周期:从出生到销毁
MediaCodec 不是那种 new 出来就能直接用的对象。它有自己的状态机,说白了就是一套严格的“成长流程”。
我个人习惯把它的生命周期分成三个阶段:
- Stopped 状态:刚创建,或者已经释放。啥也干不了。
- Executing 状态:正在干活。可以往里塞数据,往外拿数据。
- Released 状态:彻底销毁,资源归还。
你想想看,一个编解码器从创建到真正开始工作,中间要经历 configure、start 这些步骤。我见过不少新手直接跳过配置,上来就 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 内部维护了两组缓冲区:输入缓冲区和输出缓冲区。它们就像两个传送带,一个往里送原材料,一个往外送成品。
咱们来看一张图,我亲手画的:
这张图你看懂了吗?其实流程很简单:
- 你从输入缓冲区队列里“借”一个空槽。
- 把原始视频数据(比如 H.264 的 NAL 单元)塞进去。
- 把槽“还”给 MediaCodec,告诉它“可以开工了”。
- MediaCodec 解码完成后,把结果放到输出缓冲区。
- 你从输出缓冲区拿到解码后的数据(比如 YUV 图像)。
- 处理完后,把输出槽也还回去。
嗯,这里有个坑:缓冲区是有限的。如果你借了不还,或者还了不借,都会导致死锁。我曾经在一个项目里忘了调用 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 的核心就是“借-还”游戏。你借输入槽,还输入槽;借输出槽,还输出槽。别借了不还,也别还了不借。
好,今天就到这儿。下一章咱们聊聊如何选择合适的解码器,以及那些坑死人不偿命的厂商适配问题。