🎬 第七章 FFmpeg命令行实现音视频特效及复杂滤镜应用
📄 一句话概括

视频倍速只改一个东西:每帧的时间戳。setpts=2*PTS 变慢一倍、setpts=0.5*PTS 变快一倍。 加快时必须再挂一个 fps,否则编码器直接报错。

📝 本节和前面几节的关系

前面 7.1 与 7.2 都在改画面的尺寸与位置,从这一节起改时间。 音视频倍速是后面所有「节奏类」特效的基础。

一、改的是时间戳,不是帧率

两条命令先说清楚:

ffmpeg -i s7.mp4 -vf setpts=2*PTS slow.mp4

ffmpeg -i s7.mp4 -vf setpts=0.5*PTS fast.mp4

PTS 是每帧的时间戳。乘 2,每帧的间隔翻倍,同样 50 帧就摊到两倍长的时间里,看着就慢了一半。 乘 0.5 反过来,50 帧挤进一半的时间,看着就快了一倍。

注意它改的是时间戳,不是帧率,也不是帧数。 帧还是那 50 帧,一帧没多、一帧没少,只是被安排到了不同的时刻上。

二、喂 6 帧进去,看时间戳怎么变

这个区别只看帧数看不出来,得把时间戳打出来:

写法 出来的时间戳 帧数


不加滤镜 0 1 2 3 4 5 6 setpts=2PTS 0 2 4 6 8 10 6 setpts=0.5PTS 0 0 1 1 2 2 6

三种写法输出的帧数一模一样,都是 6 帧。 差别全在第二列:慢速那行间隔变成了 2,快速那行出现了两个 0、两个 1。

0 0 1 1 2 2 就是「时间戳撞车」——两帧被安排在同一个时刻。 滤镜图这一层不管,它能照原样吐出来;但编码器那一层受不了。

三、真编一遍,看时长和报错

把两条命令真跑一遍,落到文件里再量:

写法 结果 文件大小


源文件 50 帧 2.00 秒 - setpts=2PTS 3.96 秒 13615 字节 setpts=0.5PTS 报错 -

慢速那条顺利出来了,3.96 秒,差不多是源文件的两倍。 快速那条在编码阶段直接抛异常:ArgumentError Invalid argument returned 22。

滤镜图能过、编码器不过,这个组合最容易让人以为是命令写错了。 其实命令一个字都没错,错的是「两帧同一时刻」这个前提。

3.96 而不是 4.00,是因为时间戳是整数刻度,乘 2 之后有半帧的零头落不下去。 这点零头在长片子里会累积,但一般察觉不到。

四、加快必须补 fps

补法只有一步:把均匀的时间轴重新排出来。

ffmpeg -i s7.mp4 -vf setpts=0.5*PTS,fps=25 fast.mp4

链 6 帧进 几帧出


setpts=0.5PTS 6 6 setpts=0.5PTS,fps=25 6 2 setpts=2*PTS,fps=25 6 10

加了 fps=25 之后,快速那条从 6 帧掉到 2 帧, 因为撞在一起的帧被丢掉了,留下的时间戳重新排成 0 1。

慢速那条反过来涨到 10 帧,时间戳空出来的位置由 fps 补帧填上。 也就是说 fps 既能丢帧也能补帧,看哪一边占了便宜。

补了 fps 之后真编一遍:1.00 秒、8433 字节,报错消失。 代价是帧数少了,画面会比原来顿一点——快放本来就是这个效果。

五、三条记法

这一节的结论压成一张表:

要的效果 写法


放慢一倍 -vf setpts=2PTS 加快一倍 -vf setpts=0.5PTS,fps=25 只想看,不出片 ffplay -vf setpts=0.5*PTS

放慢不用补 fps,因为间隔变大不会撞车,空位由播放器按时间戳停住。 加快必须补,否则编码器一步都走不了。

至于倍速的倍数,2 与 0.5 只是例子。0.25、4 都能写,只要别让时间戳撞车。 真要 4 倍速,写 setpts=0.25*PTS,fps=25,一次到位。

六、动手 Demo:三行时间戳、两次编码、一次补帧

