24 / 32 封装、推流与前端界面

第 23 课:录制到文件 —— mux 复用

# 第 23 课:录制到文件 —— mux 复用

嘿,我是小方。 上一课(第 22 课)我们认识了"录制到文件"的输出 ffmpeg_muxer,但只讲了它的骨架 —— 它注册成一张登记表、收编码包。这一课钻进去,看它到底怎么把一个个编码包,真正写成硬盘上一个能双击播放的合法 MP4/MKV 文件。 这里有两个很值得学的点:一是 OBS 一个**"两个进程 + 一根管道"的稳健设计(录制崩了不连累你的直播);二是 libavformat(FFmpeg)封装的"三部曲"—— 一个你以后写任何"存视频文件"的程序都会用到的万能骨架**。 不过在钻代码前,我先花一节补三个词:FFmpeg、进程、管道。这三个是这一课反复出现的地基,零基础也不怕 —— 认识它们,后面就一路顺了。


# 23.1 先补三个词:FFmpeg、进程、管道

📍为什么先讲这个:这一课一上来就是"OBS 主进程用管道把包发给 obs-ffmpeg-mux 子进程,子进程用 libavformat 写文件"。这句话里有三个可能陌生的词。先认识它们,那句话就一秒看懂。

FFmpeg、进程、管道

# ① FFmpeg / libavformat 是什么

FFmpeg 是一套开源的"媒体瑞士军刀"库 —— 编码、解码、转格式、封装、解封装,音视频的脏活它几乎全能干。几乎所有播放器、转码工具、直播软件(包括 OBS)底层都在用它。 它由很多子库组成,其中 libavformat 专管**"容器的读写"**(封装 muxing / 解封装 demuxing)。

OBS 把"写文件"这活委托给 libavformat —— 就像第 20 课里,它把"视频编码"委托给 x264。OBS 自己不重新发明这些轮子。

# ② 进程(process)是什么

一个正在运行的程序,就是一个进程。 你电脑上的 OBS 是一个进程、浏览器是另一个进程,它们各自独立、互不影响:一个崩了,别的进程照样活着。

子进程(subprocess)= 由一个进程启动的另一个进程,两者是父子关系。父进程能启动它、控制它、和它通信。

# ③ 管道(pipe)是什么

管道就是两个进程之间的一根"单向水管":一个进程往里字节,另一个进程从里字节。用来在两个独立程序之间传数据。(如果你在命令行里用过 命令A | 命令B 那个 |,就是同一个东西 —— 把 A 的输出接到 B 的输入。)

有了这三个词,这一课的主线一句话就懂了:

"OBS 主进程"用"管道",把编码包发给一个叫 obs-ffmpeg-mux 的"子进程";这个子进程用"FFmpeg(libavformat)"把包写成硬盘上的文件。

至于为什么要多搞一个子进程、而不是 OBS 自己直接写文件 —— 下一节揭晓(剧透:为了安全)。


# 23.2 录制的架构:两个进程,一根管道

📍接上一节:上面那句主线,现在把它画成图、讲清"为什么这么设计"。

你可能以为"OBS 直接把文件写了",其实不是 —— OBS 主进程,把编码包用一根"管道"发给一个独立的子进程 obs-ffmpeg-mux,由它去写文件。

两个进程一根管道

  • OBS 主进程:采集 → 合成 → 编码 → 交织排序(第 22 课),最后 ffmpeg_mux_data 把排好的包写进管道
  • obs-ffmpeg-mux 子进程:一个独立的小程序(有自己的 main()),从管道读包,用 libavformat(FFmpeg) 建容器、写包、写索引,落成一个 .mp4/.mkv 文件。

为什么要拆成两个进程?—— 崩溃隔离。 封装这活儿有一定风险(网络写入超时、磁盘写满、某些格式出错都可能让 libavformat 卡死或崩溃)。把它放进独立子进程:万一它挂了,OBS 主进程只是"看到管道断了、报个错",你的直播照样继续跑,不会跟着一起崩。这是很稳的工程设计 —— 把"可能出事的脏活"隔离出去(还记得 23.1 说的"进程互不影响"吗?这就是它的实战用途)。

那主进程和子进程之间,到底传了什么?


# 23.3 主进程到底给子进程发了什么?

📍接上一节:知道了有一根管道,现在看管道里(以及启动时)到底传了什么数据。

