2、FFmpeg时间基详解:AV_TIME_BASE、AVRational、时间基转换函数av_rescale_q
做音视频开发,绕不开时间基。说白了,时间基就是一把尺子,用来度量时间。FFmpeg里时间基搞不清楚,音画同步就是空中楼阁。我刚开始接触FFmpeg时,就被这些概念绕晕过——明明都是时间,为什么有的用毫秒,有的用微秒,还有的用分数?
这一章,咱们就把时间基彻底讲透。
2.1 为什么需要时间基?
先想一个问题:视频的PTS(显示时间戳)用什么单位?
用秒?太粗糙了。视频一帧可能只有33毫秒,用浮点数存,精度和性能都是问题。
用毫秒?勉强可以,但遇到高帧率(比如120fps),每帧8.33毫秒,精度又不够了。
用微秒?精度够了,但整数范围太大,计算效率低。
FFmpeg的解决方案很巧妙——用有理数表示时间基。你想想看,一个分数既能精确表示帧率(比如30000/1001),又能通过整数运算避免浮点误差,多优雅。
核心概念:时间基(Time Base)就是每个时间单位对应的秒数。比如时间基是1/25,那么时间戳每增加1,就代表过去了1/25秒(即40毫秒)。
2.2 AV_TIME_BASE:全局参考时间基
FFmpeg定义了一个全局常量:AV_TIME_BASE,值为1,000,000。什么意思?就是FFmpeg内部用微秒作为基准单位。
#define AV_TIME_BASE 1000000
所以,当你看到AV_TIME_BASE_Q时,它其实就是:
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
也就是时间基为1/1,000,000秒。
我在项目中遇到过一个问题:直接用AV_TIME_BASE做计算,结果在32位平台上溢出了。因为微秒级的时间戳,存几小时就超过32位整数的范围了。嗯,这里要注意,int64_t才是安全的。
避坑指南:我曾经在计算视频总时长时,直接用AV_TIME_BASE * duration,结果duration是double类型,乘完精度丢失,导致seek不准。后来改用av_rescale_q才解决。
2.3 AVRational:有理数时间基
AVRational是FFmpeg中最核心的时间类型。定义很简单:
typedef struct AVRational {
int num; // 分子
int den; // 分母
} AVRational;
它表示一个分数:num/den秒。比如:
{1, 25}→ 1/25秒,适合25fps视频{1, 90000}→ 1/90000秒,MPEG-TS标准时间基{1001, 30000}→ 1001/30000秒 ≈ 33.3667ms,NTSC 29.97fps
为什么MPEG-TS要用90000?因为MPEG系统时钟频率是27MHz,除以300得到90kHz。这个数字能整除大多数帧率,避免精度损失。
| 场景 | 时间基 | 说明 |
|---|---|---|
| 25fps视频 | {1, 25} | 每帧40ms |
| 29.97fps NTSC | {1001, 30000} | 精确表示丢帧 |
| MPEG-TS | {1, 90000} | 系统层标准 |
| FFmpeg内部 | {1, 1000000} | 微秒基准 |
| 音频44100Hz | {1, 44100} | 每个采样点 |
个人习惯:我写代码时,只要涉及时间戳转换,一定用AVRational显式声明,绝不写死数字。这样代码可读性高,别人一看就知道时间基是什么。
2.4 时间基转换函数:av_rescale_q
这才是重头戏。不同封装格式、不同编码标准,时间基都不一样。怎么转换?
av_rescale_q函数就是干这个的:
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq);
它计算的是:a * bq / cq,但内部做了防溢出处理。说白了,就是把时间戳从时间基bq转换到时间基cq。
举个例子:
// 假设视频帧率25fps,时间基{1,25}
// 第100帧的PTS,想转成微秒时间基
AVRational src_tb = {1, 25}; // 源时间基
AVRational dst_tb = {1, 1000000}; // 目标时间基(微秒)
int64_t pts_in_us = av_rescale_q(100, src_tb, dst_tb);
// 结果:100 * (1/25) / (1/1000000) = 100 * 1000000 / 25 = 4,000,000 微秒 = 4秒
你想想看,如果没有这个函数,你得自己写:pts * src_tb.num * dst_tb.den / (src_tb.den * dst_tb.num),不仅麻烦,还容易溢出。
2.5 配套函数家族
除了av_rescale_q,FFmpeg还提供了一组相关函数:
- av_rescale_q_rnd:带舍入模式的版本,可以控制向上取整、向下取整、就近取整
- av_rescale:整数缩放,不涉及时间基,单纯做
a * b / c的防溢出计算 - av_compare_ts:比较两个不同时间基的时间戳大小
我最常用的是av_compare_ts。为什么?因为音视频同步时,经常要比较音频PTS和视频PTS,它们的时间基可能完全不同。直接比较整数?那是找死。
// 比较音频PTS和视频PTS
AVRational audio_tb = {1, 44100};
AVRational video_tb = {1, 25};
int64_t audio_pts = 176400; // 4秒的音频采样点
int64_t video_pts = 100; // 4秒的视频帧
if (av_compare_ts(audio_pts, audio_tb, video_pts, video_tb) > 0) {
// 音频比视频晚
}
关键点:av_compare_ts返回的是差值,不是简单的-1/0/1。返回值是a * bq - b * cq的结果,正数表示a更大。
2.6 时间基转换的完整流程
在实际项目中,时间基转换通常发生在以下几个环节:
- 解封装时:从容器中读取的时间戳,使用容器的时间基
- 解码后:帧的时间戳需要转换到流的时间基
- 同步时:所有流的时间戳统一转换到参考时钟的时间基
- 渲染时:根据显示设备的时间基做最终转换
我见过不少新手,直接把PTS当微秒用,结果画面跳来跳去。其实只要记住一句话:永远不要假设时间戳的单位,永远用AVRational做显式转换。
曾经踩过的坑:有一次处理RTSP流,RTP包的时间戳是90000时间基,但解出来后的AVPacket时间基变成了{1,1000}。我没做转换就直接用,结果音画不同步了整整3秒。后来排查半天,才发现是时间基没对齐。
2.7 知识体系总览
下面这张图,把时间基的核心概念和转换关系串起来了:
2.8 实战建议
最后,分享几个我在项目中沉淀下来的经验:
- 统一时间基:在同步模块内部,把所有时间戳统一转换成同一个时间基(我习惯用微秒),避免反复转换引入误差
- 用int64_t:所有时间戳变量都用int64_t,别用int。32位整数在微秒级时间基下,只能表示约35分钟
- 转换前检查:调用av_rescale_q前,确认源时间基和目标时间基的den不为0。虽然FFmpeg内部有检查,但自己加一层防御更安心
- 精度取舍:音频时间基通常用采样率(如44100),视频用帧率(如25)。转换时注意,音频时间戳是连续的,视频是离散的,不要直接做除法
嗯,时间基这块内容就这些。理解了AVRational,你就掌握了FFmpeg时间处理的钥匙。后面讲音画同步时,你会发现所有问题最终都归结为时间基的转换和对齐。