1. MediaCodec 管线基础概念

1.1 编解码器生命周期

MediaCodec 在 Android 多媒体管线中充当硬件/软件编解码引擎。其完整生命周期由 4 个离散状态 构成,应用层必须严格按序驱动,否则将抛出 MediaCodec.CodecException。

状态 触发 API 典型耗时 可执行操作
Uninitialized MediaCodec.createByCodecName() <1ms 获取名称、配置参数
Configured configure(format, surface, crypto, flags) 5–50ms 设置输入格式、输出表面
Executing start() 2–20ms 收发缓冲区、异步回调
Released stop() → release() 1–10ms 释放 Native 资源
常见错误:状态跳转违规
• 未调用 start() 直接 dequeueInputBuffer() → IllegalStateException
• 已 stop() 后继续 queueInputBuffer() → 无响应 / CodecException
• release() 后复用同一 MediaCodec 实例 → 必须重新 createByCodecName()
生命周期调试技巧
通过 adb shell dumpsys media.codec 查看当前编解码器实例状态。日志中搜索 onCodecEvent 可确认状态转换时间戳。

1.2 输入输出缓冲队列模型

MediaCodec 内部维护 两套环形缓冲区池:输入队列(Input Port)与输出队列(Output Port)。应用层通过 dequeue/queue 双缓冲模式与之交互,而非直接访问 I/O 内存。

队列方向 关键 API 返回意义 性能影响
Input dequeueInputBuffer(timeoutUs) ≥0: 可用缓冲区索引;-1: 超时 timeout 过大 → 输入延迟
Input queueInputBuffer(index, offset, size, pts, flags) 无返回 pts 乱序 → 输出帧重排
Output dequeueOutputBuffer(info, timeoutUs) 索引 / INFO_* 常量 未及时释放 → 队列阻塞
Output releaseOutputBuffer(index, render) 无返回 render=false 且后续未处理 → 丢帧
缓冲区索引复用规则
编解码器启动后,索引从 0 增长到 getInputFormat().getInteger("max-input-size") 上限。应用层从 dequeueXxxBuffer 获得的索引必须在同一帧处理完成前 queue 或 release,否则编解码器会挂起等待。
// 同步模式典型循环(伪代码)
while (!isEOS) {
    int inIndex = codec.dequeueInputBuffer(10_000); // timeout 10ms
    if (inIndex >= 0) {
        ByteBuffer buf = codec.getInputBuffer(inIndex);
        fillInputBuffer(buf, frameData);
        codec.queueInputBuffer(inIndex, 0, frameSize, pts, flags);
    }
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    int outIndex = codec.dequeueOutputBuffer(info, 10_000);
    if (outIndex >= 0) {
        ByteBuffer outBuf = codec.getOutputBuffer(outIndex);
        processFrame(outBuf, info);
        codec.releaseOutputBuffer(outIndex, true);
    } else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        updateOutputFormat(codec.getOutputFormat());
    }
}

1.3 同步模式与异步模式

MediaCodec 自 API 21 (Android 5.0) 起支持 异步回调模式,替代传统的轮询式同步循环。两种模式的对比直接影响管线实时性与代码复杂度。

维度 同步模式 异步模式
输入线程 主循环线程轮询 调用线程直接 queue
输出回调 手动 dequeue onOutputBufferAvailable
阻塞风险 dequeue 超时 100ms 可致 UI 卡顿 回调在内部 Handler 线程,无阻塞
配置方式 默认 codec.setCallback(callback, handler) 后 start
适用场景 简单播放、离线转码 实时流、低延迟、多管线
迁移建议
新项目优先选择异步模式,可以天然避免 dequeue 超时导致的背压问题。如果仍使用同步模式,务必在独立线程(非主线程)中运行 dequeue 循环,且 timeoutUs 不应超过 10ms 以避免管道延迟。

1.4 管线核心结构

MediaCodec 管线边界:生命周期 + 缓冲队列 Uninitialized createByCodecName configure() Configured 格式/表面已设 start() Executing dequeue/queue stop() Released release() flush() → 返回 Configured Input 缓冲池 dequeueInputBuffer queueInputBuffer MediaCodec 引擎 HW/SW 编解码 Output 缓冲池 dequeueOutputBuffer releaseOutputBuffer 异步模式入口 setCallback() onInputBufferAvailable onOutputBufferAvailable

1.5 同步/异步模式代码骨架

以下两段对比展示同步与异步模式下管线启动的差异。注意异步模式无需显式 dequeue,编解码器会自动将可用的输入/输出缓冲区通过回调交付。

// ---------- 同步模式 骨架 ----------
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
codec.start();

// 工作线程
new Thread(() -> {
    while (!isEOS) {
        int inIdx = codec.dequeueInputBuffer(TIMEOUT_US);
        if (inIdx >= 0) { /* 填充并 queue */ }
        int outIdx = codec.dequeueOutputBuffer(info, TIMEOUT_US);
        if (outIdx >= 0) { /* 处理并 release */ }
    }
    codec.stop();
    codec.release();
}).start();

// ---------- 异步模式 骨架 ----------
MediaCodec asyncCodec = MediaCodec.createDecoderByType("video/avc");
asyncCodec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec mc, int index) {
        ByteBuffer buf = mc.getInputBuffer(index);
        // 填充 buf,然后 mc.queueInputBuffer(index, ...)
    }
    @Override
    public void onOutputBufferAvailable(MediaCodec mc, int index, BufferInfo info) {
        // 处理输出,然后 mc.releaseOutputBuffer(index, render)
    }
    @Override public void onOutputFormatChanged(MediaCodec mc, MediaFormat format) { }
    @Override public void onError(MediaCodec mc, MediaCodec.CodecException e) { }
}, new Handler(Looper.getMainLooper()));
asyncCodec.configure(format, surface, null, 0);
asyncCodec.start();

1.6 性能与稳定性边界

  • 缓冲区饥饿:同步模式下 dequeueInputBuffer 超时过多 → 输入不足 → 解码器 stall。解决方案:设置 timeoutUs ≤ 5000,并监控 getInputBufferCount。
  • 输出缓冲区泄漏:未及时 releaseOutputBuffer → 输出队列满 → 编解码器阻塞(典型现象:画面卡在第 N 帧)。使用 BufferInfo.flags 判断 EOS 后及时清理。
  • 异步回调线程安全性:onOutputBufferAvailable 运行在 Handler 线程,不得在其中做 Thread.sleep() 或死循环。若需复杂渲染,将 releaseOutputBuffer 抛到异步队列。
  • 生命周期回退:flush() 可重置缓冲队列但不会离开 Executing 状态。在分辨率切换场景,应先 stop() → configure() → start(),而非 flush()。
容器化部署检查清单
• 确认 Android 设备支持所需编解码器:MediaCodecList.getCodecInfos()
• 多管线并行时,每个实例独立关联不同的 HandlerThread
• 使用 MediaCodecInfo.CodecCapabilities 检查 COLOR_FormatSurface 等能力,避免运行时 Format 不匹配