目录
[1. AVRational ------ 一切的起点](#1. AVRational —— 一切的起点)
[2. av_rescale_q ------ 最核心的函数](#2. av_rescale_q —— 最核心的函数)
[3. av_rescale_q_rnd ------ 带舍入策略的版本](#3. av_rescale_q_rnd —— 带舍入策略的版本)
[4. av_rescale ------ 最简单的缩放](#4. av_rescale —— 最简单的缩放)
[5. av_rescale_rnd ------ av_rescale 的舍入版](#5. av_rescale_rnd —— av_rescale 的舍入版)
[6. av_add_stable ------ 你可能没听过但很重要](#6. av_add_stable —— 你可能没听过但很重要)
[7. av_compare_ts ------ 比较两个时间戳](#7. av_compare_ts —— 比较两个时间戳)
[8. av_get_time_base_q ------ 一个常量](#8. av_get_time_base_q —— 一个常量)
[9. 实战:从解码到编码的完整时间戳流转](#9. 实战:从解码到编码的完整时间戳流转)
[转码时 rescale](#转码时 rescale)
[10. 常见坑汇总(血泪教训)](#10. 常见坑汇总(血泪教训))
[坑 1:忘记 rescale duration](#坑 1:忘记 rescale duration)
[坑 2:B-frame 导致 dts > pts](#坑 2:B-frame 导致 dts > pts)
[坑 3:AV_NOPTS_VALUE](#坑 3:AV_NOPTS_VALUE)
[坑 4:音频 pts 步进不对](#坑 4:音频 pts 步进不对)
[坑 5:time_base 设错了](#坑 5:time_base 设错了)
[11. 一张速查表](#11. 一张速查表)
如果你写过 FFmpeg,大概率在某个深夜对着
pts、dts、time_base发呆过。尤其是第一次遇到音画不同步,顺着代码追到av_rescale_q(),然后发现文档像天书一样。今天这篇,咱们把 FFmpeg 时间戳相关的函数从头到尾捋一遍。不搞半吊子解释,直接讲清楚每个函数干什么、什么时候用、坑在哪。
1. AVRational ------ 一切的起点
FFmpeg 里的时间不是毫秒,不是微秒,是一个分数。
typedef struct AVRational {
int num; ///< 分子
int den; ///< 分母
} AVRational;
比如:
-
1/25→ 每帧 40ms(PAL 视频) -
1/44100→ 每个音频采样约 22.7μs -
1/90000→ MPEG-TS 的标准时间基(≈11.1μs)
时间基(time_base)的含义 :1 / time_base= 每秒多少个时间单位。
真实时间(秒) = pts × av_q2d(time_base)
举个直观的例子:
AVRational tb = {1, 90000};
int64_t pts = 90000;
// 真实时间 = 90000 × (1/90000) = 1 秒
坑点 :很多人以为 time_base 越小精度越高,于是拼命缩小 den。但实际上 den 太大可能导致 int64_t溢出,尤其是在 rescale 的时候。后面会讲到。
2. av_rescale_q ------ 最核心的函数
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq);
作用 :把数值 a从一个时间基 bq转换到另一个时间基 cq。
数学公式:
result = a × bq / cq
但 FFmpeg 的实现是无损整数运算,不会先除后乘导致精度丢失。
实际例子:从输入流时间基转输出流时间基
// 输入流 time_base = 1/90000
// 输出流 time_base = 1/48000
int64_t input_pts = 180000; // 2 秒处(按 1/90000 算)
AVRational in_tb = {1, 90000};
AVRational out_tb = {1, 48000};
int64_t output_pts = av_rescale_q(input_pts, in_tb, out_tb);
// = 180000 × (1/90000) ÷ (1/48000)
// = 2 × 48000 = 96000
什么时候用?
| 场景 | 是否要用 |
|---|---|
| 从解码器拿到 pts,往编码器送 | ✅ 必须 |
| 从输入 AVStream 转输出 AVStream | ✅ 必须 |
| 自己构造 pts(如实时采集) | ✅ 必须 |
| 同一 time_base 内计算 delta | ❌ 没必要 |
致命坑:负数
av_rescale_q()不处理负数 。如果你的 pts 可能是 -9223372036854775808(INT64_MIN),直接传进去会溢出。
FFmpeg 后来补了一个安全版本:
int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
enum AVRounding rnd);
3. av_rescale_q_rnd ------ 带舍入策略的版本
enum AVRounding {
AV_ROUND_ZERO = 0, ///< 向零舍入(截断)
AV_ROUND_INF = 1, ///< 向无穷舍入
AV_ROUND_DOWN = 2, ///< 向下舍入
AV_ROUND_UP = 3, ///< 向上舍入
AV_ROUND_NEAR_INF = 5, ///< 四舍五入(默认最常用)
AV_ROUND_PASS_MINMAX = 8192, ///< 特殊:MIN/MAX 直接透传
};
为什么需要舍入?
因为时间基转换时经常除不尽
// 44.1kHz 音频,time_base = 1/44100
// 转成 1/1000 ms 时间基
// 1 个采样 = 1000/44100 ≈ 0.02267573696 ms
int64_t pts_ms = av_rescale_q(1, {1, 44100}, {1, 1000});
// = 0 (直接截断,丢了!)
用 AV_ROUND_NEAR_INF:
int64_t pts_ms = av_rescale_q_rnd(1, {1, 44100}, {1, 1000},
AV_ROUND_NEAR_INF);
// = 0 还是 0... 但至少语义对了
音频场景的关键组合
// 音频帧转时间戳
int64_t pts = av_rescale_q_rnd(
nb_samples,
{1, codec_ctx->sample_rate},
stream->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX
);
AV_ROUND_PASS_MINMAX的意思是:如果 a是 INT64_MIN或 INT64_MAX,直接返回,不计算。防止 B-frame 场景下 pts 为 AV_NOPTS_VALUE时炸掉。
4. av_rescale ------ 最简单的缩放
int64_t av_rescale(int64_t a, int64_t b, int64_t c);
数学公式:a × b / c
和 av_rescale_q 的区别:
-
av_rescale只认整数,不认 AVRational -
没有时间基语义,纯粹算数
什么时候用?当你已经有整数倍关系的时候:
// 已知:1 个视频帧 = 1024 个音频采样
int64_t audio_pts = av_rescale(video_pts, 1024, 1);
不要用 av_rescale 做时间基转换 ,除非你非常确定分母不会溢出。用 av_rescale_q,它内部做了溢出保护。
5. av_rescale_rnd ------ av_rescale 的舍入版
int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd);
跟 av_rescale_q_rnd的关系,就像 av_rescale跟 av_rescale_q的关系。
实际用途:在做非时间基的整数比例换算时用,比如像素格式转换、采样率换算的中间步骤。
6. av_add_stable ------ 你可能没听过但很重要
int64_t av_add_stable(AVRational ts_tb, int64_t ts,
AVRational tb, int64_t inc);
作用 :在时间基 ts_tb下,给 ts加上 inc个 tb单位的时间,并且保证结果是"稳定的"(不会因为舍入误差漂移)。
为什么需要它?
想象你有一个 29.97fps 的视频(30000/1001):
// 每一帧的时间增量
AVRational frame_duration = {1001, 30000}; // ≈ 33.366... ms
// 第 n 帧的 pts
pts = n × frame_duration
如果每次都用 av_rescale_q算,浮点误差会累积。1000 帧之后,误差可能大到音画不同步。
av_add_stable的做法是:记录分数部分的余数,下次累加,类似定点小数。
实际用法
int64_t next_pts = av_add_stable(stream->time_base,
last_pts,
codec_ctx->time_base,
1);
这在自己生成 pts 的编码场景(摄像头采集、屏幕录制)里几乎是必用的。
7. av_compare_ts ------ 比较两个时间戳
int av_compare_ts(int64_t ts_a, AVRational tb_a,
int64_t ts_b, AVRational tb_b);
返回值:
-
< 0:ts_a 更早 -
= 0:同时 -
> 0:ts_a 更晚
典型场景:音视频同步
int cmp = av_compare_ts(audio_pts, audio_tb,
video_pts, video_tb);
if (cmp < 0) {
// 音频慢了,丢音频或加速
} else if (cmp > 0) {
// 视频慢了,丢帧或延迟显示
}
内部实现:先把两个时间戳 rescale 到同一个时间基再比较,避免溢出。
8. av_get_time_base_q ------ 一个常量
AVRational av_get_time_base_q(void);
返回 {1, AV_TIME_BASE},其中 AV_TIME_BASE = 1000000(微秒)。
这是 FFmpeg 内部用来表示绝对时间的时间基。比如:
int64_t now_us = av_gettime_relative(); // 微秒
AVRational tb = av_get_time_base_q(); // 1/1000000
9. 实战:从解码到编码的完整时间戳流转
这是最让人头大的地方,咱们走一遍完整流程。
解码端
AVPacket pkt;
av_read_frame(fmt_ctx, &pkt);
AVStream* in_stream = fmt_ctx->streams[pkt.stream_index];
// pkt.pts 的单位是 in_stream->time_base
转码时 rescale
AVStream* out_stream = ...;
// 关键一步:转换时间基
pkt.pts = av_rescale_q_rnd(pkt.pts,
in_stream->time_base,
out_stream->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt.dts = av_rescale_q_rnd(pkt.dts,
in_stream->time_base,
out_stream->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt.duration = av_rescale_q(pkt.duration,
in_stream->time_base,
out_stream->time_base);
编码端自己生成时间戳
AVFrame* frame = ...;
frame->pts = next_pts;
next_pts = av_add_stable(enc_ctx->time_base,
next_pts,
enc_ctx->time_base,
1);
10. 常见坑汇总(血泪教训)
坑 1:忘记 rescale duration
// 只转了 pts/dts
pkt.pts = av_rescale_q(pkt.pts, in_tb, out_tb);
pkt.dts = av_rescale_q(pkt.dts, in_tb, out_tb);
// duration 忘了转 → 播放器算错帧率
// 记得转 duration
pkt.duration = av_rescale_q(pkt.duration, in_tb, out_tb);
坑 2:B-frame 导致 dts > pts
H.264 的 B 帧会让 dts 乱序。FFmpeg 内部会处理,但你往外推 packet 的时候,dts 可能不是单调递增的。如果你自己缓存 packet,必须按 dts 排序。
坑 3:AV_NOPTS_VALUE
#define AV_NOPTS_VALUE ((int64_t)UINT64_C(0x8000000000000000))
这是 FFmpeg 表示"没有时间戳"的特殊值。不要对它调用 av_rescale_q ,用 AV_ROUND_PASS_MINMAX或者直接判断:
if (pkt.pts != AV_NOPTS_VALUE) {
pkt.pts = av_rescale_q_rnd(pkt.pts, in_tb, out_tb,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
}
坑 4:音频 pts 步进不对
音频帧的 pts 应该按采样数递增,不是按帧数:
// 正确
frame->pts += frame->nb_samples;
// 错误
frame->pts += 1; // 一帧可能有 1024 个采样!
坑 5:time_base 设错了
// 视频编码器
enc_ctx->time_base = {1, 25}; // 25 fps
// 音频编码器
enc_ctx->time_base = {1, sample_rate}; // 不是 {1, 25}!
11. 一张速查表
| 函数 | 用途 | 什么时候用 |
|---|---|---|
av_rescale_q |
时间基转换 | 跨流传递时间戳 |
av_rescale_q_rnd |
带舍入的时间基转换 | 音频、需要精确舍入时 |
av_rescale |
整数比例缩放 | 已知整数比例的非时间运算 |
av_rescale_rnd |
带舍入的整数缩放 | 同上,但要控制舍入方向 |
av_add_stable |
稳定递增时间戳 | 自己生成 pts(录制、采集) |
av_compare_ts |
比较时间戳 | 音视频同步 |
av_get_time_base_q |
获取微秒时间基 | 绝对时间计算 |
12.总结
FFmpeg 的时间戳系统本质上是一个有理数算术系统,设计目标是在不引入浮点误差的前提下,优雅地处理各种帧率、采样率的转换。
理解了 av_rescale_q就是理解了 FFmpeg 时间的灵魂。剩下的,无非是搞清楚谁产生时间戳、谁消费时间戳、中间经过了几层 rescale。
下次再遇到音画不同步,别慌。先打印 pts × time_base,看看真实时间对不对。90% 的问题,答案就在那里。