第 22 课:容器格式 —— MP4 / MKV / FLV / TS,obs_output
# 第 22 课:容器格式 —— MP4 / MKV / FLV / TS,obs_output
嘿,我是小方。 上一模块,视频和音频都编码好了 —— 各是一串带时间戳的
encoder_packet。可它们现在还是两条分开的流,躺在内存里哪儿也去不了。这一课(模块 6 第一课)要干两件事: 一、把这两条流合到一起、打包成一个能用的东西 —— 要么是双击就能播的文件,要么是发给直播服务器的一路数据。这个"合流 + 打包"叫封装(muxing),打包用的格式叫容器(MP4/MKV/FLV…); 二、认识 OBS 统管"录制到文件"和"推流到服务器"的那个抽象:obs_output(又是一张登记表,你已经很熟这套路了)。 这一课的节奏:先分清最容易混的"容器 vs 编解码器",再补一节它们的"家谱"(这些标准和格式到底从哪来、为什么有这么多 —— 补上你一直缺的历史背景),然后讲封装、四种容器的脾气、"为什么录制别用 MP4"这个救命知识,最后落到obs_output和源码。
# 22.1 先分清:容器(.mp4)vs 编解码器(H.264)
📍我们在哪:流水线走到"编码之后、送出之前"。要送出,先得把两条流打包。打包之前,得先分清一对最容易混的概念。
这是新手最容易混的一对概念,先掰开:

- 编解码器(codec)= 里面装的"菜":
H.264(视频)、AAC(音频)—— 就是第 18~21 课编码出来的数据本身。"怎么压的、能压多小",由它决定。 - 容器(container)= 装菜的"盒子":
.mp4、.mkv、.flv、.ts—— 把编码好的数据打包成一个文件/流。"怎么装、能不能边写边播",由它决定。
关键理解:同一份 H.264 + AAC,可以装进不同的盒子!
- "
.mp4文件"不代表某种画质 —— 它只说明"用 MP4 这种盒子装",里面的视频可能是 H.264、HEVC、AV1……(所以有人说"MP4 画质好"是外行话,画质是里面 codec 的事)。 - 换盒子(mp4 → mkv)不用重新编码:数据没变,只是重新打包一下。所以"转封装(remux)"几乎是瞬间完成的(而"转码 transcode"要重新编码,很慢)——这个区别很实用。
- 类比:同一份菜(编码数据),可以装进饭盒、保鲜盒、外卖盒(不同容器)—— 菜没变,只是包装不同。
记牢这个区分,这一课(讲盒子)和前几课(讲菜)就不会串。不过你可能会问:这些盒子和菜,到底都是从哪冒出来的?为什么有这么多种? 下一节先补这段"家谱",你后面看到那一堆缩写就不会懵了。
# 22.2 补一节家谱:这些标准和容器,都从哪来?
📍为什么现在讲这个:上一节列了 H.264、AAC、MP4、MKV、FLV、TS 一堆名字。新手最容易在这里"劝退"——感觉是天上掉下来的黑话。其实它们各有出身,理清来历,后面就都是"熟人"了。

