🎬 第二章 FFmpeg命令行初体验
📄 一句话概括

「格式转换」其实是两件不同的事:换容器和换编码。 前者加 -c copy 几十秒过,后者必须动像素,耗时差一个量级。

📝 本节和前面几节的关系

2.1 讲了参数位置,这一节是本章第一条真正干活的命令。 2.3 抽帧、2.4 拼视频,都是从这条命令派生出来的。

一、「格式转换」这四个字底下有四件事

很多人说「帮我转个格式」,其实想要的可能是下面四件里任意一件:

说法 实际做了什么 能不能 -c copy


换容器 码流原样搬进新壳 能 换编码 解成像素再重新压 不能 换分辨率 缩放了再压 不能 换码率 用新码率重压 不能

后三行做不到 -c copy 是硬限制,不是参数没写对: 它们要改的是「像素本身」,而 -c copy 的前提就是「不碰像素」。

所以判断该走哪条,只需要问一句:我要改的东西在壳上还是在画面上? 在壳上就 -c copy,在画面上就只能重编码。

二、-c copy 省掉的到底是哪三步

回到五阶段流水线看,-c copy 把中间三步全剪掉了:

阶段 正常转码 加了 -c copy


解封装 做 做 解码 做 跳过 滤镜 做 跳过 编码 做 跳过 封装 做 做

剩下的两步都只搬字节,不看内容——所以转封装的速度基本由磁盘决定,跟分辨率无关。

这也解释了一个反直觉的现象:4K 片子和 480p 片子,转封装的耗时差不多。 但只要去掉 -c copy,两者的耗时立刻拉开几十倍。

三、三条命令覆盖绝大多数需求

不用记参数表,记住这三条就够开始用了:

要做的事 命令 耗时量级


换容器 ffmpeg -i 输入.mp4 -c copy 输出.mkv 秒级 换编码 ffmpeg -i 输入.mp4 -c:v libx264 输出.mp4 分钟级 只抽音频 ffmpeg -i 输入.mp4 -vn -c copy 输出.m4a 秒级

第一条和第三条都带 -c copy,所以都是秒级。 第三条里的 -vn 是「不要视频」,配上 -c copy 就只是把音轨原样倒出来。

第二条是唯一慢的,因为它真的在重压。这时候才需要考虑编码器选谁、 码率给多少、preset 用哪个——这些都是第五章的内容。

还有一个必须知道的限制:不是所有容器都能装所有编码。 装不进去时 ffmpeg 直接报错,不会偷偷帮你重编码——这是好事,别嫌它烦。

四、动手 Demo:转封装与转码差多少

同一份素材、同一段内容,用两条路线各转一次:一条只搬包,一条解了再编。 两条路线的耗时与输出字节都记在下面,差距比想象中大。

import time
import av

def copy_route(src, dst):
    # 转封装:不解码,直接把包搬过去
    ic = av.open(src)
    oc = av.open(dst, u"w")
    ost = oc.add_stream_from_template(ic.streams.video[0])
    for pk in ic.demux(video=0):
        if pk.dts is None:
            continue
        pk.stream = ost
        oc.mux(pk)
    oc.close()
    ic.close()

def recode_route(src, dst):
    # 转码:解成像素,再重新压一遍
    ic = av.open(src)
    cc = ic.streams.video[0].codec_context
    oc = av.open(dst, u"w")
    s = oc.add_stream(u"libx264", rate=25)
    s.width, s.height, s.pix_fmt = 160, 90, u"yuv420p"
    for pk in ic.demux(video=0):
        if not pk.size:
            continue
        for f in cc.decode(pk):
            for o in s.encode(f):
                oc.mux(o)
    for o in s.encode(None):
        oc.mux(o)
    oc.close()
    ic.close()

for name, fn in ((u"转封装", copy_route), (u"重编码", recode_route)):
    t0 = time.perf_counter()
    fn(u"src.mp4", u"out_%s" % name)
    t = (time.perf_counter() - t0) * 1000
    print(u"%s  %.1f 毫秒  %d 字节" % (name, t, os.path.getsize(u"out_%s" % name)))

实跑输出:

--------------------------------------------------------------------
音视频格式转换——转封装与转码差多少
--------------------------------------------------------------------

**① 同一个输入,两条命令,成本差一个量级**

  做法               耗时(毫秒)     输出字节     动像素吗
  -------------------------------------------------------------
  转封装 -c copy     5.0              9144         不动
  重编码             67.7             9484         动

**源文件 9606 字节、125 帧。转封装只换了外壳,重编码把解出来的 123 帧全拆开又压了一遍。**
**耗时差 13.6 倍,字节数差 340——而画质上,转封装是一点不掉,重编码是掉一点。**

**② 所以「格式转换」这个词其实指两件不同的事**

  说法         实际做了什么               什么时候用
  --------------------------------------------------------------
  换容器       码流不动,只改外面的壳     播放器不认这个后缀
  换编码       解成像素再重新压           播放器不认这种编码
  换分辨率     必须重编码                 大屏转小屏
  换码率       必须重编码                 压小一点省流量

**后三行都绕不开重编码,因为它们改的是「像素本身」。**
**只有第一行能靠 -c copy 秒过——这也是它最值钱的地方。**

**③ 判断该走哪条,问自己一个问题就够**

**「我要变的东西,是文件里面的排列,还是画面本身?」**
**排列 → 转封装;画面 → 转码。**

注意 copy_route 里一处 decode 都没有——它从头到尾只碰 Packet。 recode_route 里既有 cc.decode 又有 s.encode,中间必然过了一遍像素。

两条路线的输出字节数不一样,而且转封装那一份更小。 码流一个字节没动,少的全是容器自己的开销:mp4 的索引记在文件尾,mkv 是边写边记。

耗时差了一个数量级,而这个差距在真实素材上会更大: 素材分辨率越高、越长,重编码那一路的成本涨得越快,转封装那一路几乎不动。

还要注意一个细节:add_stream_from_template 是「照抄源流的参数」。 这一步是转封装能成立的关键——输出流的所有编码参数必须跟源流完全一致。

30 秒实验

  1. 比一次:拿一个几十 MB 的 mp4,先 -c copy 转 mkv,再去掉这个参数转一次,看耗时差几倍。
  2. 验一次:转封装后比较输入输出的大小,你会发现码流字节数一样,差的是容器开销。
  3. 试一次错:把 h264 塞进一个不支持的容器,看它报什么错。这个错以后会经常遇到。

常见误区

  1. 能转封装却去转码。-c copy 秒级的事,重编码要几分钟,画质还掉一点。先想清楚自己是不是只想换个壳。
  2. 以为 -c copy 能改编码。它只搬码流,编码方式一个字都不变。想换编码必须去掉这个参数。
  3. 以为所有容器都能装所有编码。mp4 装不进某些老编码,mkv 基本什么都能装。报错是正常的,不是命令写错。
自测三问(点开对答案) 1. 把 mp4 转成 mkv,画质会掉吗? —— 不会,只要加了 `-c copy`。码流原样搬过去,一帧都没重编。掉画质只发生在你去掉这个参数让它重压的时候。 2. 为什么转封装的耗时跟分辨率几乎无关? —— 因为它跳过了解码、滤镜、编码三步,剩下的解封装与封装都只是搬字节。搬字节的成本跟数据量有关,跟画面内容无关。 3. `-vn -c copy` 这条命令做了什么? —— `-vn` 表示不要视频流,`-c copy` 表示音频码流原样搬。合起来就是「只把音轨原封不动倒出来」,所以是秒级。

相关笔记