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

解码一共十个 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 秒实验

  1. 把 av.open 的 timeout 参数设成 1 秒,用一段网络流试,看 ① 会不会超时。
  2. 把解码循环里的帧数统计改成「只统计 I 帧」,看能不能跑到更快。
  3. 用一段 1080p 的片子重跑,看每帧耗时涨多少倍。

常见误区

  1. 以为十个 API 都要循环调用。 前六个只跑一次,在循环里调是性能灾难。
  2. 以为 av_register_all() 只是过时。 它被删了,编译直接不过。
  3. 以为一次性开销小可以忽略。 它占总时间一两成,而且起播等待就是它造成的。
自测三问(点开对答案) 1. 为什么 `av_frame_unref` 必须调? —— FFmpeg 的帧是引用计数,**不还就一直占内存**。监控是长跑程序,几小时就吃满。 2. 播 1 小时和播 10 秒,一次性开销一样吗? —— 一样。**它只和「开容器、建解码器」有关,和帧数无关。** 3. PyAV 里为什么量不出「开容器」和「找流」各自的耗时? —— `av.open()` 把两步合在一起做了,**Python 层看不到分界**。要分项只能用 C 直接调。

下一节把方向反过来:编码的四步,和解码正好对称。

相关笔记