🎬 第七章 FFmpeg解码音视频及流媒体
📄 一句话概括

-map 0:v -c copy 的意思是「要这条流、原样搬」,一个像素都不解。 抽出来的是裸流:没有时间戳、没有帧率、没有音视频对齐。

📝 本节和前面几节的关系

上一节用的是现成的播放器,这一节开始动手改文件。 抽流是命令行里最常见的手术,也是理解「容器与编码分离」最快的方式。 做完这一节,你会对「换个盒子装」这件事有具体的体感。

一、三个参数,各管一件事

一条完整的命令是这样的(这是 31 课讲过的写法):

ffmpeg -i in.mp4 -map 0:v -c copy out.h264
ffmpeg -i in.mp4 -map 0:a -c copy out.aac
参数 管什么 不写会怎样
-map 0:v 要哪条流 默认只挑「最好的一条」视频加一条音频
-c copy 不重编码 会按输出扩展名重编,慢几十倍
out.h264 用什么封装 按扩展名猜,猜错就报错

第一行的 0 是「输入文件序号」,不是「第几条流」。 0:v 是「第 0 个输入的所有视频流」,0:v:0 才是「第一条视频流」。 单输入时写不写 0 都一样,多输入时不写就会错—— 这类「某天突然出错」的配置,八成是多输入引出来的。

第二行是最容易漏的:不写 -c copy,ffmpeg 会老老实实解码再编码。 抽一条五分钟的 1080p 视频流,copy 是秒级,重编码是分钟级。 而且重编码还有画质损失——你只是想「拿出来」,结果它给你「重做一遍」。

二、裸流丢了什么

抽出来的 .h264 是「裸流」:一帧接一帧的 NAL,没有别的。 丢掉的恰恰是容器最值钱的部分:

| 信息 | mp4 里有 | 裸流里有 | |---|---|---|---| | 宽高 | stsd 里的 SPS | 只在 SPS 里,要先解出来 | | 帧率 | stts 时间表 | 没有,要自己数 | | 时间戳 | 每帧都有 | 没有,只剩帧顺序 | | 音视频对齐 | 两套时间基换算 | 没法对 |

这张表的每一行都能推出一类故障:播放器放 .h264 时「不知道多少帧每秒」, 只能猜一个(常见是 25),于是放出来快慢不对;而且不能拖,因为没有索引。

「宽高在 SPS 里」这一行也要说清:不是完全没有,是「要先解析 SPS 才知道」。 SPS 是一段带起始码的 NAL,解出来里面有分辨率、profile、level。 所以在裸流里,「参数」是以「码流的一部分」的形式存在的,而不是「文件头里的元数据」。

这就是容器存在的理由:它把「时间」和「参数」从码流里提出来,单独存一份索引。 有了索引才能随机访问,才能音视频对齐,才能拖。

三、为什么这件事这么快

Demo 里三个数:整体转封装、只抽视频、只抽音频,都在几毫秒量级,而原文件才一万字节。

「快」的原因不是文件小,是「没解码」: 转封装走的是 av_read_frame → av_interleaved_write_frame,包原样过去,一个像素都没碰。 唯一的开销是「重建索引」。

这条经验可以推广成一句判断:「几秒钟能完成,说明没解码;几十分钟,说明在重编码。」 所以看到一个「转格式」的命令跑得飞快,你就可以确定它在 copy,顺便也就知道「画质没变」。

反过来看也有用:如果有人抱怨「转个格式怎么这么慢」,八成是漏了 -c copy。 这是命令行里最常见的性能问题,而且一眼能看出来。

最后提一句 -map 的高级用法: -map 0:v -map 0:a -map 1:s 可以把「第一个文件的音视频 + 第二个文件的字幕」合成一个。 这就是「加字幕不重编码」的做法,跟抽流是同一套机制:只搬包,不碰像素。

四、动手 Demo:抽流——只动容器,不动编码

这段用 PyAV 做三次等价的 remux:整体转封装、只抽视频、只抽音频,每次都量耗时和输出大小。 注意 add_stream_from_template 这一句——它就是 -c copy 在 API 层的写法: 拿原来的流参数建新流,不重建编码器。

c1 = av.open(src)
c2 = av.open(out, "w", format=fmt)   # fmt=None 就是 mp4

# 这三行等于 -c copy:拿原流参数建新流,不建编码器
smap = {}
for st in streams:
    smap[st.index] = c2.add_stream_from_template(st)

for pk in c1.demux():                 # 只搬包
    if pk.stream.index in smap and pk.size:
        pk.stream = smap[pk.stream.index]
        c2.mux(pk)

