31 / 32 全链路、插件与脚本扩展

第 30 课:不写 C++ 也能扩展 —— Lua / Python 脚本

# 第 30 课:不写 C++ 也能扩展 —— Lua / Python 脚本

嘿,我是小方。 上一课我们写了个 C 插件 —— 但要配编译环境、改一次编译一次,门槛不低。如果你只想快速试个想法、写个自己用的小工具、或者干脆不想碰 C++/编译,还有更轻的一条路:OBS 内置的 Lua / Python 脚本。写一个文本文件,改完点一下就生效,不用编译。 这一课我会:先看一个真脚本(OBS 自带的"倒计时"工具),再讲清它背后最关键的一件事 —— 脚本调的,就是你这门课学的那套 obs_* C API,只是换了个更轻的语言外壳。理解了这点,你写脚本几乎零学习成本。


# 30.1 又一条路:脚本 —— 不写 C++、不用编译

📍我们在哪:模块 8 第二课。上一课"写插件"是重装上阵;这一课"写脚本"是轻装快跑。

又一条路

插件(第 29 课) 脚本(本课)
语言 C,编译成 .dll Lua / Python,一个文本文件
编译 要(配环境、改一次编一次) 不要(改完点重载)
适合 发布给别人用的正式功能 自动化、小工具、快速试验、自己折腾
门槛 偏高 低到几行就能跑

这一课我们看一个真脚本:OBS 自带的**"倒计时"**工具(countdown.lua)—— 它把一个文字源变成倒计时器,几十行 Lua 就扩展了 OBS。而它的秘密,和"脚本凭什么能操作 OBS"这件事,才是本课真正要讲清的。


# 30.2 一个脚本 = 定义几个 OBS 认识的"魔法函数"

📍先建立模型:脚本不是乱写的。它约定了一批"特定名字"的函数,你定义哪个,OBS 就在合适时机调哪个。

魔法函数

你的 .lua / .py 文件里,可以定义这些名字固定的函数(定义哪几个由你,没定义的 OBS 就跳过):

  • script_description → 返回一段说明文字(加载时显示);
  • script_properties → 声明有哪些控件(滑块/下拉…),OBS 据此画界面(第 27 课);
  • script_defaults → 设默认值;
  • script_update → 用户改了设置,你来处理;
  • script_load / script_unload → 启动 / 卸载时做准备/清理;
  • script_tick → 每帧调一次(要做动画/轮询时用)。

OBS 怎么找到这些函数?运行你的文件,然后**"按名字"去全局里翻**:只要你定义了叫 script_properties 的函数,OBS 加载脚本后就会 lua_getglobal 找到它(obs-scripting-lua.c:146),存起来、到时候调。你没定义的就跳过。像做"填空题":填哪格算哪格。

这和第 6/29 课"登记表 + 回调"是同一个思想 —— 只不过这次的"回调",是你脚本里的函数。


# 30.3 走一遍真脚本:倒计时(countdown.lua)

📍上手看真代码:把上面那些魔法函数,在一个真脚本里看到。

倒计时脚本

obs = obslua                                   -- 把整套 OBS API 取个短名 obs

function script_description()                  -- ① 说明文字
	return "把文字源变成倒计时器"
end

function script_properties()                   -- ② 声明控件 → OBS 自动画界面
	local props = obs.obs_properties_create()
	obs.obs_properties_add_int(props, "duration", "时长(分)", 1, 100000, 1)
	return props
end

function script_update(settings)               -- ③ 设置变了 → 读出来
	total_seconds = obs.obs_data_get_int(settings, "duration") * 60
	obs.timer_add(timer_callback, 1000)        -- 每 1000ms 调一次 timer_callback
end

function timer_callback()                      -- ④ 每秒:改文字源的文字
	local src = obs.obs_get_source_by_name(source_name)
	obs.obs_source_update(src, settings)       -- 更新它
	obs.obs_source_release(src)
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

(对照真代码:countdown.lua:1:131:106:136。)