下面这段代码先打三种写法的 PTS,再真编两遍看时长与报错。 最后补上 fps,验证帧数变化。

import av
from fractions import Fraction

TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
frs = []
for fr in c0.decode(video=0):
    frs.append(fr.to_ndarray(format=u"yuv420p"))
    if len(frs) >= 50:
        break
c0.close()

def chain(expr, fps=None):
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    p = b
    for name, args in ((u"setpts", expr), (u"fps", fps)):
        if args:
            f = g.add(name, args)
            p.link_to(f)
            p = f
    sk = g.add(u"buffersink")
    p.link_to(sk)
    g.configure()
    return g, b, sk

def pts(expr, n=6, fps=None):
    g, b, sk = chain(expr, fps)
    out = []
    for i in range(n):
        vf = av.VideoFrame.from_ndarray(frs[i], format=u"yuv420p")
        vf.pts = i
        vf.time_base = TB
        b.push(vf)
        while True:
            try:
                o = sk.pull()
            except Exception:
                break
            out.append(o.pts)
    return out

print(u"%-18s %-16s %s" % (u"写法", u"时间戳", u"帧数"))
for tag, e in ((u"不加滤镜", None), (u"setpts=2*PTS", u"2*PTS"),
               (u"setpts=0.5*PTS", u"0.5*PTS")):
    r = pts(e)
    print(u"%-18s %-16s %d" % (tag, u" ".join(str(x) for x in r), len(r)))

print(u"--- 6 帧进,几帧出")
for tag, e, f in ((u"setpts=0.5*PTS", u"0.5*PTS", None),
                  (u"setpts=0.5*PTS,fps=25", u"0.5*PTS", u"25"),
                  (u"setpts=2*PTS,fps=25", u"2*PTS", u"25")):
    print(u"%-24s %d" % (tag, len(pts(e, 6, f))))

def enc(dst, expr, fps=None):
    c = av.open(u"s7.mp4")
    stb = c.streams.video[0].time_base
    c.close()
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=stb)
    p = b
    for name, args in ((u"setpts", expr), (u"fps", fps)):
        if args:
            f = g.add(name, args)
            p.link_to(f)
            p = f
    sk = g.add(u"buffersink")
    p.link_to(sk)
    g.configure()
    oc = av.open(dst, u"w")
    st = oc.add_stream(u"libx264", rate=25)
    st.width, st.height = 160, 90
    st.pix_fmt = u"yuv420p"
    c0 = av.open(u"s7.mp4")
    for fr in c0.decode(video=0):
        b.push(fr)
        while True:
            try:
                o = sk.pull()
            except Exception:
                break
            for pk in st.encode(o):
                oc.mux(pk)
    c0.close()
    b.push(None)
    while True:
        try:
            o = sk.pull()
        except Exception:
            break
        for pk in st.encode(o):
            oc.mux(pk)
    for pk in st.encode(None):
        oc.mux(pk)
    oc.close()
    c = av.open(dst)
    d = c.duration / 1e6
    c.close()
    return d, __import__(u"os").path.getsize(dst)

print(u"--- 真编一遍")
for i, (tag, e, f) in enumerate(((u"setpts=2*PTS", u"2*PTS", None),
                                 (u"setpts=0.5*PTS", u"0.5*PTS", None),
                                 (u"setpts=0.5*PTS,fps=25", u"0.5*PTS", u"25"))):
    try:
        d, n = enc(u"sp_%d.mp4" % (i + 1), e, f)
        print(u"%-24s %.2f 秒  %d 字节" % (tag, d, n))
    except Exception as ex:
        print(u"%-24s 报错 %s" % (tag, type(ex).__name__))

实跑输出:

--------------------------------------------------------------------
7.3.1 视频倍速
--------------------------------------------------------------------

**① 视频倍速只有一条命令:改 PTS**

`ffmpeg -i s7.mp4 -vf setpts=2*PTS slow.mp4`

`ffmpeg -i s7.mp4 -vf setpts=0.5*PTS fast.mp4`

