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

转封装(remux):解码器和编码器都不参与,只换盒子。 实测比重新编码快二十多倍,画质一字不改。

📝 本节和前面几节的关系

上一节是「编码 + 封装」,这一节只有「封装」。流水线从四步变成两步。 时间基的换算问题在这一节只是露个头,正题是 04.06.03。

一、只搬包,不动像素

环节 编码+封装 转封装
读包 要 要
解码 不要 不要
编码 要 不要
写包 要 要

中间两步整段拿掉,所以快得离谱——它只搬字节,不碰像素。 画质也一字不改:数据本身没变,连重新量化的机会都没有。

二、裸流的包没有时间戳

上一节读出来的首包 pts 是 None,因为 h264 裸流不存时间戳。 所以转封装的第一步不是搬包,是「补时间戳」:按帧率自己编号。

step = 时间基分母 ÷ 帧率        # 一帧走多少刻度
pk.pts = k * step                # 第 k 帧
pk.dts = pk.pts                  # 裸流没有 B 帧重排信息,直接相等

这次的样本是 1/1200000 的时间基、30 fps,所以一帧走 40000 个刻度。 40000 ÷ 1200000 = 1/30 秒,正好对上一帧的时长。

三、实测:两条路的差距

指标 转封装 重编码
包数 30 30
时长 1.00 秒 1.00 秒
文件大小 11.4 KB 11.6 KB
耗时 几毫秒 几十毫秒
画质 一字不改 重新压一遍

耗时差二十多倍,而且这个差距会随片长拉大——转封装的耗时基本只和文件大小成正比, 转码还要加上每帧的计算。 所以「换个格式」这种需求,一律先想能不能转封装,别急着转码。

文件大小几乎一样(11.4 对 11.6 KB):因为里头装的是同一份编码数据,差的只是索引写法。

四、转封装不是万能的

它要求目标容器认得原来的编码格式。

编码 能进的容器 不能进
H.264 MP4、MKV、TS、FLV WebM
H.265 MP4、MKV、TS WebM、FLV
VP9 WebM、MKV MP4(老播放器)

H.264 进 WebM 是常见误区:WebM 只收 VP8/VP9/AV1,必须转码,没有捷径。 同理,音频也一样(AAC 进 Ogg 就不行)。判断标准就一句:容器规格里有没有这个编码的登记。

五、动手 Demo:把现成的 H.264 裸流封成 MP4,一帧都不重编码

Demo 分两条路:一条只搬包(转封装),一条老老实实重编码,两边计时对比。 裸流要自己补时间戳,否则 MP4 里时长会算错。

c = av.open("in.h264")
s = c.streams.video[0]
o = av.open("out.mp4", "w")
st = o.add_stream_from_template(s)      # 照抄编码参数,不重编
step = st.time_base.denominator // 30
k = 0
for pk in c.demux(s):
    if pk.size == 0:
        continue
    pk.pts = pk.dts = k * step           # 补时间戳
    pk.time_base = st.time_base
    o.mux(pk)
    k += 1
o.close()

实跑输出:

--------------------------------------------------------------------
把现成的 H.264 裸流封成 MP4,一帧都不重编码
--------------------------------------------------------------------
**这件事叫「转封装」(remux):解码器和编码器都不参与,只换盒子。**

  流水线从原来的四步变成两步:**读包 → 写包**。中间的「解码」「编码」整段拿掉。

  **裸流的包没有时间戳**(上一节读出来是 `None`),所以转封装的第一步不是搬包,
  是 **「补时间戳」**:按帧率自己编号。

**实测两条路:**

  指标            转封装                重编码
  ------------------------------------------------------------
  输入包数        30                    30
  输出包数        30                    30
  解出帧数        30                    30
  时长            1.00 秒               1.00 秒
  文件大小        11.4 KB               11.6 KB
  耗时            2.6 毫秒              62.9 毫秒
  画质            一字不改              重新压一遍

  **耗时那一行是本节的结论**:转封装二十倍于重编码,因为它只搬字节。
  **片子里只有 30 帧,转码那点活儿还不够垫,真要上千帧差距会拉到几十倍以上。**
  **所以「换个格式」这种需求,一律先想能不能转封装,别急着转码。**

  **但要注意:转封装不是万能的**,它要求目标容器认得原来的编码。
  **H.264 进 MP4、进 MKV、进 TS 都可以;H.264 进 WebM 就不行**,因为 WebM 只收 VP8/VP9/AV1。

**时间基这一栏要单独看,这是下一节(04.06.03)的正题:**

  环节              时间基            说明
  ------------------------------------------------------------
  裸流轨            1/1200000         FFmpeg 给的默认值
  输出轨            1/1200000         照抄模板,不变
  补时间戳步长      40000             一帧走 40000 个刻度
  读回来的轨        1/1200000         MP4 实际存的

  **这一栏四个时间基全是 1/1200000**,说明「照抄模板」确实一个刻度都没换。
  **真正要看的是「步长 40000」**:40000 ÷ 1200000 = 1/30 秒,一帧走这么多刻度,正好对上 30 fps。
  **换个时间基这个数就废了**:如果输出轨的时间基是 1/15360,一帧只要走 512 个刻度,
  **把 40000 搬过去,时间轴会慢 78 倍**。

  **这次能直接用模板的时间基,是因为输出也走同一套参数。**
  **一旦目标容器强制自己的时间基(MP4 常用 1/15360,TS 用 1/90000),就必须换算**,
  04.06.03 会给一个「不换算会错成什么样」的实测。

add_stream_from_template 是关键:它把编码参数原样抄进输出轨, 所以容器知道这是 H.264、多大、什么 profile,但不需要编码器参与。 四条路里最省事的是它,最危险的是「补时间戳」这一步——补错就是时长错,而且文件照样能打开。

30 秒实验

  1. 把 step 改成 4000(少一个零),看读回来的时长变成多少。
  2. 把 pk.dts 删掉只给 pk.pts,看 MP4 能不能写成功。
  3. 把输出格式改成 webm,看报什么错——这就是「容器不认这个编码」的样子。

常见误区

  1. 以为转封装也要解码。 它只搬包,这正是它快二十多倍的原因。
  2. 以为裸流搬过去时间戳会自动补。 不会,必须自己按帧率编号。
  3. 以为任何编码都能进任何容器。 WebM 不收 H.264,没有捷径,只能转码。
自测三问(点开对答案) 1. 转封装为什么快? —— 解码和编码两步整段拿掉,**只搬字节**,耗时基本只和文件大小成正比。 2. 裸流转封装前必须做什么? —— **补时间戳**。裸流不存 pts,不补的话容器里时长会算错。 3. H.264 能进 WebM 吗? —— 不能。WebM 只收 VP8/VP9/AV1,**必须转码**。

下一节开始讲时间基:为什么同一个「第 5 帧」在不同地方是不同的数。

相关笔记