-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 秒实验
- 把
-c copy去掉(重编码)跑一次,比较耗时差多少。 - 算一下:
0:v和0:v:0有什么区别?什么情况下结果不一样? - 判断一下:把 mkv 转成 mp4 但编码不变,该写什么参数?
常见误区
- 忘写
-c copy。 会老老实实重编码,慢几十倍还有画质损失。 你只是想「拿出来」,它给你「重做一遍」。 - 以为
0:v的0是「第几条流」。 它是「输入文件序号」。第一条视频流是0:v:0。 - 以为抽出来的
.h264能像 mp4 一样拖。 裸流没有索引也没有时间戳,只能顺序播。 - remux 时不改包的
stream指向。 包还指着旧容器的流,写入时报「流不属于这个容器」。
自测三问(点开对答案)
1. `-map 0:v -c copy` 里的两个关键部分各是什么意思? —— `0:v` 是「第一个输入的视频流」,`-c copy` 是「不重编码,原样搬包」。 两者合起来就是「抽出来」而不是「重做一遍」。 2. 抽出来的裸流丢了什么? —— 时间戳、帧率、音视频对齐,以及「文件头里的参数」。宽高只存在于 SPS 里,要先解析码流才知道。 3. 为什么 remux 几乎不花时间? —— 因为它只走 `libavformat`,不解码也不编码,包原样搬过去,唯一开销是重建索引。抽流有个后遗症: mp4 里的 H.264 是 AVCC 格式,而推流要 AnnexB。下一节讲这层「皮」——它是推流黑屏的头号原因。
- ☐ (本节完,下一节 07.02.03 h264_mp4toannexb)