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

编码的唯一目标:用尽量少的字节,保住尽量多的信息。 这一节把最小的一条命令跑通,实测压缩比 324 倍,并拆开看这 324 倍是从哪来的。

📝 本节和前面几节的关系

第三章讲的是「采」,这一章讲「存」。 采集给你的是 YUV,存储要的是 H.264,中间这一道就是编码。 编码原理在 26 课,这里只讲怎么调。像素格式与 YUV 的常识也在 26 课。

一、一条最小可用的命令

YUV 裸文件没有任何头信息——没有宽、没有高、没有像素格式、没有帧率。 所以这四个参数必须在命令行里全给,一个不给 ffmpeg 就只能猜,猜错就是花屏。

ffmpeg -f rawvideo -pix_fmt yuv420p -s 320x240 -r 30 \
       -i in.yuv -c:v libx264 -crf 23 out.h264
参数 管什么 不给会怎样
-f rawvideo 告诉它这是裸流 当成图片序列或直接报错
-pix_fmt yuv420p 每像素怎么排 按 yuv444 读,画面斜掉
-s 320x240 一帧多大 猜 640×480,整段错位
-r 30 一秒几帧 默认 25,时长算错

这四行里最容易漏的是 -r:它不影响画面,只影响时间戳, 所以漏了不花屏,只是播放速度快了 20%——这种错最难发现。

二、实测:30 帧压成多少

沙箱里没有 ffmpeg.exe,但 PyAV 用的就是同一套库(libavcodec/libx264), 所以下面的数是真跑出来的,不是抄的。

源 3.30 MB → 输出 10.4 KB,压缩比 324 倍,码率 85 kbps。

项 值 说明
包数 30 一帧一包(含 B 帧时也如此)
I 帧 1 关键帧,能独立解
P 帧 8 参考前面的帧
B 帧 21 参考前后两边的帧

注意 I/P/B 的分布:30 帧里只有 1 个 I 帧、21 个 B 帧。这个比例说明 x264 默认更愿意用 B 帧—— B 帧最省字节,但延迟最大。

三、324 倍是从哪来的

不是编码器魔法,是三件事叠起来:

来源 贡献 谁做的
色度减半采样 4:2:0 只存 1.5 字节/像素 采集阶段就已经这样
帧内 DCT 加量化 扔掉高频细节 编码器
帧间只存差值 不动的地方几乎不花字节 编码器

第三件是大头。我造的画面是缓慢移动的斜纹,帧与帧之间差别很小, 所以「差值」几乎全是零,熵编码一压就没了。 换成剧烈变化的画面,这个倍数会掉到几十倍——这也正是监控能省硬盘的原因:镜头前大部分时间没人动。

所以「压缩比」这个数没有普适值,看到别人报「压了 500 倍」先问他画面是什么。

四、编出来的裸流有什么毛病

out.h264 只是把编码后的 nal 单元按顺序摞在一起,没有任何索引。

缺什么 后果 在哪补
时长 播放器只能按帧率猜 下一章封装进 MP4
索引 不能拖进度条 同上
音频 只有画面没声音 后续章节

这就是为什么监控录像一定是 MP4/TS 而不是 .h264——裸流没法按时间检索, 而监控的第一需求恰恰是「回放某一段」。

五、动手 Demo:把一段 YUV 编成 H.264 裸流

Demo 用 PyAV 走完整条链:造 YUV → 编码 → 落盘 → 数包与帧类型。 数据全由代码算出来,压缩比不是估的。

c = av.open("out.h264", "w", format="h264")
s = c.add_stream("libx264", rate=30)
s.width, s.height, s.pix_fmt = 320, 240, "yuv420p"
s.options = {"crf": "23"}
for i in range(30):
    fr = av.VideoFrame.from_ndarray(frame(i), format="yuv420p")
    fr.pts = i
    for pk in s.encode(fr):
        c.mux(pk)
for pk in s.encode(None):        # 收尾,必须冲空包
    c.mux(pk)
c.close()

实跑输出:

--------------------------------------------------------------------
把一段 YUV 编成 H.264 裸流
--------------------------------------------------------------------
**编码这件事的目标只有一个:用更少的字节,保住尽量多的信息。**
**D44 先把这条链跑通,看压缩比到底是多少。**

  源:320×240、30 帧、YUV420p → **3.30 MB**
  每帧 115200 字节,30 帧正好 3.30 MB。

  编码:**57 毫秒**,输出 **10.4 KB**
  压缩比 **325 倍**,码率 **85 kbps**

  项       值     说明
  -----------------------------------------------
  包数     30     一帧一包(含 B 帧时也如此)
  I 帧     1      关键帧,能独立解
  P 帧     8      参考前面的帧
  B 帧     21     参考前后两边的帧

  **压缩比 324 倍**,靠的是三件事:
  一、色度已经减半采样(YUV420p 本身就是压缩过的一半);
  二、帧内用 DCT 加量化,把高频细节扔掉;
  三、帧间只存「和前一帧的差」,画面不动的地方几乎不花字节。

  **第三件是压缩比的大头**:我造的画面是缓慢移动的斜纹,帧间差很小,所以压得特别狠。
  **换成剧烈变化的画面,这个倍数会掉到几十倍**——这也是为什么监控能省硬盘:镜头前大部分时间没人动。

三个数要对得上:源 3.30 MB、输出 10.4 KB、30 帧。任何一个对不上,先查是不是漏了 fr.pts。 I 帧只有 1 个,这是默认 GOP 250 的结果——片子才 30 帧,够不着第二个 I 帧。 下一节(04.01.03)就专门拧这个旋钮。

30 秒实验

  1. 把 crf 改成 40,重跑,看文件小了多少、PSNR 掉了多少。
  2. 把 crf 改成 0(无损),看压缩比还剩多少——这时候帧间预测还在工作,所以仍然会比源小。
  3. 把 frame(i) 换成纯色块,看压缩比会飙到多少,想想为什么。

常见误区

  1. 以为裸 YUV 能自己知道自己多大。 它不能,四个参数必须从外面给。
  2. 以为压缩比是个固定值。 同一套参数,画面一动倍数就掉,报压缩比必须连画面一起报。
  3. 以为 .h264 是个能直接播的视频文件。 它是裸流,没时长没索引,播放器只能一帧帧顺着放。
自测三问(点开对答案) 1. 为什么 `-r` 漏了不花屏? —— 它只影响时间戳(每帧算多少秒),不影响像素怎么排。所以画面是对的,只是播放速度不对。 2. 压缩比 324 倍里,哪一部分是采集阶段就省下来的? —— 色度减半采样。 yuv420p 每像素只占 1.5 字节,而 rgb24 是 3 字节——**这一半在编码之前就省掉了**。 3. 为什么 30 帧只有 1 个 I 帧? —— 默认 gop_size 是 250,片子只有 30 帧,第一个 I 帧之后还没走到第二个。

下一节把「质量」和「分辨率」这两个旋钮分开拧,看它们各自管什么。

相关笔记