输入缓冲管理:dequeueInputBuffer超时处理、填充数据、队列机制

1. 原理:输入缓冲队列的工作模型

MediaCodec 内部维护一个 可用输入缓冲队列(free input buffer pool),解码器从该队列中分配缓冲区给客户端。客户端通过 dequeueInputBuffer() 获取一个非占用的缓冲区索引;若队列为空,则进入阻塞等待或返回 -1 并设置超时。填充数据后调用 queueInputBuffer() 将缓冲区交还给编码/解码管线,使其进入 已排队 状态,等待内部处理。

超时机制直接影响实时性:

  • 超时 = 0:非阻塞模式,队列空时立即返回 -1,适合与外部生产者协同。
  • 超时 > 0:阻塞等待指定微秒数,适合固定帧率推送(如 Camera 预览)。
  • 超时 < 0:无限等待直到缓冲区可用,易导致 UI 线程 ANR,应避免在主线程使用。
底层契约:dequeueInputBuffer() 返回的数量受限于 MEDIACODEC_INFO_INPUT_BUFFER_COUNT(通常 4~16),超时返回值与 MEDIACODEC_INFO_TRY_AGAIN_LATER 常量比较,而非简单判负。

2. 标准处理流程

  1. 准备缓冲区数组:启动后获取 inputBuffers(Android 5.0+ 推荐使用 ByteBuffer 数组或 Surface 输入)。
  2. 轮询 dequeueInputBuffer:设置超时参数,处理返回状态。
  3. 填充数据:将原始帧拷贝到获取的 ByteBuffer,设置 offset 和 size。
  4. 入队:调用 queueInputBuffer(index, offset, size, presentationTimeUs, flags)。
  5. 错误重试:若返回 -1 且为超时,等待后继续轮询;若返回 MEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED 等,需先处理输出端。

关键伪代码(Java):

final long TIMEOUT_US = 10_000; // 10ms
int inputIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inputIndex >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
    inputBuffer.clear();
    inputBuffer.put(frameData);
    codec.queueInputBuffer(inputIndex, 0, frameData.length, ptsUs, 0);
} else if (inputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
    // 队列满或超时,等待下一轮
    if (timeoutCounter++ > MAX_RETRIES) {
        // 考虑降帧或丢弃
    }
} else {
    // 其他状态:format change / end of stream
    handleInfo(inputIndex);
}

3. 队列机制与内部状态机

MediaCodec 输入缓冲在生命周期中经历以下状态转换:

  • FREE —— 空闲,等待客户端获取。
  • OWNED_BY_CLIENT —— 客户端通过 dequeueInputBuffer 获得所有权。
  • QUEUED —— 客户端填充后调用 queueInputBuffer 交回给解码器。
  • DECODING —— 解码器正在处理该缓冲区。
  • DRAINED —— 处理完毕,释放回 FREE 池。
输入缓冲区状态机 & 客户端操作 FREE 空闲缓冲池 OWNED_BY_CLIENT 客户端持有 QUEUED 已排队 DECODING 解码器处理中 DRAINED 处理完毕 dequeueInputBuffer (超时等待) queueInputBuffer (填充后) 内部取走 解码完成 回收入池 超时影响节点 0: 立即返回 -1 >0: 阻塞等待 <0: 无限阻塞

4. 调试手段:缓冲状态诊断

通过 dumpsys media.mediacodec 可以查看当前 MediaCodec 实例的缓冲状态。推荐在 dequeueInputBuffer 返回 -1 时打印队列深度:

// 在 adb shell 中:
dumpsys media.mediacodec | grep -A 20 "client-xyz"

日志最佳实践:

Log.v("MediaCodecIn", "dequeueInputBuffer(" + timeout + ") = " + inputIndex
      + ", queueDepth=" + codec.getInputBuffers().length // 实际队列容量
      + ", pending=" + pendingCount); // 维护的已入队帧计数
边界提示:某些 SoC(如 MTK)在低电量时可能缩短硬件内部缓冲区池,导致 dequeueInputBuffer 频繁超时。建议在 logcat 中过滤 ACodec 或 MediaCodec 关键日志进行甄别。

5. 常见错误与避坑

错误表现根因解决方案
ANR(应用无响应)主线程调用 dequeueInputBuffer(-1) 无限阻塞使用非零超时;或另开工作线程
帧率骤降/卡顿超时设置过小(如 0)导致忙等,CPU 满载设置合理超时(如 5~10ms),或结合 ConditionVariable 等待
数据错乱/花屏重复 queueInputBuffer 同一索引/未及时清理确保每个索引只被入队一次,使用集合记录已入队索引
IllegalStateException在 stop() 后仍尝试入队入队前检查 Codec 状态,或使用 try-catch 保护

6. 性能优化策略

  • 预分配 ByteBuffer:避免反复创建 DirectBuffer,使用 getInputBuffer(index) 复用。
  • 超时自适应:根据历史平均获取时间动态调整 timeoutUs,平衡延迟与 CPU 开销。
  • 批量填充:对于音轨或分片流,可在 dequeueInputBuffer 返回后一次填充多个缓冲区(若存在工作线程池)。
  • 减少锁竞争:使用专用线程处理输入队列,避免与输出端争抢 dequeueOutputBuffer 的锁。
  • 动态降帧:当检测到 dequeueInputBuffer 连续失败超过阈值时,主动丢弃当前帧,防止生产者积压。
稳定性风险:某些低端硬件在 queueInputBuffer 后内部可能因为内存压力而阻塞。务必在 queueInputBuffer 调用后,等待输出端回收之前,不要将同一缓冲区的引用传递给其他模块(如网络发送)。建议采用“引用计数”或“缓冲池”模式确保安全。

7. 部署检查清单

  1. 确认 Manifest 中已声明 <uses-permission android:name="android.permission.CAMERA" />(若使用 Surface 输入)。
  2. 在 onPause 中释放 MediaCodec,避免输入缓冲泄漏。
  3. 测试设备覆盖:骁龙 8 Gen 2、天玑 9000、麒麟 9000 等不同 SoC 的超时表现。
  4. 集成非阻塞回调:利用 MediaCodec.Callback 代替手动轮询,减少线程数。