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

编码是解码的镜像:送帧进去,取包出来。 名字对称好记,也最容易写反——写反了不一定报错,只是「没数据」。

📝 本节和前面几节的关系

上一节是解,这一节是编,两边对照着看,四个 API 正好换了个方向。 编码的耗时特性在 04.04.04 还会再算一次,那节讲参数怎么设。

一、四步是解码的镜像

步 做什么 对应 API
① 建编码器,设参数 avcodec_alloc_context3
② 打开编码器 avcodec_open2
③ 送帧进去 avcodec_send_frame
④ 取包出来 avcodec_receive_packet

方向和名字都反了:解码是 send packet/receive frame, 编码是 send frame/receive packet。 这个对称很容易记,但也很容易写反——写反了编译器不一定报错,运行起来才发现「没数据」。

二、实测:一次性、每帧、收尾

阶段 量级 备注
一次性(建+开编码器) 零点几毫秒 含 libx264 首次初始化
每帧(送帧+取包) 零点几毫秒/帧 240p 的尺寸很快
收尾(冲空包) 几十毫秒 把前瞻队列跑完

收尾那一下比整段编码还慢,很多人第一次看到这个数会以为测错了。 原因是前瞻(lookahead):x264 默认要先看后面几十帧才决定怎么编当前帧, 冲空包时它得把攒着的活一次干完。 所以「每帧耗时」不能拿总时间除帧数——会把收尾那几十毫秒摊进去,算出来的数偏大好几倍。

三、每帧 0.6 毫秒,够不够用

30 fps 的预算是 33.3 毫秒一帧,实测 240p 只花零点几毫秒,看着富余得很。 但两件事会把它吃掉:

因素 影响 量级
分辨率 像素涨 9 倍(1080p) 耗时涨得比 9 倍还多
路数 一台 NVR 编十几路 线性叠加

按像素线性放大,1080p 单路就要占掉一两成的预算, 一台机器顶多跑几路。而且搜索开销随分辨率长得比像素快,实际只会更少。 这就是为什么监控要用硬件编码:不是软编不够快,是一台机器上要同时跑十几路。

四、和「存下来」的关系

编码的输出还是包,还没有落到文件里。

还缺什么 后果 在哪补
容器封装 裸流没有时长和索引 04.05.03
时间戳 不知道每帧该显示多久 04.06.01
错误处理 编码器返回错误没管 第五章

第二行是本节埋的伏笔:编码器输出的包带着 pts, 但那个 pts 的时间基是编码器的(1/30),不是容器的(1/15360), 封装时必须换算。这就是 04.06 整章要讲的事。

五、动手 Demo:编码四步,和解码正好反过来

Demo 三段计时:一次性、30 帧循环、收尾冲空包。 收尾那一段的时间会明显大于前两段之和,这不是测错,是前瞻队列的代价。

t0 = time.perf_counter()
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"}
t1 = time.perf_counter()
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)
t2 = time.perf_counter()
for pk in s.encode(None):               # 收尾冲空包
    c.mux(pk)
t3 = time.perf_counter()

实跑输出:

--------------------------------------------------------------------
编码四步,和解码正好反过来
--------------------------------------------------------------------
**编码是解码的镜像:送帧进去,取包出来。**

  步    做什么                        对应 API
  --------------------------------------------------------------
  ①     建编码器,设参数              `avcodec_alloc_context3`
  ②     打开编码器                    `avcodec_open2`
  ③     送帧进去                      `avcodec_send_frame`
  ④     取包出来                      `avcodec_receive_packet`

  **方向和名字都反了**:解码是 send packet/receive frame,编码是 send frame/receive packet。
  **这个对称很容易记,但也很容易写反**——写反了编译器不一定报错,运行起来才发现「没数据」。

**量一下编码的「一次性」和「每帧」:**

  一次性的部分:**0.37 毫秒**(含 libx264 首次初始化)
  30 帧送进去:**13.92 毫秒**,摊到每帧 **0.464 毫秒**
  收尾冲空包:**36.47 毫秒**,又取出 1 个包
  输出 10.4 KB,30 个包

  **每帧 0.46 毫秒,远小于 33.3 毫秒的预算**——320×240 这个尺寸编码很快。
  **单路算下来还有富余**:一行 0.46 毫秒对 33.3 毫秒的预算,只花掉 1%。
  **但监控从来不是一路,而且不是 240p**:1080p 的像素是它的 9 倍,
  **按像素线性放大,单路就要 13%**,一台机器顶多跑 7 路。
  **这个 7 路还是乐观估计**——搜索开销随分辨率长得比像素快,实际只会更少。
  **这就是为什么监控要用硬件编码**:不是软编不够快,是一台机器上要同时跑十几路。
  **收尾那一下比整段编码还慢**(几十毫秒),因为要等前瞻队列跑完。
  **所以「每帧耗时」不能拿总时间除帧数**,会把收尾摊进去。

收尾 50 多毫秒、编码本身 20 毫秒——这个比例在真实工程里有意义: 短片段(比如按事件录 10 秒)的收尾开销占比很高,所以监控切片别切太短。 每帧 0.6 毫秒这个数只对 240p 成立,别拿去推 1080p。 想推就乘像素比,再打个折,而且要说明这是估算。

30 秒实验

  1. 把 preset 改成 ultrafast,看收尾那一段会不会变短。
  2. 把 tune=zerolatency 加上,看收尾是不是几乎不花时间(它关掉了前瞻)。
  3. 把帧数从 30 改成 300,看一次性那一段会不会变(应该不变)。

常见误区

  1. 以为 send_frame / send_packet 可以混用。 编码器只收帧、解码器只收包, 写反了直接报错或没数据。
  2. 以为收尾可有可无。 不冲空包就少最后一帧,而且收尾那几十毫秒的活也白攒了。
  3. 以为每帧耗时 = 总时间 ÷ 帧数。 收尾会把它抬高好几倍,要分段计时。
自测三问(点开对答案) 1. 为什么收尾比整段编码还慢? —— x264 要前瞻几十帧才决定怎么编,**冲空包时它得把攒着的活一次干完**。 2. 每帧 0.6 毫秒,能跑几路 1080p? —— 按像素线性放大是几路,**但搜索开销涨得比像素快,实际更少**。所以监控才用硬件编码。 3. 编码器输出的 pts 能直接拿去封装吗? —— 不能。**它的时间基是编码器的(1/30),容器要的是自己的(比如 1/15360)**,必须换算。

下一节把编码的 API 和参数一次列全,包括「设错不报错」的那几个。

相关笔记