分离就是把一个盒子拆成两个盒子,包原样搬过去。 两个文件加起来只有原文件的 99%——拆开不会更贵。
4.1 讲清了四层结构,这一节动手拆第一刀:把音视频分开。 4.2.3 会把它们重新装回去,来回一趟正好看清单价。
一、分离就是「只装一条流」
做法只有一句话:打开原文件,只给目标流建一条输出流,其余全部丢掉。
步骤 做什么 注意
1 打开原文件 只读 2 只建一条输出流 用模板建,参数原样复制 3 循环取包,跳过不要的流 按 stream.type 判 4 把包改指向输出流再写 不重编码的关键
第四步是整个操作的核心:包上的 stream 属性一改,同一个包就写进新盒子了。
字节没动、时间戳没动、编码参数也没动,只换了它在哪个盒子里。
对应命令行就是 -map 0:v -c copy 与 -map 0:a -c copy 两句。
-map 决定装哪条,-c copy 决定不重压——两个都要有。
二、拆开之后字节会怎样
直觉是「两个文件各有一个头,肯定更大」,实测却相反:
项 实测 说明
原文件 100% 一个盒子装两条流 只有视频 11% 画面本来就不占大头 只有音频 88% 声音反而更大 两个相加 99% 比原文件还小一点
第三行值得停下来看一眼:一份 160×90 的极简画面,音频居然占了 88%。 因为 AAC 是按固定码率压的,画面越简单,音频的相对占比就越高。
第四行那个「99%」说明容器开销是按「流」计的,不是按「文件」计的。 拆成两个盒子并没有各自多付一份固定的头,所以来回一趟几乎免费。
三、分离出来的文件能不能直接用
能,但有两个细节要注意:
问题 表现 处理
只有视频的 mp4 播放器可能报「没有音轨」 正常,本就是无声片 只有音频的 m4a 时长比原片略长 尾包补齐导致,可裁 后缀名乱写 多数播放器仍能放 靠内容识别,不靠后缀
第一行常被当成错误:从视频里拆出来的 mp4 本来就没有声,不是拆坏了。 第二行那个「略长」就是音频一包 1024 采样凑整的结果。
第三行说明后缀只是提示:.m4a 与 .mp4 在 ffmpeg 眼里由同一个 demuxer 处理,
所以把 .m4a 改名成 .mp4 照样能放——但别人家的软件未必这么宽容。
四、动手 Demo:拆成两个盒子,看字节怎么分
把同一份 2 秒素材拆成「只有视频」和「只有音频」两个文件。 关键是第四步:把包的 stream 改指到新流上,这样就不用重编码。
import av
for dst, keep in ((u"v.mp4", u"video"), (u"a.m4a", u"audio")):
c0 = av.open(u"m4.mp4")
c1 = av.open(dst, u"w")
tgt = None
for s in c0.streams:
if s.type == keep:
tgt = c1.add_stream_from_template(s)
for pk in c0.demux():
if not pk.size or pk.stream.type != keep:
continue
pk.stream = tgt # 关键:包改指向输出流
c1.mux(pk)
c1.close()
c0.close()
import os
for p in (u"m4.mp4", u"v.mp4", u"a.m4a"):
print(u"%-8s %d" % (p, os.path.getsize(p)))
实跑输出:
--------------------------------------------------------------------
4.2.1 从 MP4 里提出音频流和视频流
--------------------------------------------------------------------
**① 两条流各自装一个盒子**
文件 字节 占原文件
-----------------------------------
原文件 37982 100%
只有视频 4010 11%
只有音频 33490 88%
两个相加 37500 99%
**② 相加只有 99%,拆开反而略小**
项 值
-----------------------------
差多少字节 -482
方向 拆开后更小
**这个方向值得想一下:直觉上「两个文件各有一个头」应该更大,实测却是更小。**
**原因是容器开销主要按「流」计,不是按「文件」计——**
**原来那个 moov 索引要同时描述视频与音频两条流,拆开后每个盒子只描述一条,总量并没有翻倍。**
**所以「分离再合成」这个来回,字节成本几乎是零(差不到 1%)。**
**换句话说:转封装不要钱,要的是时间——而且要小心时间戳,这才是真正会出事的地方。**
add_stream_from_template 是这一步能「不重编码」的关键:
它把原流的编码参数(SPS/PPS、采样率、声道)整套复制过去,新盒子才认这些包。
如果改用 add_stream 直接指定 libx264 新建一条流,包就写不进去了——参数不匹配。
很多人第一次做分离失败,就是栽在「新建流」而不是「按模板建流」上。
跑完会发现 v.mp4 只有 4010 字节、a.m4a 有 33490 字节,加起来 37500,比原文件小 482。
这个「小一点」不是误差,而是说明容器开销主要记在「流」头上。
30 秒实验
- 只留视频:
ffmpeg -i 片子.mp4 -map 0:v -c copy 画面.mp4,看输出几路流。 - 只留音频:
ffmpeg -i 片子.mp4 -map 0:a -c copy 声音.m4a。 - 验证:
ffprobe -show_streams 声音.m4a,确认只剩一条 audio。
常见误区
- 用新建流代替模板流。参数不匹配,包写不进去,会报错或产出坏文件。
- 以为拆出来的无声 mp4 是坏的。它本来就只有视频,播放器提示「没有音轨」是正常的。
- 以为两个文件加起来一定比原文件大。实测是 99%,容器开销按流计不按文件计。
自测三问(点开对答案)
1. 分离时「不重编码」靠的是哪一步? —— 把包上的 `stream` 改指向输出流,包本身一个字节不改,所以不需要解码与编码。 2. 为什么必须用模板建输出流? —— 因为新流要继承原流的编码参数,参数不匹配时包写不进去。 3. 拆出来的两个文件加起来为什么可能比原文件小? —— 因为容器开销主要按流计:两条流的索引拆开写,总量并没有翻倍。- ☐ (本节完,下一节 4.2.2 h264_mp4toannexb)