搭一张图是六个动作,一步不能少。 最容易被跳过的是第六步 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 秒实验
- 把
link_to那两行对调,看报什么。 - 把
configure()去掉,抄下那句 DeprecationWarning 的原文。 - 用命名式给
scale只写width=64(不写 height),看它自己怎么算高度。
常见误区
- 省掉 configure。 能跑,但时间基是猜的。
- 以为报错会告诉你缺哪一头。 两种缺法报同一句。
- 一直用位置写法。 参数一多就记不住第几位是什么。
自测三问(点开对答案)
1. 搭图一共有哪六步? —— 建图、加源、加滤镜、加汇、连线、configure。 2. 为什么 configure 最容易被跳过? —— 不调也能跑,只是时间基会被猜成 1/1000。 3. 位置写法与命名写法该怎么选? —— 参数少用位置式,参数多或要跳着给就用命名式。- ☐ (本节完,下一节 9.3.2 AVFilter的API)