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

加滤镜不是给转码链「加一环」,而是把中间那一环换掉。 前后两头——解封装与封装——一个字都不用改。

📝 本节和前面几节的关系

第八章讲的是「把包解成帧、把帧压成包」,那时帧出来就直接进编码器。 这一章讲的是:帧出来先别进编码器,先过一道滤镜。

一、五环里的「处理」

转码这条链一直是五环,滤镜只占其中一环。 环 干什么 有没有滤镜


解封装 把文件拆成压缩包 没有 解码 把压缩包还原成像素帧 没有 处理 改像素、改尺寸、改格式 就是滤镜 编码 把像素帧重新压回去 没有 封装 把压缩包装进容器 没有

所以「加滤镜」的正确说法是「把处理那一环换成滤镜图」。 没加滤镜时这一环不是空的,它只是什么都不做——帧从解码器出来,直接交给编码器。 加上滤镜之后,帧要先从滤镜图的一头进去、另一头出来。

二、加滤镜前后,同一段素材的账

同一段素材,各编三十来个包,量三样东西。 做法 包数 总字节 耗时


不加滤镜 31 6956 二十几毫秒 加 scale 缩放 31 6053 二十几毫秒

包数一样,字节少了一成多。 因为输出尺寸从 160×120 降到 80×60,像素数只有原来的四分之一, 压出来自然小。 耗时几乎一样,两者的差值连跑两遍的抖动都能造出来。 这一条很值得记住:小尺寸视频上缩放本身几乎不花钱,贵的是编解码——解码一帧动辄几毫秒, 缩放一帧只要几十微秒。 所以指望「加个滤镜让程序变快」基本不成立; 反过来说,加滤镜的代价也没那么可怕,真正压性能的仍是编解码与 IO。

三、顺序不能反

三件事有固定的先后,错一步就做不成。 顺序 结果


先解码、再滤镜、再编码 正常 先滤镜、再解码 做不了,压缩数据没法改像素

这条听着像废话,但实际写代码很容易错位:把 push 放到 encode 之后,代码照样跑,也不报错, 只是滤镜永远收不到帧。 这类「不报错的错」最难查。 查的时候不用猜,直接在 pull 之后打一行帧数——是 0 就说明上面那一步接错了。

四、滤镜能改什么

能改的东西分四类,四类的开销差很远。 改什么 代表滤镜 开销


尺寸 scale 小,逐点重采样 像素格式 format 小,换个存法 画面位置 hflip/vflip/crop 极小,只搬不换 内容合成 overlay/drawbox 中,要算混合

前两类会改变「每帧多大」,后两类不改变尺寸。这个区别决定了编码器那边的参数要不要跟着改。 改了尺寸就必须重建编码器:编码器打开时就定死了宽高,中途变尺寸只能关掉重来。 这是直播里改分辨率要「断一下」的根本原因。

五、C 里加滤镜要动的三处

C 代码里,滤镜不是「加一段」,是「改三处」。 位置 原来 加滤镜之后


解码之后 帧直接给编码器 帧先 push 进图 编码之前 从解码器取帧 从图的 sink 取帧 时间戳 沿用解码出来的 必须自己重编

第三处是最容易漏的,也是本章最后那个 Demo 要踩的坑。 滤镜只管像素,不管时间戳——从图里出来的帧,pts 还是原来那套刻度。 同一串数字换了尺子,时间就被放大几百倍。 9.4.2 会实测出这个数。

六、动手 Demo:加滤镜前后,一条转码链差在哪

同一段素材跑两遍:一遍按老样子解码后直接编码,一遍中间插一个缩放滤镜。

import av
import av.filter
from fractions import Fraction

def run(use_filter):
    c = av.open("tiny.mp4")
    st = c.streams.video[0]
    o = av.open("out.mp4", "w")
    vs = o.add_stream("libx264", rate=25)
    vs.width, vs.height, vs.pix_fmt = 80, 60, "yuv420p"

    g = src = sink = None
    if use_filter:
        g = av.filter.Graph()
        src = g.add_buffer(width=160, height=120,
                            format="yuv420p",
                            time_base=Fraction(1, 25))
        sc = g.add("scale", "80:60")
        sink = g.add("buffersink")
        src.link_to(sc)
        sc.link_to(sink)
        g.configure()

    n = total = 0
    for pk in c.demux(st):
        if pk.size == 0:
            break
        for fr in st.codec_context.decode(pk):
            if use_filter:
                src.push(fr)          # 先滤镜
                fr = sink.pull()      # 再取回
            for op in vs.encode(fr):  # 最后才编码
                o.mux(op)
                n += 1
                total += len(bytes(op))
    o.close()
    return n, total

print(run(False))
print(run(True))

实跑输出:

--------------------------------------------------------------------
加滤镜前后,一条转码链差在哪
--------------------------------------------------------------------

**① 滤镜在五环里的位置**

  环         有没有滤镜
  ---------------------------
  解封装     没有
  解码       没有
  处理       **就是滤镜**
  编码       没有
  封装       没有

**所以「加滤镜」不是给转码链加一步,而是把中间那一步换成滤镜图。** 前后两头的代码一个字都不用改。

**② 同一段素材,各编二十几个包**

  做法              包数     总字节     耗时
  ---------------------------------------------------
  不加滤镜          31       6956       46.6 毫秒
  加 scale 缩放     31       6053       25.0 毫秒

**包数一样,字节少了约 13%。** 因为输出尺寸从 160×120 降到 80×60,像素只有原来的四分之一。

**耗时几乎一样,两者只差 21.6 毫秒。** 这点差值连跑两遍的抖动都能造出来,
说明**小尺寸视频上,缩放本身并不贵,贵的是编解码。**

**③ 顺序不能反**

  顺序                       结果
  ---------------------------------------------------------
  先解码、再滤镜、再编码     正常
  先滤镜、再解码             做不了,压缩数据没法改像素

**这一条听着废话,但实际写代码时很容易把 `g.push` 放到 `encode` 之后。**
位置错了不会报错,只会滤镜永远收不到帧。

包数一样、字节少了,因为输出从 160×120 降到 80×60。 src.push 必须在 encode 之前,写在后面不报错,只是滤镜收不到帧。

30 秒实验

  1. 把 scale 换成 hflip,看字节数变不变(提示:尺寸没变,字节几乎一样)。
  2. 把 src.push(fr) 挪到 vs.encode(fr) 之后跑一遍,看输出是不是空文件。
  3. 把输出高度改成 120(只缩宽不缩高),看编码器会不会报错。

常见误区

  1. 以为加滤镜是「多加一环」。 它是把「处理」那一环换掉。
  2. 以为加滤镜一定变慢。 小图上缩放的耗时可忽略,慢的是编解码。
  3. 滤镜之后再改尺寸却不重建编码器。 编码器的宽高打开时就定死了。
自测三问(点开对答案) 1. 加滤镜之后,转码链的哪一环变了? —— 只有「处理」那一环,解封装与封装完全不变。 2. 为什么加了 scale 之后字节少了、耗时却几乎不变? —— 输出尺寸变小所以字节少;缩放本身很便宜,耗时主要花在编解码上。 3. 滤镜处理完之后,帧上的 pts 还是原来那套吗? —— 是。滤镜只管像素,时间戳要自己按编码器的刻度重算。

相关笔记