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

合成就是把两个盒子的包装回一个盒子,字节数几乎不变。 真正要小心的是时间戳:起点不一样,合完就音画不同步。

📝 本节和前面几节的关系

4.2.1 拆开,这一节合回去,正好走完一个来回。 4.2.4 接着讲「两份完整的片子怎么接成一份」。

一、合成的三步

比分离多一步:要给两条流各建一条输出流。

步骤 做什么 容易错的地方


1 打开两个输入 时间基可能不同 2 各建一条输出流 必须按模板建 3 轮流取包写进去 包要改指向对应输出流

第三步「轮流」是自然的:读包的时候本来就是按文件里的顺序出来, 视频包和音频包交替出现,各自写进各自的输出流就行。

命令行里就是两句 -i 加一句 -map 0:v -map 1:a -c copy。 不写 -map 的话,默认只会从第一个输入选流,第二个输入的音频整份丢掉。

二、字节数几乎不变,说明没重压

这份素材上,合成的文件与原文件字节完全相同:

文件 字节 流数


原文件 37982 2 拆开再合成 37982 2

一模一样不是巧合:包没动,目录的写法也回到了同样的布局,所以总字节自然相同。 换成重编码,同样 2 秒会大出或小掉一大截——那就不是「合成」而是「重压」了。

所以判断一条命令到底是转封装还是重编码,看字节与耗时最快: 字节几乎不变、耗时几乎是零,那就是转封装。

三、三条必须守的规矩

合成看着简单,出问题基本都在这三处:

规矩 为什么 不守的后果


时间基要一致 两条流的时间单位不同 音画不同步 起点对齐到 0 各自的第一个 pts 常不为 0 开头一段空白 时长不等要截断 短的先结束 后面拖出一段静音

第三行对应命令行里的 -shortest:谁短听谁的,另一个提前收工。 不加它,输出时长按长的算,短的那条后面就是无声或静止画面。

第二行是最隐蔽的:很多采集出来的文件第一个 pts 不是 0,而是某个正数。 直接合成,开头就会空一段——看着像「音画不同步」,其实是「起点没对齐」。

四、动手 Demo:把拆出来的两条流装回一个盒子

拿 4.2.1 拆出来的「只有视频」和「只有音频」两个文件,重新装回一个 mp4。 仍然走模板建流加改包指向,不重编码。

import av, os

c1 = av.open(u"join.mp4", u"w")
outs = []
for p in (u"v.mp4", u"a.m4a"):
    c0 = av.open(p)
    for s in c0.streams:
        outs.append((c0, c1.add_stream_from_template(s)))

for c0, tgt in outs:
    for pk in c0.demux():
        if not pk.size:
            continue
        pk.stream = tgt
        c1.mux(pk)
    c0.close()
c1.close()

for p in (u"m4.mp4", u"join.mp4"):
    c = av.open(p)
    print(u"%-10s %d 字节  %d 条流" % (p, os.path.getsize(p), len(c.streams)))
    c.close()

实跑输出:

--------------------------------------------------------------------
4.2.3 用音频流和视频流合成 MP4
--------------------------------------------------------------------

**① 把上一段拆出来的两个文件重新装回一个盒子**

  文件       字节      流数
  -----------------------------
  原文件     37982     2
  合成的     37982     2

**② 字节几乎一样,说明这一趟没重编码**

**合成用的是流拷贝:包原样搬,只重写目录。**
**如果换成重编码,同样 2 秒会大出或小掉一大截——那就不是「合成」而是「重压」了。**

**③ 三条必须守的规矩**

  规矩                     为什么
  ------------------------------------------------------------
  两条流的时间基要一致     否则合成后音画不同步
  起点要对齐到 0           两段各自的第一个 pts 常常不为 0
  时长不等时要截断         短的先结束,长的会继续拖尾

**第三条对应命令行里的 `-shortest`:谁短听谁的,另一个提前收工。**
**不加它,输出时长按长的算,短的后面那段就是无声或静止。**

两个文件分别打开、分别读包、写进同一个输出容器——这就是「合成」的全部。 注意 c0 要在写完它自己的包之后再关,不能在循环里提前关。

本机实测合成后与原文件字节完全相同(37982),这是「没重编码」最硬的证据。 只要有一个字节的画面被重新压过,字节数就不可能刚好对上。

这段代码假设两条流的起点都已经对齐到 0。真做工程时要先读第一包的 pts, 把两条流各自减掉自己的起点——命令行里这一步由 -avoid_negative_ts 之类的选项处理。

30 秒实验

  1. 双输入合成:ffmpeg -i 画面.mp4 -i 声音.m4a -map 0:v -map 1:a -c copy 出.mp4。
  2. 强制截断:再加 -shortest,看输出时长变成谁的长度。
  3. 对比时长:ffprobe -show_format 出.mp4,看 duration 与两个输入的关系。

常见误区

  1. 两个输入时忘记写 -map。默认只从第一个输入选流,第二份音频整份丢掉。
  2. 不检查起点就合成。第一个 pts 不为 0 时,开头会空一段,看着像音画不同步。
  3. 时长不等时不加 -shortest。输出会按长的算,短的那条后面拖一段静音。
自测三问(点开对答案) 1. 合成时为什么字节数能跟原文件一模一样? —— 因为包原样搬、目录布局也一样,没有任何一个字节被重新编码。 2. `-shortest` 解决的是什么问题? —— 两条流时长不等时,让输出在短的那条结束时收尾,避免后面拖出静音或静止画面。 3. 「开头空一段」通常是什么原因? —— 两条流的第一个 pts 不为 0 且没有对齐,导致时间轴整体偏移。

相关笔记