5、MediaCodec解码时间戳:解码器输入输出时间戳、BufferInfo.flags标志位、解码延迟与渲染时机
各位同学,今天我们来聊聊MediaCodec解码时间戳。这个话题,说白了就是解码器怎么跟时间打交道。我刚开始做音视频时,总觉得时间戳不就是个数字嘛,传进去就行了。结果呢?画面卡顿、音画不同步,各种问题找上门来。后来才明白,时间戳是整个同步机制的命脉。
5.1 解码器输入输出时间戳
MediaCodec的工作流程,其实是个异步管道。你往输入端塞数据,它从输出端吐数据。但这里有个关键点:输入时间戳和输出时间戳是同一个东西。
什么意思呢?你给解码器喂一个数据包时,会带上这个包的presentationTimeUs。解码器处理完后,在输出端会把同样的时间戳还给你。嗯,它不会帮你改时间戳,也不会帮你算延迟。它就是个老实人,你给什么,它还什么。
核心原则:输入时间戳 = 输出时间戳。解码器不修改时间戳,只负责解码。
我在项目中遇到过一个问题:有人把时间戳设成了0,结果所有帧都挤在同一时刻渲染。画面就像幻灯片一样,一帧闪过,然后黑屏半天。排查了半天才发现,是时间戳没传对。
来看一段典型的输入输出代码:
// 输入
int inputIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
inputBuffer.clear();
inputBuffer.put(data);
codec.queueInputBuffer(inputIndex, 0, data.length, presentationTimeUs, 0);
}
// 输出
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outputIndex = codec.dequeueOutputBuffer(info, TIMEOUT_US);
if (outputIndex >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputIndex);
// info.presentationTimeUs 就是刚才输入的时间戳
render(outputBuffer, info);
codec.releaseOutputBuffer(outputIndex, true);
}
你看,info.presentationTimeUs 就是咱们刚才传进去的那个值。解码器不会帮你做任何时间上的调整。
5.2 BufferInfo.flags标志位
BufferInfo里有个flags字段,很多人容易忽略它。但说实话,这个字段在关键时刻能救命。
| 标志位 | 值 | 含义 |
|---|---|---|
| BUFFER_FLAG_KEY_FRAME | 1 | 关键帧(I帧),可以从此帧开始解码 |
| BUFFER_FLAG_CODEC_CONFIG | 2 | 编码配置数据(如SPS/PPS) |
| BUFFER_FLAG_END_OF_STREAM | 4 | 流结束标志,没有更多数据了 |
| BUFFER_FLAG_PARTIAL_FRAME | 8 | 部分帧数据(API 21+) |
我个人习惯,在输出循环里一定会检查flags。尤其是BUFFER_FLAG_END_OF_STREAM,它告诉你解码器已经处理完所有数据了。如果不处理这个标志,你的渲染循环可能会一直卡在dequeueOutputBuffer上。
小技巧:收到BUFFER_FLAG_CODEC_CONFIG时,不要急着渲染。这是配置数据,不是真正的画面帧。直接releaseOutputBuffer就好。
我曾经踩过一个坑:在解码开始时,没有处理BUFFER_FLAG_CODEC_CONFIG。结果第一帧画面花屏了。后来加了个判断,遇到配置数据直接跳过,问题就解决了。
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// 配置数据,不渲染
codec.releaseOutputBuffer(outputIndex, false);
continue;
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// 流结束,退出循环
break;
}
5.3 解码延迟与渲染时机
解码延迟,说白了就是解码器从收到数据到吐出结果的时间差。这个延迟不是固定的,它跟编码格式、码率、设备性能都有关系。
你想想看,H.264的B帧需要参考前后帧才能解码。解码器必须等后面的帧到了,才能把前面的B帧吐出来。这就导致了输出顺序和输入顺序可能不一样。
注意:解码器输出的帧顺序,不一定是显示顺序。尤其是包含B帧的视频流,输出顺序可能是:I, P, B, B, P... 但显示顺序应该是:I, B, B, P, P...
那渲染时机怎么定?我的做法是:用时间戳来决定,而不是用帧到达的顺序。
具体来说,渲染线程会维护一个时钟。每拿到一帧,就检查它的presentationTimeUs是否已经到了。如果还没到,就等一会儿;如果已经过了,就立刻渲染。
这里有个常见的误区:有人觉得解码器输出一帧就立刻渲染一帧。结果呢?B帧显示顺序乱了,画面像在倒放。嗯,我刚开始也犯过这个错。
来看一个简单的渲染调度逻辑:
long nowUs = System.nanoTime() / 1000;
long targetUs = info.presentationTimeUs;
if (targetUs > nowUs) {
// 还没到时间,等一会儿
long waitUs = targetUs - nowUs;
Thread.sleep(waitUs / 1000);
} else if (targetUs < nowUs - ALLOWED_DRIFT_US) {
// 落后太多了,丢帧
codec.releaseOutputBuffer(outputIndex, false);
continue;
}
// 时间到了,渲染
render(outputBuffer, info);
codec.releaseOutputBuffer(outputIndex, true);
ALLOWED_DRIFT_US 我一般设成 100ms。超过这个范围,说明这帧已经严重落后了,渲染出来反而会让用户觉得卡顿。
5.4 知识体系总览
为了让你更直观地理解整个流程,我画了一张图:
从这张图你能看到,整个流程其实很清晰:输入时间戳 → 解码器 → 输出时间戳 → flags检查 → 渲染时机判断。每一步都有它的作用,缺一不可。
避坑指南:我曾经在低端设备上遇到过解码延迟高达200ms的情况。那时候我用了固定延迟补偿,结果音画不同步更严重了。后来改用动态延迟跟踪,根据最近几帧的实际延迟来调整,效果好了很多。
最后说一句:时间戳这个东西,看似简单,但它是整个同步机制的基石。你把它搞明白了,音画同步的问题就解决了一大半。