输出缓冲管理

输出缓冲是 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 INFO_TRY_AGAIN FORMAT_CHANGED 有效索引 (≥0) 继续下一次dequeue 更新输出格式 render=true ? releaseOutputBuffer (true) 释放 (false) + 拷贝 回到 dequeueOutputBuffer 格式更新后继续 每次有效索引都必须调用 releaseOutputBuffer,且 render 参数必须与输出目标一致

上图清晰展示了从 dequeueOutputBuffer 到最终释放的完整决策路径,重点突出了三种状态码的处理分支以及释放后的循环回归。

4.7 部署与稳定性建议

  • 渲染线程隔离:将输出循环运行在独立线程,避免与 UI 主线程争抢 CPU。
  • 背压控制:当 dequeueOutputBuffer 频繁返回 INFO_TRY_AGAIN_LATER 时,主动降低输入队列投喂速率,防止解码器内部溢出。
  • 释放后立即处理下一帧:不要在释放后做耗时操作(如文件 I/O),将拷贝逻辑异步化或放入单独线程池。
  • 监控统计:记录每次 dequeueOutputBuffer 的耗时和状态码分布,帮助定位性能瓶颈。
调试技巧:开启 Log.v 日志跟踪输出缓冲索引序列,观察是否出现连续相同索引或长时间无输出,提前捕获缓冲区死锁。