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。但实际开发中,我们更关心的是子状态和过渡。

状态迁移图:

MediaCodec状态机迁移图 Uninitialized Configured Executing Released Error configure() start() start() release() 异常 reset() ⚠️ 避坑指南 在Executing状态下,不要调用configure()。必须先stop()回到Configured状态。 我曾经见过有人直接在Executing状态调reset(),导致OMX组件崩溃,整个Camera HAL挂掉。

各状态说明:

状态 含义 可执行操作
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流转的完整流程:

  1. 获取输入Buffer:调用dequeueInputBuffer(),从空闲队列拿一个Buffer。
  2. 填充数据:把Camera采集的YUV数据拷贝到Buffer中。
  3. 送入编码器:调用queueInputBuffer(),Buffer进入编码器输入队列。
  4. 硬件编码:OMX组件从输入队列取Buffer,送入DSP或VPU进行编码。
  5. 获取输出Buffer:调用dequeueOutputBuffer(),从输出队列拿编码后的码流。
  6. 处理数据:把码流写入文件或网络。
  7. 释放Buffer:调用releaseOutputBuffer(),Buffer回到空闲队列。

性能关键:Buffer的拷贝次数直接影响编码延迟。高通平台支持通过MediaCodec#createInputSurface()实现零拷贝,Camera数据直接进入编码器,不走CPU。

Buffer状态流转图:

Buffer状态流转图 空闲队列 输入队列 编码器 输出队列 dequeueInputBuffer() queueInputBuffer() 编码完成 releaseOutputBuffer() 回到空闲 💡 个人经验 我建议在dequeueInputBuffer()时设置超时,不要死等。Camera帧率是固定的,超时说明编码器处理不过来,需要降帧或调码率。

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占用 高(轮询) 低(回调)
延迟 较高 较低
代码复杂度 简单 中等
多路编码 困难 容易
推荐场景 简单测试 生产环境