第 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
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 里这么写,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 是一个"构建期代码生成器":它读 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)
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_script_create看扩展名:.lua/.py(obs-scripting.c:245);- 起一个内嵌解释器(Lua 用
luaL_newstate,obs-scripting-lua.c:86;或 Python); - 运行你的文件(从头到尾执行一遍);
- 按名字找
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 版(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)
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 事件
2
3
这就是为什么脚本能做自动化:定时切场景、条件开录、联动热键…… 一句话:脚本 = libobs API(改源/数据)+ obs-frontend-api(控界面),合起来就能写各种自动化小工具。
# 30.8 插件 vs 脚本:什么时候用哪个?
📍做个选择:两个都基于同一套 libobs API,但场景不同。

| 插件(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_defaults、script_update(设置变了)、script_load/unload、script_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 动手 / 观察(本课作业)
- 跑一个自带脚本:OBS →"工具 → 脚本",看是否有 Python 路径设置(有的话指向你的 Python)。加载
countdown.lua(在 OBS 安装目录data/obs-plugins/frontend-tools/scripts/),给一个文字源做倒计时。 - 改一行看效果:把
countdown.lua的script_description返回的文字改掉,点"重新加载脚本",看描述立刻变 —— 体会"免编译"。 - 写一个五行脚本:新建
hello.lua,只写obs = obslua+ 一个script_description返回 "Hello",加载它,确认它出现在脚本列表。 - 理解 SWIG:用自己的话说清 —— 为什么脚本里能调
obs.obs_source_update,而没人专门为脚本写这个函数?(提示:.i里%include "obs-source.h"。) - 想个自动化:如果要写"每隔 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 等); SWIGshared/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 / 嵌入式脚本 / 语言绑定为通用编程背景知识。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言