29 / 32 全链路、插件与脚本扩展

第 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 + 合成 —— 拼成你看到的画面

📍从"一帧原始像素"到"你预览里看到的成品"。

上 GPU + 合成

这一整段,都发生在图形线程的一次 tick 里(第 14 课),分四步:

  1. ① 上 GPU 变纹理(第 11 课):像素传上显卡 = 一张纹理(texture),后面全在 GPU 上算(快);
  2. ② 过滤镜(第 12 课):滤镜 = 一个像素着色器,采样后做点处理(变色/模糊/锐化);
  3. ③ 场景合成(第 13 课):按场景里每个源的位置/缩放/旋转(draw_transform 矩阵)摆好、叠起来;
  4. ④ 成品画面:所有源叠完 = 一张"合成好的"画面(render target)。

你在"预览"里看到的,就是这张纹理。拖动源、改大小,本质是改 draw_transform 矩阵(第 13 课)。这张成品纹理,一路送去两个地方:① 显示到预览;② 交给下一站,变成一条"视频流"。


# 28.4 第 3 站:从"一张画面"到"一条视频流"

📍成品纹理,怎么变成"发得出去的一条流"。

从画面到视频流

成品纹理还在 GPU 上,有两条路出去(第 14 课):

  • 路 A:下载回内存 → 给 CPU 编码器(x264)用;
  • 路 B:留在 GPU → 直接给显卡编码器(NVENC)用(第 19/20 课那个 .encode vs .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.encodeencoder_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/OBSBasicobs-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(推流端)源码范围内。

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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