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 知识体系总览

为了让你更直观地理解整个流程,我画了一张图:

MediaCodec 解码时间戳流程 输入数据 presentationTimeUs MediaCodec 解码器 输出数据 BufferInfo + 时间戳 时间戳处理:输入时间戳 = 输出时间戳,解码器不修改 flags 检查:KEY_FRAME | CODEC_CONFIG | END_OF_STREAM | PARTIAL_FRAME 渲染时机:比较 presentationTimeUs 与当前时钟,决定立即渲染或等待 解码延迟影响输出顺序,但渲染始终以时间戳为准

从这张图你能看到,整个流程其实很清晰:输入时间戳 → 解码器 → 输出时间戳 → flags检查 → 渲染时机判断。每一步都有它的作用,缺一不可。

避坑指南:我曾经在低端设备上遇到过解码延迟高达200ms的情况。那时候我用了固定延迟补偿,结果音画不同步更严重了。后来改用动态延迟跟踪,根据最近几帧的实际延迟来调整,效果好了很多。

最后说一句:时间戳这个东西,看似简单,但它是整个同步机制的基石。你把它搞明白了,音画同步的问题就解决了一大半。


公众号:蓝海资料掘金营,微信deep3321