第 28 课:全链路总复盘 —— 追踪一帧的完整旅程
# 第 28 课:全链路总复盘 —— 追踪一帧的完整旅程
嘿,我是小方。 这一课是**"读懂 OBS"这条主干的收官**(模块 1–7)。前面 27 课,我们把 OBS 这台机器一个零件一个零件拆开看了 —— 采集、合成、编码、封装、推流、界面。这一课不引入任何新知识,而是做一件事:拿一帧真实的画面(和一小段声音)当主角,让它从"你按下开始直播"出发,走完整条流水线,一直到观众的屏幕。 每到一站,我都会告诉你:这一步在干什么、在前面哪一课讲过、落在 OBS 的哪个函数上。(之后还有模块 8 三课,带你从"读懂"走向"动手改"。) 走完这一趟,散落在 27 课里的所有点,会连成一条线。这也是我们这门课最开始那句话的完整兑现: "一帧画面 / 一段声音,是如何从『采集 → 合成 → 编码 → 封装 → 推流 / 录制』走完整条流水线的。"
# 28.1 出发前:你按下"开始直播"
📍旅程的起点:一切从一次点击开始。

你点"开始直播"按钮 → Qt 信号 → OBSBasic 的槽 → obs_output_start(第 26 课那条"点击 → obs_ 调用"的路)。这一下,把推流(或录制)输出打开了。
与此同时,引擎里**两台"心跳"**一直在转(它们在 obs_startup 后就跑起来了,第 26 课):
- 视频心跳 · 图形线程(
obs_graphics_thread,第 14 课):每1/帧率秒 tick 一次 —— 采集 → 合成 → 输出一帧; - 音频心跳 · 音频线程(
audio_thread,第 16 课):每 tick 拉 1024 个采样 —— 混音 → 对齐 → 输出。
机器醒了。接下来,跟着一帧画面 + 一小段声音,走完整条流水线。视频和音频是两条并行的线,各有各的心跳,却要在时间戳上严丝合缝(第 16 课)。先跟视频这一帧。
# 28.2 第 1 站:采集 —— 原材料进场
📍流水线第一步:数据从哪来。

- 视频(第 2/3/10 课):采集源(屏幕/摄像头/窗口)抓到一帧 —— 它是一堆像素(第 2 课),按 YUV 存(第 3 课)。源把它交给引擎:
obs_source_output_video(libobs/obs-source.c,第 10 课),进"异步帧队列",等图形线程来取。 - 音频(第 4/15 课):WASAPI 抓到一小段声音(约 10ms)—— 它是一串采样点 PCM(第 4 课)。交给引擎:
obs_source_output_audio(obs-source.c:4029,第 15 课),重采样到引擎格式,进"深环形缓冲"。
关键:不管什么输入,进了引擎都是一个 obs_source(第 6 课"一切皆源")。而且视频可丢(丢一帧没人注意),音频不可丢(丢一个采样是可闻的咔哒)—— 所以两条线的缓冲策略不同(第 15 课)。原材料就位,先跟视频这一帧走。
# 28.3 第 2 站:上 GPU + 合成 —— 拼成你看到的画面
📍从"一帧原始像素"到"你预览里看到的成品"。

这一整段,都发生在图形线程的一次 tick 里(第 14 课),分四步:
- ① 上 GPU 变纹理(第 11 课):像素传上显卡 = 一张纹理(texture),后面全在 GPU 上算(快);
- ② 过滤镜(第 12 课):滤镜 = 一个像素着色器,采样后做点处理(变色/模糊/锐化);
- ③ 场景合成(第 13 课):按场景里每个源的位置/缩放/旋转(
draw_transform矩阵)摆好、叠起来; - ④ 成品画面:所有源叠完 = 一张"合成好的"画面(render target)。
你在"预览"里看到的,就是这张纹理。拖动源、改大小,本质是改 draw_transform 矩阵(第 13 课)。这张成品纹理,一路送去两个地方:① 显示到预览;② 交给下一站,变成一条"视频流"。
# 28.4 第 3 站:从"一张画面"到"一条视频流"
📍成品纹理,怎么变成"发得出去的一条流"。

成品纹理还在 GPU 上,有两条路出去(第 14 课):
- 路 A:下载回内存 → 给 CPU 编码器(x264)用;
- 路 B:留在 GPU → 直接给显卡编码器(NVENC)用(第 19/20 课那个
.encodevs.encode_texture2)。
中间隔着一层 video_output(第 14 课):
- 它把"忽快忽慢的渲染"整理成"严格按帧率的一条流"—— 不够就补帧、太多就丢帧(第 14 课那个
count/skipped,就是状态栏的"跳过帧"); - 它是"发布/订阅"(第 8 课那套):同一条流,能同时喂给多个订阅者(推流编码器、录制编码器、虚拟摄像头…),编码器订阅后靠
receive_video收帧。
到这,视频这一帧,变成了"视频流里的一帧",正等着被编码。但别忘了那条并行的音频线 —— 先补上它,再一起进编码。
# 28.5 并行线:声音这一路,同时在走
📍回头看音频:它在自己的心跳里,同步地往前走。

