编码是解码的镜像:送帧进去,取包出来。 名字对称好记,也最容易写反——写反了不一定报错,只是「没数据」。
上一节是解,这一节是编,两边对照着看,四个 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 秒实验
- 把
preset改成ultrafast,看收尾那一段会不会变短。 - 把
tune=zerolatency加上,看收尾是不是几乎不花时间(它关掉了前瞻)。 - 把帧数从 30 改成 300,看一次性那一段会不会变(应该不变)。
常见误区
- 以为
send_frame/send_packet可以混用。 编码器只收帧、解码器只收包, 写反了直接报错或没数据。 - 以为收尾可有可无。 不冲空包就少最后一帧,而且收尾那几十毫秒的活也白攒了。
- 以为每帧耗时 = 总时间 ÷ 帧数。 收尾会把它抬高好几倍,要分段计时。
自测三问(点开对答案)
1. 为什么收尾比整段编码还慢? —— x264 要前瞻几十帧才决定怎么编,**冲空包时它得把攒着的活一次干完**。 2. 每帧 0.6 毫秒,能跑几路 1080p? —— 按像素线性放大是几路,**但搜索开销涨得比像素快,实际更少**。所以监控才用硬件编码。 3. 编码器输出的 pts 能直接拿去封装吗? —— 不能。**它的时间基是编码器的(1/30),容器要的是自己的(比如 1/15360)**,必须换算。下一节把编码的 API 和参数一次列全,包括「设错不报错」的那几个。
- ☐ (本节完,下一节 04.04.04 视频编码流程及主要API)