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 增长到
编解码器启动后,索引从 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 以避免管道延迟。
新项目优先选择异步模式,可以天然避免 dequeue 超时导致的背压问题。如果仍使用同步模式,务必在独立线程(非主线程)中运行 dequeue 循环,且 timeoutUs 不应超过 10ms 以避免管道延迟。
1.4 管线核心结构
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 设备支持所需编解码器:
• 多管线并行时,每个实例独立关联不同的
• 使用
• 确认 Android 设备支持所需编解码器:
MediaCodecList.getCodecInfos()• 多管线并行时,每个实例独立关联不同的
HandlerThread• 使用
MediaCodecInfo.CodecCapabilities 检查 COLOR_FormatSurface 等能力,避免运行时 Format 不匹配