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

六个 API 对应六个动作,一一对得上。 两个实测各说一件事:参数能运行时改,格式会被滤镜偷偷换掉。

📝 本节和前面几节的关系

9.3.1 讲了六步动作,这一节讲每一步背后调的是哪个函数。 9.4 会把这套 API 用到完整的一条转码链里。

一、C 的 API 对照

左边是 C 的函数名,右边是 PyAV 里怎么调。 C 的 API PyAV 里


avfilter_graph_alloc av.filter.Graph() avfilter_graph_create_filter graph.add(name, args) avfilter_link a.link_to(b) avfilter_graph_config graph.configure() av_buffersrc_add_frame buffer.push(frame) av_buffersink_get_frame sink.pull() avfilter_graph_queue_command node.process_command()

七行里前六行是搭图与跑图,最后一行是「改」。 这一行最容易被忽略, 但它决定了这套 API 能不能用在直播里。 PyAV 这一列其实是薄薄一层包装,参数与顺序基本照抄 C。 所以看得懂 C 的文档,就等于看得懂 PyAV。

二、实测一:运行时改尺寸真的生效

图配好、帧推过一帧之后,再发命令。 时刻 输出宽度 图重建了吗


改之前 80 — 发两条命令之后 40 没有

process_command("width", "40") 与 process_command("height", "30")两条发下去, 下一帧出来宽度就是 40。图没拆、没重配。 这条能力在直播里很值钱:观众带宽不够时现场降分辨率,不用断流重连。 断流重连对观众的观感就是「卡了一下」。 代价是那一瞬间画面会有跳变, 而且编码器得跟着改参数——所以真实产品里通常只在小范围内动,不会从 1080p 一刀切到 360p。

三、实测二:音频滤镜会偷偷改格式

同一条音频,加不加 volume,输出格式不一样。 链 输出格式 样本数 首样本值


不加滤镜 s16 1024 1000 volume=0.5 flt 1024 0.01526 volume=2.0 flt 1024 0.06104

注意第二列:加滤镜之后格式从 s16 变成了 flt。因为 volume 内部按浮点算, 它顺手把格式也转了。 样本数没变、只有格式变了,所以这件事不会引起任何报错——你按整数去读,读出来是垃圾。 这个坑在真实项目里非常常见:音频处理链一加滤镜,下游那段代码的读法就全错了。 而错的表现是「声音变噪音」,不是异常。

四、格式变了,值为什么也变

同一个 1000,在两种格式里根本不是同一个数。 格式 范围 1000 的含义


s16 −32768 到 32767 整数,按 32768 归一 flt −1.0 到 1.0 浮点,直接就是比例

1000 除以 32768 约等于 0.0305,再乘 0.5 就是 0.01526。 所以 0.01526 不是滤镜算错,是基准换了。 这也解释了为什么乘 2 之后是 0.06104:0.0305 乘 2,正好翻倍。 两行一对照,就验证了「它只是换了刻度」。 记住一个判据:拿到音频帧先看 format.name,别默认它还是你送进去的那个格式。

五、什么时候该怀疑格式被改

三个征兆,出现任何一个都先查格式。 征兆 多半是什么


数值小得离谱(0.0x) 整数变成了浮点 数值大得离谱(几万) 浮点被当整数读 声音变噪音但没报错 读法跟格式对不上

第三种最难查,因为它不报错。 有效办法是在链的入口与出口各打一行格式,两头一对比就知道是哪一环换的。 这一招在视频那头也管用:滤镜把 yuv420p 换成 rgb24 也是同样的道理, 只是视频的错更显眼——颜色不对,肉眼就看出来了。

六、动手 Demo:AVFilter 的 API 与两处实测

先实测运行时改宽度,再拿音频跑一遍,看格式与数值怎么一起变。

import av
import av.filter
from fractions import Fraction

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)

# 音频侧:加不加 volume,输出格式不一样
def audio(args):
    ga = av.filter.Graph()
    buf = ga.add_abuffer(format="s16", sample_rate=8000,
                         layout="mono", time_base=Fraction(1, 8000))
    node = ga.add("volume", args)
    snk = ga.add("abuffersink")
    buf.link_to(node); node.link_to(snk)
    ga.configure()
    fa = av.AudioFrame(format="s16", layout="mono", samples=1024)
    fa.sample_rate = 8000
    fa.planes[0].update(bytes([0xE8, 0x03]) * 1024)
    buf.push(fa)
    out = snk.pull()
    return (out.format.name, out.samples,
            out.to_ndarray().flat[0])

print(audio("1.0"))
print(audio("0.5"))

实跑输出:

--------------------------------------------------------------------
AVFilter 的 API 与两处实测
--------------------------------------------------------------------

**① C 的 API 对照**

  C 的 API                         PyAV 里
  -----------------------------------------------------------
  avfilter_graph_alloc             av.filter.Graph()
  avfilter_graph_create_filter     graph.add(name, args)
  avfilter_link                    a.link_to(b)
  avfilter_graph_config            graph.configure()
  av_buffersrc_add_frame           buffer.push(frame)
  av_buffersink_get_frame          sink.pull()
  avfilter_graph_queue_command     node.process_command()

**② 实测一:运行时改尺寸真的生效**

改之前 **80**,发两条命令之后 **40**。**参数真的变了,图没重建。**

**这条能力在直播里很值钱**:观众带宽不够时现场降分辨率,不用断流重连。
**代价是那一瞬间的画面会有跳变。**

**③ 实测二:音频滤镜会偷偷改格式**

  链             输出格式     样本数     首样本值
  ---------------------------------------------------
  不加滤镜       s16          1024       1000
  volume=0.5     flt          1024       0.01526
  volume=2.0     flt          1024       0.06104

**注意第二列:加滤镜之后格式从 `s16` 变成了 `flt`。** 因为 volume 内部按浮点算,
它顺手把格式也转了。

**于是同一个 1000,前后两个值的写法完全不同**:整数 1000 与浮点 0.01526。
**这不是滤镜算错,是基准变了**——整数按 32768 归一到 1。

**这个坑很实际**:处理音频时拿到帧先看一眼 `format.name`,别默认还是自己送进去的那个格式。

process_command 之后宽度从 80 变 40,图没重建。 加了 volume 之后格式从 s16 变 flt,同一个 1000 变成 0.0x。

30 秒实验

  1. 只发 width 不发 height,看输出会不会变形。
  2. 把 volume 的参数换成 "3.0",验算 0.0305×3 是不是它给的值。
  3. 在音频链尾部再加一个 aformat=sample_fmts=s16,看能不能把格式拉回来。

常见误区

  1. 以为渲染链的输出格式等于输入格式。 volume 就会把它改成浮点。
  2. 以为数值变了就是滤镜算错。 多半只是刻度从整数换成了浮点。
  3. 为了改一个参数去重建整张图。 用 process_command 就够。
自测三问(点开对答案) 1. `av_buffersink_get_frame` 对应 PyAV 里的什么? —— sink.pull(),也就是在汇节点上取一帧。 2. 加 volume 之后格式为什么会变? —— 它内部按浮点运算,顺手把输出格式转成了 flt。 3. 拿到音频帧第一件该确认的事是什么? —— 看 format.name,确认格式还是不是自己送进去的那个。

相关笔记