pts 只是个整数,乘上 time_base 才是秒。 同一个文件里三个刻度并存,混着比必错。
这是整个封装层最容易算错的一节,值得慢读。 7.5 存包会绕开这套刻度,7.6 转封装会改这套刻度。
一、三个刻度同时存在
容器一个,每条流各一个。 东西 常见刻度 一格多长
容器时长 1/1000000 一微秒 视频流 1/12800 万分之一万二千秒 音频流 1/44100 一个采样
为什么视频是 1/12800 这种怪数:它是把一秒切成能被常见帧率整除的份数。 12800 能被 25、50、100 整除,所以 25 帧的片子每帧正好 512 格。 音频用 1/44100 是因为采样率就是 44100,一个 pts 一格就是一个采样点,天然对齐。 容器用微秒是为了统一记账:不管里面有几条流、尺子多怪,总账一律换算成微秒。
二、换算只有一个公式
秒数 = pts × time_base,反过来 pts = 秒数 ÷ time_base。 已知 求 怎么做
pts + time_base 秒 相乘 秒 + time_base pts 相除 两条流的 pts 能不能比 先都换成秒
第三行是全部错误的来源。 视频 pts 是 512,音频 pts 是 1024,谁大? 没法比,因为格子不一样长。 换成秒之后:视频 0.04 秒,音频 0.023 秒。 结论与数字大小完全相反。 所以规矩就一句:碰到 pts 先问「这是哪条流的」,拿到它的 time_base 再算。
三、为什么用分数不用小数
不是怀旧,是精度。 帧率 小数 攒多久出问题
25 0.04 整数,不攒 30 0.0333… 一直攒 29.97 0.03336… 一小时差几帧
用整数 pts 加整数比 time_base,只要不溢出就是精确的。 这是拿「整数运算」换「永远不漂」。
29.97 这个怪帧率就是因为这个:NTSC 当年为了避开干扰,把 30 乘了 1000/1001。
用分数写就是 30000/1001,一个数都不丢。
所以看到 30000/1001 别以为是写错了,那是正宗的 29.97。
四、帧率是从哪来的
很多人以为容器里存了一个「帧率」字段。多数时候没有。
它是从相邻两个 pts 的间隔算出来的:间隔 512 格,512 × (1/12800) = 1/25 秒,倒数就是 25 帧。
所以可变帧率的视频,average_rate 只是个平均值,真实帧率逐段不同。
拿平均值去算时长,会差。
要靠 pts 精确算时长:最后一个 pts 减去第一个,再乘 time_base,加上最后一帧的 duration。
五、转封装会改刻度
这一点在 7.6 会亲眼看到。 容器 视频 time_base 音频 time_base
mp4 1/12800 1/44100 matroska 1/1000 1/1000 mpegts 1/90000 1/90000
每个容器有自己的规定刻度,mux 的时候 FFmpeg 会帮你换算 pts。 所以你看到的是 pts 数字变了、 秒数没变。 这时候要是只比 pts 不看 time_base,就会以为「时间全乱了」。 这是排查转封装问题时最常犯的错。 正确的对照方式:两边都换成秒,再比。多写一行除法,省半天怀疑。
六、动手 Demo:时基这根尺子,一格到底多长
先列出三个刻度,再把前五个包的 pts 换成秒。
import av
c = av.open("tiny.mp4")
vs = c.streams.video[0]
a = c.streams.audio[0]
print("容器用微秒", c.duration)
print("视频", vs.time_base, "音频", a.time_base)
for pk in c.demux(vs):
if pk.size == 0:
continue
print(pk.pts, pk.pts * vs.time_base)
if pk.pts > 1536:
break
c.close()
实跑输出:
--------------------------------------------------------------------
时基这根尺子,一格到底多长
--------------------------------------------------------------------
**① 两个时间刻度并存**
东西 时间刻度 一格多长
------------------------------------------
容器时长 微秒 1/1000000 秒
视频流 1/12800 1/12800 秒
音频流 1/44100 1/44100 秒
**同一个文件里三个刻度不一样。** 容器用微秒算总账,视频按 12800 分之一秒数,
音频按 44100 分之一秒数。
**所以 `vs.duration` 与 `a.duration` 那两个数不能直接比**:一个是 **25600** 格,
一个是 **89224** 格,各自的格子长度不同。
**② 换算只有一个公式**
**秒数 = pts × time_base。** 反过来:**pts = 秒数 ÷ time_base**。
pts 乘 time_base 等于
--------------------------------------
0 0/12800 0.000 秒
512 512/12800 0.040 秒
1024 1024/12800 0.080 秒
1536 1536/12800 0.120 秒
2048 2048/12800 0.160 秒
**相邻两个 pts 差 512,乘出来正好是 1/25 秒。**
这就是那个「25 帧」的来源:**帧率不是单独存的,是从 pts 间隔里算出来的。**
**③ 为什么不用小数**
**用小数会攒误差。** 1/25 = 0.04,看着干净;
可 1/30 = 0.0333…,一秒攒一点点,一小时就能差出好几帧。
**用分数就没有这个问题**:pts 是整数,time_base 是整数比,两者相乘再除,只要分子不溢出就是精确的。
**这是拿整数换精度。**
**④ 换算成「另一个刻度」要显式做**
**把视频的 pts 直接塞给音频用,时间会彻底错掉**,因为两把尺子不一样长。
要转就用 `av.time_base` 那一套换算,或者干脆两次都先换成秒再算。
**实践里的规矩很简单**:碰到 pts 先问「这是哪条流的」,拿到对应 time_base 再换算。
**问错一次,整段就偏了。**
pts 差 512,乘出来差 1/25 秒,帧率就是这么算出来的。 容器用微秒、视频用 1/12800、音频用 1/44100,三把尺子。
30 秒实验
- 把
vs.time_base换成a.time_base去算视频的 pts,看差多少。 - 用
float(pk.time)直接拿秒数,与手算的对一对。 - 算一遍
vs.duration * vs.time_base与container.duration,看差多少。
常见误区
- 拿 pts 直接比大小。 不同流的格子不一样长。
- 把 time_base 当小数。 它是分数,要用分数算。
- 用 average_rate 算总时长。 可变帧率下会差。
自测三问(点开对答案)
1. pts 与秒数怎么换算? —— 秒数 = pts × time_base。反过来 pts = 秒数 ÷ time_base。 2. 为什么 time_base 用分数不用小数? —— 小数会攒误差,分数只要不溢出就是精确的。 3. 转封装之后 pts 变了,时间真的变了吗? —— 没有。容器的刻度变了,秒数不变。- ☐ (本节完,下一节 7.5 FFmpeg解封装后直接存储AVPacket)