第 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 是一个模子刻出来的:一堆"身份字段 + 函数指针":

// 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)(...); // 想要什么像素格式
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
抓两组重点:
- 必填(★):
id、type、codec、create、destroy、encode—— 这是一个编码器的"最小骨架"。有了它们,引擎就能建它、喂它帧、收它的包、销毁它。 - 可选:
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; ...
};
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 填默认参数
}
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);
2
这一句就是"真正把 x264 建起来":调插件的 create,它内部 x264_encoder_open(...) 打开 codec,返回的私有对象存进 context.data。引擎自始至终不知道 x264 内部长啥样,只管照表调 create。 换成 NVENC,这条链一字不改,只是 info 那张表换了一张(下一节见)。
# 19.5 驱动循环:一帧进,一个编码包出
📍承上启下:建好之后就进入日常 —— 每来一帧原始画面,照 vtable 调一次 encode 吐出一个编码包,再分发给每个收包的输出。
实例建好、obs_encoder_start 订阅了帧源(add_connection → start_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
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:交给推流/录制(去封装、发出去)
}
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,
...
};
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 | ...,
...
};
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, // 要什么采样率/格式
...
};
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),它攒够一"帧"采样(framesize由get_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.create→context.data(:631)。 - 驱动:
obs_encoder_start订阅帧源 → 每帧receive_video(:1478)组装encoder_frame→do_encode→info.encode(...)(:1421) 吐出encoder_packet→send_off_encoder_packet遍历callbacks分发给每个输出(:1382)。 - 同一张表,两种编码器:x264(
obs-x264.c:850)填.encode(CPUencoder_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 动手 / 观察(本课作业)
- 看编码器下拉框:OBS →"设置 → 输出"(高级)→ 视频编码器下拉。你机器上列出的每一项(x264 / NVENC / QSV…),背后都是一张注册进
obs->encoder_types的obs_encoder_info。 - 找必填五件套:打开
plugins/obs-x264/obs-x264.c:850的obs_x264_encoder,数一数它填了哪些字段;对照 19.2,哪些是必填、哪些是可选? - 对比 CPU/GPU 那一格:把
obs-x264.c:857(.encode)和plugins/obs-nvenc/nvenc.c:1401(.encode_texture2)并排看。用一句话说清:为什么一个收"内存帧"、一个收"GPU 纹理"?(提示:第 14 课路 A vs 路 B。) - 验证"音频同表":打开
plugins/obs-ffmpeg/obs-ffmpeg-audio-encoders.c:459,确认它填的也是obs_encoder_info,且.type = OBS_ENCODER_AUDIO、多了个.get_frame_size。 - 走一遍驱动:在
libobs/obs-encoder.c找到do_encode(:1394),确认那句encoder->info.encode(...)(:1421)—— 这就是"引擎照表调编码器"的唯一那一下。
# 下一课预告
这一课我们看清了"统一接口"的骨架:一张表、一套回调,x264 和 NVENC 长得一样。可你真要选,该选哪个?软件编码(x264)和显卡编码(NVENC/QSV)在画质、性能、占用上到底差在哪?那些 preset(ultrafast~slower)、profile、tune 又是什么?
第 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)。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言