🎬 第五章 FFmpeg命令行实现音视频转码
📄 一句话概括

改帧率有三条完全不同的路:丢帧、补帧、只改时间轴。 前两条改画面数量,第三条只改解释方式——它的结果是「快放」。

📝 本节和前面几节的关系

音频那边改的是采样率,视频这边对应的是帧率。 但视频多一个岔路:帧率既是「每秒放几张」,也可以当成「时间轴怎么算」。

一、降帧率就是丢帧

25 fps 降到 10 fps:

输出帧率 帧数 字节 时长


25 fps 50 4478 2.00 秒 10 fps 20 3558 2.00 秒

第二行帧数掉到 20,时长没变——这就是丢帧:时间轴不动,只是取样的次数少了。 字节也降了,但只降了两成,不是按帧数比例降的。

代价是运动不连贯。原来每个位置都有画面,现在两个位置之间只能靠「保持上一帧」过渡。 所以动画片用 12 fps 还行,体育比赛用 12 fps 就没法看了。

二、升帧率补不出新画面

把每一帧重复一次,文件就变成 50 fps:

做法 帧数 字节


25 fps 原样 50 4478 每帧重复一次变 50 100 6201

字节只涨了 38%,因为没有新画面——每一对相邻帧完全一样,编码器几乎不花字节。 这就是「假高帧率」:文件写着 50 fps,实际信息量还是 25 fps 的。

真要提升流畅度得靠插帧算法去「猜」中间那一帧的运动,代价是算力。 简单重复只改标签,一秒的活,效果也是零。

三、只改时间基:不丢不补,改速度

帧数不变,只把时间轴压一半:

做法 帧数 字节 实际时长


50 帧当 50 fps 写 50 4217 1.00 秒

帧数还是 50,但时长变成 1.0 秒——这是「快放一倍」,不是「提高帧率」。 画面一张没少,只是每张停留的时间短了一半。

命令行里对应两条完全不同的路:setpts=0.5*PTS 是快放,fps=50 是真补帧。 搞混这两条,做出来的片子要么动作变快,要么多了几十张重复帧。

四、动手 Demo:同一段素材,丢帧、补帧、改时间各来一次

先按 25 fps 原样编一遍当基准,再按每 2.5 帧取一帧的方式降到 10 fps。 然后把每帧重复两次变成 50 fps,最后试「50 帧当 50 fps 写」的快放。

import av, os
from fractions import Fraction

def reenc(dst, rate, keep=None, dup=1):
    c1 = av.open(dst, u"w")
    s1 = c1.add_stream(u"libx264", rate=rate)
    s1.width, s1.height, s1.pix_fmt = 160, 90, u"yuv420p"
    s1.options = {u"crf": u"26"}
    c0 = av.open(u"s5.mp4")
    idx = 0
    for i, f in enumerate(c0.decode(video=0)):
        if keep is not None and not keep(i):
            continue
        arr = f.to_ndarray()
        for k in range(dup):
            nf = av.VideoFrame.from_ndarray(arr, format=u"yuv420p")
            nf.pts = idx
            nf.time_base = Fraction(1, rate)
            idx += 1
            for p in s1.encode(nf):
                c1.mux(p)
    for p in s1.encode(None):
        c1.mux(p)
    c1.close()
    c0.close()
    return os.path.getsize(dst), idx

b25, n25 = reenc(u"fps25.mp4", 25)
b10, n10 = reenc(u"fps10.mp4", 10,
                 keep=lambda i: (i * 2) // 5 != ((i - 1) * 2) // 5)
print(u"25 fps  %d 帧 %d 字节 %.2f 秒" % (n25, b25, n25 / 25.0))
print(u"10 fps  %d 帧 %d 字节 %.2f 秒" % (n10, b10, n10 / 10.0))
b50, n50 = reenc(u"fps50.mp4", 50, dup=2)
print(u"每帧重复 50 fps  %d 帧 %d 字节 涨 %.0f%%"
      % (n50, b50, (b50 - b25) * 100.0 / b25))
bf, nf = reenc(u"fpsfake.mp4", 50)
print(u"50 帧当 50 fps 写  %d 帧 %d 字节 %.2f 秒" % (nf, bf, nf / 50.0))

实跑输出:

--------------------------------------------------------------------
5.6.1 控制视频的帧率——丢帧、补帧、还是只改时间
--------------------------------------------------------------------

**① 25 帧降到 10 帧:丢帧**

  输出帧率     帧数     字节     时长
  ---------------------------------------
  25 fps       50       4478     2.00 秒
  10 fps       20       3558     2.00 秒

**第二行帧数掉到 20,时长没变——这就是丢帧:时间轴不动,只是取样的次数少了。**
**代价是运动不连贯。原来每个位置都有画面,现在两个位置之间只能靠「保持上一帧」过渡。**

**② 25 帧升到 50 帧:补帧**

  做法                  帧数     字节
  ------------------------------------
  25 fps 原样           50       4478
  每帧重复一次变 50     100      6201

**字节只涨了 38%,因为没有新画面——每一对相邻帧完全一样,编码器几乎不花字节。**
**这就是「假高帧率」:文件写着 50 fps,实际信息量还是 25 fps 的。**

**③ 只改时间基:不丢不补,改速度**

  做法                  帧数     字节     实际时长
  -------------------------------------------------
  50 帧当 50 fps 写     50       4217     1.00 秒

**帧数还是 50,但时长变成 1.0 秒——这是「快放一倍」,不是「提高帧率」。**
**命令行里对应 `setpts=0.5*PTS`(快放)与 `fps=50`(真补帧),两条路完全不同。**

重复帧必须重建 VideoFrame 并显式给 pts 与 time_base。 直接复用同一个帧对象两次,两个包的时间戳会撞车,封装器报 returned 22。

那个 keep 表达式是「每 2.5 帧取一帧」的写法: (i * 2) // 5 每次跨过整数就取,50 帧正好取出 20 帧。

reenc 返回的输出帧数用的是自己数的 idx,不是 ffprobe 的结果。 两者应当一致;不一致说明有包被封装器丢了。

30 秒实验

  1. 命令行对照:ffmpeg -i 源.mp4 -r 10 出.mp4,这就是丢帧。
  2. 快放:ffmpeg -i 源.mp4 -vf setpts=0.5*PTS 出.mp4,时长减半、帧数不变。
  3. 插帧:minterpolate=fps=50,这才是真补帧,慢得多。

常见误区

  1. 以为 -r 50 能提升流畅度。源只有 25 帧,多出来的都是重复帧。
  2. 把 setpts 当成改帧率。它改的是时间轴,结果是快放或慢放。
  3. 重复帧不重建对象。时间戳撞车,封装器拒绝写入。
自测三问(点开对答案) 1. 降帧率时时长为什么不变? —— 丢帧只减少取样次数,时间轴还是原来那么长,画面靠保持上一帧过渡。 2. 每帧重复一次,字节为什么只涨 38%? —— 相邻两帧完全一样,编码器用极少的比特就能表示,没有新信息。 3. `setpts=0.5*PTS` 做的是什么? —— 把时间轴压一半,帧数不变、时长减半,效果是快放一倍。

相关笔记