同一条流上,包与帧的 pts 刻度可以不是同一把尺子。 「time_base 必须等于 1/framerate」是流传的说法,实测不成立。
8.3 里 open 要用到 time_base,这一节把它讲透。 本章到此收尾,第九章进滤镜。
一、三把尺子同时活着
读、写、通用换算,各一把。 尺子 典型值 用在哪
流 time_base 1/12800 包里 pts 的刻度 编码器 time_base 1/25 送进编码器的帧 pts av.time_base 1000000 微秒,跨容器通用
最容易绕晕的就是「同一条流上两把尺子」。 读的时候按流的算,写的时候按编码器的算。
av.time_base 是个常数 1000000,意思是「微秒」。它不属于任何流,是大家共用的换算单位。
二、那两个参数不必互为倒数
这一条与很多教程说的相反,但实测如此。 time_base framerate 能不能 open
1/25 25 能 1/1000 25 能 1/30 25 能
三组都能打开,编码器不检查这个约束。 所以「必须等于 1」不是规则,只是一种习惯。 那为什么大家都这么写? 因为 1/25 配 25 帧时,pts 直接等于帧号,第 7 帧写 7 就行, 不用算。 换成 1/1000 配 25 帧就要自己算:一帧跨 40 格,第 7 帧写 280。 还按帧号写 7,播放器以为两帧只隔 7 毫秒,播出来快得离谱。
三、不相等时怎么算
两个问题,答得出来就不会错。 问题 怎么答 错了会怎样
一格多长时间 看 time_base 的分母 整段速度不对 一帧占几格 1 ÷ framerate ÷ time_base 同上
这两个问题答不出来,就别急着写 pts。 时间轴错了不报错, 只能靠眼睛看出来——而「快一点」或「慢一点」人的眼睛很难判断。 验证的办法是算总时长:最后一帧的 pts 乘 time_base,该等于你预期的秒数。 对不上就是算错了。
四、换算要显式做
别指望 FFmpeg 帮你自动换算 pts。 从 到 怎么做
秒 任意刻度 乘 time_base 的倒数 刻度 A 刻度 B 先换成秒再乘 流 pts 编码器 pts 两头都换成秒对齐
「先换成秒」是最稳的一步。 直接乘两个 time_base 的比值,看着短,但会引入整数截断。 换成秒再乘,损失的只是浮点末位,肉眼看不出来。这是拿一点点精度换一大堆确定性。 转封装的时候 FFmpeg 会替你换(7.6 里看到 pts 数字变了),但自己写编码循环时没人替你换。 这两件事别混。
五、解码侧为什么没有尺子
因为解码不需要定义刻度,只需要沿用。 方向 time_base 谁定 pts 谁管
解码 码流与容器 原样带出来 编码 你自己 你自己填
所以解码器上读 time_base 会抛 RuntimeError,这不是缺陷,是在提醒你问错了对象。
要刻度就问 stream.time_base。
这一条其实很省事:解码侧的时间信息全由输入决定,你只要把它带到输出那头就行。
真正要动脑的只有编码侧——那才是你需要「定义时间」的地方。本章四节,重心其实都在这一句上。
六、动手 Demo:编解码里同时活着三把尺子
先把三把尺子摆出来,再试三组 time_base 与帧率的搭配。
import av
from fractions import Fraction as F
c = av.open("tiny.mp4")
print("流的刻度", c.streams.video[0].time_base)
c.close()
print("av.time_base", av.time_base)
for tb, fr in ((F(1, 25), F(25, 1)), (F(1, 1000), F(25, 1)),
(F(1, 30), F(25, 1))):
enc = av.CodecContext.create("libx264", "w")
enc.width, enc.height, enc.pix_fmt = 160, 120, "yuv420p"
enc.time_base = tb
enc.framerate = fr
enc.bit_rate = 200000
enc.open()
print(tb, fr, "开成功")
实跑输出:
--------------------------------------------------------------------
编解码里同时活着三把尺子
--------------------------------------------------------------------
**① 三把尺子分别是谁的**
尺子 值 谁用
-------------------------------------------------------
流 time_base 1/12800 包里 pts 的刻度
编码器 time_base 1/25 送给编码器的帧 pts
av.time_base 1000000 微秒,通用换算
**这就是最容易绕晕的地方:同一条流上,包与帧的 pts 刻度可以不一样。** 读的时候用流的,
写的时候用编码器的。
**② 这两个参数不必互为倒数**
time_base framerate 结果
--------------------------------------
1/25 25 开成功
1/1000 25 开成功
1/30 25 开成功
**三组全开成功了。** 网上流传的「time_base 必须等于 1/framerate」,
实测并不成立——**编码器不检查这个约束。**
**那为什么大家都这么写?** 因为这样写最省心:time_base 是 1/25 时,**pts 直接等于帧号**,
第 7 帧就写 7。
**换成 1/1000 配 25 帧就要自己算**:一帧跨 40 格,第 7 帧要写 280。
**还按「pts 等于帧号」写 7,播放器就以为两帧只隔 7 毫秒**,而真实该隔 40 毫秒——播出来快得离谱,
且全程不报一个错。
**所以「不必相等」不等于「可以乱设」。** 设之前先想清楚:一格是多长时间,一帧占几格。
**这两个问题答不出来,时间轴一定会错。**
**③ 换算要显式做**
从 到 怎么做
---------------------------------------------------
秒 某个刻度 乘上 time_base 的倒数
刻度 A 刻度 B 先换成秒再乘
流 pts 编码器 pts 两头都换成秒对齐
**第二行那个「先换成秒」是最稳的做法。** 直接乘比值要求两边都是整数,一不小心就截断;
换成秒是浮点或分数,损失的只是末位。
**④ 解码侧为什么读不到 time_base**
**因为解码方向不需要它。** 解码器只负责「包进帧出」,时间戳原样带着走,刻度由流说了算。
**所以读到 RuntimeError 不是出错,是在提醒你问错了对象。** 要刻度就问 `stream.time_base`,
别问解码器。
三组全开成功,说明编码器不要求两者互为倒数。 但刻度不等时,一帧占几格就得自己算。
30 秒实验
- 把
enc.time_base设成 1/1000,帧 pts 按 40 步进编一段,验证总时长。 - 算
vs.duration * vs.time_base与container.duration差多少。 - 把同一个 pts 分别用流与编码器的 time_base 换算,看差多少倍。
常见误区
- 把两把尺子混用。 读的时候用流的,写的时候用编码器的。
- 以为 time_base 必须等于 1/framerate。 实测不检查,但要自己算。
- 指望 pts 自动换算。 自己写循环时没人替你换。
自测三问(点开对答案)
1. 同一条流上有几把尺子? —— 两把:流的与编码器的。再加上通用的微秒刻度,共三处。 2. time_base 与 framerate 必须互为倒数吗? —— 不必。实测三组不互为倒数的搭配都能打开,但 pts 要自己算。 3. 为什么不建议直接乘两个 time_base 的比值? —— 会引入整数截断。先换成秒再乘更稳。- ☐ (本节完,第八章已完结,下一章 9.1.1 FFmpeg包含滤镜的转码流程)