# ① 编解码标准(codec):谁定的、为什么一代代换
视频压缩不是某家公司随便搞的,而是国际标准,由两大组织牵头制定:ISO/IEC 的 MPEG 小组和ITU-T 的 VCEG 小组。一路演进:
- MPEG-1(1993)—— VCD、还有你熟悉的 MP3(它是 MPEG-1 的音频部分);
- MPEG-2(1995)—— DVD、数字电视广播(TS 容器就出自这里);
- H.264 / AVC(2003)—— 由 ITU-T 和 MPEG 联合制定,所以它有两个名字:
H.264(ITU-T 叫法)=MPEG-4 Part 10/AVC(ISO 叫法),是同一个东西。至今最通用,到处能播; - HEVC / H.265(2013)—— 压缩效率比 H.264 翻倍,但专利授权复杂、要交钱,拖慢了普及;
- AV1(2018)—— 由开放媒体联盟(AOMedia,谷歌、Netflix、亚马逊等组的)搞,免版税,就是为了绕开 HEVC 的专利麻烦;缺点是新、编码吃算力。
为什么会有一堆竞争者? 一句话:大家在**"压缩效率 vs 专利费"**上你追我赶。H.264 赢在成熟通用,HEVC 赢在效率但输在专利,AV1 赢在免费但还年轻。你现在直播/录制,H.264 仍是最稳的默认选择(第 20 课讲编码器时也是这个结论)。
# ② 容器(container):四个盒子,四个不同的出身
四种容器不是一起设计的,而是诞生在不同年代、不同生态,各为不同需求而生:
- MP4 —— 源自苹果的 QuickTime(
.mov) 格式,后来被 ISO 标准化成MP4(ISO 基础媒体文件格式)。目标:通用,几乎所有设备都能播。 - MKV —— 开源的 Matroska 项目(2002 年起),社区驱动、免费、极其灵活,什么编码都能装。
- FLV —— Adobe Flash 时代的产物(Flash Video)。Flash 早没了,但它绑定的 RTMP 直播协议至今还用 FLV 打包(第 24 课)。
- TS —— MPEG-2 的"传输流(Transport Stream)",专为不可靠的广播信道设计(抗丢包、能自同步),所以今天的 HLS 切片直播也沿用它。
这就是"为什么有这么多"的答案:编解码器在效率与专利间竞争、一代代更替;容器则分别从苹果、开源社区、Flash、广播四个源头流传下来。而因为"菜和盒子分开"(22.1),它们能自由组合 —— 你才会见到"H.264 装进 MKV""H.264 装进 FLV 去直播"这些搭配。
🗒️ 术语速记:标准(H.264/HEVC/AV1)= 规定"怎么压"的国际文档;编解码器/codec = 实现这个标准的具体程序(第 20 课的 x264、NVENC);容器/container(MP4/MKV…)= 把压好的数据装起来的文件格式。三层别混。
认清了有哪些格式、各自什么来历,现在来看它们到底怎么把两条流"装"进一个盒子 —— 这一步叫封装。
# 22.3 封装(muxing):把两条流"交织"成一条
📍接上一节:知道了容器是"盒子",那"装"这个动作具体怎么做?答案就是"交织"。
封装(muxing,也叫"复用") 的核心工作,是把视频包、音频包按时间戳穿插排好,合成一条流,再装进容器。

看图:视频流是 V1 V2 V3 V4、音频流是 A1 A2 A3 A4 A5,封装后变成交织的一条:V1 A1 A2 V2 A3 V3 A4 A5 V4 … —— 时间戳(第 18 课那个 dts)小的排前面。
为什么要"交织",不把视频、音频各存一大段? 因为播放器是**"边读边播"**的:读到 V1 就显示这一帧、读到 A1 就播这一小段声。交织排好,播放器顺着往下读,就能音画同步,不用在文件里来回跳着找(想象视频全在文件开头、音频全在末尾 —— 播每一帧都要跳到末尾找对应的声音,慢死)。
这就是 muxing 干的事。在 OBS 里,这个"按 dts 交织"由引擎统一做好(interleave_packets,obs-output.c:2073,22.7 细看),各个输出插件拿到的都是已经排好序的一串包。
# 22.4 四种常见容器的"脾气"
📍接上一节:知道了怎么装,再看装进不同盒子有什么区别 —— 这直接决定你录制/直播该选哪个。
常见的容器有四种,同样装 H.264 + AAC,但用途、崩溃安全性、能装什么,各不相同:

| 容器 | 用途 / 特点 | 崩溃安全(录制关键) | 能装的编解码器 |
|---|---|---|---|
| MP4 | 最通用,任何设备/软件都能播 | 差(索引在末尾,崩溃→废) | 很广 |
| MKV | 灵活,啥都能装;录制首选 | 好(边写边可播) | 很广 |
| FLV | 直播老容器(RTMP 用) | 好(流式,边写边发) | 窄(H.264/AAC) |
| TS | 广播/分段(HLS 用) | 好(天生分段) | 较广 |
- MP4:兼容性之王,录完直接发朋友、传平台都能播。但有个大坑(下一节)。
- MKV:开放灵活,几乎什么编码都能装,录制首选(崩了也不怕)。
- FLV:老牌流式容器,RTMP 直播就用它;但最挑食——只认 H.264/AAC。
- TS:天生分段,适合 HLS 这种"切成小片"的直播。
一句话:录制想省心 → MKV;要兼容直接播 → MP4(但注意下节的坑);直播 → FLV(RTMP);切片直播 → TS(HLS)。 而"FLV 最挑食、MKV/MP4 最能装",在代码里就是各输出的 encoded_video_codecs 列表差异(22.6 见)。
# 22.5 为什么老手劝你"录制别用 MP4"?
📍为什么现在讲这个:上表里 MP4 的"崩溃安全"一栏写着"差"。这不是小事 —— 它能让你几小时的录像瞬间报废。单开一节讲透。
这是能救你命的一节。MP4 有个致命特性:它把"索引"写在文件的最末尾。

