第 6 课:一切皆 Source(源)
# 第 6 课:一切皆 Source(源)
嘿,我是小方。 从这一课起,咱们进入 模块 2:OBS 的世界观。前面五课讲的是「音视频本身」的知识;从现在开始,我们要真正钻进这套源码,去理解 OBS 自己是怎么把整个系统组织起来的。 而这一课,是整门课最关键的一课。我不夸张 —— 如果你只能记住这门课的一个概念,就记住今天这个。它是你读懂 OBS 全部源码的第一把钥匙:一切皆 Source。 这一课内容比较厚,我会从「是什么」讲到「怎么运转」,你跟着我一节一节来,别急。
# 6.1 先给你看个「意外」
📍我们在哪:模块 2 第一站,先抛一个反直觉的现象——摄像头、文字、滤镜、转场竟是同一种东西,把「一切皆 source」这把总钥匙的门缝先撬开。
我先不解释,直接上图。下面这些东西 —— 摄像头、屏幕采集、一张图片、一行文字、一个绿幕抠像滤镜、一个淡入淡出的转场 —— 你觉得它们有什么共同点?

凭直觉,你大概会觉得它们是八竿子打不着的几类东西:摄像头是硬件设备,文字是排版,滤镜是图像处理,转场是动画……
但在 OBS 的代码里,它们竟然是同一种东西 —— 全都叫 obs_source。
第一次听到这个,几乎每个人都会愣一下:「滤镜也是源?转场也是源?」对,在 OBS 眼里,它们没有本质区别,共用同一套数据结构、同一套接口、同一套管理逻辑。
这个设计,就是 OBS 整个架构的地基。咱们这一课,就把它彻底讲透。
如果你写过 Linux/Unix,这里有个绝佳的类比:Unix 有句名言叫「一切皆文件」—— 普通文件、硬盘设备、网络连接、管道,全都用同一套
open / read / write / close接口操作。OBS 玩的是同一招,只不过它的「文件」叫 source。
# 6.2 Source 到底是什么
📍承上启下:上一节留了悬念「它们凭什么是一种东西」,这一节先给 source 一个最朴素的定义——喂数据或加工数据的模块。
先给个最朴素的定义。OBS 源码自己在 obs-source.h 开头就写了一句话,堪称官方定义:
// libobs/obs-source.h:26
/* Sources are modules that either feed data to libobs or modify it. */
// 源,就是那些「给 libobs 喂数据」或者「加工数据」的模块。
2
3
一句话拆成两半,你就懂了:
- 「喂数据」 —— 凭空产出画面或声音的东西。摄像头、屏幕、图片、文字,都属于这类:它们是数据的源头(这也是「source = 源」的字面意思)。
- 「加工数据」 —— 不自己产数据,而是把别人的数据拿来改一改。滤镜(磨皮、抠绿幕)、转场(在两个画面之间过渡)就属于这类。
还记得第 2 课吗?咱们见过一个结构体 struct obs_source_frame(obs.h:283)—— 当时我说它是「源吐出来的一帧画面」。现在你彻底明白那个名字了:它就是某个 obs_source 产出的一帧。前几课的「原材料」,正是从一个个 source 里流出来的。
# 6.3 Source 分成四类
📍接上一节:知道了 source 是「喂/加工数据的模块」,这一节把它按用途拆成 INPUT / FILTER / TRANSITION / SCENE 四类。
虽然「都是 source」,但 OBS 还是给它们分了四个类型,用一个枚举写得清清楚楚:
// libobs/obs-source.h:33
enum obs_source_type {
OBS_SOURCE_TYPE_INPUT, // 输入源
OBS_SOURCE_TYPE_FILTER, // 滤镜
OBS_SOURCE_TYPE_TRANSITION, // 转场
OBS_SOURCE_TYPE_SCENE, // 场景
};
2
3
4
5
6
7