分两条路:配置走"命令行参数",数据走"管道"。

管道协议

# ① 启动时:命令行参数(一次性)

主进程启动子进程时(build_command_line,obs-ffmpeg-mux.c:266),把这些当命令行参数传过去:

  • 文件路径(如 .../录像.mkv)—— 决定用哪个容器;
  • 有没有视频、几条音频轨(num_tracks);
  • 每个编码器的参数:codec、宽高、采样率……

子进程据此把每条轨(stream)建好。

# ② 运行中:管道里的数据流

管道里流的是两样东西:

  • 先发一次:每条轨的头信息(extradata) = SPS/PPS 等"解码说明书"(下一节专讲);
  • 然后反复:一个个编码包,每个包的格式是 [一个小头] + [包的字节]

那个"小头"是个固定的结构体(ffmpeg-mux.h:32):

// plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.h:32
struct ffm_packet_info {
	int64_t pts;       // 显示时刻
	int64_t dts;       // 解码时刻
	uint32_t size;     // 这个包多少字节
	uint32_t index;    // 第几条轨(track_idx)
	enum ffm_packet_type type;  // 视频 / 音频
	bool keyframe;     // 是不是关键帧
};
1
2
3
4
5
6
7
8
9

主进程 write_packet(obs-ffmpeg-mux.c:603)就是先把这个小头写进管道,再把包的字节写进管道:

// plugins/obs-ffmpeg/obs-ffmpeg-mux.c:625
os_process_pipe_write(stream->pipe, (const uint8_t *)&info, sizeof(info));  // ① 小头
os_process_pipe_write(stream->pipe, packet->data, packet->size);           // ② 包的字节
1
2
3

子进程 main() 循环(ffmpeg-mux.c:1171)镜像着读:读一个 ffm_packet_info → 读它的 size 个字节 → 写进文件。读到管道关闭(返回 0),循环结束、开始收尾。

为什么先要单独发一次"头信息"?下一节讲清楚。


# 23.4 头信息(extradata / SPS-PPS):一份"解码说明书"

📍接上一节:上面说"先发一次头信息"。这个头信息到底是什么、为什么非它不可?

这是新手常忽略、但很关键的概念。编码器在压视频之前,会先产出一小段"参数",叫 extradata(H.264 里就是 SPS/PPS)。它是一份"解码说明书"。

extradata / SPS-PPS

  • 编码器最开头产出:压这段 H.264 前,先定好一套规矩 —— 分辨率、profile、用了哪些编码特性(第 20 课那个 profile)—— 写成 SPS/PPS。OBS 用 obs_encoder_get_extra_data(obs-ffmpeg-mux.c:662)拿到它。
  • 存进容器的"文件头":子进程建轨时,把 extradata 填进这条轨的 codecpar(ffmpeg-mux.c:461),avformat_write_header 把它写进文件头。
  • 播放器打开时先读它:任何播放器/解码器,必须先读到这份说明书,才知道"该怎么解"后面的帧;没有它,后面的帧就是一堆看不懂的字节。

打个比方:extradata 就是解码这段视频的"说明书"。 它每帧都一样(分辨率、参数集不变),所以只在开头发一次、存进文件头,而不是每帧都重复(那太浪费)。这就是为什么管道里"先发一次头信息,再发一个个包"—— 说明书得先到,后面的内容才解得开。

有了配置和头信息,子进程就能开工建文件了。它用的是一套固定的 libavformat 调用 —— 这套调用有个万能的"三部曲"。


# 23.5 封装的"三部曲":任何 FFmpeg 写文件都是这三步

📍核心一节:前面都是"准备"。从这里开始,才是"真正写文件"。而写文件的骨架,永远是这三步。

这是这一课最该带走的东西。所有基于 FFmpeg 的封装,都是同样三步:

封装三部曲

  1. avformat_write_header —— 写"文件头"。建好容器结构、写入各轨的参数与 extradata。开一次。
  2. av_interleaved_write_frame —— 写"每一个包"。循环:每来一个编码包,就写进文件对应的轨。反复。
  3. av_write_trailer —— 写"索引 / 收尾"。写 MP4 的 moov 索引、补齐信息、封口。停一次。

开头 write_header 一次 → 中间 write_frame 一帧一帧地反复 → 结尾 write_trailer 一次。 从 MP4 到 MKV、从录制到直播(直播也是 muxing,只是"文件"换成"网络"),底层都是这个骨架。你以后自己写"存个视频文件"的程序,也是这三步。

