2. FFmpeg 时间基体系:AVRational 与 av_rescale_q 详解

做音视频同步,说白了就是跟时间打交道。而 FFmpeg 里所有的时间,都绕不开一个东西——时间基。

我刚开始用 FFmpeg 做开发时,就被这个时间基搞晕过。明明 pts 是 90000,为什么实际播放时间才 1 秒?后来才明白,90000 这个数字本身没有意义,得配上时间基才能算出真实时间。

这一章,我们就彻底把时间基讲透。

2.1 AVRational:分数表示法

先看一个结构体:

typedef struct AVRational {
    int num;   // 分子
    int den;   // 分母
} AVRational;

嗯,就这么简单。一个分数。

在 FFmpeg 里,时间基就是用这个分数表示的。比如:

  • {1, 25} —— 每秒 25 帧,常用于 PAL 制式视频
  • {1, 30000} —— 每秒 30000 帧,NTSC 制式(实际是 29.97 fps)
  • {1, 90000} —— 每秒 90000 帧,MPEG-TS 标准时间基
  • {1, 1000000} —— 微秒级精度,常用于音频

你可能会问:为什么不用 double 或 float?

我个人的理解是:精度和确定性。浮点数在反复运算中会累积误差,而分数是精确的。1/25 就是 0.04,不会变成 0.04000000000000001。在音视频同步这种对精度敏感的场景里,差一个采样点都可能造成可感知的卡顿。

核心概念:时间基 = 每个刻度(tick)代表的秒数。

公式:实际时间(秒) = pts × time_base.num / time_base.den

举个例子:pts = 3600,time_base = {1, 90000},实际时间 = 3600 × 1 / 90000 = 0.04 秒 = 40 毫秒。

2.2 常见时间基常量

FFmpeg 预定义了几个常用时间基,直接拿来用就行:

宏定义 AVRational 值 适用场景
AV_TIME_BASE_Q {1, 1000000} FFmpeg 内部微秒时间基
AV_TIME_BASE 1000000(整数) 同上,但以整数形式使用
av_make_q(num, den) 自定义 快速构造 AVRational

我记得刚接触时,总把 AV_TIME_BASE_Q 和 AV_TIME_BASE 搞混。一个是结构体,一个是整数。用错的话编译器会报类型不匹配,但报错信息不太友好,排查起来挺费时间。

小技巧:需要构造时间基时,优先用 av_make_q(num, den),比手写结构体更安全,也更容易读。

2.3 av_rescale_q:时间基转换的核心函数

做音视频同步,最常干的事就是在不同时间基之间转换。

比如:

  • 从解码器输出的帧时间基(可能是 {1, 90000})
  • 转换到输出文件的时间基(可能是 {1, 1000000})
  • 或者转换到系统时钟进行比较

这时候就需要 av_rescale_q 出场了。

int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq);

参数说明:

  • a —— 待转换的时间戳(pts/dts)
  • bq —— 当前时间基
  • cq —— 目标时间基

返回值:转换后的时间戳。

数学本质就是:a × bq.num / bq.den × cq.den / cq.num。但 FFmpeg 内部做了溢出保护和精度优化,比自己手算靠谱得多。

注意:av_rescale_q 内部使用 64 位整数运算。如果时间戳极大(比如几十小时的 pts),仍可能溢出。FFmpeg 提供了 av_rescale_q_rnd 版本,可以指定舍入模式,精度更高。

2.4 实战:音频 pts 转换

假设我们从音频解码器拿到一个 pts,时间基是 {1, 48000}(48kHz 采样率),pts = 96000。现在要转成微秒时间基 {1, 1000000}:

AVRational src_tb = {1, 48000};
AVRational dst_tb = {1, 1000000};

int64_t src_pts = 96000;
int64_t dst_pts = av_rescale_q(src_pts, src_tb, dst_tb);

// dst_pts = 96000 * 1/48000 * 1000000/1 = 2000000 微秒 = 2 秒

我在项目中遇到过一个问题:音频 pts 转换后,跟视频 pts 做比较时总是差几十毫秒。排查了半天,发现是视频流的时间基写错了——用了 {1, 25} 而不是 {1, 24000}(实际帧率是 23.976)。

所以,一定要确认你拿到的 time_base 是正确的。建议从 AVStream->time_base 读取,不要自己硬编码。

2.5 时间基转换的完整流程

下面这张图展示了音视频同步中时间基转换的典型路径:

音视频时间基转换流程 音频流 time_base {1, 48000} 视频流 time_base {1, 90000} av_rescale_q 统一时间基 {1, 1000000} pts 比较 / 差值计算 关键点:所有流的时间戳必须转换到同一个时间基才能比较 推荐使用微秒时间基 {1, 1000000} 作为统一基准

2.6 避坑指南

这些年踩过的坑,分享几个给你:

  1. 时间基搞反了 —— av_rescale_q 的第二个参数是当前时间基,第三个是目标时间基。我曾经把顺序写反,结果 pts 被放大了 90000 倍,音频直接飞了。
  2. 忽略舍入误差 —— 默认是向下取整。如果做音视频同步,建议用 av_rescale_q_rnd 配合 AV_ROUND_NEAR_INF 或 AV_ROUND_PASS_MINMAX。
  3. pts 为 AV_NOPTS_VALUE —— 转换前一定要判断。我见过有人直接拿 AV_NOPTS_VALUE 去转换,结果得到一个巨大的负数,导致同步逻辑崩溃。
  4. 流的时间基可能变化 —— 某些编码器在关键帧前后会切换时间基。建议每次从 packet 或 frame 里重新读取 time_base,不要缓存。

推荐做法:

  • 所有内部同步逻辑统一使用微秒时间基
  • 只在输入/输出边界做时间基转换
  • 转换结果用 int64_t 存储,避免精度丢失

2.7 其他相关函数

除了 av_rescale_q,FFmpeg 还提供了几个变体:

函数 说明 适用场景
av_rescale_q_rnd 可指定舍入模式 需要精确控制舍入方向时
av_rescale 整数比例缩放,不带时间基 简单数值缩放
av_compare_ts 比较两个不同时间基的 pts 直接比较音视频 pts 大小
av_q2d AVRational 转 double 调试打印、日志输出

我个人最常用的是 av_compare_ts。它内部帮你做了时间基转换和比较,省去手动 rescale 的步骤。代码更干净,也不容易出错。

// 比较音频 pts 和视频 pts,看谁更早
AVRational audio_tb = {1, 48000};
AVRational video_tb = {1, 90000};

int64_t audio_pts = 96000;   // 2 秒
int64_t video_pts = 180000;  // 2 秒

int result = av_compare_ts(audio_pts, audio_tb, video_pts, video_tb);
// result < 0 表示音频更早,= 0 表示相等,> 0 表示视频更早