挨个说:
- INPUT(输入源) —— 凭空产出画面/声音的「源头」。摄像头、屏幕、文字、图片、浏览器…… 你在 OBS 里点「+ 添加来源」加进去的,基本都是这一类。
- FILTER(滤镜) —— 挂在另一个源「上面」,加工它产出的画面。比如给摄像头挂一个「色键」抠掉绿幕,给画面挂一个「模糊」。滤镜自己不产数据,它处理的是「宿主源」的数据。
- TRANSITION(转场) —— 夹在两个源之间,做平滑过渡。比如从场景 A 切到场景 B 时的「淡入淡出」。
- SCENE(场景) —— 把多个源组合成一幅画面。注意!场景本身也是一个 source —— 这件事很妙,咱们留到第 13 课专门讲。
虽然分了四类,但它们共享同一个底座。
# 6.4 一个容易栽的坑:「类型」和「实例」是两回事
📍转折:四类讲完,先别急着看代码——得先分清一对最容易糊的概念:「类型」(蓝图)和「实例」(对象)。
在往下讲之前,小方必须先帮你分清一对极易混淆的概念,不然后面全会糊。
你在 OBS 里可以往一个场景里加三张图片:一个 logo、一张背景、一个表情。它们都是「图片源」—— 可它们明明是三个互不相干的东西,各有各的文件路径。那「图片源」到底是一个还是三个?
答案是:「图片源」这个类型,只有一个;但「图片源的实例」,你想要几个有几个。

obs_source_info是类型 / 蓝图 —— 描述「图片源」是个什么东西,整个程序里只注册一份(由 image-source 插件在加载时注册)。obs_source_t是实例 / 对象 —— 你每往场景里加一张图,引擎就造一个新实例,它有自己的设置(自己的文件路径)。三张图,就是三个obs_source_t,但它们共享同一份蓝图。
如果你学过面向对象,这就是**「类 vs 对象」**:obs_source_info 是 class,obs_source_t 是 new 出来的 object。一个类,能造出无数对象,各有各的状态。
这对概念会贯穿后面好几课:第 7 课讲的「引用计数」管的是实例的生死;第 13 课的场景里摆的也是实例。务必记牢:info = 蓝图(一份),source_t = 实例(无数个)。
🗒️ 术语速记:INPUT(输入源:摄像头/窗口…)· FILTER(滤镜:处理别的源)· TRANSITION(转场)· SCENE(场景:装其它源)—— 四种都是
obs_source,靠type区分。另外:obs_source_info=一张"类型说明书"(所有同类源共用),obs_source(实例)=按说明书造出的一个具体对象。
# 6.5 源码印证:一张「登记表」
📍接上一节:蓝图到底长什么样?这一节翻出真身——那张所有 source 都要填的登记表
obs_source_info。
任何一个 source —— 不管它是输入、滤镜还是转场 —— 想被 OBS 认识,都得填同一张「登记表」。这张表,就是结构体 struct obs_source_info:

