20 / 32 编码基础与编码器实战

第 19 课:编码器的统一接口 —— obs_encoder

# 第 19 课:编码器的统一接口 —— obs_encoder

嘿,我是小方。 上一课(第 18 课)我们把编码的原理讲透了,也瞥见了 x264 那几个"旋钮"。可现实里 OBS 支持一大堆编码器:x264(软件)、NVENC(N 卡)、QSV(Intel 核显)、AMF(A 卡)…… 它们内部天差地别(有的在 CPU 上跑、有的在显卡专用电路上跑),OBS 却能用同一套代码创建、配置、驱动它们、收取编码包。这怎么做到的? 好消息是:你已经学过这一招了。还记得第 6 课的"一切皆 source"吗?—— 一张"登记表"(函数指针表)+ 一套 create/回调/destroy。编码器用的是一模一样的套路。所以这一课会很顺:我们把第 6 课的知识,平移到编码器上,顺便见识一个特别漂亮的"抽象兑现"——让软件编码和显卡编码,在 OBS 眼里长得完全一样。


# 19.1 又是熟悉的套路:登记表 + 回调

📍我们在哪:还在"编码"这一站。上一课讲透了原理,这一课看 OBS 怎么用一套统一接口装下 x264、NVENC 这些五花八门的编码器 —— 而且是你已经见过两次的老套路。

先把"为什么不慌"说清楚。你在这门课里,已经第三次见到同一个抽象了:

又是熟悉的套路

在哪 登记表 核心回调
第 6 课:源 obs_source_info create / video_render / destroy
第 11 课:图形后端 gs_exports device_create / gs_draw / …
★ 本课:编码器 obs_encoder_info create / encode / destroy

同一个抽象,第三次出现:插件填一张"函数指针表"(vtable),引擎照着表调,上层完全不管底下是谁;这些插件都靠 os_dlopen 动态加载(第 9 课)。你已经会读这种代码了。这一课把它用在编码器上,让"软件编码"和"显卡编码"在 OBS 眼里一模一样。


# 19.2 obs_encoder_info:编码器的"登记表"

📍接上一节:上一节说"又是登记表 + 回调"。这就是那张登记表 —— obs_encoder_info,和第 6 课的 obs_source_info 一个模子刻出来的。

翻开 obs_encoder_info(obs-encoder.h:197),它和第 6 课的 obs_source_info 是一个模子刻出来的:一堆"身份字段 + 函数指针":

obs_encoder_info 登记表

// libobs/obs-encoder.h:197(节选)
struct obs_encoder_info {
	const char *id;                  // 唯一标识,如 "obs_x264"
	enum obs_encoder_type type;      // OBS_ENCODER_VIDEO 还是 AUDIO
	const char *codec;               // "h264" / "aac" / "hevc" ...

	const char *(*get_name)(void *type_data);                      // 界面显示的名字
	void *(*create)(obs_data_t *settings, obs_encoder_t *encoder); // ★ 建实例
	void  (*destroy)(void *data);                                  // ★ 销毁
	bool  (*encode)(void *data, struct encoder_frame *frame,
	                struct encoder_packet *packet, bool *received_packet);  // ★ 一帧进 → 一个包出
	size_t (*get_frame_size)(void *data);   // 仅音频

	/* 以下可选 */
	void (*get_defaults)(obs_data_t *settings);   // 默认参数
	obs_properties_t *(*get_properties)(void *data); // 属性面板
	bool (*update)(void *data, obs_data_t *settings); // 改码率等
	bool (*get_extra_data)(...);   // 头信息
	bool (*get_sei_data)(...);     // SEI
	void (*get_video_info)(...);   // 想要什么像素格式
	...
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

抓两组重点:

  • 必填(★):idtypecodeccreatedestroyencode —— 这是一个编码器的"最小骨架"。有了它们,引擎就能建它、喂它帧、收它的包、销毁它。
  • 可选:get_defaults(默认码率/keyint)、get_properties(属性面板)、update(不重启就改码率)…… 不填就用默认,锦上添花。
  • 音频专属:get_frame_size(一帧音频多少个采样)——视频编码器从不填它。

和第 6 课对照:源是 create / video_render / destroy,编码器是 create / encode / destroy —— 换汤不换药,都是"填一张函数指针表"。 你在第 6 课学会读 obs_source_info,这里就是白送的。


# 19.3 一个编码器实例 = context + info(vtable) + state

📍承上启下:登记表是"图纸",照它建出来的运行期实例长什么样?还是那个"通用外壳 + vtable + 私有状态"的三件套。

创建出来的编码器实例,是 struct obs_encoder(obs-internal.h:1353)。它的剖面图,和第 6、7 课那个"源实例"完全是一个样:

编码器实例三件套

// libobs/obs-internal.h:1353(节选)
struct obs_encoder {
	struct obs_context_data context;   // ① 通用外壳:名字/设置/信号;context.data = 插件私有对象
	struct obs_encoder_info info;       // ② 从登记表拷来的一份 vtable(引擎调 encode 就查它)
	...
	DARRAY(struct encoder_callback) callbacks;  // ③ state:谁来收编码包
	void *media;                        //    帧从哪来(video_t / audio_t)
	struct obs_encoder_group *encoder_group;    //    多轨 GOP 对齐
	int64_t cur_pts;                    //    时间戳等状态
	/* 音频专属状态 */
	size_t framesize; size_t blocksize;
	struct deque audio_input_buffer[MAX_AV_PLANES];
	/* 视频专属状态 */
	uint32_t scaled_width, scaled_height; ...
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

三件套:

  • context(通用外壳):名字、设置、信号,和所有 OBS 对象共用的那套(第 7 课)。context.data 里放着插件的私有对象(x264 自己那个 struct 就住在这)。
  • info(vtable):创建时从登记表拷来的一份函数指针表。引擎每次要编码,就查这张表调 encode
  • state(状态):callbacks[](谁来收包)、media(帧从哪来)、时间戳、音/视频各自的缓冲……

"实例 = 通用外壳 + 一张 vtable + 私有状态" —— 这个模式,第 6 课的源、本课的编码器,完全一致。你理解了一个,就理解了全部。


# 19.4 创建一个编码器:按 id 找表 → 拷 vtable → 建实例

📍接上一节:实例的剖面看过了,现在走一遍它怎么被造出来 —— 按 id 找表、把 vtable 整个拷进实例、真正的插件对象懒创建。

创建流程和第 6 课的 obs_source_create 是一条链。入口是 obs_video_encoder_create(obs-encoder.c:160),它转手调内部的 create_encoder(:112):

创建流程

// libobs/obs-encoder.c:112(抽骨架)
static struct obs_encoder *create_encoder(const char *id, enum obs_encoder_type type, ...)
{
	struct obs_encoder_info *ei = find_encoder(id);   // ① 按 id 在全局登记表里找那张表
	...
	encoder = bzalloc(sizeof(struct obs_encoder));    // ② 分配实例
	...
	encoder->info = *ei;         // ③ ★ 把 vtable 整个拷进实例
	encoder->orig_info = *ei;
	...
	init_encoder(encoder, ...);  // 里面会调 info.get_defaults 填默认参数
}
1
2
3
4
5
6
7
8
9
10
11
12

find_encoder(:29)就是遍历全局的 obs->encoder_types 数组(所有插件注册进来的表),按 id(比如 "obs_x264")找到匹配那张。找到后 encoder->info = *ei 把这张 vtable 拷进实例 —— 从此这个实例就"认识"该怎么创建、编码、销毁了。

有个细节值得一提:info.create 不是在这时调的,而是懒创建——真正要开始编码时,才在 obs_encoder_initialize_internal(:631)里调:

// libobs/obs-encoder.c:631
encoder->context.data = encoder->orig_info.create(encoder->context.settings, encoder);
1
2

这一句就是"真正把 x264 建起来":调插件的 create,它内部 x264_encoder_open(...) 打开 codec,返回的私有对象存进 context.data引擎自始至终不知道 x264 内部长啥样,只管照表调 create 换成 NVENC,这条链一字不改,只是 info 那张表换了一张(下一节见)。


# 19.5 驱动循环:一帧进,一个编码包出

📍承上启下:建好之后就进入日常 —— 每来一帧原始画面,照 vtable 调一次 encode 吐出一个编码包,再分发给每个收包的输出。

实例建好、obs_encoder_start 订阅了帧源(add_connectionstart_raw_video(receive_video),obs-encoder.c:363 —— 这不就是第 14 课编码器 connect 到 video_output 那一步嘛),接下来就是每来一帧、编一个包的循环:

驱动循环

// libobs/obs-encoder.c:1478 → 1421(抽主干)
static void receive_video(void *param, struct video_data *frame)
{
	struct encoder_frame enc_frame = {0};
	for (size_t i = 0; i < MAX_AV_PLANES; i++) {   // ① 把 video_data 组装成 encoder_frame
		enc_frame.data[i]     = frame->data[i];
		enc_frame.linesize[i] = frame->linesize[i];
	}
	...
	do_encode(encoder, &enc_frame, &frame->timestamp);   // → 去编码
}

// do_encode(:1394) 里那句核心:
success = encoder->info.encode(encoder->context.data, frame, &pkt, &received);  // ★ :1421
1
2
3
4
5
6
7
8
9
10
11
12
13
14

那句 encoder->info.encode(...),就是整个编码的心脏:照 vtable 调编码器,一个 encoder_frame(第 18 课那个原始帧:data / linesize / pts)进去,一个 encoder_packet(第 18 课那个编码包:keyframe / pts / dts …)出来。引擎永远只调这一句,根本不管底下是 x264 还是 NVENC。

编好的包,由 send_off_encoder_packet(:1332)打上时间戳,然后**遍历所有"收包人"**发出去(:1382):

// libobs/obs-encoder.c:1379(节选)
for (size_t i = encoder->callbacks.num; i > 0; i--) {
	struct encoder_callback *cb = encoder->callbacks.array + (i - 1);
	send_packet(encoder, cb, pkt, ...);   // → cb->new_packet:交给推流/录制(去封装、发出去)
}
1
2
3
4
5

每个"收包人"(encoder_callback)就是一个订阅了这个编码器的输出(推流、录制)。一帧进、一个包出、分发给每个输出,循环往复 —— 这就是编码器在 OBS 里的一生。和第 16 课音频那条"订阅/回调"、第 8 课的信号系统,是同一种解耦思路。


# 19.6 同一张表,两种编码器:软件 vs 显卡

📍接上一节:驱动循环只调 info.encode 这一句 —— 那就把 x264 和 NVENC 并排放,看这套抽象最漂亮的兑现:同一张表,区别只在填哪一格函数指针。

现在见识这套抽象最漂亮的兑现。x264(软件)和 NVENC(显卡)填的是同一张 obs_encoder_info,只是 encode 那一格填法不同。

同一张表,两种编码器

x264(软件,吃 CPU 帧)(obs-x264.c:850):

// plugins/obs-x264/obs-x264.c:850
struct obs_encoder_info obs_x264_encoder = {
	.id     = "obs_x264",
	.type   = OBS_ENCODER_VIDEO,
	.codec  = "h264",
	.create = obs_x264_create,
	.encode = obs_x264_encode,          // ← 填 encode:收一个 CPU 里的 encoder_frame
	.caps   = OBS_ENCODER_CAP_DYN_BITRATE | OBS_ENCODER_CAP_ROI,
	...
};
1
2
3
4
5
6
7
8
9
10

NVENC(显卡,吃 GPU 纹理)(nvenc.c:1391):

// plugins/obs-nvenc/nvenc.c:1391
struct obs_encoder_info h264_nvenc_info = {
	.id     = "obs_nvenc_h264_tex",
	.type   = OBS_ENCODER_VIDEO,
	.codec  = "h264",                   // ← 和 x264 同一个 codec!
	.create = h264_nvenc_create,
	.encode_texture2 = d3d11_encode,    // ← 不填 encode,改填 encode_texture2:收一张 GPU 纹理
	.caps = OBS_ENCODER_CAP_PASS_TEXTURE | OBS_ENCODER_CAP_DYN_BITRATE | ...,
	...
};
1
2
3
4
5
6
7
8
9
10

天壤之别,就浓缩在两处:

  • x264 填 .encode:收一个 encoder_frame(内存里的像素)→ 走第 14 课**"路 A:下载到内存"**,在 CPU 上编码。哪台机器都能用,但吃 CPU。
  • NVENC 填 .encode_texture2:收一张 GPU 纹理(不下载到内存)→ 走第 14 课**"路 B:纹理直喂显卡"**,在显卡的专用编码电路上编。几乎不占 CPU,但要有支持的显卡。

那个 OBS_ENCODER_CAP_PASS_TEXTURE(obs-encoder.h:36)是个"能力位":它告诉引擎"这货能直接吃纹理,走 GPU 那条路,别费劲把画面下载到内存"。第 14 课那两条路(路 A 下载 / 路 B 直喂),在这里和编码器对上了 —— 引擎正是看这个 cap 位,决定给这个编码器走哪条路。

同一张表、同一句 obs_register_encoder、同一条 do_encode 驱动 —— 区别只在:填 .encode 还是 .encode_texture2 更妙的是,NVENC 自己还带个 _soft 后备版本(nvenc.c:1452),它改填 .encode 走 CPU 帧路径 —— 同一个插件里,两条路并存,靠"填哪一格函数指针"来切换。抽象之通用,一览无余。


# 19.7 音频编码器?—— 还是同一张表

📍承上启下:视频的软/硬编码统一了,那另一路音频(AAC/Opus)是另一套接口吗?不 —— 还是同一张 obs_encoder_info,引擎靠 type 字段分流。

那音频呢?视频有 x264/NVENC,音频有 AAC/Opus…… 它们是另一套接口吗?不,还是同一张 obs_encoder_info

音频编码器

看 FFmpeg 的 AAC 编码器(obs-ffmpeg-audio-encoders.c:459):

// plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c:459
struct obs_encoder_info aac_encoder_info = {
	.id      = "ffmpeg_aac",
	.type    = OBS_ENCODER_AUDIO,       // ← 不是 VIDEO,就这一处不同
	.codec   = "aac",
	.create  = aac_create,
	.encode  = enc_encode,              // 一「帧」音频进 → 一个包出
	.destroy = enc_destroy,
	.get_frame_size = enc_frame_size,   // ★ 音频专属:一帧多少个采样
	.get_audio_info = enc_audio_info,   // 要什么采样率/格式
	...
};
1
2
3
4
5
6
7
8
9
10
11
12

同一个 struct、同一句 obs_register_encoder()、同一条 do_encode 驱动循环。 引擎靠 type 字段分流:

  • type == OBS_ENCODER_VIDEO → 用 receive_video(第 14 课那条视频帧);
  • type == OBS_ENCODER_AUDIO → 用 receive_audio(obs-encoder.c:1727),它攒够一"帧"采样(framesizeget_frame_size 定)再调 encode

所以在 OBS 眼里,推流的视频轨、音频轨,是"同类对象"—— 都是一个 obs_encoder 实例,只是 type 不同、走的分支不同。一张表,收编了软件视频编码器、硬件视频编码器、各种音频编码器 —— 这就是"统一接口"的威力。


# 19.8 抽象的力量:加个新编码器,上层一行不改

📍收束:退一步看这套设计买来了什么 —— 加一款新编码器只要"填张表 + 注册一句",上层推流/录制/界面一行不改。这把钥匙能开整个 OBS 插件体系。

退一步,看这套设计到底买来了什么。

抽象的力量

想让 OBS 支持一款新编码器(比如明天出个新显卡编码)?你只需要两步:填一张 obs_encoder_info 表 + 调一句 obs_register_encoder 就这样。

  • 上层的推流、录制、界面代码,一行都不用改。 它们只跟"统一接口"(obs_encoder + create/encode/destroy)打交道,永远看不见 x264 或 NVENC 的名字
  • 界面里那个"编码器"下拉框,列出的就是所有注册进 obs->encoder_types 的表;你选哪个,create_encoder 就按那个 id 找表、建实例。

这正是第 6 课"一切皆 source"给你的那种畅快,又来了一次:源、图形后端、编码器…… 同一个"登记表 + 函数指针"抽象,撑起了整个 OBS 的可扩展性。 OBS 之所以能不断支持新硬件、新格式,而核心引擎几乎不动,靠的就是这套"把'变化'关进一张表里、把'不变'留在引擎里"的分层。

理解了它,你不仅看懂了编码器,还拿到了一把能读懂整个 OBS 插件体系的钥匙。


# 19.9 本课小结

  • 老套路第三次出现:编码器和第 6 课的源、第 11 课的图形后端一样,是"登记表(函数指针表 vtable)+ 回调"抽象;插件填表,引擎照表调,os_dlopen 动态加载。
  • obs_encoder_info(obs-encoder.h:197)= 编码器登记表:必填 id/type/codec/create/encode/destroy;可选 get_defaults/get_properties/update/get_extra_data/get_sei_data;音频专属 get_frame_size。和 obs_source_info 一个模子。
  • 实例 struct obs_encoder(obs-internal.h:1353)= context + info(vtable) + state:context.data 装插件私有对象,info 是拷来的 vtable,state 有 callbacks[](收包人)、media(帧源)等。
  • 创建:obs_video_encoder_create(:160)→ create_encoder(:112)→ find_encoder 按 id 找表 → encoder->info = *ei 拷 vtable(:133)→ get_defaults;真正建插件对象是懒创建 info.createcontext.data(:631)。
  • 驱动:obs_encoder_start 订阅帧源 → 每帧 receive_video(:1478)组装 encoder_framedo_encodeinfo.encode(...)(:1421) 吐出 encoder_packetsend_off_encoder_packet 遍历 callbacks 分发给每个输出(:1382)。
  • 同一张表,两种编码器:x264(obs-x264.c:850)填 .encode(CPU encoder_frame,第 14 课路 A);NVENC(nvenc.c:1391)填 .encode_texture2(GPU 纹理,第 14 课路 B)+ OBS_ENCODER_CAP_PASS_TEXTURE(obs-encoder.h:36)。区别只在填哪格函数指针 + 哪个 cap 位。
  • 音频同表:FFmpeg AAC(obs-ffmpeg-audio-encoders.c:459).type = OBS_ENCODER_AUDIO + .get_frame_size,同一个 struct、同一句注册、同一条 do_encode;引擎靠 type 分流 receive_video/receive_audio
  • 抽象的力量:加新编码器 = 填一张表 + 一句 obs_register_encoder,上层推流/录制/界面一行不改。

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