⚠️ 第 ③ 步 av_write_trailer,正是写 MP4 那个 moov 索引的地方 —— 直接呼应第 22 课:录到一半崩溃 → 走不到第 ③ 步moov 没写 → MP4 废掉。而 MKV 不靠末尾索引(边写边有结构),所以不怕。第 22 课那个"坑",在这里看到了它的代码根源。

下面把三步各自的真实调用拆开。


# 23.6 第一步细节:子进程怎么"建"一个容器

📍接上一节:三部曲的第 ① 步 write_header 不是凭空调用,它前面还有一串"搭骨架"的准备。

write_header 之前,要先把容器和每条轨搭好。这是一串 libavformat 调用(ffmpeg-mux.cffmpeg_mux_init_context):

建容器

// plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c(抽调用链)
output_format = av_guess_format(NULL, ffm->params.file, NULL);       // ① 从「.mkv/.mp4」认出格式  :957
avformat_alloc_output_context2(&ffm->output, output_format, NULL, ffm->params.file);  // ② 建输出上下文  :970
...
for (每条轨) avformat_new_stream(ffm->output, NULL);                 // ③ 建每一条轨(视频1 + 音频每轨1) :418
...
avcodec_parameters_from_context(stream->codecpar, context);          // ④ 填 codecpar + extradata  :471
...
avio_alloc_context(...)   // 或 avio_open —— 打开磁盘文件            // ⑤ 打开文件  :895/:900
...
avformat_write_header(ffm->output, &dict);                           // ⑥ ★ 写文件头  :927
1
2
3
4
5
6
7
8
9
10
11

逐步:

  • av_guess_format(文件名) —— 从扩展名(.mkv / .mp4 / .ts)认出该用哪种容器格式(第 22 课那个"扩展名决定容器")。
  • avformat_alloc_output_context2 —— 建一个"输出上下文"(AVFormatContext),这是这个文件的总管,后面所有操作都围着它。
  • avformat_new_stream × N —— 建每一条轨:视频 1 条 + 音频每轨 1 条(多轨录制就是这里建多条,23.8)。
  • ④ 填 codecpar + extradata —— 告诉每条轨:它是什么 codec、宽高/采样率多少、以及那份 extradata"说明书"(23.4)。
  • avio 打开文件 —— 打开磁盘上那个文件,准备往里写字节。(录本地文件时 OBS 用了个自带的、带缓冲的写入器 avio_alloc_context;写网络时用标准的 avio_open。)
  • avformat_write_header —— 把文件头写进去:容器的整体结构 + 各轨参数。到这,一个"空文件 + 写好头"的容器就准备好了。

接下来,就是往里一个个塞包。


# 23.7 第二、三步:一个包写进文件,直到收尾

📍接上一节:容器建好了,进入三部曲的第 ② 步(循环写包)和第 ③ 步(收尾)。

容器建好,子进程进入 main() 主循环 —— 从管道读一个包,写进文件,直到管道关闭:

写包循环

// plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c:1171(主循环,抽骨架)
while (safe_read(&info, sizeof(info))) {          // ① 从管道读一个小头(读到 0 就退出)
	safe_read(buf, info.size);                    // ② 读这个包的字节

	packet->data         = buf;                   // 组装成一个 AVPacket
	packet->pts          = rescale(info.pts);     //   换算时间基(:1080)
	packet->stream_index = get_index(...);        //   按 track_idx 找到对应的流(:1079)
	if (info.keyframe) packet->flags |= AV_PKT_FLAG_KEY;

	av_interleaved_write_frame(ffm->output, packet);  // ★ 写进文件(:1086)
}
1
2
3
4
5
6
7
8
9
10
11

那句 av_interleaved_write_frame 是核心:

  • 把这个包按它的时间戳,写进文件里对应的那条轨;
  • interleaved(交织)= 它还会做最后一层缓冲/排序,确保容器里的视频包、音频包严格按 dts 交错(第 22 课引擎已经交织过一次,容器层这里再把好一道关);
  • 你不用管字节具体怎么摆 —— libavformat 会按容器格式(MP4/MKV 各自的规矩)自动组织

# 收尾:管道关闭 → 写索引

