2. 时间戳基础:PTS(显示时间戳)与DTS(解码时间戳)的定义与区别

好,咱们正式开始聊音频同步的核心——时间戳。

很多人一上来就研究各种同步算法,结果发现连最基础的时间戳都没搞明白。我见过不少项目,明明解码器跑得飞快,声音就是跟画面对不上。查到最后,往往是 PTS 和 DTS 搞混了,或者压根没设置对。

说白了,时间戳就是给每一帧音视频数据贴的「时间标签」。告诉系统:这帧数据该在什么时候解码,什么时候显示。没有这个标签,播放器就是个瞎子。

2.1 为什么需要两个时间戳?

你可能会问:一个时间戳不就够了吗?为什么非要搞出 PTS 和 DTS 两个?

嗯,这得从视频编码的「原罪」说起。

视频编码为了压缩效率,引入了 I 帧、P 帧、B 帧。I 帧是关键帧,可以独立解码;P 帧依赖前面的帧;B 帧最麻烦,它依赖前面和后面的帧。

举个例子:一个视频的帧顺序是 I1、B2、P3、B4、P5。解码器要解码 B2,必须先拿到它后面的 P3。所以解码顺序和显示顺序天然就不一样。

核心矛盾:

  • 解码顺序:I1 → P3 → B2 → P5 → B4
  • 显示顺序:I1 → B2 → P3 → B4 → P5

这就是 DTS 和 PTS 存在的根本原因。DTS 告诉解码器「什么时候开始解」,PTS 告诉渲染器「什么时候拿出来显示」。

2.2 PTS 与 DTS 的明确定义

咱们把这两个概念掰开揉碎了讲。

属性 PTS(显示时间戳) DTS(解码时间戳)
全称 Presentation Time Stamp Decoding Time Stamp
作用 告诉渲染器:这帧数据「什么时候显示」 告诉解码器:这帧数据「什么时候解码」
对应阶段 渲染输出阶段 解码输入阶段
帧类型影响 所有帧都有 PTS 所有帧都有 DTS
关系 PTS ≥ DTS(通常如此) DTS ≤ PTS
无 B 帧时 PTS == DTS DTS == PTS

我个人习惯把 PTS 理解为「上台时间」,DTS 理解为「化妆时间」。演员(帧数据)得先化好妆(解码),才能上台(显示)。如果化妆顺序和上台顺序不一样,就得靠这两个时间戳来调度。

2.3 一个具体的例子

咱们用一段 H.264 码流来演示。假设 GOP(图像组)结构如下:

帧序号(显示顺序):0  1  2  3  4  5  6  7
帧类型:          I  B  B  P  B  B  P  B

解码顺序:        0  3  1  2  6  4  5  7
                  I  P  B  B  P  B  B  B

对应的 PTS 和 DTS 值(假设帧率 30fps,每帧间隔约 33ms):

显示顺序 帧类型 PTS (ms) DTS (ms) 说明
0 I 0 0 I 帧,PTS == DTS
1 B 33 99 B 帧依赖后面的 P 帧,DTS 晚于 PTS
2 B 66 132 同上
3 P 99 33 P 帧先解码,后显示
4 B 132 198 —
5 B 165 231 —
6 P 198 66 P 帧先解码
7 B 231 264 —

看到没?B 帧的 DTS 比 PTS 大。这意味着解码器先解后面的 P 帧,再回头解 B 帧。但显示的时候,B 帧反而排在前面。

避坑指南: 我曾经在一个项目中,直接把解复用器拿到的 PTS 当成 DTS 喂给解码器。结果解码器缓存爆了,画面卡成幻灯片。后来才发现,对于包含 B 帧的码流,PTS 和 DTS 必须分开处理。解码队列按 DTS 排序,显示队列按 PTS 排序,两个队列不能混用。

2.4 时间戳在音频中的特殊性

上面讲的都是视频。音频呢?