  1. 看编码器下拉框:OBS →"设置 → 输出"(高级)→ 视频编码器下拉。你机器上列出的每一项(x264 / NVENC / QSV…),背后都是一张注册进 obs->encoder_typesobs_encoder_info
  2. 找必填五件套:打开 plugins/obs-x264/obs-x264.c:850obs_x264_encoder,数一数它填了哪些字段;对照 19.2,哪些是必填、哪些是可选?
  3. 对比 CPU/GPU 那一格:把 obs-x264.c:857(.encode)和 plugins/obs-nvenc/nvenc.c:1401(.encode_texture2)并排看。用一句话说清:为什么一个收"内存帧"、一个收"GPU 纹理"?(提示:第 14 课路 A vs 路 B。)
  4. 验证"音频同表":打开 plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c:459,确认它填的也是 obs_encoder_info,且 .type = OBS_ENCODER_AUDIO、多了个 .get_frame_size
  5. 走一遍驱动:在 libobs/obs-encoder.c 找到 do_encode(:1394),确认那句 encoder->info.encode(...)(:1421)—— 这就是"引擎照表调编码器"的唯一那一下。

# 下一课预告

这一课我们看清了"统一接口"的骨架:一张表、一套回调,x264 和 NVENC 长得一样。可你真要选,该选哪个?软件编码(x264)和显卡编码(NVENC/QSV)在画质、性能、占用上到底差在哪?那些 preset(ultrafast~slower)、profiletune 又是什么?

第 20 课:视频编码器实战对比 —— x264 vs NVENC / QSV —— 我们钻进这两类编码器的真实差异:x264 用 CPU 换画质(preset 就是"花多少 CPU 精调"的旋钮)、NVENC 用显卡专用电路换"几乎零 CPU 占用",以及为什么直播和录制常常选不同的编码器。下节课见。


📁 本课配图:imgs/19-01 ~ imgs/19-08 📌 源码锚点: libobs/obs-encoder.h:197(struct obs_encoder_info)、:202/:205/:208(id/type/codec)、:226/:233/:247(create/destroy/encode)、:250(get_frame_size 音频)、:146(struct encoder_frame)、:35(OBS_ENCODER_CAP_* 能力位,PASS_TEXTURE=1<<1)、:43(enum obs_encoder_type); libobs/obs-module.c:1126(obs_register_encoder_s)、:911(da_push_back 进 obs->encoder_types);libobs/obs-encoder.c:29(find_encoder)、:112(create_encoder)、:133(encoder->info = *ei)、:160/:167(video/audio_encoder_create)、:631(懒调 info.create → context.data)、:363(add_connection → start_raw_video)、:1478(receive_video 组装 encoder_frame)、:1394/:1421(do_encode → info.encode)、:1382(send_off_encoder_packet 分发)、:1727(receive_audio)、:603(get_frame_size);libobs/obs-internal.h:1353(struct obs_encoder)、:1335(encoder_callback); plugins/obs-x264/obs-x264.c:850(obs_x264_encoder 表)、:857(.encode)、:632(create/x264_encoder_open)、:777(obs_x264_encode);plugins/obs-x264/obs-x264-plugin-main.c:14(obs_register_encoder); plugins/obs-nvenc/nvenc.c:1391(h264_nvenc_info 表)、:1401(.encode_texture2)、:1452(_soft 用 .encode)、:1504(register_encoders); plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c:459(ffmpeg_aac 表:type=AUDIO + get_frame_size)。

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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