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

编码比解码多一件麻烦事:要设一堆参数,而且设错不报错。 六个 API 的顺序有硬要求:私有选项必须在 open 之前设。

📝 本节和前面几节的关系

上一节讲流程,这一节讲 API 顺序和参数。 参数的效果在 04.02 已经量过一轮,这里补「怎么设进去」。 选项名与单位的坑在 04.02.02,本节只提最关键的那几条。

一、五个必设参数,两个「能用但不合适」

参数 在哪设 不设会怎样
宽高 VideoCodecContext 报错(必需)
像素格式 pix_fmt 报错或转错
帧率 framerate 时间戳算错
码率/质量 bit_rate 或私有选项 用默认值,可能糊
GOP gop_size 用默认 250,seek 很慢

前三个是必需的,不设直接报错; 后两个是「能用但不合适」——这才是最麻烦的,因为程序照跑,问题要到回放时才发现。 帧率那一行尤其隐蔽:不设帧率,编码器按默认 25 算时间戳,你按 30 fps 给的 pts 就会被打乱, 播放出来快慢不对。

二、六个 API 与硬性顺序

API 作用 顺序
avcodec_find_encoder 按名字找编码器 1
avcodec_alloc_context3 建上下文 2
av_opt_set 系列 设私有选项 3
avcodec_open2 打开 4
avcodec_send_frame 送帧 5,循环
avcodec_receive_packet 取包 6,循环

第 3 步必须在第 4 步之前:编码器打开之后再设私有选项,有的是无效的,有的直接报错。 这个顺序错在代码里很难看出来——因为 av_opt_set 本身返回成功。 这就是 04.02.02 讲的「改完看结果」那套办法的用处。

三、实测四种参数组合

参数 大小 每帧字节
crf=18 高质 8.1 KB 1030
crf=23 默认 6.3 KB 811
crf=35 低质 3.9 KB 493
bit_rate=500k 5.5 KB 699

bit_rate 那一行是「固定码率」模式,和 CRF 是两条路: CRF 是「按质量给码率」,画面复杂就自动多给;bit_rate 是「不许超」,画面复杂就降质量。

监控常用固定码率,因为要算带宽和硬盘(16 路 × 每路多少 Mbps,硬盘要多大, 都是拿码率乘出来的);离线压制用 CRF,因为要保画质。

四、三条容易踩的坑

坑 现象 怎么办
选项写在 open 之后 不报错但无效 挪到 open 之前
maxrate 忘了乘 1000 画面糊成马赛克 bps,不是 kbps
宽高是奇数 报 width not divisible by 2 改成偶数

第二行是本节最实用的:x264.exe 用 kbps,FFmpeg 用 bps,差 1000 倍。 第三行源于 yuv420p 的结构:两个色度平面各是宽高的一半,奇数除不尽, 所以 641 宽这种尺寸直接编不了。

五、动手 Demo:编码的主要 API 与参数设置

Demo 先列六个 API 的顺序,再用四种参数组合各编一遍,量体积和每帧字节。 重点是看 bit_rate 和 crf 两条路的差别。

def enc(opts, bitrate=None):
    c = av.open("t.h264", "w", format="h264")
    s = c.add_stream("libx264", rate=30)      # 1 找 + 2 建
    s.width, s.height, s.pix_fmt = 320, 240, "yuv420p"
    if bitrate:
        s.bit_rate = bitrate                  # 固定码率那条路
    else:
        s.options = {"crf": str(opts)}        # 3 设私有选项
    for i in range(30):                       # 4 打开由 encode 首次触发
        ...

实跑输出:

--------------------------------------------------------------------
编码的主要 API 与参数设置
--------------------------------------------------------------------
**编码比解码多一件麻烦事:要设一堆参数。**
**设错不报错,只是效果不对。**

  参数                  在哪设                    不设会怎样
  ----------------------------------------------------------------------
  宽高                  `VideoCodecContext`       报错(必需)
  像素格式              `pix_fmt`                 报错或转错
  帧率                  `framerate`               时间戳算错
  码率/质量            `bit_rate` 或私有选项     用默认值,可能糊
  GOP                   `gop_size`                用默认 250,seek 很慢

  **前三个是必需的**,不设直接报错;**后两个是「能用但不合适」**——这才是最麻烦的,因为程序照跑。

  API                           作用                    顺序
  ------------------------------------------------------------------
  `avcodec_find_encoder`        按名字找编码器          1
  `avcodec_alloc_context3`      建上下文                2
  `av_opt_set` 系列             设私有选项              3
  `avcodec_open2`               打开                    4
  `avcodec_send_frame`          送帧                    5,循环
  `avcodec_receive_packet`      取包                    6,循环

  **第 3 步必须在第 4 步之前**:编码器打开之后再设私有选项,**有的是无效的,有的直接报错。**
  **这个顺序错在代码里很难看出来**——因为 `av_opt_set` 本身返回成功。
**实测几种参数组合,看输出差多少:**

  参数                      大小          每帧字节
  --------------------------------------------------------
  crf=18 高质               8.1 KB        1030
  crf=23 默认               6.3 KB        811
  crf=35 低质               3.9 KB        493
  bit_rate=500k             5.5 KB        699

  **`bit_rate` 那一行是「固定码率」模式**,和 CRF 是两条路:
  CRF 是「按质量给码率」,**画面复杂就自动多给**;bit_rate 是「不许超」,**画面复杂就降质量**。
  **监控常用固定码率**,因为要算带宽和硬盘;**离线压制用 CRF**,因为要保画质。

PyAV 在你第一次 encode() 时才真正调 avcodec_open2, 所以「选项要在 open 之前设」在 Python 里表现为「要在 encode() 之前设」。 bit_rate 那行 699 字节/帧 ≈ 5.6 KB 总量,和 crf=23 的 811 差不太多, 因为这段画面本来就好压,500k 的额度用不完。想看出固定码率的威力,得用复杂画面。

30 秒实验

  1. 把 bit_rate 设成 50000(50 kbps),看画面糊到什么程度、每帧字节掉到多少。
  2. 故意把选项设在 encode() 之后(用一个已经编过的流),看是不是静默无效。
  3. 把宽改成 641,看报错原文。

常见误区

  1. 以为参数设了就行。 顺序错、名字错、单位错都静默失败,只能靠「改完看结果」验证。
  2. 以为 CRF 和固定码率可以混着用。 两条路,-crf 优先,bit_rate 退化成参考。
  3. 以为奇数宽高只是「不整齐」。 yuv420p 除不尽,直接编不了。
自测三问(点开对答案) 1. 为什么私有选项要在 `avcodec_open2` 之前设? —— 编码器打开时会把选项读进自己的内部结构,**之后改的上层副本不再生效**,有的还会报错。 2. 监控为什么用固定码率而不用 CRF? —— 要算带宽和硬盘容量。**16 路 × 每路 4 Mbps × 天数,是直接乘出来的**,CRF 给不了这个确定性。 3. 641 宽为什么编不了 yuv420p? —— 色度平面是宽高各一半,**641 除以 2 除不尽**。所以宽高必须是偶数。

下一节开始拼完整案例:从 YUV 文件一路做到能播的文件。

相关笔记