🎬 第三章 FFmpeg三大常用工具及应用选项详解
📄 一句话概括

视频选项分四组:选编码器、改画面、改节奏、改质量。 改画面和改节奏都绕不开重编码,代价比想象中大。

📝 本节和前面几节的关系

3.4 讲的是过程,这一节开始讲「改数据」。 3.6 讲音频的对应一组,两节的规律几乎一样。

一、四组参数,改的东西不同

按「改什么」分成四组,代价完全不一样:

组 选项 改的是


选编码器 -c:v / -vcodec 用谁压 改画面 -s / -pix_fmt / -aspect 像素本身 改节奏 -r / -g / -vsync 帧怎么排 改质量 -b:v / -crf / -qp 花多少字节

「只想换个编码器但画质不变」是做不到的——换编码器就等于重压一遍。 因为不同编码器的量化方式完全不同,没有「等价映射」这种东西。

唯一能「不重压」的是 -c:v copy,但那就等于不改任何画面参数。 两条路只能选一条:要么不碰画面,要么重编。

二、-s 的硬限制:尺寸必须是偶数

这是本节最容易撞上的一个坑:

尺寸 yuv420p 能编吗 原因


160x90 能 宽高都是偶数 80x44 能 宽高都是偶数 80x45 不能 色度是 2×2 取样,高度必须偶数

所以 -vf scale=80:-2 里那个 -2 不是随便写的。 它的意思是「按比例算,并且保证结果是 2 的倍数」—— 写成 -1 就只保证等比,不管奇偶,奇数高度时就会编失败。

这个坑的表现是 avcodec_open2 报「Generic error」,一点提示都没有。 第一次遇到基本查不出来,所以这里专门占一段。

三、-g 是另一件容易误会的事

关键帧间隔不是「每 N 帧截一张图」,它影响的是压缩和随机访问:

设置 实际影响 别误会成


-g 25 每 25 帧插一个关键帧 「每 25 帧截一张图」 -g 250 关键帧更稀 「画质更好」

关键帧多 = 能切的地方多、随机访问快,但文件更大。 关键帧稀 = 文件小,但拖动进度条会慢,切片时也容易对不齐。

直播场景通常把 -g 设成「2 秒的帧数」,比如 25 帧就设 50。 这样新观众进来最多等 2 秒就能从关键帧开始解,起播快。

四、动手 Demo:四组参数,改的东西完全不同

用同一份 50 帧素材,分别改尺寸、改像素格式、改帧率,量输出字节的变化。 重点是看清:字节数并不跟着像素数线性变化。

import os
import av
import numpy as np

# 造 50 帧素材
c = av.open(u"s.mp4", u"w")
s = c.add_stream(u"libx264", rate=25)
s.width, s.height, s.pix_fmt = 160, 90, u"yuv420p"
for i in range(50):
    a = np.zeros((90, 160, 3), dtype=np.uint8)
    a[:, :, 0] = (i * 4) % 256
    a[:, :, 1] = 90
    f = av.VideoFrame.from_ndarray(a, format=u"rgb24")
    for pk in s.encode(f):
        c.mux(pk)
for pk in s.encode(None):
    c.mux(pk)
c.close()

base = os.path.getsize(u"s.mp4")
print(u"原样 %d 字节" % base)

# 改尺寸:像素数变 4 倍,字节数不一定
def enc(dst, w, h):
    c0 = av.open(u"s.mp4")
    cc = c0.streams.video[0].codec_context
    c1 = av.open(dst, u"w")
    s1 = c1.add_stream(u"libx264", rate=25)
    s1.width, s1.height, s1.pix_fmt = w, h, u"yuv420p"
    for pk in c0.demux(video=0):
        if not pk.size:
            continue
        for f in cc.decode(pk):
            fr = f.reformat(width=w, height=h, format=u"yuv420p")
            for o in s1.encode(fr):
                c1.mux(o)
    for o in s1.encode(None):
        c1.mux(o)
    c1.close()
    c0.close()
    return os.path.getsize(dst)

for w, h in ((80, 44), (320, 180)):
    print(u"%dx%d  %d 字节" % (w, h, enc(u"o.mp4", w, h)))

实跑输出:

--------------------------------------------------------------------
3.5 视频选项——四组参数,改的东西完全不同
--------------------------------------------------------------------

**① 按「改什么」分成四组**

  组           选项                        改的是
  -------------------------------------------------------
  选编码器     -c:v / -vcodec              用谁压
  改画面       -s / -pix_fmt / -aspect     像素本身
  改节奏       -r / -g / -vsync            帧怎么排
  改质量       -b:v / -crf / -qp           花多少字节

**这四组的代价完全不一样:第一组只换实现,后三组都要真的重编。**
**所以只想「换个编码器但画质不变」是做不到的——换编码器就等于重压一遍。**

