1. 音画同步基础:时间基与时间戳概念解析
音画同步,说白了就是让视频帧和音频样本在播放时「对齐」。你想想看,如果画面里人物张嘴说话,声音却慢了半拍,观众立马就能察觉。我在早期做播放器项目时,就吃过这个亏——画面流畅,声音也正常,但就是感觉「嘴型对不上」。后来才明白,根本原因是对时间基和时间戳的理解不够深。
这一章,我们就从最基础的两个概念入手:时间基和时间戳。它们是整个音画同步体系的基石,FFmpeg 里几乎所有的时间计算都绕不开它们。
1.1 时间基:多媒体世界的「刻度尺」
时间基(Time Base),你可以把它理解成一把尺子。这把尺子的最小刻度是多少,决定了你能精确到多小的时间单位。
在 FFmpeg 中,时间基用 AVRational 结构体表示:
typedef struct AVRational {
int num; // 分子
int den; // 分母
} AVRational;
这个结构体就是一个分数。比如 {1, 25} 表示每 1/25 秒一个刻度,{1, 90000} 表示每 1/90000 秒一个刻度。刻度越细,时间精度越高。
核心公式:
实际时间(秒) = 时间戳 × 时间基 = 时间戳 × (num / den)
举个例子:
- 时间基
{1, 25},时间戳 100 → 实际时间 = 100 × (1/25) = 4 秒 - 时间基
{1, 90000},时间戳 360000 → 实际时间 = 360000 × (1/90000) = 4 秒
你看,同样的 4 秒,用不同的时间基,时间戳数值完全不同。这就是为什么跨流比较时间时,必须先统一时间基。
我的习惯:在分析 FFmpeg 源码时,我通常会先把所有时间戳统一转成微秒(AV_TIME_BASE 对应的单位),这样比较起来最直观,不容易出错。
1.2 AV_TIME_BASE:FFmpeg 的「标准尺」
AV_TIME_BASE 是 FFmpeg 内部定义的一个常量,值为 1000000。它对应的分数是 {1, 1000000},也就是微秒级精度。
为什么是 1000000?因为微秒足够精确,又能用整数表示大部分常见的帧率(比如 24fps、25fps、30fps 的整数倍时间)。
FFmpeg 还提供了两个辅助函数:
// 将时间戳从任意时间基转为 AV_TIME_BASE 单位
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq);
// 将时间戳从 AV_TIME_BASE 单位转为任意时间基
int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq, enum AVRounding rnd);
这两个函数内部做了分数乘法,避免了浮点运算的精度损失。我个人建议,所有跨流的时间比较,都先用 av_rescale_q 转成 AV_TIME_BASE 单位再做。
我曾经踩过的坑:直接拿两个不同时间基的时间戳做减法,结果算出来的「差值」完全不对。后来排查了半天,才发现是时间基没统一。从那以后,我写代码时都会先检查时间基是否一致,不一致就先用 av_rescale_q 转换。
1.3 PTS 与 DTS:解码顺序 vs 显示顺序
PTS(Presentation Time Stamp)和 DTS(Decoding Time Stamp)是多媒体容器中最核心的两个时间戳字段。
| 属性 | PTS | DTS |
|---|---|---|
| 全称 | Presentation Time Stamp | Decoding Time Stamp |
| 含义 | 该帧应该在什么时间显示 | 该帧应该在什么时间解码 |
| 顺序 | 显示顺序 | 解码顺序 |
| 典型场景 | B 帧的 PTS 可能小于其 DTS | I 帧和 P 帧的 DTS 通常小于 PTS |
为什么会有两个时间戳?因为视频编码时引入了 B 帧(双向预测帧)。B 帧需要依赖前后的帧才能解码,所以解码顺序和显示顺序不一致。
举个例子,一个典型的 GOP(Group of Pictures)序列:
显示顺序:I1 B2 B3 P4 B5 B6 P7
解码顺序:I1 P4 B2 B3 P7 B5 B6
你看,B2 和 B3 虽然在显示时排在 P4 前面,但解码时必须等 P4 先解码完。所以它们的 DTS 比 PTS 大。
关键点:
- 对于音频流,通常 PTS == DTS,因为音频帧之间没有依赖关系
- 对于视频流,如果编码器不使用 B 帧(比如某些低延迟场景),PTS 也等于 DTS
- 只有包含 B 帧的视频流,PTS 和 DTS 才会不同
1.4 时间基与时间戳的关系图
下面这张图展示了时间基、时间戳和实际时间之间的转换关系:
1.5 时间基在 FFmpeg 源码中的体现
在 FFmpeg 的 AVStream 结构体中,有两个关键字段:
typedef struct AVStream {
// ...
AVRational time_base; // 该流的时间基
int64_t pts; // 当前帧的 PTS(以 time_base 为单位)
int64_t dts; // 当前帧的 DTS(以 time_base 为单位)
// ...
AVRational sample_aspect_ratio; // 像素宽高比
// ...
} AVStream;
注意,pts 和 dts 的值是相对于 time_base 的。如果你直接拿两个不同流的 pts 做比较,结果毫无意义。
FFmpeg 内部在做音画同步时,会调用 av_rescale_q 将视频流和音频流的时间戳统一到同一个时间基上。这个统一后的时间基,通常就是 AV_TIME_BASE。
调试技巧:当你用 ffprobe 查看媒体文件时,输出的 time_base 就是每个流自己的时间基。比如视频流可能是 1/90000,音频流可能是 1/48000。看到这些数字时,你就知道它们各自代表什么精度了。
1.6 常见时间基对照表
| 时间基 | 精度 | 常见场景 |
|---|---|---|
{1, 1000000} |
1 微秒 | FFmpeg 内部标准(AV_TIME_BASE) |
{1, 90000} |
约 11.1 微秒 | MPEG-TS 视频流 |
{1, 48000} |
约 20.8 微秒 | 48kHz 音频流 |
{1, 44100} |
约 22.7 微秒 | 44.1kHz 音频流(CD 音质) |
{1, 25} |
40 毫秒 | 25fps 视频流(PAL 制式) |
{1, 30} |
约 33.3 毫秒 | 30fps 视频流(NTSC 制式) |
从这张表可以看出,视频流的时间基通常比较粗(毫秒级),而音频流的时间基比较细(微秒级)。这也是为什么音画同步时,通常以音频时间戳为基准——音频的精度更高,能提供更细腻的同步控制。
注意:不要以为 time_base 越小越好。精度越高,时间戳数值就越大,计算时越容易溢出。FFmpeg 内部使用 int64_t 来存储时间戳,最大能表示约 9.2 × 10^18 个刻度。以 AV_TIME_BASE 为例,能表示的最大时长约为 9.2 × 10^12 秒,约合 29 万年——嗯,足够用了。
1.7 小结
这一章我们聊了三个核心概念:
- 时间基:多媒体世界的刻度尺,用
AVRational分数表示 - AV_TIME_BASE:FFmpeg 内部的标准时间基,值为 1000000(微秒)
- PTS 与 DTS:分别表示显示顺序和解码顺序,B 帧是它们不同的根本原因