1、时间戳基础:PTS/DTS/CTS 与 Timebase
做流媒体的人,早晚都会跟时间戳打交道。我入行那会儿,第一次接触 HLS 切片,发现视频画面总是卡顿、音画不同步,查了半天才发现是时间戳单位搞错了。从那以后,我就养成了一个习惯——拿到一个流,先看时间基,再看 PTS。
这一章,咱们就把时间戳最核心的几个概念讲清楚。说白了,就是搞明白三个问题:
- 时间戳到底存的是什么?
- 为什么要有 PTS、DTS、CTS 三种?
- 不同封装格式的时间单位怎么换算?
1.1 什么是 PTS、DTS、CTS?
这三个缩写,你肯定见过。但很多人分不清它们的区别。我简单解释一下。
PTS(Presentation Time Stamp)——显示时间戳。它告诉解码器:这一帧画面应该在什么时候显示给用户看。比如一个 25fps 的视频,每帧间隔 40ms,PTS 就是按这个节奏递增的。
DTS(Decoding Time Stamp)——解码时间戳。它告诉解码器:这一帧数据应该在什么时候开始解码。注意,解码和显示不是一回事。尤其是 B 帧存在的时候,解码顺序和显示顺序是错开的。
CTS(Composition Time Stamp)——合成时间戳。这个在 MPEG-2 时代用得比较多,现在很多文档里直接把它等同于 PTS。严格来说,CTS 指的是帧在合成缓冲区里完成组合的时间点。不过在实际工程中,你看到 CTS 的地方,基本可以当作 PTS 来处理。
核心要点:PTS 决定“什么时候给人看”,DTS 决定“什么时候开始解”。两者不一定相等。
为什么会不一样?因为视频编码用了 B 帧(双向预测帧)。B 帧需要依赖前后的 I 帧和 P 帧才能解码,所以解码顺序必须先把参考帧解出来,然后才能解 B 帧。但显示的时候,B 帧要插在参考帧之间。这就导致了 DTS 和 PTS 的偏差。
举个例子:一个 GOP 的帧顺序是 I B B P,显示顺序是 1 2 3 4,但解码顺序是 1 3 4 2。你看,第 2 帧(B 帧)的解码时间比显示时间晚,但显示时间却更早。这就是 DTS 和 PTS 分离的原因。
| 概念 | 全称 | 作用 | 常见场景 |
|---|---|---|---|
| PTS | Presentation Time Stamp | 控制帧的显示时刻 | 音画同步、渲染调度 |
| DTS | Decoding Time Stamp | 控制帧的解码时刻 | 解码器输入队列排序 |
| CTS | Composition Time Stamp | 控制帧的合成时刻 | MPEG-2 TS 流、部分老旧封装 |
个人经验:我在做 RTMP 推流时,遇到过编码器输出的 DTS 和 PTS 顺序完全一致的情况。后来发现是编码器配置里把 B 帧关掉了。如果你发现 DTS 和 PTS 始终相等,先检查一下编码参数,大概率是 B 帧数为 0。
1.2 时间基(Timebase)概念
时间基,英文叫 Timebase,也有人叫它时间刻度。它本质上是一个分数,表示“每个时间单位对应多少秒”。
比如 timebase = 1/1000,意思就是每个 tick 代表 1 毫秒。timebase = 1/90000,每个 tick 代表 1/90000 秒,约 11.11 微秒。
为什么会有这么多不同的 timebase?因为不同的封装格式和标准,对时间精度的要求不一样。
- MPEG-TS:固定使用 90kHz 时钟,timebase = 1/90000。这是 MPEG-2 标准定的,目的是为了兼容 PAL/NTSC 的帧率。
- FLV/RTMP:使用毫秒级时间戳,timebase = 1/1000。简单粗暴,但精度有限。
- MP4:timebase 由
mdhdbox 中的timescale字段决定,常见值有 1000、90000、48000 等。 - WebM/Matroska:使用纳秒级时间基,timebase = 1/1000000000。精度最高,但数值也最大。
| 封装格式 | 常见 Timebase | 精度 | 典型应用 |
|---|---|---|---|
| MPEG-TS | 1/90000 | ~11.11 µs | 广播电视、HLS |
| FLV/RTMP | 1/1000 | 1 ms | 直播推流 |
| MP4 | 可变(常见 1000/90000) | 取决于 timescale | 点播文件 |
| WebM | 1/1000000000 | 1 ns | Web 视频 |
注意:不同封装之间做时间戳转换时,最容易出问题。我曾经遇到过 MP4 转 TS 时,直接把 MP4 的毫秒时间戳当成 90kHz 时间戳用,结果画面每隔几秒就跳一下。原因就是 timebase 没换算,时间戳数值直接膨胀了 90 倍。
1.3 时间戳单位与换算
时间戳的换算,本质上就是一次比例缩放。公式很简单:
目标时间戳 = 源时间戳 × (源 timebase / 目标 timebase)
或者反过来:
目标时间戳 = 源时间戳 × (目标 timebase / 源 timebase)
我习惯用第一种写法,因为更直观。举个例子:
有一个 MP4 文件,timebase = 1/1000,PTS = 45000。你想把它转成 MPEG-TS(timebase = 1/90000),那么:
TS_PTS = 45000 × (1/1000) / (1/90000)
= 45000 × (90000 / 1000)
= 45000 × 90
= 4050000
你看,数值从 45000 变成了 4050000,大了 90 倍。但实际时间没变:
实际时间 = 45000 × (1/1000) = 45 秒
实际时间 = 4050000 × (1/90000) = 45 秒
所以,时间戳的绝对值不重要,重要的是它和 timebase 的乘积——那个才是真实的时间。
换算口诀:时间戳 × timebase = 真实秒数。只要保证这个等式成立,怎么换算都不会错。
在实际代码中,FFmpeg 提供了 av_rescale_q 和 av_rescale_q_rnd 两个函数来做这件事。我建议你直接用它们,别自己手算。手算容易丢精度,尤其是做 RTC 场景时,微秒级的误差都会导致唇音不同步。
// FFmpeg 时间戳换算示例
int64_t pts_ts = av_rescale_q(
pts_mp4,
(AVRational){1, 1000}, // 源 timebase
(AVRational){1, 90000} // 目标 timebase
);
这里 AVRational 就是分数,分子是 1,分母是 timebase 的倒数。FFmpeg 内部会做整数运算,避免浮点误差。
1.4 知识体系总览
下面这张图,把这一章的核心逻辑串起来了。你可以把它当作时间戳的“地图”,以后遇到问题,先看这张图定位。
这张图从左到右,从上到下,把 PTS/DTS/CTS、Timebase、换算公式、常见封装串起来了。你以后做时间戳相关的工作,可以拿这张图当参考。
1.5 避坑指南
最后,分享几个我踩过的坑:
- 时间戳回绕:32 位时间戳最大只能表示约 49.7 天(以 90kHz 计)。直播流跑久了,时间戳会溢出。我曾经在马拉松直播中遇到过,画面突然跳到开头。解决方案是用 64 位时间戳,或者做回绕检测。
- 负时间戳:有些编码器在开头几帧会输出负的 PTS,表示解码延迟。FFmpeg 默认会处理,但如果你自己写 demuxer,记得做偏移校正。
- 时间戳抖动:网络传输或封装转换时,时间戳可能出现微小波动。我建议在解码器输入端做一个 2-3 帧的抖动缓冲区,能有效过滤掉毛刺。
我的习惯:每次拿到一个新的流,先用 ffprobe -show_packets 看一眼时间戳分布。如果发现 PTS 和 DTS 差值超过 1 秒,或者时间戳不是单调递增的,那多半有问题。早点发现,早点排查。