🎬 第八章 AVCodec编解码层理论及案例实战
📄 一句话概括

同一条流上,包与帧的 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 秒实验

  1. 把 enc.time_base 设成 1/1000,帧 pts 按 40 步进编一段,验证总时长。
  2. 算 vs.duration * vs.time_base 与 container.duration 差多少。
  3. 把同一个 pts 分别用流与编码器的 time_base 换算,看差多少倍。

常见误区

  1. 把两把尺子混用。 读的时候用流的,写的时候用编码器的。
  2. 以为 time_base 必须等于 1/framerate。 实测不检查,但要自己算。
  3. 指望 pts 自动换算。 自己写循环时没人替你换。
自测三问(点开对答案) 1. 同一条流上有几把尺子? —— 两把:流的与编码器的。再加上通用的微秒刻度,共三处。 2. time_base 与 framerate 必须互为倒数吗? —— 不必。实测三组不互为倒数的搭配都能打开,但 pts 要自己算。 3. 为什么不建议直接乘两个 time_base 的比值? —— 会引入整数截断。先换成秒再乘更稳。

相关笔记