**`PTS` 是每帧的时间戳。乘 2,每帧的间隔翻倍,同样 50 帧就摊到两倍长的时间里,看着就慢了一半。**
**乘 0.5 反过来,50 帧挤进一半的时间,看着就快了一倍。**
**注意它改的是时间戳,不是帧率,也不是帧数。**

**② 喂 6 帧进去,看时间戳怎么变**

  写法               出来的时间戳     帧数
  --------------------------------------------
  不加滤镜           0 1 2 3 4 5      6
  setpts=2*PTS       0 2 4 6 8 10     6
  setpts=0.5*PTS     0 0 1 1 2 2      6

**三种写法输出的帧数一模一样,都是 6 帧。**
**差别全在第二列:慢速那行间隔变成了 2,快速那行出现了两个 0、两个 1。**

**`0 0 1 1 2 2` 就是「时间戳撞车」——两帧被安排在同一个时刻。**
**滤镜图这一层不管,它能照原样吐出来;但编码器那一层受不了。**

**③ 真编一遍,看时长和报错**

  写法               结果              文件大小
  ---------------------------------------------------
  源文件             50 帧 2.00 秒     -
  setpts=2*PTS       3.96 秒           13615 字节
  setpts=0.5*PTS     报错              -

**慢速那条顺利出来了,3.96 秒,是源文件的两倍。**
**快速那条在编码阶段直接抛异常:`ArgumentError Invalid argument returned 22`。**
**滤镜图能过、编码器不过,这个组合最容易让人以为是命令写错了。**

**④ 快速必须补一步:fps 把时间轴拉回均匀**

  链                        6 帧进     几帧出
  -----------------------------------------------
  setpts=0.5*PTS            6          6
  setpts=0.5*PTS,fps=25     6          2
  setpts=2*PTS,fps=25       6          10

**加了 fps=25 之后,快速那条从 6 帧掉到 2 帧,**
**因为撞在一起的帧被丢掉了,留下的时间戳重新排成 `0 1`。**
**慢速那条反过来涨到 10 帧,时间戳空出来的位置由 fps 补帧填上。**

**补了 fps 之后真编一遍:1.00 秒、8433 字节,报错消失。**

**⑤ 记住三条**

  要的效果           写法
  ------------------------------------------------
  放慢一倍           -vf setpts=2*PTS
  加快一倍           -vf setpts=0.5*PTS,fps=25
  只想看,不出片     ffplay -vf setpts=0.5*PTS

**放慢不用补 fps,因为间隔变大不会撞车,空位由播放器按时间戳停住。**
**加快必须补,否则编码器一步都走不了。**

第一段三行跟本节第一张表逐条对上,注意第三行出现了两个 0 与两个 1。 第二段三行是补 fps 之后的结果:6 进 2 出、6 进 10 出。

第三段里 setpts=0.5*PTS 那一行报的是 ArgumentError, 而同一个表达式在滤镜图里跑得好好的——这就是「图能过、编码器不过」。

代码里 chain() 把「可选的两级滤镜」写成了一个循环,args 为空就跳过。 这种写法比写两遍重复的建图代码清楚,也方便再加第三级。

30 秒实验

  1. 改倍数:把 2 改成 4,看 PTS 变成 0、4、8、12,时长变成多少。
  2. 去掉 fps:把第三段的 fps 删掉,看报错是不是又回来了。
  3. 换成 settb:把 fps=25 换成 settb=1/25,看能不能救回来。

常见误区

  1. 以为 setpts 会改帧数。它只改时间戳,帧一个不多一个不少。
  2. 加快时不挂 fps。撞车之后编码器报 22,看着像命令写错。
  3. 以为 2*PTS 出来正好两倍长。实测 3.96 秒,差了半帧的零头。
自测三问(点开对答案) 1. `setpts=2*PTS` 之后帧数会变成多少? —— 不变。它只改时间戳,帧还是原来那些帧。 2. 为什么 `setpts=0.5*PTS` 单独用会报错? —— 时间戳被压成两帧同一时刻,编码器不接受非单调的时间戳。 3. 补 `fps=25` 之后为什么帧数会变少? —— 撞在一起的帧被丢掉,时间轴重新排成均匀的 25 帧每秒。

相关笔记