解码一共十个 API,前六个只跑一次,后四个每包每帧都跑。 想优化性能先看后四个——把前六个调快没有意义。
上一节讲四步,这一节把四步展开成十个 API,并标清谁调用几次。 2026 年的现状要单独记:一批老 API 已经彻底删除。
一、十个 API,两类调用次数
| API | 作用 | 调用次数 |
|---|---|---|
avformat_open_input |
开容器 | 一次 |
avformat_find_stream_info |
探真实参数 | 一次 |
avcodec_find_decoder |
按 ID 找解码器 | 一次 |
avcodec_parameters_to_context |
流参数拷进解码器 | 一次 |
avcodec_open2 |
打开解码器 | 一次 |
av_read_frame |
读一个包 | 每包 |
avcodec_send_packet |
送包 | 每包 |
avcodec_receive_frame |
取帧 | 每次可能多帧 |
av_frame_unref |
还帧 | 每帧 |
av_packet_unref |
还包 | 每包 |
后四行的 unref 别嫌麻烦:FFmpeg 的帧和包都是引用计数,不还就会一直占内存,
长跑(监控就是长跑)迟早吃满。
PyAV 用 Python 的对象生命周期自动管这个,所以本文的 Demo 里看不到 unref——但 C 里必须写。
二、老 API 已经删了,抄代码前先看年份
| 旧写法(已删) | 新写法 |
|---|---|
avcodec_decode_video2 |
send_packet + receive_frame |
avcodec_encode_video2 |
send_frame + receive_packet |
av_register_all |
已删除,什么都不用调 |
avcodec_close |
avcodec_free_context |
第三行要特别说:网上大量教程开头就是 av_register_all(),它在 FFmpeg 4.0 之后就没有了,
照抄会编译不过。
这不是「版本不同」,是「彻底删掉了」——看到教程第一行是这个,基本可以判断它至少七年没更新。
三、一次性 VS 每帧:这笔账怎么算
实测把两段分开计时:
| 部分 | 量级 | 随片长增长吗 |
|---|---|---|
| 一次性(①②建解码器) | 几毫秒 | 不增长 |
| 每帧(③④送与取) | 每帧零点几毫秒 | 线性增长 |
一次性那部分占总时间一两成,看着不小,但它不随帧数增长。 播 10 秒和播 1 小时,这部分开销完全一样——这就是「起播要等一下、之后很流畅」的原因。
所以优化要分场景:起播慢就查 ①②(多半是 find_stream_info 在读太多数据),
播起来卡就查 ③④(解码器太慢,或者像素格式转换没开硬件)。
四、每帧那部分还能再压吗
能,但不在解码器本身。三个常见手段:
| 手段 | 省什么 | 代价 |
|---|---|---|
| 跳过非参考帧 | 直接不解 B 帧 | 画面会顿 |
| 多线程解码 | 多个核一起解 | 内存翻倍 |
| 硬解(NVDEC 等) | CPU 几乎不占 | 兼容性差、有延迟 |
第一行监控里很常见:只解 I/P 帧,B 帧直接扔,反正监控画面本来就不追求流畅, 省下来的算力可以多解几路。 第三行是趋势:现在 NVR 基本都带硬解,一路 1080p 只占几个百分点的 CPU。
五、动手 Demo:解码的主要 API,名字和用途对齐
Demo 分两段:先把十个 API 和调用次数列清,再用 PyAV 量「一次性」和「每帧」的比例。 PyAV 把十个 API 包成了几个方法,但底层走的是同一串调用。
t0 = time.perf_counter()
c = av.open("in.h264") # open_input + find_stream_info
s = c.streams.video[0] # find_decoder + parameters_to_context
cc = s.codec_context # 等价于 avcodec_open2 前的准备
t1 = time.perf_counter()
n = 0
for pk in c.demux(s):
if pk.size == 0:
continue
for fr in pk.decode():
n += 1
t2 = time.perf_counter()
print("一次性 %.2f ms, 每帧 %.3f ms" % ((t1-t0)*1000, (t2-t1)*1000/n))
实跑输出:
--------------------------------------------------------------------
解码的主要 API,名字和用途对齐
--------------------------------------------------------------------
**FFmpeg 的解码 API 从 3.x 到 4.x 改过一次**,**老的 `avcodec_decode_video2` 已经删除**,
新的是 send/receive 两个函数。
API 作用 调用次数
--------------------------------------------------------------------------
`avformat_open_input` 开容器 一次
`avformat_find_stream_info` 探真实参数 一次
`avcodec_find_decoder` 按 ID 找解码器 一次
`avcodec_parameters_to_context` 把流参数拷进解码器 一次
`avcodec_open2` 打开解码器 一次
`av_read_frame` 读一个包 每包
`avcodec_send_packet` 送包 每包
`avcodec_receive_frame` 取帧 每次可能多帧
`av_frame_unref` 还帧 每帧
`av_packet_unref` 还包 每包
**这十行里,只有后四个是「每帧都跑」的。**前六个是一次性的初始化。
**所以性能优化先看后四个**,把前六个调快没有意义。
**2026 现状:老 API 已删。**
旧写法(已删) 新写法
--------------------------------------------------------------------
`avcodec_decode_video2` `send_packet` + `receive_frame`
`avcodec_encode_video2` `send_frame` + `receive_packet`
`av_register_all` 已删除,什么都不用调
`avcodec_close` `avcodec_free_context`
**第三行要特别说**:网上大量教程还有 `av_register_all()`,**在 FFmpeg 4.0 之后就没有了**,
照抄会编译不过。
**这不是「版本不同」,是「彻底删掉了」**——抄代码前先看年份。
**用 PyAV 把这十步走一遍,量一下「一次性」和「每帧」的差距:**
一次性部分(开容器+取流+建解码器):**2.74 毫秒**
每帧部分(28 帧的送与取):**9.38 毫秒**,摊到每帧 0.335 毫秒
**一次性部分占总时间的 23%**,看着不小,**但它不随帧数增长。**
**播 10 秒和播 1 小时,这部分开销完全一样**——这就是「起播要等一下、之后很流畅」的原因。
PyAV 的 av.open() 一口气干了 ① ② 两步,find_stream_info 也在里面,
所以分不出「找流」和「开容器」各自的耗时——这就是为什么表里只有两段。
要分项计时只能用 C。这里如实说明「分不出」,不硬凑三个数。
30 秒实验
- 把
av.open的timeout参数设成 1 秒,用一段网络流试,看 ① 会不会超时。 - 把解码循环里的帧数统计改成「只统计 I 帧」,看能不能跑到更快。
- 用一段 1080p 的片子重跑,看每帧耗时涨多少倍。
常见误区
- 以为十个 API 都要循环调用。 前六个只跑一次,在循环里调是性能灾难。
- 以为
av_register_all()只是过时。 它被删了,编译直接不过。 - 以为一次性开销小可以忽略。 它占总时间一两成,而且起播等待就是它造成的。
自测三问(点开对答案)
1. 为什么 `av_frame_unref` 必须调? —— FFmpeg 的帧是引用计数,**不还就一直占内存**。监控是长跑程序,几小时就吃满。 2. 播 1 小时和播 10 秒,一次性开销一样吗? —— 一样。**它只和「开容器、建解码器」有关,和帧数无关。** 3. PyAV 里为什么量不出「开容器」和「找流」各自的耗时? —— `av.open()` 把两步合在一起做了,**Python 层看不到分界**。要分项只能用 C 直接调。下一节把方向反过来:编码的四步,和解码正好对称。
- ☐ (本节完,下一节 04.04.03 视频编码过程简介)