3、音视频编码时间戳:H.264/H.265的VUI与HRD参数,AAC的采样率与时间戳关系,编码帧类型对时间戳的影响
时间戳对齐这件事,很多人一上来就扎进 PTS/DTS 的计算公式里。但我个人的习惯是——先搞清楚编码器给了你什么「原材料」。说白了,编码器输出的码流里,本身就藏着时间戳的基因。H.264/H.265 的 VUI 和 HRD,AAC 的采样率,还有帧类型,这些才是时间戳对齐的底层逻辑。
这一章,我们就把这些「基因」拆开来看。
3.1 H.264/H.265 的 VUI 参数:时间基线的源头
VUI(Video Usability Information)是码流里的一段元数据。它不直接参与解码,但它告诉解码器:视频的时间轴怎么画。
我遇到过不少开发者,拿到一个 H.264 裸流,直接按 25fps 去算时间戳,结果音画不同步。查了半天,发现 VUI 里写的帧率是 29.97。嗯,这就是典型的「不看说明书就开车」。
VUI 里跟时间戳最相关的字段,就两个:
| 字段 | 含义 | 典型值 |
|---|---|---|
num_units_in_tick |
一个时间刻度对应的时钟周期数 | 1001、1000 |
time_scale |
每秒的时钟周期数 | 30000、60000、90000 |
fixed_frame_rate_flag |
是否固定帧率 | 1(固定)或 0(可变) |
帧率的计算公式很简单:
frame_rate = time_scale / (num_units_in_tick * 2)
举个例子:time_scale = 30000,num_units_in_tick = 1001,算出来就是 30000 / (1001 * 2) ≈ 14.985。等等,这不是 29.97?对,因为 H.264 的 VUI 里,帧率是针对场(field)的,所以要多乘一个 2。你想想看,如果直接拿这个值当帧率去算时间戳,那肯定对不上。
3.2 HRD 参数:解码器的「时间契约」
HRD(Hypothetical Reference Decoder)是 H.264/H.265 里一个更硬核的东西。它定义了一个虚拟解码器,用来约束码流的时序行为。说白了,就是告诉解码器:你什么时候该开始解码,什么时候该输出。
HRD 参数里,跟时间戳直接相关的有:
- cpb_removal_delay:当前图像从 CPB(编码图像缓冲区)中移除的延迟时间。
- dpb_output_delay:当前图像从 DPB(解码图像缓冲区)中输出的延迟时间。
- initial_cpb_removal_delay:初始缓冲延迟,决定了首帧的启动时间。
这些参数的单位是 90kHz 时钟。为什么是 90kHz?因为 MPEG-2 时代就定下来的标准,H.264/H.265 沿用了。90kHz 的精度足够覆盖绝大多数帧率(包括 29.97、23.976 这些非整数帧率)。
initial_cpb_removal_delay 设得特别大。解码器等了半天才开始输出,导致端到端延迟飙到 2 秒以上。后来我强制把这个参数改小,延迟才降下来。所以,HRD 参数不是摆设,它直接影响你的延迟指标。
3.3 AAC 的采样率与时间戳关系
音频这边,逻辑比视频简单一些。AAC 的编码单位是帧,每帧包含固定数量的采样点。采样率决定了每帧的时长。
AAC 的帧长是固定的:
- LC-AAC:每帧 1024 个采样点
- HE-AAC:每帧 2048 个采样点(因为用了 SBR,频带翻倍)
- AAC-LD:每帧 512 个采样点(低延迟模式)
每帧的时长计算公式:
frame_duration_ms = (samples_per_frame / sample_rate) * 1000
举个例子:
| 采样率 | 帧类型 | 每帧采样数 | 每帧时长 |
|---|---|---|---|
| 44100 Hz | LC-AAC | 1024 | 23.22 ms |
| 48000 Hz | LC-AAC | 1024 | 21.33 ms |
| 44100 Hz | HE-AAC | 2048 | 46.44 ms |
| 48000 Hz | HE-AAC | 2048 | 42.67 ms |
所以,音频时间戳的计算逻辑是:第 N 帧的 PTS = N × frame_duration_ms。前提是编码器没有丢帧、没有变速。但现实世界里,编码器偶尔会丢帧(比如网络不好时),这时候就不能简单累加了。我一般会维护一个 expected_pts 和 actual_pts 的映射表,发现差值超过一个帧长时,就做插值或重采样。
3.4 编码帧类型对时间戳的影响
帧类型直接影响 DTS 和 PTS 的关系。这个不搞清楚,时间戳对齐就是空中楼阁。
三种主要帧类型:
- I 帧(关键帧):可以独立解码,不需要参考其他帧。PTS = DTS。
- P 帧(前向预测帧):参考前面的 I 帧或 P 帧。PTS 可能大于 DTS,也可能等于(如果编码器没有做 B 帧重排)。
- B 帧(双向预测帧):参考前后的帧。PTS 和 DTS 的差值最大,因为 B 帧需要等后面的帧解码完才能输出。
举个例子,一个典型的 GOP 结构:I B B P B B P
编码器输出的顺序(DTS 顺序)是:
I(0) P(3) B(1) B(2) P(6) B(4) B(5) ...
但显示顺序(PTS 顺序)是:
I(0) B(1) B(2) P(3) B(4) B(5) P(6) ...
你看,B 帧的 DTS 比 PTS 大。这就是为什么解复用器必须同时维护 PTS 和 DTS 两个队列。如果你只拿 PTS 去排序,那解码器就乱套了。
3.5 知识体系总览
下面这张图,把这一章的核心逻辑串起来了。你可以把它当作时间戳对齐的「地图」:
从这张图可以看得很清楚:VUI 和 HRD 是视频时间戳的「宪法」,AAC 采样率是音频时间戳的「尺子」,帧类型是 PTS/DTS 关系的「调节器」。三者缺一不可。
最后说一句:不要迷信编码器。很多编码器写的 VUI 参数是错的,HRD 参数是估算的,AAC 的帧边界也可能不干净。我每次拿到一个新的码流,都会用 ffprobe 先扫一遍,再手动算一遍时间戳,两相对比,才能放心往下走。