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

解码、滤镜、编码、封装四段串起来,文件就出来了。 但时长会是素材的几百倍——因为滤镜不替你换算 pts。

📝 本节和前面几节的关系

9.3 讲搭图,9.4.1 讲命令行,这一节用 API 把整条链落成文件。 这是第九章的收尾,也是全书第一次踩到「时间戳没对齐」的实战坑。

一、整条链跑完,账先记下

跑完先看五个数,不要急着看画面。 项 值


输出文件 filtered.mp4 帧数 50 包数 50 体积 约 1 万字节 读回来的时长 1003.56 秒

文件出来了、尺寸对了、包数也对——但时长大得离谱。 素材本身只有 2.02 秒,读回来却是一千多秒。 这种现象最容易被放过:播放器能播,画面也对,只有进度条不对。不看时长就永远发现不了。

二、坑在哪:三把尺子

同一串数字,换了尺子量,时间就差几百倍。 哪一环 用哪把尺子 说明


解码出来的帧 流的 time_base 比如 1/12800 编码器要的帧 编码器的 time_base 比如 1/25 容器里的时间 微秒 容器自己再换一次

滤镜出来的帧,pts 还是源码流的刻度,而编码器是照着 1/25 去理解的。 同一串数字从 1/12800 的尺子挪到 1/25 的尺子上,时间就被放大了几百倍。 关键在于:滤镜这一环不会替你换算 pts,它只管像素。 这是设计使然,不是 bug——它不知道你要把帧交给谁。 这就是 8.4 讲的「三把尺子」在实战里的样子。那节讲的是概念,这里看到的是它的后果。

三、修法:给帧重新编号

一行代码,按帧序号重编 pts。 做法 读回来的时长


不换算 pts 1003.56 秒 按帧号重编(o.pts = i) 2.00 秒

o.pts = i 的意思是「第 i 帧的显示时刻就是第 i 个单位」,配合编码器的 1/25, 50 帧正好 2 秒。 这一行是整个 9.4 最值钱的东西。 滤镜链跑通不算完,时间戳对齐才算完。 真实项目里更常见的写法是按时间算:o.pts = i * (源 time_base / 编码器 time_base), 这样变速、丢帧、补帧时才不会错。按帧号重编只适合「一帧不丢」的场合。

四、这套写法的复用价值

把滤镜那一段换掉,整条链就能干别的事。 换掉哪一段 变成什么


scale 换成 overlay 加水印 scale 换成 eq 调亮度对比度 一条链换成两条 同时出大小两版 输出改成推流地址 直播

四段骨架不动,只换中间——这正是责任链的价值在代码层面的体现。 第十三章的直播推流就是这个骨架加一次替换:把封装的目标从文件换成RTMP 地址,其余全一样。 所以这一节不是练习,是后面几章的地基。

五、第九章收尾

四句话,把这一章的账结掉。 记住的 一句话


滤镜在哪 五环里的「处理」,前后两头不用改 图怎么搭 源、滤镜、汇,连线、配好 简单与复杂 看几进几出,不看名字 最容易错 pts 不换算,时长差几百倍

四句话里最后一句最贵,因为它是唯一「不报错但结果错」的一条。 第十章换到音频那头,重采样要处理的也是「格式与刻度」,只是换了一套名词:采样率、声道布局、 样本格式。坑的形状一模一样。

六、动手 Demo:用 API 走完整条滤镜链,并踩一个真的坑

解码 → 滤镜 → 编码 → 封装,跑完先读时长;再补一行 pts 换算,重跑一遍对照。

import av
import av.filter
from fractions import Fraction

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

    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()

    i = 0
    for pk in c.demux(st):
        if pk.size == 0:
            break
        for fr in st.codec_context.decode(pk):
            src.push(fr)
            fr = sink.pull()
            if fix_pts:
                fr.pts = i          # 按帧号重编
            for op in vs.encode(fr):
                o.mux(op)
            i += 1
    o.close()
    r = av.open(out)
    return r.duration / 1000000.0, i

print("不换算", run("bad.mp4", False))
print("按帧号重编", run("ok.mp4", True))

实跑输出:

--------------------------------------------------------------------
用 API 走完整条滤镜链,并踩一个真的坑
--------------------------------------------------------------------

**① 整条链:解码 → 滤镜 → 编码 → 封装**

  项               值
  ---------------------------------
  文件             filtered.mp4
  帧数             50
  包数             50
  体积             10280 字节
  读回来的时长     1003.56 秒

**文件出来了,尺寸对了,包数也对——但时长大得离谱。** **1003.56 秒**,而素材本身只有 2.02 秒。

**② 坑在哪**

**滤镜出来的帧,pts 还是源码流的刻度**(1/12800 那种),而编码器是照着 1/25 去理解的。
**同一串数字换了尺子,时间就被放大了几百倍。**

**这就是 8.4 说的「三把尺子」在实战里的样子。** **滤镜这一环不会替你换算 pts,它只管像素。**

**③ 修法:给帧重新编号**

加上一行 `o.pts = i`(按帧序号重新编号),时长变成 **2.00 秒**。

  做法           时长
  -----------------------------
  不换算 pts     1003.56 秒
  按帧号重编     2.00 秒

**这一行是整个 9.4 最值钱的东西。** 滤镜链跑通不算完,**时间戳对齐才算完。**

**④ 这一章的收尾**

  记住的         一句话
  ---------------------------------------------------
  滤镜在哪       五环里的「处理」,前后两头不用改
  图怎么搭       源、滤镜、汇,连线、配好
  简单与复杂     看几进几出,不看名字
  最容易错       pts 不换算,时长差几百倍

**四句话,就是这一章的账。** 第十章换到音频那头,重采样要处理的也是「格式与刻度」,
只是换了一套名词。

不换算的时长是一千多秒,按帧号重编之后是 2.00 秒。 滤镜不会替你换算 pts,它只管像素——这一行得自己加。

30 秒实验

  1. 把 rate=25 改成 rate=50,再跑一遍,看两组的时长各是多少。
  2. 把 fr.pts = i 改成 fr.pts = i * 2,看时长变成几秒。
  3. 在滤镜链尾部加一个 format=yuv420p,看输出体积变不变。

常见误区

  1. 只验「文件出来了、画面对了」。 时长、pts、音画同步都要一起验。
  2. 以为滤镜会顺手换算时间戳。 它只管像素,时间戳得自己算。
  3. 按帧号重编就当万事大吉。 丢帧、变速的场合要按时间算。
自测三问(点开对答案) 1. 为什么加了滤镜之后时长会差几百倍? —— 滤镜出来的帧 pts 还是源码流的刻度,编码器却按自己的 time_base 理解。 2. `o.pts = i` 这一行在做什么? —— 把第 i 帧的显示时刻定成第 i 个单位,配合编码器时基换算成秒。 3. 四段骨架里哪一段换掉就变成「加水印」? —— 滤镜那一段,把 scale 换成 overlay。

相关笔记