📹 第四章 H.264/H.265视频编码并存储
📄 一句话概括

pts 是「刻度数」,不是秒。同一个时刻在不同时间基下是不同的数。 所以跨时间基比 pts,等于比两种货币的数字却不看汇率。

📝 本节和前面几节的关系

上一节反复说「要配时间基」,这一节把时间基讲透。 换算公式只有一句,但漏一步就全错——下一节和再下一节各给一个实测。

一、四层时间基,一层都不能混

层 时间基 一秒多少刻度 谁定
容器时长 1/1000000 1000000 FFmpeg 写死
MP4 视频轨 1/15360 15360 容器
编码器 1/30 30 帧率

第一行和第二行不是一回事:container.duration 的单位是微秒,和视频轨的 1/15360 差了 65 倍。 所以算时长必须写清说的是哪一层——c.duration 是微秒,stream.duration 才是轨自己的刻度。

编码器那一行还有个坑:刚建完编码器去读 time_base 拿到的是 None, 必须等它真的打开了才有值。 实测就是这么个结果:add_stream 之后读是 None,调 start_encoding() 之后读到 1/30。

二、同一个时刻,四种写法

时刻 1/30 1/15360 1/90000 1/1000000
0.000 秒 0 0 0 0
0.500 秒 15 7680 45000 500000
1.000 秒 30 15360 90000 1000000

0.5 秒在 1/30 里是 15,在 1/15360 里是 7680——同一个时刻的两个完全不同的数。 这也是为什么「pts 直接比较大小」只在同一时间基下成立。

三、换算只有一句公式

新值 = 旧值 × 旧时间基 ÷ 新时间基,四舍五入。 C 接口里这句就是 av_rescale_q(pts, 旧tb, 新tb),内部用 64 位整数算, 不会像浮点那样丢精度。 PyAV 没把 av_rescale_q 暴露出来, 所以本文的 Demo 都是int(round(pts * 旧 / 新)) 自己算——原理一样,只是少了整数保护。

四、时间基选多细,决定时间戳多准

存法 一帧刻度 取整当步长 一小时偏多少
1/1000(毫秒) 33.33 33 36.0 秒
1/90000(TS) 3000 3000 0 秒
1/15360(MP4) 512 512 0 秒

36 秒是这么算出来的:30 fps 一小时 108000 帧,每帧少 1/3 毫秒, 108000 ÷ 3 = 36000 毫秒 = 36 秒。 这就是 TS 用 1/90000 而不是 1/1000 的原因——它不是随便定的, 是「60 fps 的整数倍,还尽量细」。

注意这个偏差只在「拿整数当步长乘帧数」时累积。 如果你的代码是「每帧 pts 单独四舍五入」,误差不会累积——但很多老代码就是拿 33 毫秒当步长乘的。

五、动手 Demo:时间基——pts 是「刻度数」,不是「秒」

Demo 先读出四层时间基,再把同一个时刻换算成不同基下的值。 最后用一个反例算出「选错时间基,一小时偏 36 秒」这笔账。

c = av.open("in.mp4")
print("容器时长", c.duration)          # 微秒
print("视频轨时间基", c.streams.video[0].time_base)
o = av.open("t.mp4", "w")
st = o.add_stream("libx264", rate=30)
print("编码器时间基", st.codec_context.time_base)   # None!
o.start_encoding()
print("open 之后", st.codec_context.time_base)      # 1/30

实跑输出:

--------------------------------------------------------------------
时间基——pts 是「刻度数」,不是「秒」
--------------------------------------------------------------------
**同样一个「第 1 秒」,在不同地方存的数完全不一样。**
**这就是所有人第一次读 ffprobe 输出时的困惑。**

**四层各有各的时间基,一层都不能混:**

  层            时间基          一秒多少刻度    谁定
  ------------------------------------------------------------------
  容器时长      1/1000000       1000000         FFmpeg 写死
  MP4 视频轨    1/15360         15360           容器
  编码器        1/30            30              帧率

  **注意第一行和第二行不是一回事**:`container.duration` 的单位是微秒,
  和视频轨的 `1/15360` 差了 65 倍。
  **这就是为什么算时长必须写清说的是哪一层**——`c.duration` 是微秒,
  `stream.duration` 才是轨自己的刻度。

  **编码器那一行还有个坑**:刚 `add_stream` 完去读 `time_base` 拿到的是 `None`,
  **必须等编码器真的打开了才有值**。
  这次是调 `start_encoding()` 逼它打开,
  之后读到 `1/30`——**这就是「原来没开就取不到」的实证,不是猜的。**

