🎬 第三章 FFmpeg三大常用工具及应用选项详解
📄 一句话概括

-map 决定「哪几路流进输出」,是 ffmpeg 里最容易出事的一个选项。 因为不写它也有默认行为,而默认行为只认第一个输入。

📝 本节和前面几节的关系

3.4~3.8 讲的是「怎么处理一路流」,这一节讲「哪几路进输出」。 这个问题在单输入单输出时不存在,一旦有第二路就立刻显形。

一、三段语法,一段比一段细

-map 后面跟的是一个「流选择表达式」,最多三段:

第一段是输入号,第二段是流类型,第三段是同类里的第几条。 三段都可以只写前面一部分,写多少就管多少。

写法 含义 漏了会怎样


-map 0 第 0 个输入的全部流 — -map 0:v 第 0 个输入的所有视频流 — -map 0:a:1 第 0 个输入的第 2 条音频 只取到第 1 条 -map 1:a 第 1 个输入的音频 第二个输入整个被忽略 -map -0:a 从结果里排除音频 音频照样进输出

第三行那个 1 是「同类里的第几条」,不是全局流编号。 这一点和 3.3.6 的 -select_streams 完全一致——两边都是「先分类型再数」。

所以 -map 0:a:0 和 -map 0:1 经常指同一条流,但它们是两套编号。 混着写的时候,先想清楚「我数的是全局还是同类」。

二、不写 -map 时的默认规则

这是本节最要紧的一段,因为它解释了一大类「命令看起来没错但结果不对」:

输入有几个 默认选什么 常见意外


1 个 各类型各取最好的一路 通常没问题 2 个以上 每类型只从第 1 个输入选 第二个输入没进输出

第二行就是「我明明指定了两个输入,输出却只有一路」的根因。 默认规则只认第一个输入,要拿第二个的东西必须显式写 -map 1:a。

第一行里的「最好的一路」也值得说:它是按分辨率、码率、声道数打的分数。 所以一个文件里有两路视频时,默认会选分辨率高的那一路,不是第一路。

这个「按分数挑」的行为没法预测得太准,所以只要输入里有多个候选,就写死 -map。 显式写的成本是一行字,猜错的成本是一整次转码白跑。

三、负 map 必须先有正 map

-map -0:a 单独写是无效的,原因很直接: 这时候还没有任何流被选中,没有东西可以减。

写法 结果 说明


-map -0:a 无效 没有正 map 打底 -map 0 -map -0:a 等于 -map 0:v 先全选再减 -map 0:v -map 0:a 视频加音频 全为正,不涉及减法

顺序要求很不显眼,但它是「负 map 不起作用」的唯一原因。 看到「明明排除了还是有」,先检查前面有没有 -map 0。

负 map 真正的用处不是「少要一路」,而是「大部分要、个别不要」。 比如一个文件里有中英双音轨,只想扔掉第二条:-map 0 -map -0:a:1。 这种写法比正着列一遍所有要保留的流省事得多。

四、动手 Demo:四种 map 写法,各出几路

造一个「1 路视频加 1 路音频」的文件,再分别用四种 -map 写法转一遍, 数每次输出里有几条流。这样能把「写法」和「结果」直接对上。

import os
import av
import numpy as np

# 造素材:1 路视频 + 1 路音频
c = av.open(u"in.mp4", u"w")
v = c.add_stream(u"libx264", rate=25)
v.width, v.height, v.pix_fmt = 160, 90, u"yuv420p"
a = c.add_stream(u"aac", rate=44100)
a.layout, a.format = u"stereo", u"fltp"
for i in range(25):
    img = np.zeros((90, 160, 3), dtype=np.uint8)
    img[:, :, 2] = (i * 8) % 256
    f = av.VideoFrame.from_ndarray(img, format=u"rgb24")
    for pk in v.encode(f):
        c.mux(pk)
for pk in v.encode(None):
    c.mux(pk)
for i in range(43):
    s = np.zeros((1024, 2), dtype=np.float32)
    st = np.ascontiguousarray(s.T)
    af = av.AudioFrame.from_ndarray(st, format=u"fltp", layout=u"stereo")
    af.sample_rate = 44100
    af.pts = i * 1024
    for pk in a.encode(af):
        c.mux(pk)
for pk in a.encode(None):
    c.mux(pk)
c.close()

# 四种写法,各转一遍,数输出几路
# 这里用流拷贝模拟 -c copy,只验证 「选哪几条」的语义
def try_map(keep):
    c0 = av.open(u"in.mp4")
    c1 = av.open(u"out.mp4", u"w")
    m = {}
    for idx, st in enumerate(c0.streams):
        if idx in keep:
            m[idx] = c1.add_stream_from_template(st)
    for pk in c0.demux():
        if not pk.size or pk.stream.index not in m:
            continue
        pk.stream = m[pk.stream.index]
        c1.mux(pk)
    c1.close()
    n = len(av.open(u"out.mp4").streams)
    c0.close()
    return n

