同一批包,装进五种容器,体积与刻度全变,画质一点不变。 avi 那条路在 PyAV 走不通——这类边界要老实写出来。
7.5 是「不要容器」,这一节是「换一个容器」。 这一章到此收尾,第八章进编解码层。
一、转封装的四步
一次解码都不做,只搬字节。 步 做什么 关键函数
一 打开输入 av.open(M) 二 建输出与映射 add_stream_from_template 三 包换流号再 mux pk.stream = 新流 四 关输出 container.close()
第二步有两个易错点。 一是不能用 add_stream(template=...),
要用 add_stream_from_template();二是必须留一张新旧流号的映射表。
第三步那个 pk.stream 赋值最容易忘:忘了就直接 mux,包还记着旧流号,写出去就乱。
二、五种容器,四种成了一种不成
同一批 138 个包,装进五个容器。 容器 结果 体积
matroska 成 45155 mpegts 成 74824 flv 成 46382 mov 成 46188 avi 失败 —
ts 最大,是别人的 1.6 倍。 因为它在每个包前面都加 4 字节包头,还反复插节目表。 拿它做直播对,存片就浪费。 mkv 最小,省掉了 mp4 里那些对齐与预留。但也就省百分之几,别指望它救空间。 四个成功的体积都在 4.5 万到 7.5 万之间,原文件 46137。 这个量级本身就是个结论: 容器开销很小。
三、刻度被改写了
这是转封装最容易让人以为出问题的地方。 容器 视频 time_base 音频 time_base
原 mp4 1/12800 1/44100 matroska 1/1000 1/1000 mpegts 1/90000 1/90000
mux 的时候 FFmpeg 会把 pts 换算成目标容器的刻度。 所以 pts 数字变了、秒数没变。 mkv 用 1/1000 是毫秒,够细也不太大; ts 用 1/90000 是历史原因,当年按 90 kHz 时钟定下来的。 排查这类问题时唯一正确的做法:两边都换成秒再比。 只看 pts 会得出「时间轴被破坏」的错误结论。
四、avi 为什么不行
它报的是 InvalidDataError,不是不支持这个容器。
原因是码流格式不匹配:avi 这条老路要求 h264 用 Annex-B 码流,
而 mp4 里存的是另一种封装(AVCC),中间得插一个比特流过滤器做转换。
命令行 ffmpeg 会自动插这个过滤器,所以 ffmpeg -i a.mp4 a.avi 一般没问题。
PyAV 没有这个入口——dir() 里查不到任何 bsf 相关属性。
所以结论只能老实写:PyAV 这条路,mp4 转 avi 走不通。 要么改用命令行,要么换目标容器。
这类边界写清楚比绕过去有用。 硬凑一个「我看似成功了」的方案,到用的人手里才炸,
那才是真正的麻烦。
五、这一章的账
把七节串起来看,封装层就三句话。 要记住的 一句话
容器是什么 箱子,换箱子不换货 怎么操作 开、找、读、写,状态机顺序不能乱 最容易错 时基混用、空包没跳、流号没改
这三句话覆盖了本章九成的问题。 剩下的那一成,靠的是每换一个容器实测一次,而不是背规则。 下一章进编解码层,那里管的是「压缩包 ↔ 原始帧」,封装层的东西一个都不碰。 分层最大的好处就在这里:可以分开学。
六、动手 Demo:五种容器转封装,只换箱子不换货
同一批包往五个容器里各装一遍,记体积、包数、读回来的刻度。
import av
for tag in ("matroska", "mpegts", "flv", "mov", "avi"):
p = "out." + tag
try:
ic = av.open("tiny.mp4")
oc = av.open(p, "w", format=tag)
cmap = {}
for st in ic.streams:
cmap[st.index] = oc.add_stream_from_template(st)
for pk in ic.demux():
if pk.size == 0:
continue
pk.stream = cmap[pk.stream.index]
oc.mux(pk)
oc.close()
ic.close()
r = av.open(p)
print(tag, [str(s.time_base) for s in r.streams])
r.close()
except Exception as e:
print(tag, "失败", type(e).__name__)
实跑输出:
--------------------------------------------------------------------
五种容器转封装,只换箱子不换货
--------------------------------------------------------------------
**① 同一个 mp4,往五个容器里各装一遍**
**这次不碰编码器**:拿模板流建立输出,再把包原样 mux 进去。
目标容器 体积 包数 读回来的 time_base 时长
----------------------------------------------------------------------------
matroska 45155 138 1/1000/1/1000 2.023
mpegts 74824 138 1/90000/1/90000 2.043
flv 46382 138 1/1000/1/1000 2.023
mov 46188 138 1/12800/1/44100 2.023
avi 失败 InvalidDataError — —
**五个里成了 4 个。** 原文件 **46137 字节**。
**② 体积差在哪**
**ts 最大,因为它在每个包前面都要加 4 字节包头,还要反复插节目表。** 拿它当直播流没错,
拿来存片就浪费。
**mkv 最小,因为它省掉了 mp4 里那些对齐与预留空间。** 但省得有限,通常在百分之几。
**记住一句话**:转封装的体积变化只跟「装箱开销」有关,**跟画质一点关系都没有。**
**③ time_base 被容器改了**
**mp4 里视频是 1/12800、音频是 1/44100;进了 mkv 与 flv 全变成 1/1000;进了 ts 全变成 1/90000。**
**这不是 bug,是容器自己的规定。** 每个容器有自己的时间刻度,
mux 的时候 FFmpeg 会帮你把 pts 换算过去。
**所以转封装前后 pts 的数字会变,秒数不变。** 只比 pts 不看 time_base,
就会得出「时间全乱了」的错误结论。
**④ avi 那个为什么失败**
它报的是 `InvalidDataError`。
**avi 这条老路要求 h264 用 Annex-B 码流**,而 mp4 里存的是另一种封装,
中间得插一个比特流过滤器做转换。
**命令行 ffmpeg 会自动插**,所以 `ffmpeg -i a.mp4 a.avi` 一般没问题。
**PyAV 没有这个入口**——`dir()` 里查不到任何 bsf 相关属性。
**所以结论要老实写:PyAV 这条路,mp4 转 avi 走不通。** 要么改用命令行,要么换目标容器。
**这类「本机做不到」的边界,写清楚比绕过去有用。**
四个成功、avi 失败,失败原因是码流格式不匹配。 刻度全变了:mkv 1/1000、ts 1/90000、mov 保持原样。
30 秒实验
- 把
pk.stream那行删掉再跑,看输出文件的时间戳乱成什么样。 - 统计每个输出文件的包数,看是不是都等于输入的 138。
- 试试 flv 装 h265,看它报什么(提示:容器不支持这个编码)。
常见误区
- 忘了给
pk.stream换流号。 写出去时间戳全乱。 - 拿 pts 直接比前后。 刻度变了,要先换成秒。
- 以为 avi 失败是 PyAV 的 bug。 是缺比特流过滤器,而它不提供入口。
自测三问(点开对答案)
1. 转封装为什么快? —— 一次解码都不做,只把包搬进新容器再写一份新索引。 2. 转封装之后 time_base 变了,时间变了吗? —— 没有。换算过去了,秒数不变,只是刻度不一样。 3. avi 失败的根本原因是什么? —— avi 要求 Annex-B 码流,mp4 里是另一种封装,中间要插比特流过滤器。- ☐ (本节完,第七章已完结,下一章 8.1 编解码原理流程及API解析)