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

GOP 决定关键帧隔多远:短了好拖动、大了省字节。 但 x264 会在画面突变处自己补关键帧,实际间隔可能比设置更密。

📝 本节和前面几节的关系

码率决定「每秒花多少字节」,GOP 决定这些字节怎么分配。 它也是「能不能快速拖动」这个体验指标的直接来源。

一、三档 GOP 各编一遍

同一段素材,GOP 设成 10、25、50:

GOP 设置 关键帧数 首个间隔 字节


10 6 0.24 秒 5619 25 3 1.00 秒 4896 50 2 1.76 秒 4719

按设置算,GOP 10 在 25 fps 下应当每 0.40 秒一个关键帧,实测第一个间隔却是 0.24 秒。 因为 x264 除了按间隔插关键帧,还会在画面突变处自己补一个。

这段素材每帧亮度都在跳,所以它补得比设置还密。真实素材上这个偏差会小得多。 这也提醒一件事:设了 GOP 不等于关键帧就按那个间隔出现,只能说「不超过」。

二、字节为什么没怎么变

三行字节相差不到一成,看起来 GOP 对体积没影响:

对比 字节差


GOP 10 与 GOP 50 多 19% 关键帧数差 多 4 个

关键帧多了确实更费,但这段素材太好压,每个关键帧也只有一两千字节。 实拍素材上差距会明显得多:一个关键帧可能就是几十 KB。

所以「GOP 越短体积越大」这个规律方向是对的,但幅度要看素材。 在简单画面上试出来的比例,不能直接套到真实片子上。

三、两头都要付钱

GOP 短与长各有利弊,没有全局最优:

GOP 好处 代价


短 拖动快、容易做切片 体积大 长 体积小、压缩率高 拖动慢、切片难

直播切片是个典型场景:切点必须落在关键帧上,GOP 越长,切片越难对齐。 所以直播常用「2 秒一个关键帧」,代价是比长 GOP 多花一点带宽。

点播也吃这一条:用户拖进度条时,播放器要找到最近的关键帧再往后解。 GOP 50 意味着最坏要等两秒的画面解完,体验上就是「拖了没反应」。

四、动手 Demo:三档 GOP 各编一遍,数关键帧与间隔

用 GOP 10、25、50 各编一遍,数出关键帧个数与第一个间隔。 关键帧靠解复用出来的包标记来数,不需要真的解码。

import av, os
from fractions import Fraction

def enc_gop(dst, g):
    c1 = av.open(dst, u"w")
    s1 = c1.add_stream(u"libx264", rate=25)
    s1.width, s1.height, s1.pix_fmt = 160, 90, u"yuv420p"
    s1.options = {u"crf": u"26", u"g": u"%d" % g}
    c0 = av.open(u"s5.mp4")
    idx = 0
    for f in c0.decode(video=0):
        nf = av.VideoFrame.from_ndarray(f.to_ndarray(), format=u"yuv420p")
        nf.pts = idx
        nf.time_base = Fraction(1, 25)
        idx += 1
        for p in s1.encode(nf):
            c1.mux(p)
    for p in s1.encode(None):
        c1.mux(p)
    c1.close()
    c0.close()
    c = av.open(dst)
    tb = float(c.streams.video[0].time_base)
    keys = []
    for pk in c.demux(video=0):
        if pk.size and pk.is_keyframe:
            keys.append(float(pk.pts) * tb)
    c.close()
    gap = keys[1] - keys[0] if len(keys) > 1 else 0.0
    return os.path.getsize(dst), len(keys), gap

for g in (10, 25, 50):
    b, k, gap = enc_gop(u"gop%d.mp4" % g, g)
    print(u"GOP %-2d  关键帧 %d  首个间隔 %.2f 秒  %d 字节" % (g, k, gap, b))

实跑输出:

--------------------------------------------------------------------
5.6.3 控制视频的 GOP——关键帧隔多远
--------------------------------------------------------------------

**① 三档 GOP 各编一遍**

  GOP 设置     关键帧数     首个间隔     字节
  --------------------------------------------
  10           6            0.24 秒      5619
  25           3            1.00 秒      4896
  50           2            1.76 秒      4719

**按设置算,GOP 10 在 25 fps 下应当每 0.40 秒一个关键帧,实测第一个间隔却是 0.24 秒。**
**因为 x264 除了按间隔插关键帧,还会在画面突变处自己补一个——这段素材每帧亮度都在跳,**
**所以它补得比设置还密。真实素材上这个偏差会小得多。**

**第三列才是关键帧的真实间隔:想精确跳到某一秒,最坏要等这么久。**

**② 字节没怎么变,为什么**

**三行字节相差不到一成。关键帧多了确实更费,但这段素材太好压,**
**每个关键帧也只有一两千字节,多几个看不出来。实拍素材上差距会明显得多。**

**③ GOP 的两头都要付钱**

  GOP     好处                   代价
  ----------------------------------------------
  短      拖动快、容易做切片     体积大
  长      体积小、压缩率高       拖动慢、切片难

**直播切片是个典型场景:切点必须落在关键帧上,GOP 越长,切片越难对齐,**
**所以直播常用「2 秒一个关键帧」,代价是比长 GOP 多花一点带宽。**

pk.is_keyframe 读的是包标记,不用解码,所以很快。 要判断「这个文件能不能快速拖动」,数这个就够。

pk.pts 是时间基单位,要乘 time_base 才是秒。 忘了乘会得到一串整数,看起来像帧号而不是时间。

g 是 x264 的选项名,等价于命令行的 -g。 它只是「最大间隔」,不是「固定间隔」——场景切换会让它更密。

30 秒实验

  1. 命令行对照:ffmpeg -i 源.mp4 -g 25 -keyint_min 25 出.mp4,keyint_min 才管「最少隔多少」。
  2. 关掉场景切换补帧:加 -sc_threshold 0,关键帧就只按 -g 来。
  3. 看关键帧位置:ffprobe -show_frames -select_streams v 找 key_frame=1。

常见误区

  1. 以为 -g 25 就是每 25 帧一个关键帧。场景切换会让它更密,只能当上限。
  2. 以为 GOP 越短体积越大一定成立。方向对,但简单素材上幅度很小。
  3. 忘了 pts 要乘 time_base。算出来的间隔会是帧号,不是秒。
自测三问(点开对答案) 1. 为什么设了 GOP 10,实测首个间隔只有 0.24 秒? —— x264 会额外在场景切换处补关键帧,所以实际间隔可能比设置更密。 2. GOP 短有什么坏处? —— 关键帧多了体积大。关键帧是完整画面,比预测帧贵得多。 3. 要固定关键帧间隔不能多于 25 帧,该给什么参数? —— `g=25` 管最大间隔;想更严格控制还要配 `keyint_min` 与 `sc_threshold`。

相关笔记