看最后那几行:obs.obs_get_source_by_name / obs.obs_source_update / obs.obs_source_release —— 这些函数你在第 6、15、29 课全见过!去掉 obs. 前缀,就是 libobs 的 C 函数。这不是巧合 —— 是本课的核心真相。


# 30.4 关键真相:脚本 = 用脚本语言,调你学过的 C API

📍本课的心脏:脚本不是"另一套东西",它就是 libobs 那套 C API。

脚本就是调 C API

同一个函数,C 里这么写,Lua/Python 里就加个 obs. 前缀,一一对应:

C(第 29 课的插件) 脚本(Lua / Python)
obs_source_update(src, s); obs.obs_source_update(src, s)
obs_data_get_int(s, "x"); obs.obs_data_get_int(s, "x")
obs_get_source_by_name(n); obs.obs_get_source_by_name(n)
obs_frontend_recording_start(); obs.obs_frontend_recording_start()

所以:你这门课学的每一个 obs_* 函数,在脚本里都能直接用(加 obs. 前缀)。 脚本不是"另学一套" —— 它就是 libobs 那套 C API,换个语言、换个更轻的用法而已。你读了 28 课源码攒下的 API 知识,在脚本里 100% 复用

那"C 函数怎么就能被 Lua/Python 调"呢?靠一个叫 SWIG 的工具。


# 30.5 SWIG:C 头文件,怎么变成能被 Lua/Python 调的?

📍揭开机制:没人手写几千个绑定 —— 一个工具自动生成。

SWIG

SWIG 是一个"构建期代码生成器":它读 libobs 的 C 头文件,自动生成 Lua/Python 的绑定模块。流程:

libobs 的 C 头文件(obs.h / obs-source.h / obs-frontend-api.h …)→ SWIG 读它们 → 自动生成绑定模块(obslua / obspython)。

关键就在那两个 .i 接口文件里的两类行(obslua.i):

%module obslua                 // 模块名 = obslua(所以 Lua 里 obs = obslua)
...
%include "obs.h"               // 把这些 C 头「喂」给 SWIG
%include "obs-source.h"
%include "obs-properties.h"
%include "obs-frontend-api.h"  // ← 这行让脚本能调 obs_frontend_*(30.7)
1
2
3
4
5
6

(对照:obslua.i:1:95-100:109;Python 版 obspython.i:1:117-122。)

所以"脚本能调所有 obs_*"不是有人一个个写的,是 SWIG 从头文件自动生成的。 你每读一个 .h,其实就是在读"脚本里能调的东西"的清单 —— C 和脚本,共享同一套 API 定义。(少数需要特殊处理的,如 timer_add、热键,用 %ignore 排除后手写补上,obs-scripting-lua.c:988。)


# 30.6 OBS 怎么跑你的脚本(按扩展名分派)

📍运行时:你在"工具→脚本"里加个文件,OBS 内部怎么把它跑起来。

OBS 怎么跑脚本

五步:

  1. 你加一个脚本文件(工具菜单 → 脚本 → +);
  2. obs_script_create 看扩展名:.lua / .py(obs-scripting.c:245);
  3. 起一个内嵌解释器(Lua 用 luaL_newstate,obs-scripting-lua.c:86;或 Python);
  4. 运行你的文件(从头到尾执行一遍);
  5. 按名字找 script_* 函数,在合适时机调它们(30.2)。

两个关键点:

  • obs_script_create 按文件扩展名分派:.lua 走 Lua 后端、.py 走 Python 后端;
  • "内嵌解释器"= OBS 进程里跑着一个 Lua/Python 引擎;你的脚本就在它里面执行,能直接调 SWIG 绑定好的 obs_*

"改完即生效"是怎么做到的? 点"重新加载脚本",OBS 就把解释器里旧的清掉、重新运行一遍你的文件、重新抓 script_* 函数。不用编译、不用重启 OBS —— 这就是脚本"轻"的地方。


# 30.7 Python 也一样;而且脚本能"驱动界面"

📍两点补充:换 Python 只是换个 import;而且脚本能反过来控制 OBS 界面。

