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

x264.exe --crf 23 和 av_opt_set(ctx, "crf", "23") 不是一套名字。 最坑的不是名字,是单位:x264 用 kbps,FFmpeg 用 bps,差 1000 倍。

📝 本节和前面几节的关系

上一节讲了 preset 的效果,这一节讲「怎么写对名字」。 这一节的核心方法只有一个:改完看结果变不变,不看返回值。

一、名字对照表

x264.exe 写法 FFmpeg/PyAV 写法 作用
--crf 23 crf=23 质量(越小越好)
--keyint 30 gop_size=30 关键帧间隔
--bframes 3 max_b_frames=3 B 帧数量
--preset medium preset="medium" 速度与体积
--tune zerolatency tune="zerolatency" 低延迟调优
--profile high profile="high" 编码档位
--vbv-maxrate 2000 maxrate=2000000 最大码率
--vbv-bufsize 4000 bufsize=4000000 缓冲大小

最后两行要注意单位:x264.exe 用 kbps,FFmpeg 的 maxrate/bufsize 用 bps。 照着文档填 2000,实际变成 2 kbps,画面糊成马赛克——而且不报错,只是效果离谱。 这个单位差是我见过最多的错,比名字写错还常见,因为名字写错起码不生效, 单位写错是「生效了但错得离谱」。

二、怎么验证「真生效了」

这是本节最值钱的一段。FFmpeg 设了不存在的选项时, av_opt_set 找不到就当没看见——不报错、不警告。 所以「设了没生效」和「名字写错了」在返回值上完全一样。

唯一可靠的办法:改完看结果变不变。

选项 文件大小 I 帧 B 帧 真生效吗
(基线) 10.4 KB 1 21 —
crf=40 5.5 KB 1 21 是(大小变了)
crf=15 13.9 KB 1 21 是(大小变了)
gop_size=5 33.9 KB 6 18 是(I 帧变多)
max_b_frames=0 10.8 KB 1 0 是(B 帧没了)
preset=ultrafast 19.2 KB 1 0 是(大小变了)
nonexistent=1 10.4 KB 1 21 否(被忽略)

最后一行是关键:文件大小和基线一模一样,但它不报错、不警告。 这就是 FFmpeg 的坑:你以为配置没生效,其实是名字写错了。

三、两个意外发现

意外一:preset=ultrafast 把 B 帧变成 0 了。 这就是 preset 的真实含义:它不是开关,是一整套参数预设值, 会顺带改掉你没注意的东西。所以「只调 preset」时,别以为其它参数还是原来的。

意外二:gop_size=5 同时把 B 帧从 21 降到 18。 因为 GOP 只有 5 帧,能塞的 B 帧本来就不多——这不是副作用,是参数之间本来就有耦合。

结论:报告任何一次「调参效果」时,必须把同时变化的其它指标一起写出来, 否则读者会以为是这个参数单独起作用。

四、x264-params 后门

所有 x264 私有参数都能从这里传,写法是「键=值、冒号分隔」:

ffmpeg -i in.yuv -c:v libx264 \
       -x264-params "ref=3:me=umh:subme=7" out.h264

这是最后一招:某个选项在 FFmpeg 里找不到对应名字, 先去 x264 --fullhelp 里找原名,再从这儿塞进去。 代价是拼错不报错,所以更要按上面那个「改完看结果」的办法验。

五、动手 Demo:命令行选项名 vs 库选项名

Demo 先把对照表列出来,再逐个改一项,看结果变不变。 nonexistent=1 那一行是故意写的,用来证明「不报错不等于生效」。

CASES = [("基线", {}), ("crf=40", {"crf": "40"}),
         ("crf=15", {"crf": "15"}),
         ("gop_size=5", {"gop_size": 5}),
         ("max_b_frames=0", {"max_b_frames": 0}),
         ("preset=ultrafast", {"preset": "ultrafast"}),
         ("nonexistent=1", {"nonexistent": "1"})]
for name, opt in CASES:
    c, s = open_enc("case.h264")
    s.options = opt
    ...  # 编完统计大小 / I 帧 / B 帧

实跑输出:

--------------------------------------------------------------------
命令行选项名 vs 库选项名
--------------------------------------------------------------------
**`x264.exe --crf 23` 和 `av_opt_set(ctx, "crf", "23")` 用的不是一套名字。**
**这一节把它们对齐,以后查文档不用来回猜。**

  x264.exe 写法            FFmpeg/PyAV 写法        作用
  ----------------------------------------------------------------------
  `--crf 23`               `crf=23`                 质量(越小越好)
  `--keyint 30`            `gop_size=30`            关键帧间隔
  `--bframes 3`            `max_b_frames=3`         B 帧数量
  `--preset medium`        `preset="medium"`        速度与体积
  `--tune zerolatency`     `tune="zerolatency"`     低延迟调优
  `--profile high`         `profile="high"`         编码档位
  `--vbv-maxrate 2000`     `maxrate=2000000`        最大码率
  `--vbv-bufsize 4000`     `bufsize=4000000`        缓冲大小

  **最后两行要注意单位**:x264.exe 用 kbps,**FFmpeg 的 `maxrate`/`bufsize` 用 bps**,
  差 1000 倍。
  **这个单位差是最常见的错**:照着文档填 `2000`,实际变成了 2 kbps,画面糊成马赛克。

  基线:crf 默认、GOP 默认、B 帧默认 → **10.4 KB,1 个 I 帧,21 个 B 帧**

**然后逐个改一项,看「真生效了没有」**——不是看报不报错,是看结果变不变:

  选项                 文件大小     I 帧     B 帧     真生效吗
  ----------------------------------------------------------------------
  crf=40               5.5 KB       1        21       是(大小变了)
  crf=15               13.9 KB      1        21       是(大小变了)
  gop_size=5           33.9 KB      6        18       是(I 帧变多)
  max_b_frames=0       10.8 KB      1        0        是(B 帧没了)
  preset=ultrafast     19.2 KB      1        0        是(大小变了)
  nonexistent=1        10.4 KB      1        21       否(被忽略)

  **`nonexistent=1` 那行是关键**:文件大小和基线一模一样,**但它不报错、不警告。**
  **这就是 FFmpeg 的坑**:设了不存在的选项,
  `av_opt_set` 找不到就当没看见——**你以为是配置没生效,其实是名字写错了。**
  **验证办法只有一条:改完看结果变不变**,不看返回值。

  **`preset=ultrafast` 那一行还有个意外**:B 帧变成 0 了,因为这一档默认就不开 B 帧。
  **这就是 preset 的真实含义**:它不是一个开关,**是一整套参数的预设值**,
  会顺带改掉你没注意的东西。
  **所以「只调 preset」时,别以为其它参数还是原来的。**

  **`x264-params` 是后门**:所有 x264 私有参数都能从这里传,写法是 `ref=3:me=umh` 这种「键=值、
  冒号分隔」。
  **查不到的选项,先去 x264 的 `--fullhelp` 里找名字,再从这儿塞。**

这个 Demo 的方法比结论重要:任何编码参数的效果验证,都该这么写——基线跑一遍,改一项跑一遍, 比的是结果,不是返回值。 nonexistent=1 那行不能省:它证明「不报错」这件事本身不可信, 这是全节最有说服力的一格数据。

30 秒实验

  1. 把 maxrate 按 x264 的习惯填 2000(不乘 1000),看码率掉到多少。
  2. 用 x264-params "ref=1" 试一次,看文件大小变不变。
  3. 故意把 crf 拼成 crff,确认它静默失败——并记下你应该怎么发现它。

常见误区

  1. 以为设了选项就一定生效。 名字错、单位错都静默失败,必须改完看结果。
  2. 以为 preset 只改速度。 它是一整套预设,会顺带把 B 帧、参考帧一起改掉。
  3. 以为 maxrate 2000 就是 2000 kbps。 FFmpeg 里是 bps,要写 2000000。
自测三问(点开对答案) 1. 为什么设了不存在的选项不报错? —— `av_opt_set` 在选项表里查不到名字就当没这一项,返回码不区分「设成功」和「没找到」。 **所以只能靠结果验证。** 2. `preset=ultrafast` 为什么 B 帧变 0? —— 这一档的预设值里 B 帧数就是 0。**preset 是一整套参数,不是单一开关。** 3. `--vbv-maxrate 2000` 对应 FFmpeg 里怎么写? —— `maxrate=2000000`。**x264 用 kbps,FFmpeg 用 bps,差 1000 倍。**

下一节换 H.265:多花几倍时间去换体积,这笔账在监控里划不划算。

相关笔记