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 时间基转换的完整流程
下面这张图展示了音视频同步中时间基转换的典型路径:
2.6 避坑指南
这些年踩过的坑,分享几个给你:
- 时间基搞反了 —— av_rescale_q 的第二个参数是当前时间基,第三个是目标时间基。我曾经把顺序写反,结果 pts 被放大了 90000 倍,音频直接飞了。
- 忽略舍入误差 —— 默认是向下取整。如果做音视频同步,建议用
av_rescale_q_rnd配合AV_ROUND_NEAR_INF或AV_ROUND_PASS_MINMAX。 - pts 为 AV_NOPTS_VALUE —— 转换前一定要判断。我见过有人直接拿 AV_NOPTS_VALUE 去转换,结果得到一个巨大的负数,导致同步逻辑崩溃。
- 流的时间基可能变化 —— 某些编码器在关键帧前后会切换时间基。建议每次从 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 表示视频更早