Python 与驱动界面

Python 版(url-text.py)和 Lua 版长得几乎一样,只是开头 import 一下:

import obspython as obs                         # 取短名 obs(对应 %module obspython)

def script_update(settings):                    # 一样的魔法函数
	url = obs.obs_data_get_string(settings, "url")
	obs.timer_add(update_text, interval * 1000) # 一样的定时器

def update_text():
	src = obs.obs_get_source_by_name(name)      # 一样的 obs_ 函数
	obs.obs_source_update(src, settings)
1
2
3
4
5
6
7
8
9

(对照:url-text.py:1:42:16。)Lua 和 Python 只是"宿主语言"不同,调的是同一套绑定 API —— 你会一个,就会另一个。

而且,脚本还能"指挥 OBS 界面":因为 .i 文件里也 %include "obs-frontend-api.h"(第 27 课那套稳定接口),所以脚本能调 obs.obs_frontend_*:

obs.obs_frontend_set_current_scene(scene)   -- 切场景
obs.obs_frontend_recording_start()          -- 开始录制
obs.obs_frontend_add_event_callback(on_event)  -- 听 OBS 事件
1
2
3

这就是为什么脚本能做自动化:定时切场景、条件开录、联动热键…… 一句话:脚本 = libobs API(改源/数据)+ obs-frontend-api(控界面),合起来就能写各种自动化小工具。


# 30.8 插件 vs 脚本:什么时候用哪个?

📍做个选择:两个都基于同一套 libobs API,但场景不同。

插件 vs 脚本

插件(C,第 29 课) 脚本(Lua/Python,本课)
语言 / 交付 C,编译成 .dll Lua/Python,一个文本文件
要编译吗 要(配环境、改一次编一次) 不要(改完点重载)
能力上限 全功能:新源 / 编码器 / 输出… 受绑定限制(大部分 obs_* 都有)
速度 原生,快 解释执行,略慢(但够用)
最适合 发布给别人用的正式功能 自己的自动化、小工具、快速试验

口诀:快速试个想法 / 自己用 → 脚本;要做成正式功能、发给别人 → 插件。 两个都基于同一套 libobs API,不是谁取代谁,而是各有各的场景。


# 30.9 全景回顾:脚本这条路

全景回顾

写一个 .lua/.py(定义 script_* 魔法函数)→ 放进脚本目录(工具→脚本→加载)→ 调 SWIG 绑定的 obs.obs_*(= libobs C API)+ obs_frontend_*(控界面)→ 改完点重载即生效。

模块 8 进度:29 写第一个插件(C 滤镜)→ 30 脚本(Lua/Python)→ 31 接下来去哪(全课收官)。

带走一句话:脚本不是"另学一套",而是"用更轻的方式,调你已经学会的 libobs API"。 读懂了 OBS 的 C API(这门课干的事),你就同时具备了"写插件"和"写脚本"两种扩展能力。


