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
- 解复用器把它们提取出来,分别挂到音视频 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 常见问题与排查思路
最后,我总结几个实际项目中经常遇到的时间戳问题:
- PTS 抖动:相邻帧的 PTS 差值忽大忽小。原因通常是源文件封装不规范,或者解复用器实现有 bug。
- DTS 回退:DTS 突然变小。这可能是码流损坏,或者 GOP 结构异常。
- PTS 与 DTS 差距过大:说明 B 帧数量异常多,解码器缓存压力大。
- 音频 PTS 不连续:常见于网络流,丢包导致时间戳跳变。
排查技巧: 我遇到时间戳问题时,第一件事就是打印原始时间戳日志。用 av_log 把每个 packet 的 PTS、DTS、duration 都打出来,然后画成折线图。一眼就能看出是哪里出了问题。别上来就改代码,先看数据。
好了,PTS 和 DTS 的基础就讲到这里。记住一句话:DTS 管解码顺序,PTS 管显示顺序,音频两者合一。后面讲同步算法时,你会反复用到这两个概念。