# 普通 MP4 的坑:索引(moov)在最后
MP4 里有个叫 moov(moov atom) 的东西,是整个文件的"索引/目录"——告诉播放器"每一帧在文件的什么位置、时间戳多少"。没有它,播放器打不开文件。 而普通 MP4 是这么写的:
- 一路往后写视频/音频数据,
moov等到"正常关闭"时,才写在文件末尾; - 正常停止录制 → 写好
moov→ 文件能正常播放 ✓; - 录到一半崩溃 / 断电 / 强杀进程 →
moov还没写上 → 播放器找不到索引 → 整段录像废掉!(几个小时的录制,一崩全没,血泪教训。)
# MKV / OBS 的 Hybrid MP4:边写边能播
- MKV 是"流式"容器:天生没有"末尾索引"这回事,写到哪儿都能播。崩了、断电了,文件照样打得开(顶多丢最后一点)。
- OBS 较新的"Hybrid MP4"(混合 MP4)录制格式:它给 MP4 加了保护 —— 录制时先在开头写一个临时的
moov+ 分片(fragments)写,所以崩了也能播;等你正常停止,再"软重整(soft-remux)"成标准的快速启动 MP4,兼顾兼容和安全。(源码在plugins/obs-outputs/mp4-output.c,22.8 提。)
💡 实用建议:
- 求稳:录制用 MKV。 真崩了/断电了,文件照样能打开;事后一键"转封装(remux)成 MP4"(不重编码,秒完成)。这是 OBS 老用户的标准操作。
- 或者:用 OBS 的"Hybrid MP4"录制格式(不是最老那个纯 MP4)—— 它给 MP4 加了崩溃保护。总之,**别用最老的那个"MP4"**去录长时间的重要内容。
# 22.6 obs_output:又是一张登记表(第四次了)
📍转折:前面 5 节都在讲"格式/容器"这些概念。现在回到 OBS 代码 —— 它用什么来统管"录制/推流"这件事?答案又是那个熟悉的模式。
讲完"盒子",来看 OBS 用什么统管输出。你已经猜到了 —— 又是一张登记表 + 一套回调。这是你在这门课里第四次见到这个"vtable + 回调"套路了(源、图形后端、编码器、输出;其中图形后端是同一套路、但走 os_dlopen 加载,严格说不算 obs_register 登记表 —— 第 25 课细说)。