# 30.10 本课小结

  • 脚本 = 轻量扩展:写 .lua/.py 文本文件,不编译,改完点"重载"即生效。适合自动化、小工具、快速试验、自己用。
  • 魔法函数:脚本定义一批"固定名字"的函数,OBS 运行你的文件后按名字去找并在合适时机调(lua_getglobal,obs-scripting-lua.c:146):script_description(说明)、script_properties(控件→自动画界面,第 27 课)、script_defaultsscript_update(设置变了)、script_load/unloadscript_tick(每帧)。定义哪个用哪个。同"登记表+回调"思想。
  • 核心真相:脚本 = 调 libobs C API:每个 obs_* 函数在脚本里加 obs. 前缀即可用(obs.obs_source_update = C 的 obs_source_update)。这门课学的 API 全部复用。
  • SWIG:构建期代码生成器,读 libobs 的 C 头文件(.i%include "obs.h" 等,obslua.i:95)自动生成 Lua/Python 绑定(%module obslua,:1)。所以"能调所有 obs_*"是自动生成的,C 和脚本共享同一套 API 定义。少数特例(timer/hotkey)%ignore+手写补(obs-scripting-lua.c:988)。
  • 运行:obs_script_create 按扩展名分派(.lua/.py,obs-scripting.c:245)→ 起内嵌解释器(luaL_newstate)→ 运行文件 → 抓 script_*。重载 = 清掉重跑,不用编译/重启。
  • Python 同理 + 驱动界面:import obspython as obs,同一套 API;.i%include "obs-frontend-api.h"(obslua.i:109)→ 脚本能调 obs.obs_frontend_*(切场景/开录/听事件,第 27 课)→ 这是自动化的基础。
  • 插件 vs 脚本:插件(C/编译/全功能/发布)vs 脚本(Lua-Py/免编译/受绑定限制/自己用)。口诀:试想法/自用→脚本;正式功能/发布→插件。同一套 libobs API。

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

  1. 跑一个自带脚本:OBS →"工具 → 脚本",看是否有 Python 路径设置(有的话指向你的 Python)。加载 countdown.lua(在 OBS 安装目录 data/obs-plugins/frontend-tools/scripts/),给一个文字源做倒计时。
  2. 改一行看效果:把 countdown.luascript_description 返回的文字改掉,点"重新加载脚本",看描述立刻变 —— 体会"免编译"。
  3. 写一个五行脚本:新建 hello.lua,只写 obs = obslua + 一个 script_description 返回 "Hello",加载它,确认它出现在脚本列表。
  4. 理解 SWIG:用自己的话说清 —— 为什么脚本里能调 obs.obs_source_update,而没人专门为脚本写这个函数?(提示:.i%include "obs-source.h"。)
  5. 想个自动化:如果要写"每隔 5 分钟自动切一次场景"的脚本,你会用哪几个函数?(提示:obs.timer_add + obs.obs_frontend_set_current_scene。)

# 下一课预告

到这里,你能懂 OBS,也能用插件和脚本它。最后一课,给你"靠自己继续走下去"的方法和地图:怎么高效地读源码(那把万能钥匙)、怎么调试(日志是第一现场)、我们这门课没讲到的进阶主题都在源码的哪里、以及去哪找答案。

第 31 课:接下来去哪 —— 调试、读源码方法、进阶主题地图 —— 模块 8、也是整门课的最后一课。我们不学新概念,而是把"如何继续自学 OBS 和音视频"这件事讲透,给你一张能一直用下去的地图。下节课见。


📁 本课配图:imgs/30-01 ~ imgs/30-09 📌 源码锚点: 脚本核心 shared/obs-scripting/obs-scripting.c:132(obs_scripting_load)、:245(obs_script_create 按扩展名分派)、:71(支持的扩展名);obs-scripting.h:57-60(obs_script_get_properties/save/update); Lua 加载/魔法函数 shared/obs-scripting/obs-scripting-lua.c:86(luaL_newstate 内嵌解释器)、:125/:132(loadbuffer/pcall 运行脚本)、:146(找 script_properties)、:152(script_update)、:164(script_defaults)、:175(script_description)、:188(script_load)、:201(script_tick)、:988(add_hook_functions 手写补 timer_add 等); SWIG shared/obs-scripting/obslua/obslua.i:1(%module obslua)、:95-100(%include obs.h 等)、:109(%include obs-frontend-api.h);obspython/obspython.i:1(%module obspython)、:117-122:131; 真脚本 frontend/plugins/frontend-tools/data/scripts/countdown.lua:1(obs=obslua)、:131(script_description)、:106(script_properties/add_int)、:136(script_update/obs_data_get_int)、:58(timer_add)、:172(hotkey/signal);url-text.py:1(import obspython as obs)、:42(script_update)、:16(obs_get_source_by_name); 脚本对话框 frontend/plugins/frontend-tools/scripts.cpp:633(obs_scripting_load)、:342(obs_script_create)、:636(obs_frontend_add_tools_menu_qaction)。 注:SWIG / 嵌入式脚本 / 语言绑定为通用编程背景知识。

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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