音频这一路(第 15-17 课):
- 采集(第 15 课):WASAPI 抓声音 →
obs_source_output_audio; - 混音(第 16 课):
audio_thread每 tick 拉 1024 采样,mix_audio把多路相加(核心就一句*(mix++) += *(aud++)); - 对齐(第 16 课):按时间戳摆位、缓冲等待(宁等不丢),
sync_offset微调; - 音量/监听(第 17 课):推子(cubic 曲线)、电平表、监听 —— 挂在每个源上;
- 最后成一条
audio_output流,1024 采样一包,等着编码。
视频和音频是两台独立的心跳,凭什么能同步? 靠一把"共同的时间尺":采集时,每一帧画面、每一段声音,都盖上同一个纳秒时间戳(第 16 课);混音时按时间戳摆位,编码时音频锚定到配对的视频编码器起点。于是播放器拿到的音画,时间戳对得上。这就是"音视频同步"的根:不是让两条线跑得一样快,而是给它们同一把尺、各自标好刻度。
现在,视频一帧 + 音频一包,都准备好了 —— 一起进编码。
# 28.6 第 4 站:编码 —— 把庞大的原始数据压成字节
📍流水线的"压缩机":裸数据太大(第 5 课),必须压。

- 视频编码(第 18/19/20 课):交给编码器的
info.encode回调(第 19 课)—— x264(CPU)或 NVENC(显卡,第 20 课)。它榨干冗余:I 帧独立、P/B 帧只存差异,关键帧 + GOP(第 18 课)。吐出encoder_packet,带pts/dts/keyframe(第 18 课)。 - 音频编码(第 21 课):同一张登记表(
type=AUDIO),委托 FFmpeg 做 AAC / Opus。靠"听觉掩蔽"扔掉听不见的(第 21 课),压约 10 倍(比视频温和);每个音频包都是关键帧(无帧间预测);码率 CBR/VBR 怎么选(第 21 课)。
同一个 obs_encoder 抽象(第 19 课的登记表 + 回调),装下所有编码器 —— x264/NVENC/AAC 长得一样。现在,视频流 + 音频流,都成了一串串带时间戳的 encoder_packet。两条流还是分开的 —— 下一站把它们合并、送出去。
# 28.7 第 5 站:封装 + 送出 —— 合并两条流,发往终点
📍最后一步"上行":合流、装盒、送出门。

首先,obs_output 交织(第 22 课):把视频包 + 音频包,按 dts 排成一条(interleave_packets),让播放器能边读边同步播。然后分两条路(靠一个 SERVICE 标志分家,第 22 课):
- 录制 → 文件(第 23 课):独立子进程用 libavformat 写文件,三部曲
write_header → 循环 av_interleaved_write_frame → write_trailer;容器 MP4/MKV(录制别用老 MP4,第 22 课)。 - 推流 → 服务器(第 24/25 课):RTMP 握手连上平台(第 24 课);一个个包包成 FLV 标签 →
RTMP_Write;网络卡了丢 P/B 帧、保关键帧(第 18/24 课);服务器地址/推流码来自obs_service(第 25 课)。
录制和推流,是同一个 obs_output,差一个 SERVICE 标志:一个 av_interleaved_write_frame 写进文件,一个 RTMP_Write 发进 socket。 到这,你的画面 + 声音,已经离开了你的电脑,飞向硬盘 或 直播平台。整条"上行"流水线,走完了。
# 28.8 终点:观众的屏幕 —— 一切反着来一遍
📍旅程的另一半:你在 OBS 里做的每一步,观众端反过来做一遍。

包到了平台 → 转码 + CDN 全球分发 → 观众的播放器拉流 → 解封装 + 解码 → 显示一帧。
解码 = 编码的镜像:你的编码器"压",观众的解码器"解";你的封装器"装",观众的解封装器"拆"。而且那些细节全都对上了 —— SPS/PPS"解码说明书"(第 23 课)先到,后面的帧才解得开;关键帧先到,才能起播。
从你屏幕上的一帧,到观众屏幕上的同一帧 —— 中间就是这 28 课讲的全部。 你现在,能说清这一路上每一步在干什么、在 OBS 的哪段代码里 —— 这就是这门课的目标。
# 28.9 一张大图:一帧画面的一生 = 这门课的全部