**同一个时刻,换几种时间基各自要写多少:**

  时刻      1/30        1/15360     1/90000       1/1000000
  --------------------------------------------------------------
  0.000 秒  0           0           0             0
  0.500 秒  15          7680        45000         500000
  1.000 秒  30          15360       90000         1000000
  1.033 秒  31          15872       93000         1033333

  **0.5 秒在 1/30 里是 15,在 1/15360 里是 7680**——同一个时刻的两个完全不同的数。
  **所以「pts 的大小」只有配上同一个时间基才能比**,跨时间基比 pts 等于比两种货币的数字不看汇率。

**换算公式只有一句:**

  **新值 = 旧值 × 旧时间基 ÷ 新时间基**,四舍五入。

  C 接口里这句就是 `av_rescale_q(pts, 旧tb, 新tb)`,它内部用 64 位整数算,
  **不会像浮点那样丢精度**。
  **PyAV 没有把 `av_rescale_q` 暴露出来**,
  所以本文所有 Demo 都用 `int(round(pts * 旧 / 新))` 自己算,**原理一样,只是少了整数保护**。

**顺手验一下「四舍五入会不会累积误差」**:

  从 1/30 换到 1/90000 再换回来,看能不能原样还原:

  项                            值
  --------------------------------------------------
  换过去又换回来,还原失败的帧  0 个
  最大偏差                      0 个刻度

  **这次是 0,因为 90000 能被 30 整除**,每一步都是整数,换回来当然还是原值。
  **真会出问题的是选了个除不尽的基**,比如有人图省事用 1/1000(毫秒):
  30 fps 一帧是 33.33 个刻度,**每帧都得砍掉 0.33 毫秒**。

  **这个砍法是会累积的**,因为它不是「每帧四舍五入」而是「拿 33 当步长乘帧数」:

  存法            一帧刻度      取整当步长      一小时偏多少
  --------------------------------------------------------------
  1/1000(毫秒)  33.33         33              36.0 秒
  1/90000(TS)   3000          3000            0 秒
  1/15360(MP4)  512           512             0 秒

  **36 秒是这么算出来的**:30 fps 一小时 108000 帧,每帧少 1/3 毫秒,
  108000 ÷ 3 = 36000 毫秒 = 36 秒。
  **这就是 TS 用 1/90000 而不是 1/1000 的原因**。
  **它不是随便定的,是「60 fps 的整数倍,还尽量细」。**

同一个 time_base 属性,open 前后读到两个不同的东西(None 和 1/30), 这就是「没开就取不到」的实证。 换算表里 1.033 秒那一行(31/15872/93000/1033333)是故意加的: 它证明「时刻」本身带小数,取整会花掉几个刻度。

30 秒实验

  1. 把 1.0333333 秒换算到 1/1000,看四舍五入成多少,误差多大。
  2. 用 float(c.duration) / 1000000 算时长,和你以为的秒数比。
  3. 把编码器 rate 改成 25,看它的时间基变成多少。

常见误区

  1. 以为 pts 的单位是毫秒。 它是「时间基的刻度数」,不同地方基不一样。
  2. 以为 container.duration 和 stream.duration 一个单位。前者是微秒,后者是轨的刻度。
  3. 以为时间基随便选个细的就行。 选错会累积偏差,1/1000 存 30 fps 一小时偏 36 秒。
自测三问(点开对答案) 1. 0.5 秒在 1/30 和 1/15360 下分别是多少? —— 15 和 7680。**同一个时刻,两个数差 512 倍。** 2. 换算公式是什么? —— 新值 = 旧值 × 旧时间基 ÷ 新时间基,四舍五入。C 里是 `av_rescale_q`。 3. 为什么编码器的时间基刚建完读不到? —— 编码器还没打开,**时间基要等 `avcodec_open2` 之后才定下来**。

下一节用实测告诉你「不换算会错成什么样」——转封装换时间基。

相关笔记