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 时间基与时间戳的关系图

下面这张图展示了时间基、时间戳和实际时间之间的转换关系:

时间基与时间戳转换关系图 原始时间戳 (PTS / DTS) 时间基 A (如 {1, 90000}) av_rescale_q() 分数乘法转换 AV_TIME_BASE (微秒单位) 实际时间(秒) 时间戳 × 时间基 转换步骤说明: 1. 获取原始时间戳(PTS/DTS)及其对应的时间基 2. 调用 av_rescale_q() 将时间戳转为 AV_TIME_BASE 单位 3. 除以 AV_TIME_BASE(1000000)得到实际秒数 4. 跨流比较时,统一到 AV_TIME_BASE 单位后再做差值计算

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 帧是它们不同的根本原因