// libobs/obs-source.h:222
struct obs_source_info {
const char *id; // 唯一名字,如 "image_source"
enum obs_source_type type; // 哪一类(INPUT / FILTER / …)
uint32_t output_flags; // 能干啥(有视频?有音频?)
const char *(*get_name)(void *type_data); // 界面显示名
void *(*create)(obs_data_t *settings, obs_source_t *source); // 怎么创建
void (*destroy)(void *data); // 怎么销毁
uint32_t (*get_width)(void *data); // 宽
uint32_t (*get_height)(void *data); // 高
/* 下面是可选的回调 */
void (*video_render)(void *data, gs_effect_t *effect); // 怎么把自己画出来
void (*video_tick)(void *data, float seconds); // 每帧更新
obs_properties_t *(*get_properties)(void *data); // 属性面板
void (*update)(void *data, obs_data_t *settings); // 设置变了怎么办
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
别被这么多字段吓到,抓住它的本质:这是一张「契约」。
那些 (*create)、(*video_render) 是函数指针 —— 说白了就是「填空题」:插件把自己的函数地址填进去。插件不用懂引擎内部怎么运转,只要把这张表填好,然后调一句:
obs_register_source(&info);
引擎就知道怎么创建它、渲染它、管理它了 —— 至于它内部到底是摄像头还是一行文字,引擎根本不关心。这就是「接口与实现分离」最经典的样子。
给你个更接地气的比方:
obs_source_info就像乐高积木底部那一圈标准凸点。不管这块积木是车轮、是门、还是人仔,只要凸点规格一致,就能咔哒一声拼到同一块底板上。OBS 引擎就是那块底板,它只认凸点(接口),不在乎积木长什么样。
🔧 小方彩蛋(给有 C/C++ 基础的同学): 你可能注意到了,
obs_source_info整个就是一堆函数指针。这其实是 C 语言手搓出来的「虚函数表(vtable)」!C++ 里你写个虚函数virtual void render(),编译器背地里就是给每个类生成一张这样的函数指针表,实现「同一个调用、不同的实现」的多态。OBS 用的是纯 C,没有virtual关键字,于是它自己手动搞了一张 vtable ——obs_source_info。引擎调source->info.video_render(...),就等价于 C++ 的虚函数派发。看懂这一层,你就明白:「一切皆 Source」本质上是在用 C 实现面向对象的多态。
# 6.6 一个 Source 能干啥:用「标志位」声明
📍承上启下:登记表里有个
output_flags字段,这一节单拎出来讲——source 用一组 bit 告诉引擎「我能干什么」。
回头看登记表里的 output_flags 字段。它干嘛用的?让一个 source 告诉引擎「我能干什么」 —— 用的是一组「位标志」(bit flag),一个 bit 一个开关:
// libobs/obs-source.h:88 起
#define OBS_SOURCE_VIDEO (1 << 0) // 我有画面
#define OBS_SOURCE_AUDIO (1 << 1) // 我有声音
#define OBS_SOURCE_ASYNC (1 << 2) // 我异步喂帧(摄像头那种)
#define OBS_SOURCE_CUSTOM_DRAW (1 << 3) // 我要自定义绘制
#define OBS_SOURCE_INTERACTION (1 << 5) // 我能响应鼠标键盘
2
3
4
5
6
用的时候,用按位或 | 把需要的开关「点亮」组合起来。看几个例子:

- 一张图片:只有画面,没声音 →
OBS_SOURCE_VIDEO; - 一个麦克风:只有声音,没画面 →
OBS_SOURCE_AUDIO; - 一个摄像头:既有画面又有声音,而且是设备异步送帧 →
OBS_SOURCE_VIDEO | OBS_SOURCE_AUDIO | OBS_SOURCE_ASYNC。
引擎拿到这组标志,就知道该怎么对待这个 source:有 VIDEO 的去走渲染管线,有 AUDIO 的去走音频混音,有 ASYNC 的就准备好接收它随时送来的帧。一个整数,几个 bit,把「我是什么、我能干啥」说得明明白白。
# 6.7 眼见为实:同一张表,填出三种东西
📍为什么现在讲这个:光说不够,这一节翻三个真实插件的源码,亲眼看图片/滤镜/转场怎么填同一张表——「一切皆 source」的硬证据。
光听我说没意思,咱们直接翻三个真实存在的源码,看它们怎么填这张表 —— 一个图片(输入)、一个调色(滤镜)、一个淡入淡出(转场):

// 图片(输入)—— plugins/image-source/image-source.c:363
static struct obs_source_info image_source_info = {
.id = "image_source",
.type = OBS_SOURCE_TYPE_INPUT, // ← 注意这一行
.output_flags = OBS_SOURCE_VIDEO | ...,
.video_render = image_source_render,
.get_width = image_source_getwidth,
...
};
// 调色(滤镜)—— plugins/obs-filters/color-correction-filter.c:721
struct obs_source_info color_filter = {
.id = "color_filter",
.type = OBS_SOURCE_TYPE_FILTER, // ← 只有这里不一样
.output_flags = OBS_SOURCE_VIDEO | ...,
.video_render = ...,
};
// 淡入淡出(转场)—— plugins/obs-transitions/transition-fade.c:140
static struct obs_source_info fade_transition = {
.id = "fade_transition",
.type = OBS_SOURCE_TYPE_TRANSITION, // ← 这里又不一样
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
看出来了吗?三个功能天差地别的东西 —— 一张静态图、一个调色算法、一个过渡动画 —— 填的是同一个 struct obs_source_info,主要区别就在 .type 那一行。然后,它们都通过同一个注册口进入 OBS:
// plugins/image-source/image-source.c:400
obs_register_source(&image_source_info);
2
这,就是「一切皆 Source」最硬的代码证据。 不是我编的比喻,是 OBS 真真切切这么写的。
到这里,「source 是什么、怎么定义」就讲完了。但你可能还有一肚子问号:它产出的画面怎么流出来?你点一下「添加来源」,背后发生了什么?滤镜到底怎么挂上去?接下来这几节,咱们钻进「运转机制」。
# 6.8 一帧画面,两条路从 source 流出来
📍转折:定义讲完,开始看运转——source 产出的画面怎么流出来?这一节讲异步推帧和 GPU 渲染两条路。
第 2 课咱们见过 obs_source_frame(内存里的一帧),第 6.6 节又见到 video_render(画自己)。它俩是什么关系?其实,source 把画面交给引擎,有两条完全不同的路:

- ① 异步喂帧(走内存) —— 摄像头、视频文件这类:数据是设备/解码器不定时送来的,源在拿到一帧后,调
obs_source_output_video()(obs.h:1416)把这帧原始像素(就是obs_source_frame)交给引擎。这条路是「源主动推」,什么时候有帧什么时候推 —— 所以它需要OBS_SOURCE_ASYNC标志。 - ② GPU 同步渲染 —— 图片、文字这类:数据现成就在显存里,根本不用走内存搬运。引擎每一帧主动调它的
video_render()(obs-source.h:351),源就在显卡上直接把自己画出来。这条路是「引擎主动拉」。
现在你彻底懂 ASYNC 标志的意义了:它就是源在告诉引擎「我走哪条路」。 摄像头点亮 ASYNC(我会异步推帧),图片不点(你每帧来调我就行)。一个标志位,把两套截然不同的出帧机制,统一在了同一个 obs_source 抽象之下。
# 6.9 你点一下「+ 图像」,背后发生了什么
📍接上一节:知道了帧怎么流出来,这一节回到更前面一步——你点「+ 图像」,一个实例是怎么被造出来的。
光说「实例是 new 出来的」太抽象。咱们把「往场景里加一张图片」这件事,从你点鼠标开始,一步步追到底:

- 你在界面点「+ 图像」,选好图片路径、点确定;
- 界面调引擎的创建函数
obs_source_create("image_source", "我的logo", settings, …)(obs.h:1046)—— 注意第一个参数,正是那张蓝图的 id; - 引擎拿着 id 去注册表里查 —— 找到当初
obs_register_source注册进来的那张image_source_info蓝图; - 引擎调用蓝图里的
.create回调,让插件分配这个实例的私有数据(读图、建纹理……); - 一个
obs_source_t实例诞生,挂进场景,出现在你的预览里。
一条龙下来,你会发现:「id」是把界面的一次点击,翻译成具体实现的关键暗号。 界面只知道字符串 "image_source",引擎靠它在注册表里对上号,剩下的全交给插件填的那张表。界面、引擎、插件三方解耦,全靠这套 id + 登记表的机制。
# 6.10 滤镜到底怎么「挂在源上」
📍承上启下:实例会创建了,再补一块前面反复提到、却没说清的机制——滤镜到底怎么挂在源身上。
前面反复说「滤镜挂在源上面」,到底怎么挂?其实很简单:每个源都带着一串滤镜,画面渲染时,依次穿过它们。

比如你给摄像头加了「色键」和「模糊」两个滤镜,那么渲染时,画面就是:摄像头 → 色键 → 模糊 → 最终画面,像流水线上的工序一样一道道过。
代码里,这条链就明明白白挂在 struct obs_source 上:
// libobs/obs-internal.h:940 (struct obs_source 内部)
DARRAY(struct obs_source *) filters; // 我身上挂的滤镜列表
2
你加一个滤镜,本质就是调:
// libobs/obs.h:1149
obs_source_filter_add(source, filter); // 把 filter 挂到 source 上
2
注意看 filters 数组的元素类型 —— struct obs_source *!滤镜本身也是一个 source。 所以这条链理论上能一直串下去:给滤镜再挂滤镜也说得通。统一抽象带来的灵活性,在这里体现得淋漓尽致。
顺带说一句,滤镜也分两种,正好对应 6.8 的两条路:异步滤镜(实现
filter_video回调,加工内存里的帧)和效果滤镜(实现video_render,在 GPU 上加一层 shader)。色键、模糊这些都是后者。
# 6.11 掀开盖子:一个实例里到底装着什么
📍承上启下:前面一直把实例当黑盒用,这一节掀开盖子,看
struct obs_source内部到底装了哪几样东西。
咱们一直把 obs_source_t 当个「不透明的句柄」用 —— 你只拿得到一个指针,看不见里面。但既然都讲到这份上了,小方带你掀开盖子,看看一个实例内部真实的样子(obs-internal.h:806 的 struct obs_source):

一个实例,主要装着这几样:
context—— 共享底座。所有 obs 对象(source、output、encoder…)都有的一块,装着这个实例的设置(obs_data)、名字、信号。下一课的obs_data就藏在这。info—— 那张蓝图的副本。也就是说,实例自己揣着一份它所属「类型」的全部回调(vtable)。引擎调source->info.video_render(...),就是从这取的函数指针。- 运行时状态 ——
active(是否激活)、showing(是否可见)、enabled、volume(音量)、muted…… - 异步帧缓冲 ——
cur_async_frame、async_frames[],专给 6.8 那条「异步喂帧」路用的。 - 滤镜列表 —— 就是 6.10 的
filters[]。 - 音频缓冲 —— 音频采样缓冲、重采样器、音画同步偏移……
把这张图记住,你就有了一个特别清爽的心智模型:
一个实例 = 它的类型(
info)+ 它的设置(context)+ 它的运行时状态。
简单、统一,而且对每一种 source 都成立。
# 6.12 引擎和 Source 怎么对话:回调全景
📍接上一节:实例内部有张
info(vtable),这一节把它上面那一长串回调铺开——引擎和 source 沟通的全部「暗号」。
回头看 6.5 那张登记表,我只列了几个字段。其实 obs_source_info 里有一长串回调。它们是引擎和 source 沟通的全部「暗号」—— 你按需实现,引擎在合适的时机替你调:

按用途归个类(都在 obs-source.h:222 往下):
- 生与灭:
create/destroy—— 实例的出生和死亡; - 设置 / 存档:
update(设置变了)、get_properties(生成属性面板)、get_defaults、save/load(存档读档); - 渲染:
video_tick(每帧更新状态)、video_render(画出来); - 滤镜专属:
filter_video/filter_audio(只有滤镜类型才用); - 显示状态:
activate/deactivate、show/hide; - 交互:
mouse_click/mouse_move/mouse_wheel/key_click(有INTERACTION标志才会收到)。
你不用全实现 —— 做个静态图片,可能就填 create / destroy / video_render / get_width / get_height 几个;做个能点击的浏览器源,才需要那些鼠标键盘回调。填多填少,取决于你的 source 有多复杂。 引擎永远只在「该调的时候」调你填了的那些。
# 6.13 顺手认识几个公共 API
📍收束:机制都讲完,顺手把你以后写代码会直接用到的几个 source 公共函数认个脸,顺便给下一课的引用计数埋个头。
最后,把你以后写代码会直接用到的几个 source 公共函数,放一起认个脸(都在 obs.h):
obs_source_t *obs_source_create(const char *id, const char *name,
obs_data_t *settings, obs_data_t *hotkey_data); // 创建实例 obs.h:1046
void obs_source_update(obs_source_t *source, obs_data_t *settings); // 改设置 obs.h:1112
void obs_source_filter_add(obs_source_t *source, obs_source_t *filter); // 挂滤镜 obs.h:1149
obs_source_t *obs_source_get_ref(obs_source_t *source); // 拿一个引用 obs.h:1062
void obs_source_release(obs_source_t *source); // 释放引用 obs.h:1057
2
3
4
5
6
最后两个 —— obs_source_get_ref / obs_source_release —— 跟「引用计数」有关:一个摄像头源可能同时被好几个场景引用,谁都不能擅自把它删了。这套「谁还在用、什么时候才能真正释放」的机制,正是下一课的主角。 这里先混个脸熟。
# 6.14 源码深读:从「点击」到「实例诞生」,代码到底怎么跑
📍为什么现在讲这个:前面 6.9 讲的是流程图,这一节把它落到真代码——翻开
.c,看蓝图怎么进注册表、实例怎么被 create 出来。
前面 6.9 那张「添加来源全流程」图讲的是流程;这一节,小方带你翻开 .c,把那条流程落到一行行真代码上。我们顺着两件事走:蓝图怎么进注册表,和实例怎么被造出来。
# 一、注册:把蓝图拷进引擎的「花名册」
6.7 节那三个插件都调了 obs_register_source(&info)。它其实是个宏,真身在这(obs-module.c:956):
// libobs/obs-module.c:956 (节选)
void obs_register_source_s(const struct obs_source_info *info, size_t size)
{
struct obs_source_info data = {0};
...
if (info->type == OBS_SOURCE_TYPE_INPUT) array = &obs->input_types;
else if (info->type == OBS_SOURCE_TYPE_FILTER) array = &obs->filter_types;
... // 按 type 分桶
if (get_source_info2(info->id, info->version)) // 重名检查:同 id 已注册过?
goto error;
memcpy(&data, info, size); // ★ 把整张蓝图(含全部函数指针)拷进引擎
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
关键是 memcpy(&data, info, size) 这一行:引擎把你那张 obs_source_info(连同所有函数指针)整个复制一份,存进自己的「花名册」。 它还按 type 把蓝图分门别类放进 input_types/filter_types/transition_types 几个数组 —— 这就是 6.3 节「四种类型」在内存里的样子。从此引擎就知道"image_source 这种源该怎么造、怎么画、怎么销毁"了。
# 二、查表:按 id 翻花名册
6.9 节说"引擎拿 id 去注册表查蓝图",代码朴素得可爱(obs-source.c:53):
// libobs/obs-source.c:53
struct obs_source_info *get_source_info(const char *id)
{
for (size_t i = 0; i < obs->source_types.num; i++) {
struct obs_source_info *info = &obs->source_types.array[i];
if (strcmp(info->id, id) == 0) // id 对上了
return info; // 返回这张蓝图
}
return NULL; // 没这号源
}
2
3
4
5
6
7
8
9
10
就是一个线性扫描:挨个比对 id 字符串,对上就返回那张蓝图。它正是"字符串 id → 具体实现"那座桥。
# 三、创建:实例诞生的全过程
主菜来了。obs_source_create 最终走进 obs_source_create_internal(obs-source.c:436),我把骨架抽出来,你对着下面的图看:
// libobs/obs-source.c:436 (抽骨架)
static obs_source_t *obs_source_create_internal(const char *id, const char *name, ...)
{
struct obs_source *source = bzalloc(sizeof(struct obs_source)); // ① 分配一个全 0 的空白实例
const struct obs_source_info *info = get_source_info(id); // ② 按 id 查蓝图(上面那个函数)
if (info)
source->info = *info; // ③ 把蓝图整张拷进实例(实例自带一份 vtable)
...
obs_source_init_context(source, settings, name, ...); // ④ 建 context:设置 obs_data + 引用计数账本
if (info && info->get_defaults)
info->get_defaults(source->context.settings); // ⑤ 让蓝图填好默认设置
...
if (info && info->create)
source->context.data = // ★⑥ 调插件自己的 create 回调!
info->create(source->context.settings, source);// 插件在这里读图、建纹理,返回它的私有数据
...
if (!private)
obs_source_dosignal(source, "source_create", NULL); // ★⑦ 广播「我出生了」信号
return source; // ⑧ 返回崭新实例
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

逐条对一下,前面好几课全在这里"显形":
- ③
source->info = *info—— 6.4 节说"实例自带一份蓝图副本",就是这一行干的;也是 6.11 节实例剖面图里那个info格子的来历。 - ④
obs_source_init_context—— 它把下一课(第 7 课)要讲的obs_data设置和引用计数控制块都建起来,生命周期管理从这一刻就位。 - ⑤
info->get_defaults和 ★⑥info->create—— 两次回调插件填进 vtable 的函数。尤其 ⑥ 是灵魂:引擎把设置递过去,插件在自己的create里干私活(读图、建 GPU 纹理),返回一坨私有数据存进context.data。引擎全程不知道、也不关心这坨数据长什么样。 - ★⑦
obs_source_dosignal("source_create")—— 广播"有个新源诞生了"。界面订阅了这个信号,就能把新源加进列表显示。(又是第 8 课的信号。)
# 四、然后呢?引擎每帧怎么「用」它
实例造好挂进场景后,引擎每渲染一帧,就调它的 video_render(obs-source.c:2809):
source->info.video_render(data, effect); // 引擎按下 vtable 里的「渲染」按钮
又是 source->info.<回调> 的形式 —— 跟创建时的 info->create、销毁时的 info->destroy 一模一样。引擎和插件之间所有的"对话",都是通过这张 vtable 上一个个函数指针进行的。 6.5 节那个"C 手搓多态"的彩蛋,在运行时实打实地兑现了。
# 生与死,正好镜像
把这一节和下一课(第 7 课)的"源码深读"并排放,你会看到一组漂亮的对称:
| 阶段 | 调 vtable | 发信号 |
|---|---|---|
诞生 create_internal | info->create(...) | dosignal("source_create") |
消亡 destroy_defer | info->destroy(...) | dosignal("destroy") |
一个对象从生到死,引擎做的就是:在恰当的时机,按下 vtable 上对应的那个按钮,再对外喊一嗓子。 看懂这组镜像,你就真正握住了 OBS 对象模型的脉络。
# 6.15 为什么非这么设计不可?
📍收束:绕了一整课,最后回到最根本的问题——费这么大劲把万物都做成 source,到底图什么?
绕了一大圈,回到最根本的问题:费这么大劲把摄像头、文字、滤镜、转场都硬塞成「同一种东西」,图什么?
图的是整个引擎可以只写一套代码。

OBS 引擎要做的事可不少:渲染画面、合成场景、刷新预览、生成属性面板、把工程存成文件再读回来…… 如果摄像头、文字、滤镜各是一套独立类型,这些功能每一样都得为每种类型分别写一遍,代码量爆炸。
但因为一切皆 source,引擎核心只需要跟 obs_source 这一个接口打交道:
- 要渲染?对任何 source 都调它的
video_render,不管背后是摄像头还是文字; - 要做属性面板?对任何 source 都调它的
get_properties; - 要存档?把任何 source 的
obs_data序列化就行。
一套代码,通吃所有 source。 而最爽的是 —— 你哪天想给 OBS 加个全新来源(比如「股票行情滚动条」),不用改引擎一行代码:填一张 obs_source_info 表、obs_register_source 注册,引擎立刻就会渲染它、管理它、把它存进工程文件。整个系统对扩展是敞开的。
这就是「一切皆 Source」的回报,也是为什么我说它是读懂 OBS 的第一把钥匙 —— 后面你看到的 scene、filter、transition、preview、录制推流…… 全都建立在这个抽象之上。 钥匙在手,后面的门才推得开。
# 6.16 本课小结
- OBS 最核心的抽象是
obs_source:凡是「给引擎喂音视频」或「加工音视频」的东西,都是一个 source(obs-source.h:26)。 - 分四类(
obs_source_type,:33):INPUT(产数据)、FILTER(挂在源上加工)、TRANSITION(夹在两源之间)、SCENE(组合多源,自己也是 source)。 - 类型 vs 实例:
obs_source_info是蓝图(注册一份),obs_source_t是实例(想造几个造几个)—— 即面向对象的「类 vs 对象」。 - 任何 source 都填同一张「登记表」
obs_source_info(:222):id/type/output_flags+ 一堆函数指针;填好后obs_register_source注册。那堆函数指针,本质是 C 手搓的虚函数表(vtable)。 - 代码证据:
image_source(INPUT)、color_filter(FILTER)、fade_transition(TRANSITION)填同一结构体,主要差别就在.type。 - 运转机制:① 出帧两条路 —— 异步推帧(
obs_source_output_video,obs.h:1416)vs GPU 渲染(video_render,obs-source.h:351),由ASYNC标志区分;② 创建靠 id 查注册表 → 调.create(obs_source_create,obs.h:1046);③ 滤镜挂在filters[]链上(obs-internal.h:940),滤镜也是 source;④ 实例内部 =info(类型)+context(设置)+ 运行时状态。 - 这么设计的回报:引擎只认一个接口,渲染/合成/预览/属性/存档全写一套通吃;加新来源 = 填张表,不动引擎。这是读懂后续所有架构的钥匙。
- 源码深读(6.14):注册
obs_register_source_s把蓝图memcpy进引擎花名册(obs-module.c:956);创建obs_source_create_internal(obs-source.c:436)→get_source_info(id)查蓝图(:53)→source->info=*info拷 vtable →info->create(...)调插件(:492)+dosignal("source_create");每帧渲染调source->info.video_render(...)(:2809)。生(create)与死(destroy)正好镜像:都是"按 vtable 上的按钮 + 发信号"。
# 6.17 动手 / 观察(本课作业)
- 在界面里数 source:打开 OBS,点「+ 添加来源」,列表里的(图像、文本、视频采集设备、显示器采集……)全是
type = INPUT的 source;再右键某来源 →「滤镜」,你加的每个滤镜,都是type = FILTER的 source。 - 造两个实例:往场景里加两张不同的图片。它们共享同一份
image_source_info蓝图,却是两个独立的obs_source_t实例 —— 体会一下「类 vs 对象」。 - 翻一个真实的登记表:打开
plugins/image-source/image-source.c:363,对照 6.5,看image_source_info填了哪些字段、.type是什么、.output_flags点亮了哪些开关、实现了哪些回调。 - (进阶)顺一条调用链:在源码里搜
obs_source_create,看看frontend是在哪里、用什么 id 调它来创建图片源的。
# 下一课预告
我们认识了「source 是什么、怎么运转」,但还有个问题没回答:这些 source 实例,运行时怎么被创建、怎么被销毁、怎么不会用着用着就崩?一个被三个场景同时引用的摄像头源,谁来决定它什么时候该释放?(还记得 6.13 那两个 get_ref / release 吗?)
第 7 课:对象怎么生、怎么死 —— 我会带你看 OBS 怎么用引用计数和弱引用管理对象的生命周期,以及那个无处不在、藏在每个实例 context 里的设置载体 obs_data。这是 C 语言项目管理复杂对象的硬功夫,也是你写插件迟早要打交道的东西。下节课见。
📁 本课配图:
imgs/06-01~imgs/06-13📌 源码锚点:libobs/obs-source.h:26(定义)、:33(类型)、:88(标志)、:222(obs_source_info)、:351(video_render)、:564(register 宏)、libobs/obs-module.c:956(obs_register_source_s 实现)、libobs/obs-source.c:53(get_source_info)、:436(create_internal)、:492(info->create)、:509(source_create 信号)、:2809(info.video_render 调用)、libobs/obs-internal.h:806(struct obs_source)、:940(filters)、libobs/obs.h:1046(create)、:1057(release)、:1062(get_ref)、:1112(update)、:1149(filter_add)、:1416(output_video)、plugins/image-source/image-source.c:363/400、plugins/obs-filters/color-correction-filter.c:721、plugins/obs-transitions/transition-fade.c:140
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言