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

转封装只搬包,转码要解开每一帧再压回去,代价差一个数量级。 能 copy 就别重编码,这是整章最省钱的一句话。

📝 本节和前面几节的关系

5.1.1 与 5.1.2 分别讲了视频和音频怎么压;这一节把两条线合起来, 算清楚「换盒子」与「换编码」各要付多少钱。

一、三个词先分清

日常说话混着用,落到命令上是三件不同的事:

做法 动没动编码 典型参数


转封装 没动,只换盒子 -c copy 转码 解码再编码 -c:v libx264 混流 多路合成一路 -filter_complex

判断标准就一条:目标设备认不认现在的编码。 认,就用 copy;不认,才值得付一次重编码的代价。

第三行「混流」最容易搞混:画面叠字幕、画中画、多机位拼接, 这些都必须解码,所以它们天然属于「转码」这一侧,省不掉。

二、代价实测:两条路差多少

同一份 2 秒素材,两条路各跑一遍:

做法 字节 耗时


源文件 38933 — 转封装 38933 0.006 秒 重编码 5678 0.041 秒

转封装这一行与源文件一个字节都不差——它没动画面,只是把包换了层包装。 重编码那一行只剩源的 15%,代价是慢了七倍:中间多了「解码 + 编码」两步。

注意这个倍数不能当定律:素材只有 2 秒,进程启动的固定开销占了大头。 换成十分钟的片子,倍数会拉到几十倍,那时差距就完全由 CPU 决定了。

三、什么情况下才真需要转码

四种情况,只有四种:

情况 为什么必须转码


目标设备不认这个编码 老电视只认 H.264 要改分辨率或码率 手机流量有限,得压小 要剪接或加特效 一动画面就得重压 多路源要统一 素材编码混杂,先统一再拼

反过来,下面这些都不需要转码:只改后缀名、只换容器、只提取某条流。 这三件事一律用 -c copy,速度快到几乎感觉不到。

最后提醒一句方向问题:转码是单向的。 想把 720p 压成 480p 再变回 720p,得到的不是原来的 720p,而是「糊过的 720p」。

四、动手 Demo:转封装与重编码,各计一次时

先做一次纯转封装:把两条流原样搬到新文件,包不动。 再做一次重编码:解码每一帧,用 CRF 23 压回去。两条路都计时。

import av, os, time

t0 = time.time()
c0 = av.open(u"s5.mp4")
c1 = av.open(u"cp.mp4", u"w")
mp = {s.index: c1.add_stream_from_template(s) for s in c0.streams}
for pk in c0.demux():
    if not pk.size:
        continue
    pk.stream = mp[pk.stream.index]
    c1.mux(pk)
c1.close()
c0.close()
t_copy = time.time() - t0
print(u"转封装 %d 字节  %.3f 秒" % (os.path.getsize(u"cp.mp4"), t_copy))

t0 = time.time()
c0 = av.open(u"s5.mp4")
c1 = av.open(u"re.mp4", u"w")
v1 = c1.add_stream(u"libx264", rate=25)
v1.width, v1.height, v1.pix_fmt = 160, 90, u"yuv420p"
v1.options = {u"crf": u"23"}
cc = c0.streams.video[0].codec_context
for pk in c0.demux(video=0):
    if not pk.size:
        continue
    for f in cc.decode(pk):
        for o in v1.encode(f):
            c1.mux(o)
for f in cc.decode(None):
    for o in v1.encode(f):
        c1.mux(o)
for o in v1.encode(None):
    c1.mux(o)
c1.close()
c0.close()
print(u"重编码 %d 字节  %.3f 秒" % (os.path.getsize(u"re.mp4"), time.time() - t0))

实跑输出:

--------------------------------------------------------------------
5.1.3 转码与转封装的代价差多少
--------------------------------------------------------------------

**① 同一份文件,两条路各跑一遍并计时**

  做法       字节      耗时
  ------------------------------
  源文件     38933     —
  转封装     38933     0.006 秒
  重编码     5678      0.041 秒

**转封装这一行与源文件一模一样,一个字节都不差:它没动画面,只是把包换了层包装。**
**重编码那一行只剩源的 15%,代价是花了 7 倍时间:中间多了「解码 + 编码」两步。**
**素材只有 2 秒,绝对耗时都不大;换成十分钟的片子,这个倍数会很扎眼。**

**② 转码这条流水线的四段**

  段             做什么                 能不能省
  ---------------------------------------------------
  解封装         把包从盒子里取出来     必要
  解码           包变帧                 必要
  滤镜与编码     改画面再压回去         改参数时必要
  封装           包装进新盒子           必要

**只要目标盒子认原编码,中间两段就能整段跳过——这就是 `-c copy`。**
**反过来说,只要动了画面或声音,中间两段就一段都省不掉。**

转封装的核心就两行:add_stream_from_template 造一条同参数的流, 再把包上的 stream 指针改成新流,直接 mux 出去。包里的数据一个字节没动。

add_stream_from_template 会把编码参数、时间基、附加数据一起抄过来。 少了这一步,muxer 会因为「不知道这条流是什么编码」而拒绝。

重编码那一段只处理了视频,音频原样没碰。 这是故意的:两条路比的必须是同一个对象,都只看视频流的变化。

真实项目里要音视频一起转,就得在同一个 demux 循环里按 pk.stream.type 分发, 分别喂给视频流和音频流——分成两个 decode 循环容易漏帧。

30 秒实验

  1. 只换盒子:ffmpeg -i 片子.mp4 -c copy 出.mkv,秒出,字节几乎不变。
  2. 只改后缀:把 .mp4 直接改成 .mkv 也能播,但严格说那是「容器与后缀不符」,不规范。
  3. 强制转码:-c:v libx264 -crf 23,对比耗时,感受一次「解码 + 编码」的价格。

常见误区

  1. 该 copy 却重编码。只为换个后缀就重压一遍,白白掉一次画质还慢几十倍。
  2. 以为 -c copy 什么都能转。目标容器不认这个编码时,它会直接报错或写出坏文件。
  3. 以为转码画质不变。只要重编码就一定有损失,CRF 18 也只是「几乎看不出」。
自测三问(点开对答案) 1. 转封装与转码最本质的区别是什么? —— 有没有解码再编码。转封装只搬包,转码要把每一帧解开再压回去。 2. 为什么转封装的文件可能比源大几个字节? —— 换了容器,头部结构与索引写法不同,包本身没变,差的是盒子的开销。 3. 什么时候必须转码? —— 目标设备不认原编码、要改分辨率或码率、要动画面(剪接加特效)、多路源要统一。

相关笔记