// libobs/obs-output.h:41(节选)
struct obs_output_info {
const char *id; // "ffmpeg_muxer" / "rtmp_output"
uint32_t flags; // ★ ENCODED / SERVICE / AV …(下面细说)
...
void *(*create)(obs_data_t *settings, obs_output_t *output); // 建实例
void (*destroy)(void *data);
bool (*start)(void *data); // 开始输出
void (*stop)(void *data, uint64_t ts);
void (*encoded_packet)(void *data, struct encoder_packet *packet); // ★ 收编码包 → 写文件/发服务器
...
const char *encoded_video_codecs; // 这盒子能装哪些视频编码
const char *encoded_audio_codecs; // 能装哪些音频编码
const char *protocols; // 推流协议(仅 SERVICE 输出)
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
和前三次一模一样的套路,不再赘述(找 id → 拷 vtable → info.create 建实例 → 引擎照表调,obs-output.c:196/:228/:250)。这里只抓两个关键的 flag 位(obs-output.h:24):
OBS_OUTPUT_ENCODED:表示这个输出收"编码包"(不是裸帧)—— 录制、推流都是它。对应必须实现encoded_packet回调。OBS_OUTPUT_SERVICE:表示这个输出推到"服务器" —— 只有推流输出才置这一位,而且必须填protocols(如"RTMP;RTMPS")。
录制 vs 推流,就靠 flags 里那个 OBS_OUTPUT_SERVICE 分家: 文件 muxer(ffmpeg_muxer / mp4_output)不置 SERVICE → 写硬盘;rtmp_output 置 SERVICE + protocols → 推服务器。同一张登记表,一个 flag 之差,就是"录到文件"和"发到全世界"的分界。
# 22.7 源码深读:编码包怎么走到文件/服务器
📍接上一节:认识了
obs_output这张表,现在看引擎怎么把编码包"喂"给它。
把整条路走一遍:两个编码器吐出的包,怎么变成硬盘上的文件、或发出去的直播流。

// 1) obs_output_start → 输出「订阅」它的每个编码器(第 19 课那套发布/订阅)
// hook_data_capture 选回调:视频+音频都有 → interleave_packets
// 然后 obs_encoder_start(encoder, interleave_packets, output) (obs-output.c:2439)
// 2) 每个编码包到达 interleave_packets(obs-output.c:2222),按 dts 插进队列(排序):
// (先丢弃开头非关键帧的视频,等到第一个关键帧才开始 —— :2237)
if (out->dts_usec < cur_packet->dts_usec) // obs-output.c:2089
break; // 找到插入位置
da_insert(output->interleaved_packets, idx, out);
// 3) 队列凑齐、排好后,逐个弹出、交给输出插件(obs-output.c:1756):
output->info.encoded_packet(output->context.data, &out); // ★ 到这,muxer/rtmp 收到包
2
3
4
5
6
7
8
9
10
11
12
三步:
- ① 订阅:
obs_output_start时,输出把interleave_packets作为回调,订阅它的视频/音频编码器(obs_encoder_start,第 19 课那套"编码器 → 回调"的发布/订阅)。 - ② 交织排序:每个编码包到
interleave_packets(obs-output.c:2222)→ 按dts插进有序队列(:2073)→send_interleaved逐个弹出最前面那个。(开头还有"等第一个关键帧才开始"的逻辑,:2237—— 呼应第 18 课:没关键帧解不了。) - ③ 交给插件:
output->info.encoded_packet(...)(:1756)—— 到这一步,MP4 muxer 也好、RTMP 也好,拿到的都是"已经排好序的一串包",各自只负责"怎么装 / 怎么发"。
关键:交织(interleave)在引擎里统一做好,输出插件不用管排序。 这又是一次漂亮的分层:引擎干"通用的脏活"(交织、等关键帧、丢包),插件只干"专属的活"(写 MP4 / 发 RTMP)。
# 22.8 两类输出:录到文件 vs 推到服务器
📍收束:前面说"一个 flag 分家"。现在把两个真实的输出摆一起,看这句话怎么落到代码上。
最后落到两个真实的输出,看它们怎么用同一个接口、走向不同的目的地。

# 录制 → 文件 muxer
// plugins/obs-ffmpeg/obs-ffmpeg-mux.c:863
struct obs_output_info ffmpeg_muxer = {
.id = "ffmpeg_muxer",
.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | OBS_OUTPUT_CAN_PAUSE, // 没有 SERVICE
.encoded_packet = ffmpeg_mux_data,
...
};
2
3
4
5
6
7
- 不置
SERVICE→ 目的地是本地硬盘的一个文件。 - 容器由"文件扩展名"决定:你保存成
.mp4/.mkv/.ts,子进程里用 FFmpeg 的av_guess_format认出该用哪种格式(ffmpeg-mux.c:954)。 - 一个巧思:经典
ffmpeg_muxer把编码包用管道发给一个独立的obs-ffmpeg-mux子进程去封装(obs-ffmpeg-mux.c:305,os_process_pipe_create2)。为什么?万一封装那部分卡死/崩溃,不连累 OBS 主程序(你的直播还能继续)。这是很稳的工程设计(下一课细讲)。 - (更稳的录制,用前面说的
mp4_output"Hybrid MP4",plugins/obs-outputs/mp4-output.c:597,自带崩溃保护。)
# 推流 → 服务输出
// plugins/obs-outputs/rtmp-stream.c:1796
struct obs_output_info rtmp_output_info = {
.id = "rtmp_output",
.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE | OBS_OUTPUT_MULTI_TRACK_AV, // ★ SERVICE
.protocols = "RTMP;RTMPS",
.encoded_video_codecs = "h264;av1", // (+hevc)
.encoded_audio_codecs = "aac", // 只认 aac!
...
};
2
3
4
5
6
7
8
9
- 置了
SERVICE+protocols→ 目的地是一个直播服务器(Twitch / B 站 / YouTube …)。 - 它绑定一个
obs_service(第 25 课),从中拿到服务器地址 + 推流码。 - 在网络上走 FLV 格式的数据(所以
encoded_audio_codecs只认aac—— 呼应 22.4 "FLV 最挑食",也呼应 22.2 "FLV 出自 Flash/RTMP")。 - 网络卡了要丢包时,走第 18 课那套"优先丢 P/B 帧、保关键帧"(
rtmp-stream.c的drop_frames)。
全部区别,浓缩在一个 flag(OBS_OUTPUT_SERVICE)+ 一个 protocols 字段上。 上层的"开始录制 / 开始直播",调的是同一个 obs_output_start —— 又一次"统一接口"的胜利。
# 22.9 模块 6 开场:打包与送出
站在流水线上看看我们在哪儿:

视频编码(18~20)→ 音频编码(21)→ 封装 + 输出(本模块)→ 文件 / 直播(成品)。 这一课打的是模块 6 的地基。往后:
- 第 23 课:录制到文件 —— mux 复用的更多细节;
- 第 24 课:推流到服务器 —— RTMP 协议到底怎么发;
- 第 25 课:直播平台怎么接入 ——
obs_service。
这一课你需要带走的:分清"盒子 vs 菜"、知道格式的"家谱"(为什么有这么多)、看懂"交织成一条流"、记住"录制别用老 MP4"、认识 obs_output 这个统一输出接口。 有了它们,后面两课就是往里填细节。
# 22.10 本课小结
- 容器 vs 编解码器:codec(H.264/AAC)= 编码出的数据本身(菜);container(.mp4/.mkv/.flv/.ts)= 打包成文件/流的盒子。同一份 H.264+AAC 可装进不同盒子;换盒子(remux)不重编码,秒完成。
- 格式家谱:编解码标准由 ISO/MPEG + ITU-T 牵头,MPEG-1/2 → H.264/AVC(2003,联合制定,最通用)→ HEVC(2013,效率翻倍但专利复杂)→ AV1(2018,AOMedia,免版税)。竞争点 = 效率 vs 专利。容器四个不同出身:MP4(苹果 QuickTime→ISO)、MKV(开源 Matroska)、FLV(Adobe Flash/RTMP)、TS(MPEG-2 传输流/HLS)。
- 封装(muxing)= 交织:把视频包、音频包按
dts穿插排成一条,装进容器 —— 让播放器"边读边播"就能音画同步。OBS 里由interleave_packets(obs-output.c:2073)统一做。 - 四种容器:MP4(最兼容,但索引在末尾、崩溃危险)、MKV(灵活+崩溃安全,录制首选)、FLV(RTMP 流式、最挑食只认 H.264/AAC)、TS(分段、HLS 用)。
- 为什么录制别用 MP4:MP4 的索引(
moov)写在文件末尾,录到一半崩溃/断电 → moov 没写 → 整段废掉。MKV 天生边写边可播;OBS 的 Hybrid MP4(mp4_output)先写临时 moov+分片、正常停再软重整,兼顾兼容与安全。别用最老的纯 MP4 录长内容。 obs_output= 第四张登记表(obs-output.h:41):id/flags/create/start/stop/encoded_packet+encoded_video_codecs/encoded_audio_codecs(盒子能装啥)+protocols。两个关键 flag:ENCODED(收编码包)、SERVICE(推服务器)。- 包的流向:
obs_output_start订阅编码器 →interleave_packets按dts排序(:2222,先等关键帧:2237)→send_interleaved弹出 →info.encoded_packet(:1756) 交给 muxer/rtmp。交织在引擎统一做,插件只管写/发。 - 两类输出:
ffmpeg_muxer/mp4_output(无 SERVICE,写硬盘文件,容器由扩展名定;经典 muxer 用独立子进程封装以隔离崩溃)vsrtmp_output(有 SERVICE + protocols,推 RTMP 服务器,走 FLV 只认 aac)。同一个obs_output_start,一个 flag 之差。
# 22.11 动手 / 观察(本课作业)
- 分清盒子和菜:说清
.mp4和H.264分别是"容器"还是"编解码器";为什么"MP4 画质更好"是句外行话? - 理一遍家谱:H.264 为什么有
H.264/AVC/MPEG-4 Part 10三个名字?AV1 为什么会出现(它解决了 HEVC 的什么问题)? - 看录制格式:OBS →"设置 → 输出 → 录像",看"录像格式"下拉(mkv/mp4/hybrid mp4/flv/ts/mov)。你现在用的是哪个?根据 22.5,该换吗?
- 体会 remux:OBS 菜单"文件 → 重新封装录像(Remux Recordings)"—— 把一个 mkv 转成 mp4,观察它几乎瞬间完成(因为不重编码,只换盒子)。
- 理解 moov 的坑:用自己的话讲清,为什么普通 MP4 录到一半崩溃就打不开,而 MKV 不会?
- 翻源码:打开
libobs/obs-output.h:41的obs_output_info,找到flags、encoded_packet、protocols;再对比plugins/obs-ffmpeg/obs-ffmpeg-mux.c:863(无 SERVICE)和plugins/obs-outputs/rtmp-stream.c:1796(有 SERVICE),确认那个 flag 差异。
# 下一课预告
这一课认识了"录制到文件"的输出(ffmpeg_muxer),但只讲了它的骨架。它到底怎么把一个个编码包,真正写成硬盘上一个合法的 MP4/MKV 文件?那个"独立子进程"又是怎么工作的?
第 23 课:录制到文件 —— mux 复用 —— 我们钻进 ffmpeg_muxer,看它怎么初始化容器、写文件头、把交织好的包一个个 av_interleaved_write_frame 写进去、收尾写好索引,以及"多轨录制"(把麦克风单独存一轨)是怎么做的。下节课见。
📁 本课配图:
imgs/22-01~imgs/22-09📌 源码锚点:libobs/obs-output.h:41(struct obs_output_info)、:24(flags:VIDEO/AUDIO/AV/ENCODED/SERVICE/…)、:45(flags 字段)、:58(encoded_packet)、:80/:81(encoded_video/audio_codecs)、:87(protocols);libobs/obs-module.c:1061(obs_register_output_s)、:1077(SERVICE→protocols / ENCODED→encoded_packet 校验);libobs/obs-output.c:196(obs_output_create)、:228(output->info = *info)、:250(info.create → context.data)、:2439(start_video_encoders 订阅)、:2222(interleave_packets)、:2237(等第一个关键帧)、:2073/:2089(按 dts 插入排序)、:1681/:1756(send_interleaved → info.encoded_packet)、:359/:371(actual_start → info.start)、:506(info.stop);libobs/obs-internal.h:1210(struct obs_output)、:1229(interleaved_packets)、:1250(video/audio_encoders / service);plugins/obs-ffmpeg/obs-ffmpeg-mux.c:863(ffmpeg_muxer 表)、:1254(默认扩展名 mp4)、:305(start_pipe 子进程)、:113(FFMPEG_MUX)、plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c:954(av_guess_format);plugins/obs-outputs/mp4-output.c:597(mp4_output "Hybrid MP4")、:329("Writing Hybrid MP4/MOV")、plugins/obs-outputs/mp4-mux.c:2632(写临时 moov)、:2955(mp4_mux_finalise 软重整);plugins/obs-outputs/flv-output.c:613(flv_output)、plugins/obs-outputs/rtmp-stream.c:1796(rtmp_output:SERVICE + protocols)。 注:编解码标准/容器的历史与年代为通用行业背景知识,非 OBS 源码内容。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言