🎬 第四章 FFmpeg命令行实现音视频转封装
📄 一句话概括

分离就是把一个盒子拆成两个盒子,包原样搬过去。 两个文件加起来只有原文件的 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 秒实验

  1. 只留视频:ffmpeg -i 片子.mp4 -map 0:v -c copy 画面.mp4,看输出几路流。
  2. 只留音频:ffmpeg -i 片子.mp4 -map 0:a -c copy 声音.m4a。
  3. 验证:ffprobe -show_streams 声音.m4a,确认只剩一条 audio。

常见误区

  1. 用新建流代替模板流。参数不匹配,包写不进去,会报错或产出坏文件。
  2. 以为拆出来的无声 mp4 是坏的。它本来就只有视频,播放器提示「没有音轨」是正常的。
  3. 以为两个文件加起来一定比原文件大。实测是 99%,容器开销按流计不按文件计。
自测三问(点开对答案) 1. 分离时「不重编码」靠的是哪一步? —— 把包上的 `stream` 改指向输出流,包本身一个字节不改,所以不需要解码与编码。 2. 为什么必须用模板建输出流? —— 因为新流要继承原流的编码参数,参数不匹配时包写不进去。 3. 拆出来的两个文件加起来为什么可能比原文件小? —— 因为容器开销主要按流计:两条流的索引拆开写,总量并没有翻倍。

相关笔记