转封装只搬包,转码要解开每一帧再压回去,代价差一个数量级。 能 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 秒实验
- 只换盒子:
ffmpeg -i 片子.mp4 -c copy 出.mkv,秒出,字节几乎不变。 - 只改后缀:把
.mp4直接改成.mkv也能播,但严格说那是「容器与后缀不符」,不规范。 - 强制转码:
-c:v libx264 -crf 23,对比耗时,感受一次「解码 + 编码」的价格。
常见误区
- 该 copy 却重编码。只为换个后缀就重压一遍,白白掉一次画质还慢几十倍。
- 以为
-c copy什么都能转。目标容器不认这个编码时,它会直接报错或写出坏文件。 - 以为转码画质不变。只要重编码就一定有损失,CRF 18 也只是「几乎看不出」。
自测三问(点开对答案)
1. 转封装与转码最本质的区别是什么? —— 有没有解码再编码。转封装只搬包,转码要把每一帧解开再压回去。 2. 为什么转封装的文件可能比源大几个字节? —— 换了容器,头部结构与索引写法不同,包本身没变,差的是盒子的开销。 3. 什么时候必须转码? —— 目标设备不认原编码、要改分辨率或码率、要动画面(剪接加特效)、多路源要统一。- ☐ (本节完,下一节 5.2.1 利用 FFmpeg 提取视频的 YUV 像素数据)