2. 音视频基础
上一章我们聊了音画同步为什么是个麻烦事。这一章,咱们直接切入核心——时间戳。
说白了,时间戳就是给每一帧画面、每一段音频贴上的「时间标签」。没有这个标签,播放器就是个瞎子,根本不知道什么时候该干什么。
我个人习惯把时间戳理解成「快递单号」。视频文件就像一箱快递,解码器是分拣员,显示器是收件人。PTS 和 DTS 就是贴在每个包裹上的两张单子,告诉分拣员和收件人:这包裹该什么时候处理、什么时候签收。
2.1 为什么需要两个时间戳?
你可能会问:一个时间戳不就够了吗?为什么非要搞出 PTS 和 DTS 两个?
嗯,这里有个关键点——视频编码不是按顺序来的。
想想看,我们看的视频是连续的。但编码器为了压缩效率,搞出了三种帧:
- I 帧(关键帧):完整的画面,可以独立解码。相当于一本书的目录页。
- P 帧(前向预测帧):只记录与前一帧的差异。解码它之前,必须先有前一帧。
- B 帧(双向预测帧):记录与前后两帧的差异。解码它之前,前后两帧都得准备好。
问题就出在 B 帧上。举个例子:
假设一个视频的帧顺序是:I1 - B2 - P3 - B4 - P5
播放顺序(PTS)是:1, 2, 3, 4, 5
但解码顺序(DTS)呢?B2 需要 P3 才能解码,所以解码器必须先拿到 P3,再回头解 B2。实际解码顺序是:I1, P3, B2, P5, B4
看到了吗?解码顺序和显示顺序完全不一样。这就是为什么我们需要两个时间戳。
核心结论:
- DTS(Decoding Time Stamp):告诉解码器「什么时候开始解这一帧」
- PTS(Presentation Time Stamp):告诉显示器「什么时候显示这一帧」
对于没有 B 帧的编码(比如大部分直播场景),PTS 和 DTS 是相等的。但只要有 B 帧,两者就必然不同。
2.2 时间戳的「时间基」
聊时间戳,绕不开一个概念——时间基(Time Base)。
我记得刚入行时,看到 FFmpeg 里一堆 AVRational 结构体,完全懵了。后来才明白,时间基其实就是「刻度」的精度。
比如:
- 时间基是 1/1000,意味着每个刻度代表 1 毫秒
- 时间基是 1/90000,每个刻度代表约 11 微秒(MPEG-TS 标准)
时间戳的值乘以时间基,才是真正的秒数。
// FFmpeg 中时间基的定义
typedef struct AVRational {
int num; // 分子
int den; // 分母
} AVRational;
// 示例:时间基为 1/1000
AVRational time_base = {1, 1000};
// 时间戳值 5000,对应实际时间
double seconds = 5000 * (1.0 / 1000); // = 5.0 秒
实战经验:我在做播放器时,遇到过时间戳溢出问题。有些视频的时间基是 1/90000,时间戳值很大,如果用 32 位整数存,超过 2^31 就会溢出。后来统一改用 64 位整数,再也没出过问题。
2.3 PTS 和 DTS 在容器中的存储
不同的封装格式,存储时间戳的方式也不同。我整理了一个表格:
| 封装格式 | 时间基 | 存储方式 | 常见问题 |
|---|---|---|---|
| MP4 | 1/90000 或自定义 | 每个 sample 有 pts/dts,dts 可推导 | B 帧场景下 pts/dts 顺序可能颠倒 |
| TS | 1/90000(固定) | PES 包头携带 pts/dts 标志位 | 33 位时间戳,约 26.5 小时回绕 |
| FLV | 1/1000(毫秒) | 每个 tag 头部携带 24/32 位时间戳 | 超过 4.66 小时会溢出 |
| MKV | 1/(纳秒精度) | 每个 block 有相对时间戳 | 精度高但计算复杂 |
这里有个坑——我曾经在解析 FLV 时,发现播放到 5 小时左右画面突然卡住。查了半天,原来是 FLV 的时间戳只有 32 位,单位是毫秒,最大值约 4.66 小时。超过这个值,时间戳回绕到 0,播放器以为回到了起点。
避坑指南:处理 FLV 或 TS 流时,一定要做时间戳回绕检测。我一般用「连续差值法」:如果当前时间戳比上一帧小很多(比如差值超过 2^31),就认为是回绕,加上一个偏移量。
2.4 时间戳的同步机制
有了 PTS 和 DTS,播放器怎么用它们做同步?
流程大概是这样的:
- 解复用:从容器中读出每个 packet 的 pts 和 dts
- 解码:按 dts 顺序送入解码器,解码后得到原始帧
- 排序:解码后的帧按 pts 重新排序(因为 B 帧打乱了顺序)
- 渲染:根据系统时钟和 pts 的差值,决定立即显示还是等待
我习惯把这一步叫做「时钟对齐」。播放器内部维护一个参考时钟(通常用音频时钟),视频渲染线程不断比较当前帧的 pts 和参考时钟:
- 如果 pts 比时钟大:说明帧来早了,等待
- 如果 pts 比时钟小:说明帧来晚了,丢帧或加速播放
这里有个细节——音频的 pts 和 dts 通常是相等的,因为音频没有 B 帧的概念。所以很多播放器直接用音频时钟做主时钟,视频去对齐音频。
2.5 一个完整的 PTS/DTS 示例
咱们用 FFmpeg 命令行看看实际的时间戳:
# 查看视频文件的时间戳信息
ffprobe -show_frames input.mp4 | grep -E "pkt_pts|pkt_dts|pict_type" | head -20
# 输出示例(简化):
pkt_pts=0 pkt_dts=0 pict_type=I
pkt_pts=3003 pkt_dts=1001 pict_type=B
pkt_pts=1001 pkt_dts=2002 pict_type=P
pkt_pts=4004 pkt_dts=3003 pict_type=B
pkt_pts=2002 pkt_dts=4004 pict_type=P
看到没?pts 是 0, 3003, 1001, 4004, 2002(显示顺序),而 dts 是 0, 1001, 2002, 3003, 4004(解码顺序)。
如果时间基是 1/1000,那么:
- 第 0 秒显示 I 帧
- 第 1.001 秒显示 P 帧(但实际解码顺序中它是第 2 个被解码的)
- 第 3.003 秒显示 B 帧(它其实在第 1.001 秒就被解码了,但一直等到现在才显示)
这就是 B 帧的「延迟显示」特性。
2.6 时间戳的常见问题与排查
我在项目中遇到过不少时间戳相关的问题,列几个典型的:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 画面卡顿、音画不同步 | pts 不连续或跳变 | 用 ffprobe 检查 pts 差值是否稳定 |
| 播放器崩溃 | dts 大于 pts(理论上不可能) | 检查编码器是否输出异常 |
| 快进后声音正常画面不动 | 关键帧(I 帧)的 pts 未对齐 | 检查 seek 后是否从 I 帧开始解码 |
| 直播流延迟越来越大 | 时间戳未从 0 开始或存在漂移 | 用 wireshark 抓包分析 rtp 时间戳 |
我的排查习惯:遇到音画不同步,第一步不是看代码,而是用 ffprobe 把时间戳 dump 出来,画个折线图。pts 应该是严格递增的,如果出现平台期或跳变,问题就找到了。
2.7 一张图看懂 PTS 与 DTS
下面这张 SVG 图,把整个流程串起来了:
这张图从左到右展示了完整流程。注意看视频路径上的「排序缓冲区」——这是 B 帧场景下必不可少的环节。没有它,画面顺序就是乱的。
2.8 小结
这一章我们聊了:
- PTS 和 DTS 为什么是两个不同的东西
- 时间基的概念和常见取值
- 不同封装格式下时间戳的存储差异
- 播放器如何用时间戳做同步
- 实际排查问题的方法
时间戳是音画同步的基石。搞懂了 PTS 和 DTS,后面讲同步策略、丢帧补偿、时钟漂移处理,你就能很快上手。