26 / 32 封装、推流与前端界面

第 25 课:直播平台是怎么接入的 —— obs_service

# 第 25 课:直播平台是怎么接入的 —— obs_service

嘿,我是小方。 上一课结尾,rtmp_output 从一个 obs_service 拿到了"服务器地址 + 推流码"。这一课就讲这个 obs_service —— 它怎么替你"记住"成百上千个直播平台的服务器、码率上限、编码限制,甚至一键登录自动填推流码。 这一课有一个特别漂亮的设计思想,我会重点讲:平台信息是"数据"不是"代码" —— OBS 支持约 84 个平台,却不是写了 84 份代码,而是一份代码 + 一个 JSON 文件。理解了这一点,你会对"怎么优雅地支持超多平台"有新认识。这也是模块 6(打包与送出)的收官课,末尾我会把这一路的抽象串起来复盘。


# 25.1 先看现象:选个"Twitch",怎么就全知道了?

📍我们在哪:上一课 rtmp_output 说"从 obs_service 拿地址 + 推流码"。这一课就来揭开这个 obs_service —— 先看它带来的"魔法"现象。

选平台的现象

你在 OBS"设置 → 直播"里,**服务(平台)**下拉选"Twitch",然后神奇的事发生了:

  • 服务器下拉里自动出现一堆可选(香港、美西……),地址 rtmp://live-hkg… 你根本不用查;
  • 界面提示该平台建议码率 ≤ 6000、关键帧 2 秒、只支持 H.264;
  • 换个平台(比如 B 站),这些又全变;
  • 有的平台甚至能**"连接账号"一键登录**,推流码自动填好。

它凭什么什么都知道? 服务器地址、码率上限、编码限制,它怎么就门儿清?换平台又怎么全跟着变?

答案就是这一课的主角 obs_service —— 你在下拉里选的那个平台,背后是一个 obs_service 对象,它替你记住了这个平台的一切。(选"自定义"则相反:服务器、推流码全靠你自己填,也没有任何建议 —— 25.5 讲。)

而它这份"记性"的秘密,藏在一个 JSON 文件里。


# 25.2 关键洞察:平台信息是"数据",不是"代码"

📍这一课最重要的一句话:先别急着看代码。先理解这个设计思想,后面一切都顺了。

数据 vs 代码

OBS 支持约 84 个直播平台。假如每个平台写一份代码(Twitch.cYouTube.cB站.c……),会怎样?

  • 每加一个平台、每改一次服务器地址,都要改代码、重新编译、发新版 OBS;
  • 平台一多就爆炸,根本维护不过来。行不通。

OBS 的做法:一份代码 + 一份数据。

  • 一份代码 = rtmp_common(一个 obs_service 类型)。它只会一件事:读 JSON、照着回答
  • 一份数据 = services.json。里面是 84 个平台的清单:每个平台的服务器地址、码率上限、编码要求……全是数据
  • 加平台 / 改地址 = 改这个 JSON 文件,不用改代码、不用重新编译(甚至能联网自动更新,25.7)。

这就是"数据驱动(data-driven)":把"会变的东西(平台清单)"抽成数据,"不变的逻辑(读清单)"才是代码。 这是软件设计里非常常见、非常有用的一招 —— 记住它,你以后设计"要支持很多种类似东西"的系统时,都该想想能不能这么干。

下面先看这份"数据"长什么样。


# 25.3 services.json 长什么样(一个平台 = 一段数据)

📍接上一节:既然平台是数据,那就直接看数据。约 84 个平台、3594 行,全是同一种条目 —— 看懂一条,就看懂了全部。

services.json 结构

以 Twitch 那一条为例(plugins/rtmp-services/data/services.json:5):

