责任链就是「一个请求顺着链子往下传,谁也拦不住谁」。 滤镜链是责任链,转码链本身也是责任链——第四章已经见过一次。
9.1.1 讲了滤镜在五环里的位置,这一节讲它为什么长成「链」的样子。 第四章的击鼓传花讲的是模块之间的链,这里讲的是滤镜之间的链。
一、什么是责任链
三个角色,缺一不可。 角色 在滤镜里是什么 职责
请求 一帧画面 被处理的那个东西 处理者 一个滤镜节点 干自己那一件事 链 link_to 连起来的路 把请求传下去
关键是「每个处理者只干一件事,干完就往下传」。缩放节点不看颜色,调色节点不管尺寸,各管一段。 这样写的好处是加法式扩展:要加一个效果,只是在链里插一个节点,其余节点的代码一个字都不用动。
二、第四章已经见过一次
FFmpeg 整个框架就是责任链搭起来的,滤镜只是最显眼的那一段。 哪一层 链上的节点 传的是什么
转码链 解封装→解码→编码→封装 包与帧 滤镜链 scale→hflip→format 帧 协议链 AVIO 的读写回调 字节
三层的形状一模一样:一个数据、一串处理器、一路传到底。 所以看懂了滤镜链,等于又看了一遍整个框架,只是数据从包换成帧、从帧换成字节。
三、链子越长就越慢吗
实测:同一条链,长度从 0 加到 3,各跑三十帧。 链长 帧数 耗时
0 30 几毫秒 1 30 几毫秒 2 30 几毫秒 3 30 几毫秒
四次结果全挤在同一个量级里,看不出「越长越慢」,最长的那条链甚至比最短的还快一点点。 原因不是滤镜不花钱,是量级差太远:解码一帧几毫秒, 而翻转这种「逐像素搬一遍」在 160×120 上只要几十微秒。噪声把信号盖住了。 想量滤镜本身,就别把解码算进去:拿现成的帧反复 push/pull,只计这一段时间。 这是量任何「小开销」时的通用手法。
四、责任链真正的价值
不在省时间,在可拆装。 要做的事 责任链写法 硬编写法
加一个效果 插一个节点 改函数体 关掉某个效果 删一个节点 加 if 判断 调整顺序 换连线 重写函数
硬编写法最要命的不是难看,是「加一个效果要动老代码」。动老代码就有回归风险, 责任链把这种风险关在一个新节点里。 这也是直播里能「按需开关美颜」的原因:开关不过是决定图上有没有那个节点。
五、链子的两端为什么是端点
buffer 与 buffersink 不是滤镜,它们是链的两端。 节点 像什么 为什么不算滤镜
buffer 水龙头 它不处理,只供数据 buffersink 下水口 它不处理,只收数据
责任链要有头有尾才成立:没头不知道数据从哪来,没尾不知道结果交给谁。 这也解释了一个常见报错:少任何一头都配不通,而且报的都是同一句「Invalid argument」, 看不出缺哪头。所以查错要按「头、尾、中间」三段过一遍。
六、动手 Demo:链子越长就越慢吗
同一条素材,链长 0/1/2/3 各跑一遍,只量「解码+滤镜」这一段。
import time
import av
import av.filter
from fractions import Fraction
def build(chain):
g = av.filter.Graph()
src = g.add_buffer(width=160, height=120,
format="yuv420p",
time_base=Fraction(1, 25))
prev = src
for name, args in chain:
node = g.add(name) if args is None else g.add(name, args)
prev.link_to(node)
prev = node
sink = g.add("buffersink")
prev.link_to(sink)
g.configure()
return src, sink
chains = [ [], [("hflip", None)],
[("hflip", None), ("vflip", None)],
[("hflip", None), ("vflip", None),
("scale", "160:120")] ]
for ch in chains:
src, sink = build(ch)
t = time.perf_counter()
for i in range(30):
src.push(make_frame(i))
sink.pull()
print(len(ch), (time.perf_counter() - t) * 1000)
实跑输出:
--------------------------------------------------------------------
链子越长就越慢吗
--------------------------------------------------------------------
**① 同一条链,长度从 0 加到 3**
链长 帧数 耗时
------------------------------
0 30 4.0 毫秒
1 30 3.5 毫秒
2 30 6.4 毫秒
3 30 4.6 毫秒
**四次都在 3.5 到 6.4 毫秒之间,看不出「越长越慢」。** 最长的那条链甚至比最短的还快一点点。
**原因是量级差太远。** 解码一帧动辄几毫秒,
而 hflip 这种「逐像素搬一遍」在 160×120 的图上只要几十微秒。**噪声把信号盖住了。**
**② 那该量什么才对**
**要量滤镜本身,就别把解码算进去。** 拿现成的帧反复送进图里,只计 push 与 pull 的时间。
**这个结论比「链越长越慢」有用**:真实系统里滤镜几乎不会是瓶颈,
**找性能问题先怀疑编解码与 IO,别先怀疑滤镜。**
**但责任链的价值本来也不在省时间**,在可拆装:加一个滤镜只是多一次 `g.add` 与一个 `link_to`,
其余代码一律不动。
四组结果全在一个量级,链长 3 反而不比链长 0 慢——噪声盖住了信号。 要量滤镜本身,就别把解码算进去,只量 push 与 pull 之间那段。
30 秒实验
- 把链长加到 10(十次 hflip),看耗时有没有明显变化。
- 把
scale换成scale=1280:720(放大八倍),再看耗时的台阶。 - 把每次 push 的帧换成新分配的大帧(640×480),比较两组的差值。
常见误区
- 以为链越长越慢。 小图上多挂几个滤镜,差别在噪声里。
- 把解码时间算进滤镜的账。 解码几毫秒、滤镜几十微秒,混在一起看不出来。
- 以为 buffer 也是个滤镜。 它只是端点,不做任何处理。
自测三问(点开对答案)
1. 责任链里每个处理者该干几件事? —— 一件。这样加效果才是插节点,而不是改老代码。 2. 为什么实测看不出「链越长越慢」? —— 解码的耗时比滤镜大两三个数量级,噪声把滤镜那点开销盖住了。 3. 责任链最大的好处是什么? —— 可拆装:加、删、换顺序都只动连线,不动已有节点的代码。- ☐ (本节完,下一节 9.1.3 简单滤镜和复杂滤镜之间的区别)