🎬 第九章 AVFilter过滤器层理论及案例实战
📄 一句话概括

搭一张图是六个动作,一步不能少。 最容易被跳过的是第六步 configure,因为它不调也能跑。

📝 本节和前面几节的关系

9.1 讲原理与形状,这一节把它落成固定的六步动作。 下一节 9.3.2 讲这六步背后的 API。

一、六步,一步不能少

每一步都有它缺了之后的具体后果。 步 做什么 少了会怎样


一 建图 没地方放节点 二 加源 buffer 帧喂不进去 三 加滤镜 没什么可干 四 加汇 buffersink 配不通 五 连线 link_to 配不通 六 configure 能跑,但时间基是猜的

前五步的错都会当场报出来,第六步的错不会。 这就是它危险的地方。 所以「能跑」不等于「对了」——这在音视频里是个反复出现的主题, 第 9.4.2 那个时长差几百倍的坑也是这一类。

二、第六步为什么最容易被跳过

不调 configure 真的能跑,这是实测。 做法 结果 时间基从哪来


调了 正常出帧 你给的那个 没调 也出帧 PyAV 按 1/1000 猜

PyAV 在背后替你配了一次,代价是它得猜时间基。 猜的是 1/1000, 同时报一句 DeprecationWarning——而这句警告跑在日志里,很容易被淹没。 1/1000 恰好是「毫秒刻度」,看上去很合理,所以错得不明显。 等你发现时长不对、音画不同步,已经在查别的地方了。 结论很简单:configure 显式调,time_base 显式给。 两行代码,省掉后面半天的排查。

三、参数两种写法

同一条链,两种写法结果一样。 写法 例子 适合


位置式 "80:60" 参数少的滤镜 命名式 "width=80:height=60" 参数多、要跳着给

位置写法短,但只对参数少的滤镜好用。 像 overlay 有十几项参数, 位置写法根本记不住第几个是什么。 命名写法还有个好处:可以只给想改的那几项,其余走默认值。位置写法必须从第一个给起, 中间跳不过去。 所以实际写代码时,养成用命名式的习惯:多打几个字,省掉查第几位的功夫。

四、三种错法

三种错,报错信息能提供的线索完全不同。 错法 报什么 线索够不够


缺 sink ArgumentError 不够,要自己查 缺 buffer ArgumentError 不够,同上 push 与 encode 顺序反 不报错 完全没有

前两种报同一句话,第三种一句都不报。 三种都得靠「按结构过一遍」来查。 最有效的排查顺序是「从两头往中间」:先确认头直连尾能跑通,再一段一段把中间加回去。 这跟调试任何流水线都是同一个思路。

五、一张能跑的最小代码

把六步写成代码,就是这个样子。 行 写什么


一 g = Graph() 二 src = g.add_buffer(宽、高、格式、时间基) 三 node = g.add("scale", "80:60") 四 sink = g.add("buffersink") 五 src.link_to(node); node.link_to(sink) 六 g.configure()

多加一个滤镜,只多一行 add 与一行 link_to,前面那些行一个字不用改。 这就是责任链的好处,在代码里看得最清楚。 写好这六行,再往里喂帧就行。 后面的复杂度全在「选哪个滤镜、参数怎么给」上, 不在搭图的形状上。

六、动手 Demo:搭一张滤镜图的六个动作

按六步搭一次,再试位置写法与命名写法是否等价,最后用命令改参数。

import av
import av.filter
from fractions import Fraction

def build(args):
    g = av.filter.Graph()
    s = g.add_buffer(width=160, height=120, format="yuv420p",
                      time_base=Fraction(1, 25))
    sc = g.add("scale", args)
    k = g.add("buffersink")
    s.link_to(sc); sc.link_to(k)
    g.configure()
    s.push(make_frame(0))
    return k.pull().width

print("位置写法", build("80:60"))
print("命名写法", build("width=80:height=60"))

# 图搭好之后,发命令改参数
g = av.filter.Graph()
s = g.add_buffer(width=160, height=120, format="yuv420p",
                  time_base=Fraction(1, 25))
sc = g.add("scale", "80:60")
k = g.add("buffersink")
s.link_to(sc); sc.link_to(k)
g.configure()
s.push(make_frame(0))
print("改之前", k.pull().width)
sc.process_command("width", "40")
sc.process_command("height", "30")
s.push(make_frame(1))
print("改之后", k.pull().width)

实跑输出:

--------------------------------------------------------------------
搭一张滤镜图的六个动作
--------------------------------------------------------------------

**① 六步,一步不能少**

  步     做什么                 少了会怎样
  ------------------------------------------------------
  一     建图                   没地方放节点
  二     加源(buffer)         帧喂不进去
  三     加滤镜                 没什么可干
  四     加汇(buffersink)     配不通
  五     连线 link_to           配不通
  六     configure              能跑,但时间基是猜的

**顺序里最容易被跳过的是第六步**,因为不调也能跑。
但它会按 **1/1000** 去猜时间基,**猜错了后面的时间戳全歪。**

**② 参数两种写法,结果一样**

`"80:60"` 出 **80**,`"width=80:height=60"` 出 **80**。**两种都行。**

**位置写法只对参数少的滤镜好用。** 参数一多(比如 overlay 有十几个),就得用 `名字=值` 那种,
否则根本记不住哪个位置是什么。

**③ 图搭好之后,还能改参数**

图搭好之后对 scale 节点发两条命令,出帧宽从 **80** 变成 **40**。

**这条能力很实用**:直播中要动态改画质与尺寸,
不必把整张图拆了重搭——**拆图会导致几帧丢失,而改参数不会。**

位置式与命名式结果一样,但命名式能跳着给参数。 process_command 让宽度从 80 变 40,图没重建。

30 秒实验

  1. 把 link_to 那两行对调,看报什么。
  2. 把 configure() 去掉,抄下那句 DeprecationWarning 的原文。
  3. 用命名式给 scale 只写 width=64(不写 height),看它自己怎么算高度。

常见误区

  1. 省掉 configure。 能跑,但时间基是猜的。
  2. 以为报错会告诉你缺哪一头。 两种缺法报同一句。
  3. 一直用位置写法。 参数一多就记不住第几位是什么。
自测三问(点开对答案) 1. 搭图一共有哪六步? —— 建图、加源、加滤镜、加汇、连线、configure。 2. 为什么 configure 最容易被跳过? —— 不调也能跑,只是时间基会被猜成 1/1000。 3. 位置写法与命名写法该怎么选? —— 参数少用位置式,参数多或要跳着给就用命名式。

相关笔记