当你点"停止录制":

  • 主进程关掉管道的写端 → 子进程的 safe_read 返回 0跳出循环 → 调 av_write_trailer(ffmpeg-mux.c:207)写索引 → 关文件;
  • 主进程这边,os_process_pipe_destroy(obs-ffmpeg-mux.c:469)会**"等"子进程真正写完、退出**,才算录制结束 —— 保证文件完整落盘。

所以"正常点停止"很重要:它才走得到 av_write_trailer,MP4 的 moov 索引才写得上(又一次呼应第 22 课的坑)。强杀 OBS = 跳过收尾 = MP4 可能废掉。


# 23.8 多轨录制:把麦克风单独存一轨

📍补一个实用功能:三部曲讲完了。顺带看一个你在设置里能开、后期很有用的功能 —— 它正好用到前面的 track_idx

一个文件里可以有好几条音频轨,后期能单独处理某一路。

多轨录制

  • 视频编码器 → 文件里的流 0(video);
  • 音频轨 1(比如总混音,第 16 课那条混好的)→ 流 1(audio);
  • 音频轨 2(比如单独的麦克风)→ 流 2(audio)。

怎么做到的?每个编码包都带着一个 track_idx(第几条轨),子进程按它写进对应的流(get_index,ffmpeg-mux.c:1023):

  • 每条音频轨 = 一个独立的音频编码器(obs_output_get_audio_encoder(output, N));它产出的包,track_idx 就是 N(get_encoder_index,obs-output.c:1409)。
  • 子进程建轨时(23.6 的第 ③ 步)按轨数建多条音频流;写包时按 track_idx 对号入座。
  • 这需要输出置了 OBS_OUTPUT_MULTI_TRACK 标志(第 22 课,obs-ffmpeg-mux.c:865)。

好处:比如把"麦克风"单独存一轨 —— 后期剪辑时,你能只调麦克风音量、或把游戏声和人声分开处理,不用重录。这是进阶创作者常用的功能。


# 23.9 录制到文件:全程回顾

把整条路串起来:

录制全程回顾

编码器(吐 encoder_packet)→ 交织(引擎按 dts 排,第 22 课)→ ffmpeg_mux_data(写进管道:头 + 包)→ 子进程 libavformat(三部曲写文件)→ .mp4/.mkv 成品。

两个要带走的东西:

  1. 封装的"三部曲"万能骨架:avformat_write_headerav_interleaved_write_frame(循环)→ av_write_trailer。任何 FFmpeg 写文件都是它。
  2. 崩溃隔离的子进程设计 + "正常停止才写 moov" —— 连起了第 22 课"录制别用老 MP4"的道理:MP4 的索引靠最后那一步 av_write_trailer,走不到就废;MKV/Hybrid MP4 不怕。

# 23.10 本课小结

  • 先补的三个词:FFmpeg = 开源"媒体瑞士军刀"库(几乎所有播放器/软件都用),libavformat 是其中专管容器读写的部分,OBS 把"写文件"委托给它;进程 = 一个运行中的程序(各自独立、崩了互不影响),子进程 = 一个进程启动的另一个;管道 = 两进程间的单向水管(和命令行 | 同理)。
  • 两个进程,一根管道:OBS 主进程把编码包用管道发给独立子进程 obs-ffmpeg-mux,由它用 libavformat 写文件。目的是崩溃隔离(封装崩了不连累主程序/直播)。
  • 传什么:配置(文件路径、轨数、codec 参数)走命令行参数;数据走管道 —— 先发一次每轨的 extradata 头信息,再反复发 [ffm_packet_info 小头(pts/dts/size/track/type/keyframe)] + [包字节](ffmpeg-mux.h:32)。
  • extradata / SPS-PPS = 解码"说明书":编码器开头产出的参数集(分辨率/profile/特性),存进容器文件头;播放器必须先读它才能解后面的帧。所以只发一次、放最前面。
  • 封装"三部曲"(万能骨架):① avformat_write_header(写文件头,一次,ffmpeg-mux.c:927)→ ② av_interleaved_write_frame(每包一次,循环,:1086)→ ③ av_write_trailer(写索引/收尾,一次,:207)。
  • 建容器(第一步细节):av_guess_format(按扩展名)→ avformat_alloc_output_context2avformat_new_stream×N → 填 codecpar+extradata → avio 开文件 → avformat_write_header
  • 写循环(第二、三步):main() 循环 safe_read 头+字节 → 组 AVPacket(pts/dts/stream_index/keyframe)→ av_interleaved_write_frame(容器层再按 dts 交织一道);管道关闭 → av_write_trailer → 关文件。os_process_pipe_destroy 等子进程写完才算结束。
  • 正常停止才写 moov:MP4 的 moov 靠第 ③ 步写;强杀 = 跳过 = MP4 废(第 22 课的坑,代码根源在此)。
  • 多轨录制:每包带 track_idx,子进程建多条流、对号入座(get_index,ffmpeg-mux.c:1023)。每条音频轨 = 一个独立音频编码器,track_idx=N。可把麦克风单独存一轨,方便后期。需 OBS_OUTPUT_MULTI_TRACK

