📹 第四章 H.264/H.265视频编码并存储
📄 一句话概括

GOP 是两个 I 帧之间的距离,它同时决定三件事:文件大小、seek 快慢、出错恢复速度。 三件事互相打架,所以没有「最优 GOP」,只有「为谁让步」。

📝 本节和前面几节的关系

上一节拧的是「编多清楚」,这一节拧的是「多久重来一次」。 I/P/B 的参考关系在 26 课,这里只讲间隔的影响。

一、GOP 是什么

GOP = Group of Pictures,从某个 I 帧开始,到下一个 I 帧之前为止的一组帧。 GOP 长度 = 10 就是「每 10 帧一个 I 帧」,30 帧的片子就有 3 个 I 帧。

为什么需要 I 帧:P/B 帧都是差值,必须从某个「完整画面」开始累加。这个起点就是 I 帧。 所以 I 帧是「随机访问点」——想跳到第 25 帧,最近的 I 帧在第 20 帧,那就从 20 开始解 5 帧。

二、实测:三种 GOP 的代价

GOP 文件大小 I 帧数 seek 平均要解几帧
10 20.9 KB 3 5.0 帧
30 10.4 KB 1 —(只 1 个 I)
120 10.4 KB 1 —(只 1 个 I)

seek 平均要解几帧 = GOP ÷ 2:目标点平均落在 GOP 中间,得从这个 GOP 开头的 I 帧解过来。 GOP 120 理论上平均要解 20 帧,按 30 fps 算是 0.66 秒——这就是「拖进度条要等」的来源。

表里 GOP 30 和 120 那两行算不出来,是样本不够:片子只有 30 帧,整片就一个 I 帧, 没有「两个 I 帧之间的距离」。 这个横杠不是 0,是「测不了」。报数时这一层必须写清, 否则读者会得出「GOP 越长 seek 越快」——结论正好反了。

GOP 10 的文件大了一倍(20.9 对 10.4 KB)。 因为 I 帧是完整画面,一个 I 帧的字节数顶十几个 P 帧。

三、三件事怎么取舍

| 想要 | GOP 该 | 代价 | |---|---|---|---| | 文件小 | 长(120 以上) | seek 慢、出错恢复慢 | | seek 快、低延迟 | 短(10~30) | 文件大 | | 监控实时预览 | 1~2 秒(30~60) | 折中 |

监控为什么不选最短的:1 秒一个 I 帧已经能让「点开就出画面」,再短文件要涨两三成, 而硬盘是监控最贵的成本项之一。

「出错恢复」这一栏要说清:网络丢包时,P/B 帧丢了会让后续帧全花, 一直花到下一个 I 帧才恢复。GOP 越长,花屏持续越久。所以丢包严重的链路要缩短 GOP。

四、命令行怎么写

| 参数 | 作用 | 备注 | |---|---|---|---| | -g 30 | 最大 GOP | 最常用,别称 keyint | | -keyint_min 30 | 最小 GOP | 防止场景切换太频繁插 I 帧 | | -sc_threshold 0 | 关掉场景切换检测 | 直播常用,让 GOP 严格等长 | | -force_key_frames 2 | 第 2 秒强制 I 帧 | 切片录制用 |

-sc_threshold 是个容易忽略的旋钮:默认 x264 会在「画面突变」时插一个 I 帧, 所以实际 GOP 往往比设的短。 要严格等长(比如按秒切片)就必须关掉它,否则切出来的片子长度不齐。

五、动手 Demo:GOP 是「多久能给一次完整画面」

Demo 只改 gop_size(10/30/120),其余参数全不动,量文件大小和 I 帧数。 seek 代价是用公式算的,不是测的——测它需要一段更长的片子,这里如实标注。

for gop in (10, 30, 120):
    c = av.open("gop%d.h264" % gop, "w", format="h264")
    s = c.add_stream("libx264", rate=30)
    s.width, s.height, s.pix_fmt = 320, 240, "yuv420p"
    s.gop_size = gop
    s.options = {"crf": "23", "sc_threshold": "0"}
    ...
n_i = sum(1 for fr in decode(path) if int(fr.pict_type) == 1)

实跑输出:

--------------------------------------------------------------------
GOP 是「多久能给一次完整画面」
--------------------------------------------------------------------
**GOP 就是两个 I 帧之间的距离。**
**它同时决定了三件事:文件大小、seek 快慢、出错恢复速度。**

  同一份源,只改 GOP(每秒一个关键帧 vs 两秒一个 vs 全片一个):

  GOP     文件大小     I 帧数     seek 平均要解几帧
  -----------------------------------------------------
  10      20.9 KB      3          5.0 帧
  30      10.4 KB      1          —(只 1 个 I)
  120     10.4 KB      1          —(只 1 个 I)

  **seek 平均要解几帧 = GOP ÷ 2**:因为目标点平均落在 GOP 中间,得从这个 GOP 开头的 I 帧解过来。
  **GOP 120 时理论上平均要解 20 帧**——按 30 fps 算就是 0.66 秒,**这就是「拖进度条要等」的原因。**
  **表里 GOP 30 和 120 那两行算不出来**:片子只有 30 帧,整片就一个 I 帧,
  没有「两帧之间的距离」可算。
  **这个横杠不是 0,是「样本不够」**——报数时这种区分必须写清,
  否则读者会以为「GOP 越长 seek 越快」,结论正好反了。

**三件事互相打架,必须取舍:**

  想要                GOP 该              代价
  ---------------------------------------------------------------
  文件小              长(120 以上)      seek 慢、出错恢复慢
  seek 快、低延迟     短(10~30)         文件大
  监控实时预览        1~2 秒(30~60)     折中

  **监控为什么不选最短**:1 秒一个 I 帧已经能让「点开就出画面」,再短文件会涨两三成。
  **而如果开了智能编码(只在有运动时编细节)**,GOP 还要再放长,否则每个 I 帧都要完整编一次,
  省不下来。

sc_threshold 必须设 0,否则「GOP 30」可能编出 3 个 I 帧,表就对不上了。 这是复现这类实验的第一个坑。 「样本不够写横杠」这一条要养成习惯:宁可留空,不要填 0。填 0 会让读者得出反向结论。

30 秒实验

  1. 把片子加长到 300 帧(10 秒),重跑,看 GOP 30 和 120 的 seek 代价能不能算出来。
  2. 把 sc_threshold 去掉(用默认),看 GOP 30 那行的 I 帧数会不会变多。
  3. 用 GOP 1(全 I 帧)编一遍,看文件大多少——这就是「无损编辑」格式的代价。

常见误区

  1. 以为 GOP 只影响文件大小。 它还决定 seek 粒度和花屏恢复时间,三件事一起变。
  2. 以为设了 -g 30 就一定是 30。 默认开着场景切换检测,实际可能更短, 要等长得关 sc_threshold。
  3. 以为测不出来就填 0。 样本不够要写「测不了」,填 0 会把结论带反。
自测三问(点开对答案) 1. 为什么 I 帧越多文件越大? —— I 帧是完整画面,一个 I 帧的字节数顶十几个 P 帧。 GOP 缩短就是把省字节的 P/B 换成了费字节的 I。 2. 网络丢包严重时,GOP 该长还是短? —— 短。丢一包会让后续帧一直花到下一个 I 帧,**GOP 越短,花屏持续时间越短**。 3. 按秒切片录像为什么必须关场景切换检测? —— 否则画面一突变就插 I 帧,**切出来的每段长度不齐**,后续按时间索引就对不上。

下一节换到 libx264 的选项本身:preset 到底是什么。

相关笔记