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

转码时 pts 挂在帧上,不是包上。 改 pts 必须同时改 frame.time_base,只改一半等于换算两遍——全塌成一个点。

📝 本节和前面几节的关系

上一节换的是包,这一节换的是帧。错的地方不同,症状一模一样。 这一段有个和 C 语言的分工差异,书上的写法照搬到 PyAV 会出错,必须说清。

一、帧自带时间基

解码出来的帧,frame.time_base 是解码器填好的(这次是 1/15360),而编码器要的是 1/30。

项 值
解码器所在的流时间基 1/15360
帧自带的 time_base 1/15360
编码器要的时间基 1/30

第一行和第三行不一样,所以直接搬 pts 是错的。 第二行是关键:AVFrame 上没有时间基这个字段,但 PyAV 在帧上带了一个,它会按这个基去换算。

二、三种写法,只有一种错

写法 改了什么 读回时长 对不对
原样送进去 什么都不改 1.00 秒 对
只改 pts fr.pts = 换算值 0.03 秒 错
pts + time_base 一起改 两个都换成 1/30 1.00 秒 对

第三种(只改 pts)为什么崩,得看它的 pts 变成了什么:首包和末包都是 0, 所有帧的时间戳塌成一个点。 原因是换算算了两遍:你把 512 改成 1(按 1/30 算过一遍),但帧上还挂着 1/15360, PyAV 又按 1/15360 把它折回 1/30,1 ÷ 15360 秒再乘 30 = 0.002 个刻度,取整就是 0。

所以「改 pts 必须同时改 time_base」是 PyAV 的规矩,缺一半就等于算两次。

三、C 和 PyAV 的分工不同

环境 帧上有时间基吗 谁负责换算
C(书里的写法) 没有 你自己 av_rescale_q
PyAV(本文) 有,解码器填的 库按它换,你也别乱改

这不是谁对谁错,是两套约定:C 里时间基挂在 codec context 上,帧只是一堆裸数据; PyAV 为了少踩坑,把时间基塞进了帧。 看懂这一层,就不会被「书上说要换算,我照做反而错了」这种事搞懵。

四、固定套路,背下来

步 做什么
① 记下输入流时间基 itb
② 给输出流设时间基 otb
③ 每帧 frame.pts = av_rescale_q(frame.pts, itb, otb)
④ 转封装同一套,作用在 packet.pts 和 packet.dts 上

最省事的其实是「什么都不改」:原样送进去,库会按帧上带的时间基自己换。 前提是你别去动它——一旦手动改 pts,就得连 time_base 一起改。

这一章从头到尾其实只讲了一件事:时间戳是个「数 × 基」的组合,动了一半就等于全错。

五、动手 Demo:转码换时间基,改动 pts 时别把时间基漏了

Demo 先查帧自带的时间基,再用三种写法各生成一个文件,比读回时长。 三种写法的区别只有两行代码,结果却是 1.00 秒和 0.03 秒的差别。

for fr in c.decode(s):
    src = fr.pts                       # 单位是 1/15360
    if mode == "shift":
        fr.pts = rescale(src, tb_in, Fraction(1, 30))
    elif mode == "both":
        fr.pts = rescale(src, tb_in, Fraction(1, 30))
        fr.time_base = Fraction(1, 30)  # 这一行是关键
    for pk in st.encode(fr):
        o.mux(pk)

实跑输出:

--------------------------------------------------------------------
转码换时间基,改动 pts 时别把时间基漏了
--------------------------------------------------------------------
**转码(transcode)和转封装不一样**:中间要解码成帧、再编码成新包。
  **帧上的 pts 是解码器给的,编码器要的是自己时间基下的数。**

**查一下解码出来的帧,它自带时间基:**

  项                      值
  --------------------------------------------------
  解码器所在的流时间基    1/15360
  帧自带的 time_base      1/15360
  编码器要的时间基        1/30

  **第一行和第三行不一样**,所以直接搬 pts 是错的。
  **第二行是关键**:`AVFrame` 上没有时间基这个字段,但 PyAV 在帧上带了一个 `time_base`,
  **它会按这个基去换算**。