把整条路压成一张图,每一站标着它在第几课、靠哪个关键函数:
| 站 | 课 | 关键函数 |
|---|---|---|
| 采集 | 第 10/15 课 | obs_source_output_video / _audio |
| 合成 | 第 11-13 课 | gs_draw_sprite · draw_transform |
| 输出管线 | 第 14 课 | obs_graphics_thread · video_output |
| 编码 | 第 18-21 课 | info.encode → encoder_packet |
| 封装 | 第 22 课 | interleave_packets(按 dts) |
| 送出 | 第 23-25 课 | av_interleaved_write_frame / RTMP_Write |
(音频在另一条并行线上:采集→混音→对齐→编码,两线靠同一把时间尺同步。)
但比具体函数更该记住的,是贯穿全程的几个"大主意":
- 一切皆 source(第 6 课):输入/滤镜/转场/场景,都是源;
- 登记表 + 回调(第 6/11/19/22/25 课):源/图形/编码/输出/服务,五处一个套路;
- 发布/订阅(第 8/14 课):信号解耦,一处产出、多处订阅;
- 数据驱动(第 25/27 课):会变的做成数据(服务/属性),逻辑通用;
- 时间戳对齐(第 16 课):音视频同一把纳秒尺,才能同步;
- 感知编码(第 18/21 课):扔掉人感觉不到的,才压得狠。
遇到没讲过的子系统,先找它的 xxx_info 结构体、找注册它的地方、找引擎在哪调这些回调 —— 这把"登记表 + 回调"的万能钥匙,能开 OBS 大部分的门。
# 28.10 你已经学会了什么 · 接下来去哪
回头看,你从"被术语劝退",走到了这里:
- 模块 1(原材料):像素、分辨率、帧率、RGB/YUV、采样/位深/PCM、裸数据为什么必须压;
- 模块 2(世界观):一切皆 source、对象的生死(引用计数)、信号与回调、插件加载;
- 模块 3(视频链路):采集、GPU/纹理/着色器、滤镜、场景合成、视频管线与转场;
- 模块 4(音频链路):WASAPI 采集、混音与对齐/同步、音量/电平表/监听;
- 模块 5(编码):为什么能压、I/P/B、
obs_encoder、x264 vs NVENC、音频编码与码率; - 模块 6(打包送出):容器、
obs_output、录制 mux、RTMP 推流、obs_service; - 模块 7(串起来):界面
OBSApp/OBSBasic、obs-frontend-api与属性面板、以及这一课的全链路复盘。
接下来去哪? 引擎和界面你都读通了。接下来的模块 8(第 29–31 课),换个姿势:动手写。 三件事:
- 第 29 课:写你的第一个插件 —— 一个简单的视频滤镜,把第 12 课那套
.effect+ 回调,自己动手填一遍; - 第 30 课:不写 C++ 也能扩展 —— 用 OBS 的 Lua / Python 脚本(基于
obs-frontend-api,第 27 课); - 第 31 课:接下来去哪 —— 读源码的方法、调试、进阶主题地图,给你一张能一直用下去的图。
⚠️ 一个提醒:OBS 官方在
CONTRIBUTING.rst里明确不接受主要由 AI 生成的代码提交。本课程把源码当作学习材料来读、来讲,和"提交 AI 生成的 PR"是两回事 —— 请把这里学到的东西,用来理解和创造你自己的作品,而不是不加理解地往上游提交。
# 28.11 小结:主干,通关了
一开始,你可能只是好奇:"点一下『开始直播』,屏幕上的画面到底经历了什么?"
现在你知道了 —— 而且不是模模糊糊地知道,是能指着源码说清每一步地知道。一帧画面怎么上 GPU、被合成、被压成字节、被封装、被推向全世界;一段声音怎么被采集、混音、和画面对齐 —— 这条主干,你从头到尾追通了。
"读懂 OBS"这件事,到这里画上句号。 但你的路才刚展开:下一步不是"看",是"做"。模块 8 带你把学到的反过来用 —— 写个插件、写个脚本,让 OBS 照你说的做。(更完整的"临别赠言",留到全课最后一课第 31 课。)
# 下一课预告
主干读通了,但你还只是"读者"。最后三课(模块 8)让你变成"作者" —— 从最小、最好上手的一种扩展开始。
第 29 课:写你的第一个插件 —— 一个简单的视频滤镜 —— 我们把第 6/9/11/12 课学的(登记表、插件加载、着色器、滤镜三步)拼在一起,从零搭一个"亮度"滤镜:模块骨架 → 登记表 → create 加载着色器 → 属性滑块 → video_render 三步 → .effect。写完你会发现:所有滤镜,都是这一个形状。下节课见。
📁 本课配图:
imgs/28-01~imgs/28-09📌 本课不引入新源码,而是把前 27 课的关键锚点连成一条线。核心站点回顾: 采集libobs/obs-source.c(obs_source_output_video/obs_source_output_audio:4029,第 10/15 课);合成libobs/obs-scene.c(scene_video_render/draw_transform,第 13 课)+graphics/(第 11 课);输出管线libobs/obs-video.c:1163(obs_graphics_thread)+libobs/media-io/video-io.c(video_output,第 14 课);音频libobs/audio-monitoring+obs-audio.c:90(mix_audio)+media-io/audio-io.c:205(audio_thread,第 16 课);编码libobs/obs-encoder.c:1421(info.encode,第 19 课)+plugins/obs-x264/obs-nvenc/obs-ffmpeg(第 20/21 课);封装/输出libobs/obs-output.c:2073(interleave_packets)/:1756(info.encoded_packet,第 22 课)+plugins/obs-ffmpeg/ffmpeg-mux(第 23 课)+plugins/obs-outputs/rtmp-stream.c(第 24 课)+plugins/rtmp-services(第 25 课);界面frontend/(OBSApp/OBSBasic,第 26/27 课)。 注:观众端解码/CDN 分发为通用背景,不在 OBS(推流端)源码范围内。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言