🎬 第四章 精通FFmpeg框架流程:击鼓传花之责任链设计模式
📄 一句话概括

七个库串成一条链,每一环的产物形状都不同。 测得出来才看得出瓶颈:小文件上换格式比解码贵十几倍。

📝 本节和前面几节的关系

4.1 讲了形状,这一节把形状套到真实的 FFmpeg 上。 下一篇 4.3 只放大解码这一环。

一、七个库在链上的位置

第二章按「库」讲,这一节按「环」讲,两者要对得起来。 环 库 产物


读字节 libavformat(协议在它内部) 字节 拆容器 libavformat 包 解码 libavcodec 帧 换格式 libswscale/libswresample 改了格式的帧 滤镜 libavfilter 处理过的帧 编码 libavcodec 包 写出去 libavformat 文件或流

libavutil 不在链上,它在链下面。 每个环都在用它,但它自己不产生任何产物。 libavdevice 与 libpostproc 也不在链上:前者是链的入口换成设备, 后者是链上已经没人用的一个旧环。 认清「在链上」与「在链下」的区别,第二章那张八库表才真正落地。

二、每一环的产物形状不同

形状不同决定了能不能跳过。 环 产物能不能直接播


字节 不能,还不是任何一种格式 包 不能,是压缩的 帧 不能,格式可能不对 改了格式的帧 能,只要显示端认

「帧」与「能显示的帧」是两种东西。 yuv420p 的帧算法能算,但多数显示接口只吃 rgb 或 nv12。 这一步转换常被忽略,却是最容易出问题的地方:颜色偏了、画面倒了、宽高反了,多半在这一环。

三、实测:每一环各花多少时间

理论说完了,看数据。 环 实测结论


open 毫秒级,主要是读文件头 demux 很快,只搬字节 decode 真算,但小文件上量不大 换格式 反而最贵,见下面的解释

这个结果反直觉,原因在「固定开销」四个字。 这个测试文件只有 160×120、50 帧。 帧越小,每帧调一次接口的固定成本占比就越大,换格式这一步每次要建缩放上下文、算系数, 这些成本与像素数关系不大。 换成 1080p 素材,结论会反过来:解码变成大头,换格式降到次要位置。 所以这张表说的是「小文件上固定开销很贵」,不是「解码一直很便宜」。

四、不测就猜,通常会猜错

这一节最该拿走的是方法,不是那几个数。 遇到卡顿,先给每一环打时间戳,再判断。 猜「解码慢」的结果,一半以上会猜错——真凶往往是缩放、重采样,或者干脆是读盘慢。 打时间戳只要三行:一进一出各取一次 perf_counter。成本低到没有理由不做。 唯一要注意的是别在循环里打,那样测的是打印速度不是处理速度。 要在整段前后各取一次,一次算总量。

五、动手 Demo:七库串成的链,每一环各花多少时间

同一份文件,给六个环节各打一个时间戳。

import time

import av

t = {}
t0 = time.perf_counter()
ic = av.open("_media32/tiny.mp4")
t1 = time.perf_counter()
st = ic.streams.video[0]
frames = [f for p in ic.demux(st) if p.size for f in p.decode()]
t2 = time.perf_counter()
arrs = [f.to_ndarray(format="rgb24") for f in frames]
t3 = time.perf_counter()
print("open %.2f" % ((t1 - t0) * 1000))
print("demux+decode %.2f" % ((t2 - t1) * 1000))
print("rgb24 %.2f" % ((t3 - t2) * 1000))
ic.close()

实跑输出:

--------------------------------------------------------------------
七库串成的链,每一环各花多少时间
--------------------------------------------------------------------

**① 先看这条链有哪几环**

  环         归哪个库                     产物
  --------------------------------------------------------
  读字节     协议层(libavformat 内)     缓冲里的字节
  拆容器     libavformat                  带时间戳的包
  解码       libavcodec                   原始帧
  换格式     libswscale/swresample       能显示的帧
  滤镜       libavfilter                  处理过的帧
  写出去     libavformat                  文件或网络流

**② 各环耗时实测**

  环               毫秒       占多少
  --------------------------------------
  open             3.22       3.8%
  取流             0.01       0.0%
  demux 51 包      0.59       0.7%
  decode 50 帧     4.09       4.8%
  rgb24 50 帧      77.47      90.7%
  close            0.09       0.1%
  合计             85.46      100%

**换格式那一环花了 77 毫秒,是解码的 19 倍。**

**这个结果反直觉,但要看清前提:** 这个文件只有 160×120,解码 50 帧才 4.1 毫秒。
**帧太小的时候,每帧的固定开销压过内容本身**,所以换格式反而显得贵。

换成 1080p 的素材,解码会变成大头,换格式的相对占比会降下来。
**别把这一张表当成普遍规律**,它说的是「小文件上固定开销很贵」。

**③ 这条链的价值:能看见每一环的成本**

播放器卡顿的时候,这张表能直接指方向:demux 慢是 IO 或网络问题,decode 慢是 CPU 不够,
换格式慢是尺寸或格式选错了。

**不测就猜,通常会猜错。** 很多「解码慢」的抱怨,实测下来是重采样或缩放开太大。

换格式那一栏通常是最大的,在这么小的文件上尤其明显。 数次跑动的绝对毫秒会变,比例关系稳定。

30 秒实验

  1. 把 format="rgb24" 换成 "yuv420p",看时间差多少。
  2. 把 50 帧全部转成 numpy 存起来,看内存涨多少。
  3. 换一个 720p 的文件跑同一段,看哪一栏变成大头。

常见误区

  1. 把小文件上的结论当普遍规律。 帧越小,固定开销占比越大,结论会反转。
  2. 在循环里打时间戳。 测出来的主要是打印开销。
  3. 只看总时间不分环节。 总时间慢不知道改哪里,分环节才知道。
自测三问(点开对答案) 1. 为什么 libavutil 不在链上? —— 它不产生任何产物,是链下面那层公用的地基,每个环都在用。 2. 为什么小文件上「换格式」反而最贵? —— 每帧的固定开销(建上下文、算系数)与像素数关系不大,帧越小占比越高。 3. 给每环打时间戳要注意什么? —— 别在循环内部打,要在整段前后各取一次,否则测的是打印开销。

相关笔记