实跑输出:

--------------------------------------------------------------------
抽流——只动容器,不动编码
--------------------------------------------------------------------
**`-map 0:v -c copy` 这三个字符的意思是「要哪条流、然后原样搬」**。
**`-c copy` 就是「不重编码」,所以它只经过 `libavformat`。**

  原始文件:**10017 字节**

  做法                 动了哪些库      耗时 毫秒     结果大小
  -----------------------------------------------------------------
  整体转封装           只有 format     2.1           10017 字节
  只抽视频(裸流)     只有 format     1.7           6411 字节
  只抽音频(裸流)     只有 format     1.9           810 字节

**三个数一比就明白**:
**转封装几乎不花时间**——**因为它一个像素都没解,只是把包从一套索引搬到另一套索引。**
**这就是「换个盒子装」的全部成本。**

**但抽出来的「裸流」有个代价**:

  信息           mp4 里有吗              h264 裸流里有吗
  ----------------------------------------------------------------------
  宽高           在 stsd 里的 SPS 中     **只在 SPS 中,要先解出来**
  帧率           在 stts 时间表里        **没有**,要自己数
  时间戳         每帧都有                **没有**,只有帧顺序
  音视频对齐     靠两套时间基换算        **没法对**,各是各的

**这张表解释了为什么「裸流」难处理**:**容器的作用就是把「时间」这件事记下来**,
**裸流里只剩「一帧接一帧」。**
**所以 ffplay 能直接放 `.h264`**,**但它只能按「猜的帧率」播,而且不能拖。**

**顺便说为什么 `-map 0:v` 那个 `0` 不能省**:**它是「输入文件序号」**,
**多输入的时候(比如加字幕、加背景音)不写就会错。**
**`0:v` 是「第 0 个输入的所有视频流」,`0:v:0` 才是「第一条视频流」。**
**这个区别在有多个视频轨的片子(比如 MVC 3D)上很要命。**

三行耗时都在两毫秒量级,这就是「不解码」的价格。 换成重编码,同样一万字节的片子也要几百毫秒——差两个数量级。

输出大小那列也有信息:抽视频 6411 字节、抽音频 810 字节,两个加起来不等于原文件的 10017。 差的那两千多是容器开销(索引、头部、对齐填充)。 这反过来说明「容器不是免费的」——它是用空间换「能拖、能对齐」。

pk.stream = smap[...] 这一句是最容易漏的: 包是从旧容器里读出来的,它的 stream 指向旧流, 不改指向就 mux 会报「流不在这个容器里」。 这个错在 PyAV 里报得很清楚,在 C 里就是 av_interleaved_write_frame 返回一个负数。

顺便解释「为什么裸流不能直接播」:不是解码器不认,是「没有时间轴」。 播放器要按时间显示帧,而裸流只有顺序,于是它只能「一帧一帧往下推,推多快算多快」。

30 秒实验

  1. 把 -c copy 去掉(重编码)跑一次,比较耗时差多少。
  2. 算一下:0:v 和 0:v:0 有什么区别?什么情况下结果不一样?
  3. 判断一下:把 mkv 转成 mp4 但编码不变,该写什么参数?

常见误区

  1. 忘写 -c copy。 会老老实实重编码,慢几十倍还有画质损失。 你只是想「拿出来」,它给你「重做一遍」。
  2. 以为 0:v 的 0 是「第几条流」。 它是「输入文件序号」。第一条视频流是 0:v:0。
  3. 以为抽出来的 .h264 能像 mp4 一样拖。 裸流没有索引也没有时间戳,只能顺序播。
  4. remux 时不改包的 stream 指向。 包还指着旧容器的流,写入时报「流不属于这个容器」。
自测三问(点开对答案) 1. `-map 0:v -c copy` 里的两个关键部分各是什么意思? —— `0:v` 是「第一个输入的视频流」,`-c copy` 是「不重编码,原样搬包」。 两者合起来就是「抽出来」而不是「重做一遍」。 2. 抽出来的裸流丢了什么? —— 时间戳、帧率、音视频对齐,以及「文件头里的参数」。宽高只存在于 SPS 里,要先解析码流才知道。 3. 为什么 remux 几乎不花时间? —— 因为它只走 `libavformat`,不解码也不编码,包原样搬过去,唯一开销是重建索引。

抽流有个后遗症: mp4 里的 H.264 是 AVCC 格式,而推流要 AnnexB。下一节讲这层「皮」——它是推流黑屏的头号原因。

相关笔记