3、高通MediaCodec API详解:OMX组件模型、MediaCodec状态机、Buffer流转机制、异步模式
这一章,我们深入MediaCodec的内部。很多做Camera编码的同学,API调得飞起,但一遇到性能瓶颈或诡异bug就抓瞎。说白了,就是没搞懂底层那套机制。
我个人习惯,在讲API用法之前,先把模型和状态机讲透。你想想看,连组件怎么跑、状态怎么跳都不知道,出了问题怎么定位?
3.1 OMX组件模型:编码器的“魂”
MediaCodec底层依赖的是OpenMAX IL(Integration Layer)标准。高通平台上的硬件编码器,本质上就是一个OMX组件。
OMX组件的核心结构:
- 组件句柄:OMX_HANDLETYPE,代表一个编码器实例。
- 端口(Port):每个组件有输入端口和输出端口。输入吃原始YUV数据,输出吐编码后的H.264/H.265码流。
- 缓冲区(Buffer):数据通过Buffer在端口间流转。Buffer有头信息(OMX_BUFFERHEADERTYPE),包含数据指针、大小、时间戳、标志位等。
- 隧道化(Tunneling):高通平台支持组件间直接传递Buffer,绕过CPU拷贝。这在Camera编码中非常关键,能大幅降低延迟。
关键点:OMX组件是状态驱动的。你调用MediaCodec的start(),底层就是在触发OMX组件的状态迁移。
我在项目中遇到过一个问题:编码器启动后,输入Buffer一直不返回。查了半天,发现是OMX组件卡在Loaded->Idle的过渡状态,因为输出端口没有分配足够的Buffer。嗯,这里要注意,端口Buffer的分配顺序很重要。
3.2 MediaCodec状态机:别在错误状态做错误的事
MediaCodec的状态机,说白了就四个状态:Stopped、Configured、Executing、Released。但实际开发中,我们更关心的是子状态和过渡。
状态迁移图:
各状态说明:
| 状态 | 含义 | 可执行操作 |
|---|---|---|
| Uninitialized | 刚创建,未配置 | configure() |
| Configured | 已配置参数,未启动 | start()、release() |
| Executing | 正在编码 | queueInputBuffer()、dequeueOutputBuffer()、stop()、flush() |
| Released | 已释放资源 | 无 |
| Error | 发生致命错误 | reset()、release() |
⚠️ 重要:在Executing状态下,如果调用了stop(),必须等待所有输入Buffer返回,否则会导致Buffer泄漏。我踩过这个坑,内存越跑越高。
3.3 Buffer流转机制:数据是怎么跑起来的
Buffer流转,是MediaCodec最核心的机制。很多开发者只记得调queue和dequeue,但不知道背后发生了什么。
Buffer流转的完整流程:
- 获取输入Buffer:调用dequeueInputBuffer(),从空闲队列拿一个Buffer。
- 填充数据:把Camera采集的YUV数据拷贝到Buffer中。
- 送入编码器:调用queueInputBuffer(),Buffer进入编码器输入队列。
- 硬件编码:OMX组件从输入队列取Buffer,送入DSP或VPU进行编码。
- 获取输出Buffer:调用dequeueOutputBuffer(),从输出队列拿编码后的码流。
- 处理数据:把码流写入文件或网络。
- 释放Buffer:调用releaseOutputBuffer(),Buffer回到空闲队列。
性能关键:Buffer的拷贝次数直接影响编码延迟。高通平台支持通过MediaCodec#createInputSurface()实现零拷贝,Camera数据直接进入编码器,不走CPU。
Buffer状态流转图:
3.4 异步模式:别再写死循环了
早期的MediaCodec示例代码,清一色都是同步模式:一个死循环里不断dequeue、queue。这种写法在简单场景下能用,但在Camera编码这种高实时性场景下,问题很大。
同步模式的痛点:
- CPU空转,浪费功耗。
- 无法及时响应Camera帧率变化。
- 多路编码时,线程管理复杂。
异步模式的优势:
- 通过Callback通知,避免轮询。
- 系统自动管理线程,减少开发者负担。
- 延迟更低,适合实时编码。
💡 建议:从Android 5.0开始,MediaCodec就支持异步模式。我个人习惯,所有新项目都直接用异步模式,省心省力。
异步模式的核心API:
// 设置异步回调
MediaCodec codec = MediaCodec.createEncoderByType("video/avc");
codec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 有输入Buffer可用,填充数据
ByteBuffer inputBuffer = codec.getInputBuffer(index);
// 填充Camera数据...
codec.queueInputBuffer(index, 0, size, pts, flags);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
// 有编码后的数据输出
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
// 处理码流...
codec.releaseOutputBuffer(index, false);
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 处理错误
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 输出格式变化,比如SPS/PPS更新
}
});
// 启动编码器
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();
⚠️ 注意:异步模式下,Callback是在内部线程中调用的。不要在Callback中做耗时操作,否则会阻塞Buffer流转。我曾经见过有人在onOutputBufferAvailable里写文件,导致编码器卡死。
同步 vs 异步对比:
| 维度 | 同步模式 | 异步模式 |
|---|---|---|
| CPU占用 | 高(轮询) | 低(回调) |
| 延迟 | 较高 | 较低 |
| 代码复杂度 | 简单 | 中等 |
| 多路编码 | 困难 | 容易 |
| 推荐场景 | 简单测试 | 生产环境 |