**三种写法,各生成一个文件:**

  写法                    改了什么                  读回时长      对不对
  ----------------------------------------------------------------------------
  原样送进去              什么都不改                1.00 秒       对
  只改 pts                `fr.pts = 换算值`         0.03 秒       错
  pts + time_base 一起改  两个都换成 1/30           1.00 秒       对

  **原片的读回时长是 1.00 秒**,拿它当基准。

**第三种(只改 pts)为什么会崩,得看它的 pts 变成了什么:**

  文件                时间基        首包 pts      末包 pts
  --------------------------------------------------------------
  原片                1/15360       0             14848
  只改 pts            1/15360       0             0
  两个都改            1/15360       0             14848

  **「只改 pts」那一行的首包和末包都是 0**,所有帧的时间戳塌成一个点,时长只剩 0.03 秒。
  **原因是换算算了两遍**:你把 512 改成了 1(按 1/30 算过一遍),但帧上还挂着 1/15360,
  PyAV 又按 1/15360 把它折回 1/30,
  **1 ÷ 15360 秒再乘 30 = 0.002 个刻度,取整就是 0**,全塌了。

  **所以「改 pts 必须同时改 time_base」是 PyAV 的规矩**,缺一半就等于算两次。

**C 语言里没有这个坑,因为 `AVFrame` 根本没有 `time_base` 字段:**

  环境                帧上有时间基吗          谁负责换算
  --------------------------------------------------------------------
  C(书里的写法)     没有                    你自己 `av_rescale_q`
  PyAV(本节)        有,解码器填的          库按它换,你也别乱改

  **这不是谁对谁错,是两套约定**:C 里时间基挂在 codec context 上,帧只是一堆裸数据;
  PyAV 为了少踩坑,把时间基塞进了帧。
  **看懂这一层,就不会被「书上说要换算,我照做反而错了」这种事搞懵。**

**一句话总结本节:**

  ①     转封装:改 `packet.pts`/`dts`,同时改 `packet.time_base`
  ②     转码:改 `frame.pts`,同时改 `frame.time_base`
  ③     不想改就别改——原样送进去,库会按帧上带的时间基自己换
  **三条里最省事的是第三条,前提是你别去动它。**

「只改 pts」的末包 pts 是 0, 这就是「塌成一个点」的量化证据——不是时长算错了,是所有帧都挤在 0 时刻。 注意原样送进去那一行是对的:库自己按 1/15360 折到 1/30。 所以你越帮越忙的时候,症状就是「越过越错」。

30 秒实验

  1. 把 fr.time_base 改成 1/90000 而不是 1/30,看还能不能对上。
  2. 用 frame.time 属性(秒,浮点)代替 pts,看还有没有这个坑。
  3. 把编码器 rate 改成 25,重跑三种写法,看结论会不会变。

常见误区

  1. 以为改 pts 就够了。 PyAV 还会按帧自带的时间基换一次,等于算两遍。
  2. 以为书上的 av_rescale_q 照搬到 PyAV 就行。 C 的帧没有时间基,PyAV 的有,行为不一样。
  3. 以为时长 0.03 秒是「编码失败」。 编码成功了,是时间戳全塌到 0,容器只能给个最短时长。
自测三问(点开对答案) 1. 为什么只改 pts 会把所有帧压到 0? —— PyAV 又按帧上的 1/15360 把你改后的数折回 1/30,**1 ÷ 15360 × 30 ≈ 0.002,取整成 0**。 2. C 和 PyAV 在这件事上最大的差别是什么? —— C 的 `AVFrame` 没有时间基字段,**换算全归你**; PyAV 给帧带了时间基,**库会替你换,你改了 pts 就得连基一起改。** 3. 最省事的写法是什么? —— **什么都不改,原样送进编码器**,库按帧上的时间基自己换。

下一章转到视频监控的另一半:ONVIF 与 GB/T 28181 协议。

相关笔记