输入缓冲管理: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. 标准处理流程
- 准备缓冲区数组:启动后获取
inputBuffers(Android 5.0+ 推荐使用 ByteBuffer 数组或 Surface 输入)。 - 轮询 dequeueInputBuffer:设置超时参数,处理返回状态。
- 填充数据:将原始帧拷贝到获取的 ByteBuffer,设置
offset和size。 - 入队:调用
queueInputBuffer(index, offset, size, presentationTimeUs, flags)。 - 错误重试:若返回
-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 池。
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. 部署检查清单
- 确认 Manifest 中已声明
<uses-permission android:name="android.permission.CAMERA" />(若使用 Surface 输入)。 - 在
onPause中释放 MediaCodec,避免输入缓冲泄漏。 - 测试设备覆盖:骁龙 8 Gen 2、天玑 9000、麒麟 9000 等不同 SoC 的超时表现。
- 集成非阻塞回调:利用
MediaCodec.Callback代替手动轮询,减少线程数。