容器不是一种视频格式,它是个盒子:装若干条流,再加一张目录。 「MP4 文件」的准确说法是「用 MP4 盒子装了 H.264 加 AAC」。
第三章讲的是「查选项」,从这一章开始动手改文件。 第一件事就是搞清楚:一个 mp4 里到底有几样东西。
一、一个文件的四层账
从外到里,每一层都有自己的账:
层 叫什么 管什么
盒子 容器/封装 把几条流装在一起,写一张目录 流 track 一条视频或一条音频 包 packet 封装的最小单位,带时间戳 帧 frame 解出来的画面或声音
「封装」就是把包按某种规矩排进盒子,「解封装」就是反过来把包取出来。 这两个动作都不碰画面本身——包里的字节一个都不改。
而「解码」才是把包变回帧,那一步才真的动数据。 分清「封装」与「解码」,是这一整章的地基。
二、包与帧不是一一对应的
最容易想错的一处:以为一帧就是一个包。 实际上大多数时候接近,但不严格相等:
情况 包与帧 原因
视频无 B 帧 基本相等 一帧压成一个包 编码器收尾 包多 1 flush 时吐一个尾包 音频 各自计数 一包固定 1024 采样
第二行那个尾包很容易被忽略,但它是「解完必须 flush」这个规矩的由来。 不 flush,最后那一小段数据就丢了——表现是「时长少了一点点」。
第三行的音频更要小心:一包 1024 采样,跟「一帧画面」完全不是一个概念。 所以「这个文件有多少帧」这种问题,问音频是没有意义的。
三、转封装为什么不花钱
因为包可以原样搬走,只有目录要重写:
动作 要做什么 代价
转封装 取包、改目录、写包 几乎不花 CPU 重编码 解帧、算、编包 CPU 与时间都贵 分离 取包、写两个盒子 同转封装
第一行的代价「几乎不花 CPU」是有前提的:盒子接不接这种编码。 盒子不接,就只能走第二行——这是第四章与第五章的分界线。
所以判断「能不能只转封装」,问的永远是「目标盒子认不认这种码流」, 而不是「这两个格式能不能互转」——后者是个伪问题。
四、动手 Demo:一个文件的四层账:盒子、流、包、帧
造一份 2 秒的素材:1 路视频(50 帧)加 1 路音频(2 秒)。 然后分别数:文件多大、有几条流、解出多少包、解出多少帧。 四个数字摆在一起,四层结构就具体了。
import av
import numpy as np
# 造素材:1 路视频 + 1 路音频(完整代码见 Demo 素材函数 _mk)
import av
c = av.open(u"m4.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"
# 50 帧画面 + 2 秒声音,写入 m4.mp4
...
c.close()
c = av.open(u"m4.mp4")
print(u"容器名 %s" % c.format.name)
print(u"流数 %d" % len(c.streams))
c.close()
# 数包
c = av.open(u"m4.mp4")
nv = na = 0
for pk in c.demux():
if not pk.size:
continue
if pk.stream.type == u"video":
nv += 1
else:
na += 1
c.close()
print(u"视频包 %d 音频包 %d" % (nv, na))
# 数帧
c = av.open(u"m4.mp4")
fv = sum(1 for _ in c.decode(video=0))
c.close()
print(u"视频帧 %d" % fv)
实跑输出:
--------------------------------------------------------------------
4.1 容器与封装/解封装——一个文件的四层账
--------------------------------------------------------------------
**① 容器不是「一种视频格式」,它是个盒子**
**盒子里装着若干条流,每条流由一种编码格式压出来,再加一张目录(索引)。**
**所以「MP4 文件」这个说法本身就不准确——准确说法是「用 MP4 这个盒子装了 H.264 加 AAC」。**
项 值
------------------------------------------
容器格式名 mov,mp4,m4a,3gp,3g2,mj2
流数 2
流类型 video、audio
时长 2.00 秒
文件字节 37982
**② 解封装出包,解码出帧**
统计项 视频 音频
---------------------------------
包数 50 88
包字节合计 2552 32331
**③ 帧数才是「解出来」的东西**
统计项 值
---------------------------------------
视频帧数 50
音频帧数 87(每帧 1024 采样)
包字节占比 92% 的文件是包
**视频 50 帧,2 秒 × 25 帧/秒,对得上;音频 87 帧 × 1024 采样约 2.02 秒,也够 2 秒。**
**包数与帧数并不一一对应:视频这一路相等(50 对 50),音频那一路包数多 1(88 对 87),**
**多出来的是编码器收尾时吐的那个尾包——这也是「解码完必须 flush」的由来。**
**第三行说明剩下的那部分是容器自己花的钱——头、索引、每个包的描述信息。**
**这就是「转封装只改盒子、不改内容」的物质基础:包可以原样搬走,只有目录要重写。**
四层账在这份素材上的比例是:文件 37982 字节,其中 92% 是包,剩下 8% 是盒子的开销。 这个 8% 就是「转封装要重写的那部分」——只有目录,没有数据。
视频 50 包对 50 帧,音频 88 包对 87 帧——差的那一个就是 flush 时吐的尾包。 所以「包数等于帧数」这句话只在视频这一侧、且没有 B 帧延迟时才好用。
注意代码里「数包」和「数帧」是两次独立打开文件:一次只做解封装,一次做了完整的解码。
这两件事的开销差着一个数量级——这也解释了为什么 -c copy 那么快。
30 秒实验
- 看容器与本流:
ffprobe -show_format -show_streams 片子.mp4,先认盒子再认流。 - 只转封装:
ffmpeg -i 片子.mp4 -c copy 出.mp4,观察耗时几乎是零。 - 强制重编码:
ffmpeg -i 片子.mp4 -c:v libx264 -crf 28 出.mp4,对比耗时与体积。
常见误区
- 把「MP4」当成一种视频编码。MP4 是盒子,里面装的可能是 H.264,也可能是 H.265。
- 以为一帧就是一个包。编码器收尾会多吐一个尾包,音频更是「一包 1024 采样」。
- 以为格式之间「能不能互转」是个格式问题。真正的问题是「目标盒子认不认这种码流」。
自测三问(点开对答案)
1. 封装与解码的区别是什么? —— 封装只是把包按规矩排进盒子,不碰画面字节;解码才是把包还原成画面或声音。 2. 为什么 `-c copy` 那么快? —— 因为它只做解封装加封装,跳过了解码、滤镜、编码三步,CPU 几乎不干活。 3. 「这个文件有多少帧」这句话对音频成立吗? —— 不成立。音频的包是固定 1024 采样一块,跟「帧」不是同一个概念。- ☐ (本节完,下一节 4.2.1 从 MP4 文件中提取音频流和视频流)