# 23.11 动手 / 观察(本课作业)

  1. 看子进程:开始录制后,在任务管理器里找 obs-ffmpeg-mux.exe —— 就是本课那个独立封装进程。停止录制后它会退出。(顺便体会 23.1 讲的"进程":它和 OBS 是两个独立进程。)
  2. 理解三部曲:用自己的话说清 avformat_write_header / av_interleaved_write_frame / av_write_trailer 各干什么;为什么第三步对 MP4 生死攸关?
  3. 开多轨录制:OBS →"设置 → 输出 → 录像",开启多个音频轨,并把麦克风指定到单独一轨(高级音频属性里勾选轨道)。录一段,用支持多音轨的播放器(如 VLC)看它有几条音轨。
  4. 验证 extradata 的必要性:用自己的话解释,为什么"解码说明书"(SPS/PPS)必须放在文件最前面、而不是每帧都带一份?
  5. 翻源码:打开 plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c,找到那三句:avformat_write_header(:927)、av_interleaved_write_frame(:1086)、av_write_trailer(:207);再到 obs-ffmpeg-mux.c:625 看主进程怎么把 ffm_packet_info + 数据写进管道。

# 下一课预告

录制到文件搞定了 —— 那是把包写到本地硬盘。可"直播"是把这些包发到远端的服务器,让全世界实时看到。这中间隔着一个网络协议:RTMP。它怎么和服务器握手、怎么把一个个编码包"发"出去、网络卡了又怎么办?

第 24 课:推流到服务器 —— RTMP 协议 —— 我们钻进 rtmp_output,看它怎么连上直播服务器、把 FLV 格式的音视频包一个个推送出去、以及网络拥塞时那套"优先丢 P/B 帧保关键帧"的取舍(第 18 课的伏笔)。下节课见。


📁 本课配图:imgs/23-01 ~ imgs/23-09 📌 源码锚点: OBS 主进程侧 plugins/obs-ffmpeg/obs-ffmpeg-mux.c::14(struct ffmpeg_muxer,.h)、:266(build_command_line 命令行参数)、:305/:309(start_pipe / os_process_pipe_create2 起子进程)、:379/:444(ffmpeg_mux_start)、:438(obs_output_begin_data_capture)、:667(send_headers 发 extradata)、:662(obs_encoder_get_extra_data)、:773(ffmpeg_mux_data)、:603(write_packet)、:608(填 ffm_packet_info)、:625/:632(os_process_pipe_write 头+数据)、:501(ffmpeg_mux_stop)、:469(os_process_pipe_destroy 等子进程)、:865(flags:MULTI_TRACK/CAN_PAUSE);ffmpeg-mux.h:32(struct ffm_packet_info)、:22(enum ffm_packet_type); 子进程侧 plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c::1135(main)、:1171(主循环 safe_read)、:598(safe_read)、:1011/:941(ffmpeg_mux_init_context)、:955/:957(av_guess_format)、:970(avformat_alloc_output_context2)、:564(init_streams)、:418(avformat_new_stream)、:471/:557(avcodec_parameters_from_context)、:461/:548(extradata)、:895/:900(avio 开文件)、:927(★ avformat_write_header)、:1066(ffmpeg_mux_packet)、:1023(get_index 轨→流)、:1086(★ av_interleaved_write_frame)、:207(★ av_write_trailer)、:184/:188(avio_close / avformat_free_context); libobs/obs-output.c:1409(get_encoder_index → track_idx)、:2233(assign track_idx)。 注:FFmpeg/进程/管道为通用计算机背景知识,非 OBS 源码内容。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>