2、时间戳的奥秘:PTS与DTS,音视频时间基的换算,时间戳漂移问题
做音视频同步,说白了就是跟时间戳打交道。我见过太多人,代码写得飞起,结果画面卡成幻灯片,声音却跑得飞快——十有八九是时间戳没搞明白。这一章,咱们就把时间戳的底裤扒干净。
2.1 PTS 与 DTS:一对孪生兄弟
先问个问题:你看到的视频帧,真的是按编码顺序播放的吗?
不是。编码器为了压缩效率,会打乱帧的顺序。这就引出了两个关键概念:
- PTS(Presentation Time Stamp):显示时间戳。告诉解码器「这帧该什么时候显示」。你眼睛看到的就是它。
- DTS(Decoding Time Stamp):解码时间戳。告诉解码器「这帧该什么时候解码」。解码器按这个顺序干活。
为什么要有两套?因为视频编码里有三种帧:
| 帧类型 | 全称 | 特点 |
|---|---|---|
| I 帧 | 关键帧 | 完整图像,不依赖其他帧。解码起点。 |
| P 帧 | 前向预测帧 | 依赖前面的 I 帧或 P 帧。 |
| B 帧 | 双向预测帧 | 依赖前后两个方向的帧。压缩率最高,但会打乱顺序。 |
B 帧的存在,让 DTS 和 PTS 不再相等。举个例子:
编码顺序(DTS): I1 P2 B3 P4 B5
显示顺序(PTS): I1 B3 P2 B5 P4
你看,B3 在编码时排在 P2 后面,但显示时却要插到 P2 前面。解码器必须先把 P2 解出来,才能解 B3。所以 DTS 和 PTS 必须分开记录。
核心原则:解码器按 DTS 干活,渲染器按 PTS 干活。两者缺一不可。
2.2 时间基的换算:别被单位坑了
时间戳本身只是一个整数,比如 3600、7200。它到底代表多少秒?这取决于时间基(time_base)。
时间基是一个分数,比如 1/90000、1/1000、1/30。换算公式很简单:
实际时间(秒) = 时间戳 × 时间基
举个例子:
- 时间戳 = 3600,时间基 = 1/90000 → 实际时间 = 3600 × (1/90000) = 0.04 秒
- 时间戳 = 3600,时间基 = 1/1000 → 实际时间 = 3600 × (1/1000) = 3.6 秒
同一个数字,时间基不同,意义天差地别。我在项目中遇到过好几次,音频流和视频流的时间基不一样,直接相加比较,结果同步全乱套。
常见的几种时间基:
| 场景 | 时间基 | 说明 |
|---|---|---|
| MPEG-TS 流 | 1/90000 | 标准 PCR 时钟,精度高 |
| FLV/MP4 | 1/1000 | 毫秒级,简单粗暴 |
| 原始帧率 | 1/30、1/60 | 按帧数算,容易丢精度 |
我的习惯:在代码里统一将所有时间戳换算成微秒(1/1000000 秒)再比较。这样精度够,而且不容易出错。
换算代码示例(C 风格伪代码):
// 将时间戳从源时间基转换到目标时间基
int64_t rescale_timestamp(int64_t ts, AVRational src_base, AVRational dst_base) {
// 公式:ts * (src_base / dst_base)
return av_rescale_q(ts, src_base, dst_base);
}
// 使用示例:将 1/90000 时间基的 3600 转为 1/1000
int64_t ts_90000 = 3600;
AVRational src = {1, 90000};
AVRational dst = {1, 1000};
int64_t ts_ms = rescale_timestamp(ts_90000, src, dst);
// 结果:ts_ms = 40(毫秒)
2.3 时间戳漂移:无声的杀手
时间戳漂移,指的是音视频时间戳在长时间推流后,逐渐偏离真实时间。这是推流卡顿最常见的元凶之一。
为什么会漂移?
- 编码器时钟不准:编码器自己的时钟和系统时钟有偏差。比如编码器认为 1 秒过去了,实际系统只过了 0.999 秒。
- 网络抖动:RTP 包到达时间不均匀,接收端按到达时间推算时间戳,越推越偏。
- 整数溢出:32 位时间戳最大只能表示约 13 小时(以 90kHz 时钟算)。超过后回绕到 0,不做处理的话同步直接崩。
我曾经踩过的坑:有一次做 7x24 小时直播监控,跑到第 13 个小时画面突然卡死。查了半天,发现是时间戳溢出后没有做回绕补偿。从那以后,我所有时间戳处理都强制用 64 位整数。
如何检测漂移?
// 伪代码:检测时间戳漂移
int64_t last_pts = 0;
int64_t last_real_time = 0;
void on_frame_received(int64_t pts, int64_t real_time_us) {
int64_t pts_diff = pts - last_pts;
int64_t real_diff = real_time_us - last_real_time;
// 如果 PTS 增量与实际时间增量偏差超过 10%,报警
if (abs(pts_diff - real_diff) > real_diff * 0.1) {
printf("警告:时间戳漂移!PTS增量=%lld, 实际增量=%lld\n", pts_diff, real_diff);
}
last_pts = pts;
last_real_time = real_time_us;
}
2.4 核心逻辑:一张图看懂
下面这张图,把 PTS、DTS、时间基、漂移的关系串起来了。你想想看,整个音视频同步的骨架,其实就这么点东西。
2.5 避坑指南:实战中的时间戳陷阱
- 不要直接比较不同时间基的时间戳。先统一换算,再比较。我见过有人直接把 90kHz 的 PTS 和毫秒级的 PTS 做差,结果差了 90 倍。
- 注意 B 帧的 PTS 回退。B 帧的 PTS 可能比前一个 P 帧小,这是正常的。但如果你用 PTS 做 buffer 排序,必须按 DTS 排。
- 处理时间戳回绕。32 位时间戳溢出后,要检测回绕并加上偏移。64 位基本不用担心,但也要留个心眼。
- 音频时间戳要连续。音频帧的 PTS 应该是严格递增的,间隔等于 frame_size / sample_rate。如果发现音频 PTS 跳变,大概率是编码器抽风了。
我的经验:在调试阶段,把每个帧的 PTS、DTS、时间基都打印出来。肉眼扫一遍,很多问题当场就能发现。别嫌日志多,出问题的时候它就是救命稻草。
嗯,时间戳这块就讲这么多。说白了,PTS 和 DTS 是音视频同步的基石,时间基换算是基本功,漂移检测是保命技。这三样拿下了,后面讲同步策略的时候你才能游刃有余。