2、时间戳基础:PTS、DTS 与 time_base
做音画同步,说白了就是跟时间戳打交道。我刚开始接触多媒体开发时,总觉得时间戳不就是个数字嘛,有什么好研究的?结果第一次做播放器,音频视频各跑各的,画面都播到一半了,声音还没开始。嗯,从那以后我才明白——时间戳才是音画同步的命根子。
这一节我们就把 PTS、DTS 和时间基这三个概念彻底讲清楚。它们不复杂,但容易混淆。尤其是 DTS 和 PTS,很多人一开始都搞反过。
2.1 什么是 PTS(显示时间戳)
PTS 全称 Presentation Time Stamp,中文叫“显示时间戳”。它表示一帧数据应该被显示出来的时刻。
举个例子:你有一个视频文件,帧率是 30fps。理论上每帧应该间隔 33ms 显示。但实际编码时,每一帧的 PTS 值就是告诉播放器:“嘿,我这一帧应该在时间轴上的这个点出现。”
核心理解:PTS 是给显示设备看的。播放器拿到一帧数据后,会检查它的 PTS,然后决定什么时候把它送到屏幕上。
我在项目中遇到过一个问题:某个视频源的 PTS 不是从 0 开始的,而是从 1000 开始的。播放器没处理好,结果前 1000 个单位时间里画面一直是黑的。后来我加了个 PTS 归一化处理,才解决。
2.2 什么是 DTS(解码时间戳)
DTS 全称 Decoding Time Stamp,中文叫“解码时间戳”。它表示一帧数据应该被解码器处理的时刻。
你可能会问:解码和显示不是一回事吗?
还真不是。这就要说到视频编码里的一个关键概念——B 帧。
视频编码时,为了压缩效率,会用到三种帧类型:
- I 帧:关键帧,可以独立解码
- P 帧:向前参考帧,依赖前面的 I 帧或 P 帧
- B 帧:双向参考帧,依赖前后两个方向的帧
B 帧的问题在于:它需要参考后面的帧才能解码。所以解码顺序和显示顺序是不一致的。
| 帧序号 | 帧类型 | DTS(解码顺序) | PTS(显示顺序) |
|---|---|---|---|
| 1 | I 帧 | 0 | 0 |
| 2 | P 帧 | 1 | 2 |
| 3 | B 帧 | 2 | 1 |
| 4 | B 帧 | 3 | 3 |
看到没?B 帧的 DTS 是 2,但 PTS 是 1。也就是说,解码器先解码第 3 帧,但显示的时候要等到第 1 帧之后才显示它。
注意:不是所有编码格式都有 B 帧。比如 H.264 的 Baseline Profile 就不支持 B 帧。这种情况下 DTS 和 PTS 是相等的。但一旦遇到 B 帧,DTS 和 PTS 就会错开。
2.3 什么是时间基(time_base)
时间基,英文叫 time_base,是 FFmpeg 里最基础也最容易忽略的概念。
说白了,时间基就是一个刻度单位。它告诉系统:时间戳里的 1 个单位,到底对应多少秒。
比如:
- time_base = 1/1000,表示每个单位是 1 毫秒
- time_base = 1/90000,表示每个单位是 1/90000 秒(约 11.1 微秒)
- time_base = 1/25,表示每个单位是 0.04 秒(常用于 25fps 视频)
我个人习惯用 AVRational 来表示时间基。FFmpeg 里是这样定义的:
typedef struct AVRational {
int num; // 分子
int den; // 分母
} AVRational;
所以 time_base = 1/1000 就是 {num=1, den=1000}。
为什么要用分数而不是浮点数?因为分数可以精确表示很多有理数。比如 1/25 用浮点数就是 0.040000...,但分数就是精确的 1/25。
2.4 PTS、DTS 与 time_base 的关系
这三者的关系其实很简单:
实际时间(秒) = 时间戳值 × time_base
举个例子:
- PTS = 3600
- time_base = 1/90000
- 实际显示时间 = 3600 × (1/90000) = 0.04 秒 = 40 毫秒
所以 PTS 和 DTS 本身只是整数,真正有意义的是它们乘以 time_base 之后得到的实际时间。
经验之谈:做音画同步时,我建议把所有时间戳都统一换算成微秒(microseconds)或毫秒(milliseconds)来处理。因为不同流的时间基可能不一样,直接比较 PTS 值会出问题。统一换算后再比较,就安全多了。
2.5 知识体系图
下面这张图把 PTS、DTS 和 time_base 的关系画清楚了:
2.6 常见问题与避坑
做时间戳处理时,有几个坑我踩过,分享给你:
- 时间基不一致:音频流和视频流的时间基可能不同。比如视频用 1/90000,音频用 1/48000。直接比较 PTS 值会出错。必须先统一换算。
- PTS 回绕:时间戳是整数,总有上限。当 PTS 达到最大值后会回绕到 0。我曾经在一个长时间录制的项目中遇到过这个问题,画面突然跳回开头。解决方案是用相对时间戳,或者用 64 位整数。
- B 帧导致的 DTS/PTS 错位:解码器输出帧的顺序是按 DTS 来的,但显示必须按 PTS。如果你直接把解码后的帧送去显示,画面顺序就乱了。必须做重排序。
曾经踩过的坑:有一次我写播放器,没处理 B 帧的重排序。结果画面里的人物动作是倒着走的——先张嘴再闭嘴,先抬手再放下。排查了半天才发现是 DTS 和 PTS 搞混了。从那以后,我每次处理视频帧都会先检查有没有 B 帧。
2.7 代码示例:时间戳换算
下面是一个简单的 C 语言示例,演示如何把 PTS 换算成实际时间:
#include <stdio.h>
#include <stdint.h>
typedef struct {
int num;
int den;
} AVRational;
// 将时间戳换算为微秒
int64_t pts_to_us(int64_t pts, AVRational time_base) {
// pts * num / den,先乘后除避免精度损失
return pts * time_base.num * 1000000 / time_base.den;
}
int main() {
// 视频流:time_base = 1/90000
AVRational video_tb = {1, 90000};
int64_t video_pts = 3600;
// 音频流:time_base = 1/48000
AVRational audio_tb = {1, 48000};
int64_t audio_pts = 1920;
int64_t video_us = pts_to_us(video_pts, video_tb);
int64_t audio_us = pts_to_us(audio_pts, audio_tb);
printf("视频 PTS 实际时间:%ld 微秒\n", video_us);
printf("音频 PTS 实际时间:%ld 微秒\n", audio_us);
printf("时间差:%ld 微秒\n", audio_us - video_us);
return 0;
}
输出结果:
视频 PTS 实际时间:40000 微秒
音频 PTS 实际时间:40000 微秒
时间差:0 微秒
你看,虽然两个流的 PTS 值不同(3600 vs 1920),但换算成实际时间后都是 40 毫秒。这就是 time_base 的作用——它让不同时间基的流可以统一比较。
2.8 小结
这一节我们讲了三个核心概念:
- PTS:告诉播放器什么时候显示
- DTS:告诉解码器什么时候解码
- time_base:告诉系统时间戳的单位是什么
记住一句话:PTS 和 DTS 是整数,time_base 是刻度,实际时间 = 整数 × 刻度。搞懂这个,后面做音画同步就顺了。