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。你想想看,如果直接拿这个值当帧率去算时间戳,那肯定对不上。

我的习惯:解析 H.264/H.265 码流时,第一件事就是读 VUI。如果 VUI 不存在,再 fallback 到容器层的帧率。但说实话,很多编码器写 VUI 并不规范,所以最好两个都校验一下。

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 这些非整数帧率)。

我曾经踩过的坑:有一次做低延迟直播,编码器输出的 HRD 参数里 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 的映射表,发现差值超过一个帧长时,就做插值或重采样。

关键点:视频的时间戳单位是 90kHz,音频的时间戳单位是采样率。对齐时,必须统一到一个时间基上。我习惯把音频时间戳也转成 90kHz 单位,这样跟视频 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 去排序,那解码器就乱套了。

我个人的经验:在封装 MP4 或 TS 时,如果码流里有 B 帧,一定要把 DTS 也写进去。MP4 的 stts box 只存 PTS,但你可以用 ctts box 存 PTS 和 DTS 的偏移。很多播放器只读 stts,结果 B 帧的显示顺序就错了。嗯,这个问题在 iOS 的原生播放器上尤其常见。

3.5 知识体系总览

下面这张图,把这一章的核心逻辑串起来了。你可以把它当作时间戳对齐的「地图」:

音视频编码时间戳知识体系 编码时间戳知识体系 H.264/H.265 VUI num_units_in_tick time_scale fixed_frame_rate_flag → 帧率 = time_scale/(num*2) H.264/H.265 HRD cpb_removal_delay dpb_output_delay initial_cpb_removal_delay → 单位:90kHz 时钟 AAC 采样率与帧长 LC-AAC: 1024 采样/帧 HE-AAC: 2048 采样/帧 AAC-LD: 512 采样/帧 → 帧时长 = 采样数/采样率 编码帧类型 I 帧: PTS = DTS P 帧: PTS ≥ DTS B 帧: PTS < DTS → 影响 PTS/DTS 排序 时间戳对齐输出 统一时间基:90kHz 视频 PTS/DTS 分离 音频 PTS 按帧累加 音视频交叉复用 HRD 约束检查 核心原则:视频时间戳来自 VUI/HRD,音频时间戳来自采样率,帧类型决定 PTS/DTS 关系。三者统一到 90kHz 时间基后,才能做对齐。

从这张图可以看得很清楚:VUI 和 HRD 是视频时间戳的「宪法」,AAC 采样率是音频时间戳的「尺子」,帧类型是 PTS/DTS 关系的「调节器」。三者缺一不可。

最后说一句:不要迷信编码器。很多编码器写的 VUI 参数是错的,HRD 参数是估算的,AAC 的帧边界也可能不干净。我每次拿到一个新的码流,都会用 ffprobe 先扫一遍,再手动算一遍时间戳,两相对比,才能放心往下走。