音频编码通常没有 B 帧这种依赖关系。常见的 AAC、MP3、PCM,解码顺序和播放顺序完全一致。所以对于纯音频流,PTS 和 DTS 是相等的。

但这里有个坑:音频的 PTS 不是按「帧」算的,而是按「采样点」算的。

音频 PTS = 采样点序号 / 采样率

举例:
采样率 44100Hz,第 1024 个采样点的 PTS = 1024 / 44100 ≈ 23.2ms

我建议你在处理音频时间戳时,始终用采样点作为基准单位,不要用毫秒。因为音频解码器内部是按采样点对齐的,用毫秒容易产生累积误差。

2.5 时间戳的传递链路

咱们用一张图把整个流程串起来。

PTS / DTS 在音视频同步中的传递链路 封装格式 (MP4/TS/MKV) 解复用器 (Demuxer) 音视频分离 (PES/AVPacket) 视频解码器 (按 DTS 入队) 音频解码器 (PTS == DTS) 视频渲染器 (按 PTS 显示) 音频输出设备 (按 PTS 播放) 关键点 • 视频:DTS 决定解码顺序 • 视频:PTS 决定显示顺序 • 音频:PTS == DTS • 同步点:统一时钟基准

从这张图可以清楚看到:

  • 封装格式里同时存着 PTS 和 DTS
  • 解复用器把它们提取出来,分别挂到音视频 packet 上
  • 视频解码器按 DTS 排序解码,解码后的帧按 PTS 排序等待显示
  • 音频解码器因为 PTS == DTS,处理起来简单很多

注意: 有些封装格式(比如某些老旧的 AVI)可能只存了 PTS,没有 DTS。这时候解码器需要自己根据帧类型推算 DTS。我曾经踩过这个坑——从 AVI 里读出来的 B 帧 PTS 是乱的,因为没有 DTS 信息,解码器把 B 帧当 P 帧解,画面全是马赛克。

2.6 时间戳的基准与单位

时间戳本身是个数值,它需要配合「时间基准」才有意义。

在 FFmpeg 里,时间基准用 AVRational 表示,也就是分数形式:

// 常见时间基准
AVRational tb1 = {1, 1000};   // 毫秒级精度
AVRational tb2 = {1, 90000};  // MPEG TS 流标准,90kHz 时钟
AVRational tb3 = {1, 48000};  // 音频采样率基准

// 时间戳转秒
double seconds = timestamp * av_q2d(time_base);

我个人习惯在项目里统一用微秒(us)作为内部时间戳单位。原因很简单:

  • 微秒精度足够高(1秒 = 1,000,000 微秒)
  • 整数运算,没有浮点误差
  • 跨平台移植方便

你想想看,如果用浮点数表示秒,做加减法时累积误差会越来越大。尤其是长时间播放,几个小时下来可能偏了好几秒。用整数微秒就没这个问题。

2.7 常见问题与排查思路

最后,我总结几个实际项目中经常遇到的时间戳问题:

  1. PTS 抖动:相邻帧的 PTS 差值忽大忽小。原因通常是源文件封装不规范,或者解复用器实现有 bug。
  2. DTS 回退:DTS 突然变小。这可能是码流损坏,或者 GOP 结构异常。
  3. PTS 与 DTS 差距过大:说明 B 帧数量异常多,解码器缓存压力大。
  4. 音频 PTS 不连续:常见于网络流,丢包导致时间戳跳变。

排查技巧: 我遇到时间戳问题时,第一件事就是打印原始时间戳日志。用 av_log 把每个 packet 的 PTS、DTS、duration 都打出来,然后画成折线图。一眼就能看出是哪里出了问题。别上来就改代码,先看数据。

好了,PTS 和 DTS 的基础就讲到这里。记住一句话:DTS 管解码顺序,PTS 管显示顺序,音频两者合一。后面讲同步算法时,你会反复用到这两个概念。