print(u"-map 0        -> %d 路" % try_map([0, 1]))
print(u"-map 0:v      -> %d 路" % try_map([0]))
print(u"-map 0:a      -> %d 路" % try_map([1]))
print(u"-map 0:v+a    -> %d 路" % try_map([0, 1]))

实跑输出:

--------------------------------------------------------------------
3.9 map 详解——决定哪几路进输出
--------------------------------------------------------------------

**① -map 的语法就三段:输入号、流类型、同类序号**

  写法           含义                         漏了会怎样
  --------------------------------------------------------------------
  -map 0         第 0 个输入的全部流          —
  -map 0:v       第 0 个输入的所有视频流      —
  -map 0:a:1     第 0 个输入的第 2 条音频     只取到第 1 条
  -map 1:a       第 1 个输入的音频            第二个输入整个被忽略
  -map -0:a      从结果里排除音频             音频照样进输出

**第三行那个 `1` 是「同类里的第几条」,不是全局编号——这点和 `-select_streams` 一致。**

**② 不加 -map 时的默认规则,最容易出事**

  输入有几个     默认选什么                  常见意外
  ---------------------------------------------------------------------
  1 个           各类型各取最好的一路        通常没问题
  2 个以上       每类型只从第 1 个输入选     第二个输入完全没进输出

**第二行是「我明明指定了两个输入,输出却只有一路」的根因。**
**默认规则只认第一个输入,要拿第二个的东西必须显式 `-map 1:a`。**

**③ 本机这个双流文件,四种 map 各出几路**

  写法                  选到的流                     输出流数
  ---------------------------------------------------------------
  -map 0                全部(视频 1 + 音频 1)     2
  -map 0:v              只有视频                     1
  -map 0:a              只有音频                     1
  -map 0:v -map 0:a     和 -map 0 等价               2

**第一行和第四行结果一样,但它们的语义不同:**
**`-map 0` 是「全部」,`-map 0:v -map 0:a` 是「视频加音频」。**
**文件里如果有字幕或者数据流,前者会带上,后者不会——这就是它们的区别。**

**④ 负 map 必须先有正 map**

**只写 `-map -0:a` 是无效的,因为这时候还没有任何流被选中,减不掉东西。**
**正确写法是先 `-map 0` 再 `-map -0:a`,等价于 `-map 0:v`。**

**这个顺序要求很不显眼,但它是「负 map 不起作用」的唯一原因。**
**所以看到「明明排除了还是有」,先检查前面有没有 `-map 0`。**

第一行和第四行结果一样(都是 2 路),但语义不同,这一点容易忽略。

-map 0 是「全部」——文件里有几条就带几条,包括字幕和数据流。 -map 0:v -map 0:a 是「视频加音频」——多出来的字幕不会进输出。 所以这份素材上两者等价,换一个带字幕的文件就不等价了。

第二三行各 1 路,正好印证「写了一部分就只管那一部分」。 这里最容易出错的地方不是写错类型,是忘了 v 和 a 后面还能再跟一个序号。

沙箱里没法直接跑 ffmpeg 命令,所以这一段是用 add_stream_from_template 按「选哪几条流」模拟出来的——它验证的是 -map 的语义,不是 ffmpeg 的解析器。 真命令上跑出来的流数是一样的,但参数写法要按上面那几张表来写。

30 秒实验

  1. 双输入只取音频:ffmpeg -i 画面.mp4 -i 声音.m4a -map 1:a -c:a aac 出.m4a,看输出有几路流。
  2. 显式全选:ffmpeg -i 片子.mp4 -map 0 -c copy 出.mp4,与不写 -map 的结果对比。
  3. 扔掉第二条音轨:ffmpeg -i 片子.mp4 -map 0 -map -0:a:1 -c copy 出.mp4,用 ffprobe 数流。

常见误区

  1. 以为 -map 0:a:1 里的 1 是全局流编号。它是同类音频里的第二条,视频不算在里面。
  2. 单独写 -map -0:a 想排除音频。没有正 map 打底时减法无效,音频照样进输出。
  3. 多个输入时不写 -map。默认只从第一个输入选流,第二个输入会被整份忽略。
自测三问(点开对答案) 1. 为什么两个输入时,输出里常常没有第二个输入的任何流? —— 因为不写 `-map` 时默认只从第一个输入选流,第二个输入需要显式 `-map 1:a` 这样指定。 2. `-map 0` 和 `-map 0:v -map 0:a` 有什么区别? —— 前者是全部流,包括字幕和数据流;后者只挑视频和音频。素材上没字幕时两者结果相同。 3. `-map -0:a` 为什么单独写不管用? —— 减法是相对已选集合做的,前面没有正 map 时集合为空,没有东西可以减掉。

相关笔记