七个库串成一条链,每一环的产物形状都不同。 测得出来才看得出瓶颈:小文件上换格式比解码贵十几倍。
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 秒实验
- 把
format="rgb24"换成"yuv420p",看时间差多少。 - 把 50 帧全部转成 numpy 存起来,看内存涨多少。
- 换一个 720p 的文件跑同一段,看哪一栏变成大头。
常见误区
- 把小文件上的结论当普遍规律。 帧越小,固定开销占比越大,结论会反转。
- 在循环里打时间戳。 测出来的主要是打印开销。
- 只看总时间不分环节。 总时间慢不知道改哪里,分环节才知道。
自测三问(点开对答案)
1. 为什么 libavutil 不在链上? —— 它不产生任何产物,是链下面那层公用的地基,每个环都在用。 2. 为什么小文件上「换格式」反而最贵? —— 每帧的固定开销(建上下文、算系数)与像素数关系不大,帧越小占比越高。 3. 给每环打时间戳要注意什么? —— 别在循环内部打,要在整段前后各取一次,否则测的是打印开销。- ☐ (本节完,下一节 4.3 FFmpeg 的解码流程分析)