**② 改尺寸:字节数不跟着像素数走**

  尺寸                像素数     输出字节     相对原样
  --------------------------------------------------------
  160x90(原样)      14400      5060         1.00×
  80x44               3520       3788         0.75×
  320x180             57600      9702         1.92×
  640x360             230400     17994        3.56×
  80x45(奇数高)     3600       编不出来     —

**最后一行是本节最容易撞上的一个硬限制:yuv420p 的色度是 2×2 取样,**
**宽高都必须是偶数,奇数尺寸直接编不出来。**
**所以在算缩放尺寸时,`-vf scale=80:-2` 里那个 `-2` 不是随便写的——**
**它的意思是「按比例算,并且保证结果是 2 的倍数」。**

**再看字节数那一列,本机的结论跟直觉不一样:**
**80x44 的像素数只有原样的 24%,字节数却是 0.75 倍。**
**反方向也一样:640x360 的像素数是 16 倍,字节数只有 3.56 倍。**

**这个「缩不下去也涨不上来」的原因是素材太简单:50 帧纯色渐变,**
**原样也才 5060 字节,里面有很大一块是容器头与索引,跟分辨率无关。**
**换成一段真实拍摄的长片子,字节数才会跟像素数大致成正比。**

**③ 改像素格式:同一张画面,三种存法**

  像素格式     每像素位数     输出字节
  ----------------------------------------
  yuv420p      12             4838
  yuv444p      24             4666
  rgb24        24             编不出来

**注意 420 和 444 的字节数几乎没有差别,甚至 444 还略小一点。**
**原因是这段素材过于简单,编码器的自适应量化波动比格式差异还大。**
**所以这条参数的作用是「控制中间计算精度」,不是「控制文件大小」。**
**要真看出 444 的代价,得用高细节画面(噪点、细纹理)配无损编码。**

**④ 改帧率:扔掉一半的帧**

  帧率     帧数      输出字节
  -------------------------------
  25       50        5060
  12       约 25     3300

**帧率减半,字节数降到 0.65 倍——没有跟着减半。**
**因为剩下的帧还是要独立承担容器开销,而且帧数少了之后帧间的冗余也少了。**
**所以「压小」时这一招不如降分辨率好用:降分辨率一动就是成倍的像素减少。**

**⑤ -g 是另一件容易误会的事**

  说法       实际影响                 别误会成
  ------------------------------------------------------------
  -g 25      每 25 帧插一个关键帧     「每 25 帧截一张图」
  -g 250     关键帧更稀               「画质更好」

**关键帧多 = 能切的地方多、随机访问快,但文件更大。**
**关键帧稀 = 文件小,但拖动进度条会慢,还容易在切片时对不齐。**

像素数从 14400 变成 57600(四倍),字节数却只涨了不到两倍。 原因是素材太简单:50 帧纯色渐变,压缩后本来就很小,容器开销占了大头。

换成真实拍摄的长片子,字节数才会跟像素数大致成正比。 这不是「理论错了」,而是「样本太小,固定成本盖过了变量差异」。

试 80×45 会直接编不出来—— yuv420p 的色度是 2×2 取样,宽度和高度都必须是偶数。 所以 scale 表达式里要用 -2 而不是 -1。

这一段的教训值得记住: 做这类对比实验,素材的复杂度决定了你能不能看出规律。 纯色渐变适合验证「代码跑不跑得通」,不适合验证「压缩率是多少」。

30 秒实验

  1. 缩一半:ffmpeg -i 片子.mp4 -vf scale=iw/2:-2 -c:v libx264 出.mp4,注意 -2 而不是 -1。
  2. 改像素格式:ffmpeg -i 片子.mp4 -pix_fmt yuv444p -c:v libx264 出.mp4,比体积与兼容性。
  3. 改帧率:ffmpeg -i 片子.mp4 -r 12 -c:v libx264 出.mp4,看帧数与体积一起降。

常见误区

  1. 用 scale=iw/2:-1。-1 只保证等比,不保证偶数。奇数高度时 yuv420p 编不出来。
  2. 以为换编码器能不掉画质。换编码器就等于重压一遍,量化方式完全不同,没有等价映射。
  3. 以为 -g 是截图间隔。它是关键帧间隔,影响压缩率与随机访问速度,跟截图无关。
自测三问(点开对答案) 1. 为什么 80×45 用 yuv420p 编不出来? —— 因为 yuv420p 的色度是 2×2 取样,宽高必须是偶数。45 是奇数,色度平面排不下。 2. `scale=iw/2:-1` 和 `scale=iw/2:-2` 有什么区别? —— `-1` 只保证等比缩放,结果可能是奇数;`-2` 保证等并且结果是 2 的倍数,避免编码失败。 3. 关键帧间隔设小了有什么代价? —— 文件变大。关键帧能独立解,体积比普通帧大得多,插得越密总字节越多。

相关笔记