{
  "name": "Twitch",                          // 平台名(下拉里显示的)
  "servers": [                               // 服务器列表(多个区域)
    { "name": "Asia: Hong Kong",
      "url": "rtmp://live-hkg.twitch.tv/app" },   // ← 地址在这
    { "name": "US West", "url": "..." }
  ],
  "recommended": {                           // 推荐 / 限制
    "keyint": 2,                             // 关键帧 2 秒
    "max video bitrate": 6000,               // 码率上限
    "max audio bitrate": 320
  },
  "supported video codecs": ["h264"]         // 只支持 H.264
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

每个字段,回答了 25.1 那些"它怎么知道"的问题:

  • servers → 服务器下拉框 + 地址(第 24 课那个 rtmp:// 地址,就来自这里的 servers[].url);
  • max video bitrate → 码率上限(超了就警告/夹住);
  • keyint → 强制关键帧间隔;
  • supported video codecs → 允许的编码(x264 输出的得是这些之一)。

整个文件开头有个 "format_version": 5(:3),84 个平台条目全长这样。平台的一切知识,就是这么一段段 JSON。

现在看"读这份数据"的那份代码 —— obs_service


# 25.4 obs_service:又见那个熟悉的"vtable + 回调"

📍进代码了:读 JSON、回答问题的,是一个 obs_service。而它的结构,你已经见过四次了。

obs_service 登记表

// libobs/obs-service.h:47(节选)
struct obs_service_info {
	const char *id;         // "rtmp_common" / "rtmp_custom"
	void *(*create)(obs_data_t *settings, obs_service_t *service);
	void  (*destroy)(void *data);
	...
	const char *(*get_connect_info)(void *data, uint32_t type);      // ★ 给地址 / 推流码(第 24 课)
	void (*get_max_bitrate)(void *data, int *video_bitrate, int *audio_bitrate);  // 码率上限
	const char **(*get_supported_video_codecs)(void *data);          // 支持哪些编码
	void (*get_max_fps)(void *data, int *fps);                       // 帧率上限
	void (*apply_encoder_settings)(void *data, obs_data_t *v, obs_data_t *a);  // ★ 把限制写进编码器
	bool (*can_try_to_connect)(void *data);                          // 现在能连吗(校验)
};
1
2
3
4
5
6
7
8
9
10
11
12
13

又是一张登记表 + 一套回调,套路和前面完全一样(找 id → 拷 vtable → info.create 建实例 → 引擎照表调;obs-service.c:75/:91/:92)。只是它的回调特别"懂行"——全是"这个平台的知识":地址、码率上限、支持的编码、要不要强制某些参数……

🗒️ 一点如实说明(避免你被我早先的说法误导):用 obs_register_* 登记的其实是 4 类:源(第 6 课)、输出(第 22 课)、编码器(第 19 课)、服务(本课),它们分别在 obs-module.c:956 / :1061 / :1126 / :1168 注册。图形后端(第 11 课)用的是"同一个思路",但走的是 os_dlopen 动态加载,不算 obs_register 登记表。 所以严格说"登记表"有 4 张;但"vtable + 回调"这个套路,你已经见过五处了(源/图形后端/编码器/输出/服务)—— 这才是贯穿全课的那把万能钥匙。

代码里的 obs_service 类型,其实只有两种。


# 25.5 两条路:选平台(rtmp_common)vs 自定义(rtmp_custom)

📍接上一节:登记表要有人来填。填它的,代码里只有两个 —— 对应你在"服务"下拉里的两种选择。

两条路

rtmp-services 插件在 obs_module_load 里,只注册了两个 obs_service 类型(rtmp-services-main.c:125):

// plugins/rtmp-services/rtmp-services-main.c:125
obs_register_service(&rtmp_common_service);   // 选平台(读 JSON)
obs_register_service(&rtmp_custom_service);   // 自定义(自己填)
1
2
3
  • 选平台 → rtmp_common(rtmp-common.c:1160):你从下拉里选"Twitch / B 站 / …",它去 services.json按名字找到这个平台(find_service,:607),读出服务器地址、码率上限、编码要求……地址自动填、码率超了会提醒/夹住、编码限制自动应用,省心、不易配错。覆盖约 84 个主流平台。
  • 自定义 → rtmp_custom(rtmp-custom.c:179):你自己填一个服务器地址 + 推流码,它只是原样存下来(rtmp-custom.c:25)——不读 JSON、没有任何码率建议 / 编码限制。用在:JSON 里没有的平台、自建服务器(nginx-rtmp)、SRT/RIST 等高级场景。"你是高手,自己负责"的路。

两者都是 obs_service,填的是同一张登记表,只是 .id 不同、回调实现不同。 而对 rtmp_output 来说毫无区别:它照样调 obs_service_get_connect_info 拿"地址 + 推流码"(第 24 课)—— 这就是抽象的好处,上层不关心你选的是哪种服务。

rtmp_common 从 JSON 读到的信息,具体怎么"送"到各个地方?


# 25.6 平台的"知识",怎么送到该去的地方

📍接上一节:一个 obs_service 实例读了 JSON,它读到的东西不止给输出用,而是同时喂给四处。

信息怎么到达

一个 obs_service 实例(你选的那个平台),同时给四个地方供货:

  • ① 给输出(rtmp_output,第 24 课):get_connect_info地址 + 推流码(obs-service.c:448 转调 info.get_connect_info;rtmp_common 里从 JSON 的 servers[].url 取,rtmp-common.c:824)。
  • ② 给前端界面:get_max_bitrate(:402,读 JSON 的 max video bitrate,rtmp-common.c:958)、get_supported_video_codecs(:416,读 supported video codecs,rtmp-common.c:1019)→ 界面据此警告"码率太高"、限制编码下拉
  • ③ 给编码器设置:apply_encoder_settings(:283)→ rtmp_common_apply_settings(rtmp-common.c:813)强制 keyint、强制 CBR、把码率夹到上限以内(apply_video_encoder_settings,:721)。这就是"选了 Twitch,你的编码器参数自动被规整"的真相。
  • ④ 给开播前校验:can_try_to_connect(:458)+ initialize(:271)→ 在 obs_output_start(obs-output.c:396)真正连接之前校验(:403):不满足就不让开播

所以"选一个平台"= 让一个 obs_service 实例,同时给输出"地址"、给界面"限制"、给编码器"参数"、给开播"校验"。 libobs 里全是一行行 obs_service_get_xxx 包装函数,内部就转调 info.get_xxx 那个回调(obs-service.c)——又是我们熟悉的"引擎照表调"。


# 25.7 services.json 从哪来?会自动更新吗?

📍补一个实际问题:既然平台是数据,那这份数据从哪来、会不会过时?这正是"数据驱动"最大的好处所在。

JSON 自动更新

有两份 services.json,一份保底、一份保鲜:

  • ① 自带版(bundled):OBS 安装包里就带一份 services.json(obs_module_file("services.json"))。保证:断网也能用、刚装好就有平台列表
  • ② 缓存版(自动更新):联网时,OBS 后台下载更新的 services.json 存到缓存(obs_module_config_path(...);更新逻辑 rtmp-services-main.c:107,会校验 format_version=5 和版本号)。
  • ③ 用哪个?:优先用"缓存版"(更新),没有 / 校验失败,才回退到"自带版"(open_services_file,rtmp-common.c:379)。

为什么要搞这么一套? 直播平台经常变:换服务器地址、调整码率上限、新平台上线、老平台下线……如果这些写在代码里,你就得等 OBS 发新版才能用 —— 太慢。做成可自动更新的数据文件:平台变了,OBS 后台悄悄拉一份新 JSON,你无感就用上了。

这正是"数据驱动(25.2)"的最大好处:会变的部分能独立更新,不牵动程序本身。


# 25.8 "一键登录":OAuth 帮你把推流码填好

📍补最后一块:25.1 提到有的平台能"一键登录"。这是前端的一层便利,顺带讲清,你就把 25.1 的现象全解释完了。

一键登录 OAuth

有些平台(Twitch / YouTube / Restream)支持**"连接账号"**,不用你手动去平台后台复制推流码:

  1. 点"连接账号"(OBS 直播设置里);
  2. 弹出浏览器,去平台登录 + 授权(OAuth 授权页);
  3. 平台发回令牌,OBS 拿到一个临时凭证 / 推流码;
  4. 自动填进 service:obs_service_update 把它写进 service 的 "key" 设置。

这段是**前端(Qt)**的事:基类 Auth / OAuthStreamKey(frontend/oauth/),子类 TwitchAuth / YoutubeAuth / RestreamAuth;OnStreamConfig() 把令牌 obs_data_set_string(settings, "key", …)obs_service_update(OAuth.cpp:196)。

闭环:OAuth 把推流码写进 service 的 "key" 设置 → 开播时,rtmp_output 照样用 obs_service_get_connect_info(…, STREAM_KEY) 把它读出来(第 24 课)。所以"一键登录"只是"帮你填推流码"的前端便利,底层推流那套一点没变。

到这,25.1 那些"魔法"就全解释清楚了:地址来自 JSON、限制来自 JSON、一键登录来自 OAuth,而串起这一切的,是 obs_service


# 25.9 模块 6 收官:打包与送出,全讲完了

模块 6 收官

从"两条编码流"到"文件 / 直播间",模块 6 这一路的抽象都认全了:

  • 第 22 课:容器(盒子)+ obs_output(统一输出接口);
  • 第 23 课:录制到文件(libavformat 三部曲);
  • 第 24 课:推流 RTMP(握手 + 丢帧保命);
  • 第 25 课:直播平台接入(obs_service + JSON 数据驱动)。

而站得更高看,这门课贯穿始终的一个模式 —— "vtable(登记表)+ 回调" —— 你已经见过五次了:

抽象 API 管什么
第 6 课 obs_source 一切输入/滤镜/转场
图形后端 第 11 课 gs_exports D3D11 / OpenGL / Metal
编码器 第 19 课 obs_encoder x264 / NVENC
输出 第 22 课 obs_output 录制 / 推流
服务 本课 obs_service 直播平台

带走一句话:认识了这个"登记表 + 回调"模式,你就有了读懂 OBS 任何子系统的万能钥匙。 遇到没讲过的子系统,先找它的 xxx_info 结构体、找注册它的地方、找引擎在哪调这些回调 —— 十有八九就是这套。

音视频的"原理 + OBS 实现"这条主线,到这里就打通了。 下一模块(模块 7)换个视角:从"界面"回看全局,再完整追踪一帧画面的一生,把整条流水线串成一个故事。


# 25.10 本课小结

  • 现象:选个平台 → 服务器地址自动填、码率/关键帧/编码限制自动提示、甚至一键登录。背后是一个 obs_service 对象替你"记住"了这个平台。
  • 关键洞察(数据驱动):约 84 个平台不是 84 份代码,而是一份代码(rtmp_common)+ 一份数据(services.json)。加平台/改地址 = 改 JSON,不用改代码。"会变的抽成数据,不变的逻辑才是代码。"
  • services.json:每个平台一段 JSON —— name + servers[](name+url)+ recommended{keyint, max video bitrate, max audio bitrate} + supported video codecsformat_version: 5,约 84 个平台。第 24 课的服务器地址就来自 servers[].url
  • obs_service = 熟悉的 vtable + 回调(obs-service.h:47):id/create/destroy + 一堆"平台知识"回调 get_connect_info(地址+推流码)、get_max_bitrateget_supported_video_codecsapply_encoder_settings(把限制写进编码器)、can_try_to_connect(校验)。用 obs_register_ 登记的是 4 类(源/输出/编码器/服务);"vtable+回调"这个套路则出现了五处(加上图形后端,它走 os_dlopen)。*
  • 两个服务类型:rtmp_common(rtmp-common.c:1160,选平台→读 JSON→自动填/限制)vs rtmp_custom(rtmp-custom.c:179,自定义→原样存 URL+key→无限制,高级/自建/SRT 场景)。都注册于 rtmp-services-main.c:125。对 rtmp_output 无区别(照调 get_connect_info)。
  • 信息流向:一个 obs_service 实例同时供货四处 —— 输出(get_connect_info→地址/key)、界面(get_max_bitrate/get_supported_video_codecs→警告/限制)、编码器(apply_encoder_settings→强制 keyint/CBR/夹码率,rtmp-common.c:721)、开播校验(can_try_to_connect+initialize,obs-output.c:403)。libobs 的 obs_service_get_xxx 包装 → 转调 info.get_xxx
  • JSON 来源 + 自动更新:自带版(obs_module_file,保底)+ 缓存版(obs_module_config_path,联网自动下载更新,校验 format_version);优先缓存、回退自带(open_services_file,rtmp-common.c:379)。好处:平台变了不用等 OBS 发新版。
  • OAuth 一键登录:前端(frontend/oauth/,TwitchAuth/YoutubeAuth/RestreamAuth)走 OAuth 授权 → OnStreamConfig 把推流码写进 service 的 "key"(OAuth.cpp:196)→ 开播时 rtmp_output 照常读出。只是"帮你填推流码"的便利。
  • 模块 6 完。五处"vtable + 回调":源(6)/图形后端(11)/编码器(19)/输出(22)/服务(25)。

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

  1. 找到 services.json:在你电脑的 OBS 安装目录里找到 data/obs-plugins/rtmp-services/services.json,搜索你常用的平台(如 "Bilibili"),看它的 serversrecommended
  2. 理解数据驱动:用自己的话说清 —— 为什么 OBS 支持几百个平台却只有两个 service 类型的代码?"数据驱动"好在哪?
  3. 对比 common vs custom:在 OBS 直播设置里,分别选一个平台和"自定义",观察"服务器"和"码率提示"的区别。什么时候该用"自定义"?
  4. 观察码率被夹:选一个有码率上限的平台(如 Twitch 6000),把视频码率设成 20000,看 OBS 是否提示/限制(对应 apply_encoder_settings 的夹取)。
  5. 翻源码:打开 plugins/rtmp-services/rtmp-services-main.c:125 看两个 obs_register_service;再到 rtmp-common.c:1160rtmp_common_service 那张表;最后到 rtmp-common.c:958get_max_bitrate 怎么从 JSON 读 max video bitrate

# 下一课预告

到这里,整条引擎流水线(采集 → 合成 → 编码 → 封装 → 输出)你都走完了。但有一个视角我们一直"从底往上"看,还没"从上往下"看过:用户点的那些按钮、拖的那些滑块,是怎么驱动这台引擎的? 那个你天天在用的界面,又是怎么搭起来的?

第 26 课:界面是怎么搭起来的 —— OBSAppOBSBasic —— 我们进入模块 7:把它们串起来,从前端(C++/Qt6)的视角回看全局:程序入口 OBSApp、主窗口 OBSBasic、以及界面和 libobs 引擎之间那道桥。下节课见。


📁 本课配图:imgs/25-01 ~ imgs/25-09 📌 源码锚点: libobs/obs-service.h:47(struct obs_service_info)、:104(get_connect_info)、:96(get_max_bitrate)、:98(get_supported_video_codecs)、:84(apply_encoder_settings)、:106(can_try_to_connect)、:37-45(OBS_SERVICE_CONNECT_INFO_* 枚举); libobs/obs-module.c:1168(obs_register_service_s;另 3 张登记表 :956 源 / :1061 输出 / :1126 编码器);libobs/obs-internal.h:1474(struct obs_service);libobs/obs-service.c:103(obs_service_create)、:91/:92(拷 vtable + info.create)、:448/:455(obs_service_get_connect_info)、:402(get_max_bitrate)、:416(get_supported_video_codecs)、:283(apply_encoder_settings)、:271(initialize)、:458(can_try_to_connect);libobs/obs-output.c:1148/:1158(set_service)、:1162(get_service)、:396/:403(start 前校验 can_try_to_connect + initialize); plugins/rtmp-services/rtmp-services-main.c:93(obs_module_load)、:125-126(注册两个 service)、:107(自动更新)、:36(confirm_service_file 校验);rtmp-common.c:1160(rtmp_common_service 表)、:379(open_services_file 缓存优先)、:332(open_json_file + format_version 校验)、:607(find_service 按名字匹配)、:824(url 来自 JSON server)、:958(get_max_bitrate 读 JSON)、:1019(get_supported_video_codecs 读 JSON)、:813/:721(apply_settings → 强制 keyint/CBR/夹码率);rtmp-custom.c:179(rtmp_custom_service)、:25(原样存 server+key); plugins/rtmp-services/data/services.json:3(format_version 5)、:5(Twitch 条目)、:198(recommended);data/package.json(更新 url + 版本); 前端 OAuth:frontend/oauth/Auth.hpp:5OAuth.hpp:41(OAuthStreamKey)、TwitchAuth.hpp/YoutubeAuth.hpp/RestreamAuth.hppOAuth.cpp:196(OnStreamConfig 写入 key)。 注:"数据驱动"设计思想、OAuth 流程为通用背景知识,平台清单为 OBS 数据文件内容。

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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