7 / 32 Source、对象、插件与渲染

第 6 课:一切皆 Source(源)

# 第 6 课:一切皆 Source(源)

嘿,我是小方。 从这一课起,咱们进入 模块 2:OBS 的世界观。前面五课讲的是「音视频本身」的知识;从现在开始,我们要真正钻进这套源码,去理解 OBS 自己是怎么把整个系统组织起来的。 而这一课,是整门课最关键的一课。我不夸张 —— 如果你只能记住这门课的一个概念,就记住今天这个。它是你读懂 OBS 全部源码的第一把钥匙:一切皆 Source。 这一课内容比较厚,我会从「是什么」讲到「怎么运转」,你跟着我一节一节来,别急。


# 6.1 先给你看个「意外」

📍我们在哪:模块 2 第一站,先抛一个反直觉的现象——摄像头、文字、滤镜、转场竟是同一种东西,把「一切皆 source」这把总钥匙的门缝先撬开。

我先不解释,直接上图。下面这些东西 —— 摄像头、屏幕采集、一张图片、一行文字、一个绿幕抠像滤镜、一个淡入淡出的转场 —— 你觉得它们有什么共同点?

一切皆 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 喂数据」或者「加工数据」的模块。
1
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,       // 场景
};
1
2
3
4
5
6
7

Source 的四种类型

挨个说:

  • 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:

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);       // 设置变了怎么办
	...
};
1
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);
1

引擎就知道怎么创建它、渲染它、管理它了 —— 至于它内部到底是摄像头还是一行文字,引擎根本不关心。这就是「接口与实现分离」最经典的样子。

给你个更接地气的比方: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)  // 我能响应鼠标键盘
1
2
3
4
5
6

用的时候,用按位或 | 把需要的开关「点亮」组合起来。看几个例子:

能力标志 output_flags

  • 一张图片:只有画面,没声音 → 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,           // ← 这里又不一样
	...
};
1
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);
1
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 出来的」太抽象。咱们把「往场景里加一张图片」这件事,从你点鼠标开始,一步步追到底:

添加来源全流程

  1. 你在界面点「+ 图像」,选好图片路径、点确定;
  2. 界面调引擎的创建函数 obs_source_create("image_source", "我的logo", settings, …)(obs.h:1046)—— 注意第一个参数,正是那张蓝图的 id;
  3. 引擎拿着 id 去注册表里查 —— 找到当初 obs_register_source 注册进来的那张 image_source_info 蓝图;
  4. 引擎调用蓝图里的 .create 回调,让插件分配这个实例的私有数据(读图、建纹理……);
  5. 一个 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;   // 我身上挂的滤镜列表
1
2

你加一个滤镜,本质就是调:

// libobs/obs.h:1149
obs_source_filter_add(source, filter);   // 把 filter 挂到 source 上
1
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:806struct obs_source):

实例内部

一个实例,主要装着这几样:

  • context —— 共享底座。所有 obs 对象(source、output、encoder…)都有的一块,装着这个实例的设置(obs_data)、名字、信号。下一课的 obs_data 就藏在这。
  • info —— 那张蓝图的副本。也就是说,实例自己揣着一份它所属「类型」的全部回调(vtable)。引擎调 source->info.video_render(...),就是从这取的函数指针。
  • 运行时状态 —— active(是否激活)、showing(是否可见)、enabledvolume(音量)、muted……
  • 异步帧缓冲 —— cur_async_frameasync_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_defaultssave / load(存档读档);
  • 渲染:video_tick(每帧更新状态)、video_render(画出来);
  • 滤镜专属:filter_video / filter_audio(只有滤镜类型才用);
  • 显示状态:activate / deactivateshow / 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
1
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);                        // ★ 把整张蓝图(含全部函数指针)拷进引擎
	...
}
1
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;                             // 没这号源
}
1
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;                                                  // ⑧ 返回崭新实例
}
1
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 里的「渲染」按钮
1

又是 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 动手 / 观察(本课作业)

  1. 在界面里数 source:打开 OBS,点「+ 添加来源」,列表里的(图像、文本、视频采集设备、显示器采集……)全是 type = INPUT 的 source;再右键某来源 →「滤镜」,你加的每个滤镜,都是 type = FILTER 的 source。
  2. 造两个实例:往场景里加两张不同的图片。它们共享同一份 image_source_info 蓝图,却是两个独立的 obs_source_t 实例 —— 体会一下「类 vs 对象」。
  3. 翻一个真实的登记表:打开 plugins/image-source/image-source.c:363,对照 6.5,看 image_source_info 填了哪些字段、.type 是什么、.output_flags 点亮了哪些开关、实现了哪些回调。
  4. (进阶)顺一条调用链:在源码里搜 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/400plugins/obs-filters/color-correction-filter.c:721plugins/obs-transitions/transition-fade.c:140

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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