输出缓冲管理
输出缓冲是 MediaCodec 管线中从编码器/解码器输出到消费者(Surface、文件、网络等)的关键桥梁。本章聚焦 dequeueOutputBuffer 的返回状态码处理、缓冲区释放决策以及渲染时机的精确控制,帮助你构建无阻塞、低延迟的输出侧循环。
4.1 dequeueOutputBuffer 状态码全解析
dequeueOutputBuffer(MediaCodec.BufferInfo, long timeoutUs) 返回一个 int 值,可能为以下五种之一:
| 状态码 | 含义 | 典型处理行为 |
|---|---|---|
INFO_TRY_AGAIN_LATER (-1) | 当前无可用输出缓冲区,需重试 | 等待 timeoutUs 后再次调用,或切换线程让步 |
INFO_OUTPUT_FORMAT_CHANGED (-2) | 输出格式发生改变(如分辨率、编码配置变更) | 读取 getOutputFormat() 更新参数;仅在编码器/流发生变化时出现 |
INFO_OUTPUT_BUFFERS_CHANGED (-3) | 输出缓冲区集合被重新分配(API 21+ 极少出现) | 重新获取 getOutputBuffers()(已废弃),或依赖 ByteBuffer 模式自行重新映射 |
≥ 0(有效索引) | 成功获得一个输出缓冲区,数据位于 BufferInfo 中 | 读取数据,随后必须 releaseOutputBuffer |
关键认知:状态码
-3 在 API 21+ 中几乎不会出现(推荐使用 ByteBuffer 模式),大多数应用只需处理前三个分支即可。
4.2 缓冲区释放:releaseOutputBuffer 的两个语义
一旦获得有效输出缓冲区(索引 ≥0)并完成数据读取或渲染,必须调用 releaseOutputBuffer(int index, boolean render) 归还缓冲区给编解码器。参数 render 决定最终行为:
render = true—— 缓冲区内容将送至 Surface 消费(适用于解码渲染场景)。
若配置了 Surface 输入,此操作将在 Surface 上显示一帧;必须在适当的时间点(如完成合成后)调用。render = false—— 纯软件释放,缓冲区不会显示到 Surface,适用于文件打包、网络推送或预处理分析。
此外,渲染时机并非调用 releaseOutputBuffer(..., true) 的瞬间,而是由 BufferInfo.presentationTimeUs 决定。系统会等到该时间戳对应的 VSync 信号到达时才真正合成上图,因此释放调用应尽量及时,避免阻塞解码管线。
4.3 典型输出循环实现
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (isRunning) {
int outIndex = codec.dequeueOutputBuffer(info, 10_000); // 10ms超时
if (outIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 暂无数据,可执行其他任务或sleep短暂等待
continue;
} else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFmt = codec.getOutputFormat();
// 更新封装器、渲染器或记录日志
continue;
} else if (outIndex < 0) {
// INFO_OUTPUT_BUFFERS_CHANGED 忽略
continue;
}
// 有效索引
if (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
// 流结束标记,处理剩余数据
}
// 处理帧数据(拷贝或送入渲染)
if (renderToSurface) {
codec.releaseOutputBuffer(outIndex, true); // 渲染到Surface
} else {
// 获取ByteBuffer,拷贝数据(如文件写入)
ByteBuffer buf = codec.getOutputBuffer(outIndex);
// ... 处理数据 ...
codec.releaseOutputBuffer(outIndex, false); // 仅释放
}
}
性能建议:输出循环应保持与显示刷新率同步,避免提前调用
releaseOutputBuffer 导致 Surface 内部队列堆积。对于纯编码/转封装场景,在完成数据写入后再释放,render = false。
4.4 渲染时机与回调交互
当输出缓冲区被 render = true 释放后,可以通过 OnFrameRenderedListener(API 23+)监听实际渲染完成事件:
codec.setOnFrameRenderedListener(new MediaCodec.OnFrameRenderedListener() {
@Override
public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) {
// 表示该帧已被Surface消费,此时可安全释放相关资源
Log.d("Render", "Frame rendered at " + presentationTimeUs);
}
}, handler);
该回调用于精确测量渲染延迟、驱动滑动窗口缓冲区管理,以及实现低延迟渲染管线的背压控制。
4.5 常见错误与稳定性优化
- 遗漏 releaseOutputBuffer —— 一旦 dequeueOutputBuffer 返回有效索引,无论后续是否使用数据,都必须释放;否则会造成缓冲区泄漏,解码器最终阻塞。
- 错误使用 render 参数 —— 在非 Surface 模式下调用
render = true会抛出IllegalStateException;反之,在 Surface 模式下调用render = false会导致视频无法显示。 - 超时设置不合理 ——
timeoutUs过短会增加 CPU 空转,过长则降低响应性。建议视频场景使用 10~20ms,音频场景使用 5~10ms。 - 格式变更未及时处理 —— 忽略
INFO_OUTPUT_FORMAT_CHANGED可能导致后续帧解析错误,应在收到后立即更新封装参数。
致命陷阱:某些设备在输出结束标记(
BUFFER_FLAG_END_OF_STREAM)后仍可能多返回一个有效缓冲区(长度为 0),需额外判断 info.size > 0 再作处理。直接释放空缓冲区即可。
4.6 输出缓冲管理流程
上图清晰展示了从 dequeueOutputBuffer 到最终释放的完整决策路径,重点突出了三种状态码的处理分支以及释放后的循环回归。
4.7 部署与稳定性建议
- 渲染线程隔离:将输出循环运行在独立线程,避免与 UI 主线程争抢 CPU。
- 背压控制:当
dequeueOutputBuffer频繁返回INFO_TRY_AGAIN_LATER时,主动降低输入队列投喂速率,防止解码器内部溢出。 - 释放后立即处理下一帧:不要在释放后做耗时操作(如文件 I/O),将拷贝逻辑异步化或放入单独线程池。
- 监控统计:记录每次
dequeueOutputBuffer的耗时和状态码分布,帮助定位性能瓶颈。
调试技巧:开启
Log.v 日志跟踪输出缓冲索引序列,观察是否出现连续相同索引或长时间无输出,提前捕获缓冲区死锁。