恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
OBS Studio 脚本系统实战指南:Python 与 Lua 脚本 API、生命周期函数与底层实现解析
首页
资讯中心
/
OBS Studio 脚本系统实战指南:Python 与 Lua 脚本 API、生命周期函数与底层实现解析
OBS Studio 脚本系统实战指南:Python 与 Lua 脚本 API、生命周期函数与底层实现解析
发布时间:2026/9/5 20:21:02
OBS Studio 脚本系统实战指南Python 与 Lua 脚本 API、生命周期函数与底层实现解析【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studioOBS Studio 从 21.0 版本开始内置了完整的脚本子系统支持 Python 3 和 Luajit 2约等同于 Lua 5.2允许用户在不编译任何原生插件模块的情况下扩展功能、添加特性或自动化操作。本文以仓库文档 scripting.rst 为骨架结合 shared/obs-scripting 中的 C 层实现与 plugins/frontend-tools/data/scripts 中的官方示例脚本完整讲解脚本的生命周期函数、定时器机制、Lua 专属的源码注册能力以及与 C API 的回调差异——读完后你可以独立编写可热加载/热重载的 OBS 脚本并理解其背后逐帧调度、回调管理与引用释放的实现细节。脚本入口与运行环境脚本功能在 OBS Studio 的Tools工具菜单 → Scripts脚本中访问会弹出脚本对话框。该对话框允许在程序运行期间实时添加、删除和重载脚本对应前端工具插件中的 Add Scripts / Remove Scripts / Reload Scripts 按钮本地化条目见 az-AZ.ini。脚本能力由frontend-tools插件集成其构建入口直接挂载了脚本子系统CMakeLists.txt 中有一行add_subdirectory(${CMAKE_SOURCE_DIR}/shared/obs-scripting ...)即 shared/obs-scripting/CMakeLists.txt 定义的脚本模块。注意事项原文档 NOTE 的完整保留所有 API 绑定通过 Python 的obspython模块和 Lua 的obslua模块提供在 Windows 或 macOS 上使用 Python 时必须下载并安装与 OBS 架构匹配的 Python 版本然后在脚本对话框的 Python Settings 标签页中设置 Python 安装路径。源码中同样体现了这一约束obs-scripting.c 的obs_scripting_load()中只有非 Windows 且非 macOS 平台才会自动调用obs_scripting_load_python(NULL)Win32 和 macOS need user-provided Python library paths部分 C API 函数因回调机制被重新实现即原文档 Other Differences From the C API 一节详见本文 与 C API 的差异 一节。警告原文档 WARNING 完整保留由于绑定覆盖了整个 API编写不当的脚本可能泄漏内存甚至使程序崩溃。编写脚本时应保持谨慎并检查日志文件中的内存泄漏计数器确认你的脚本没有泄漏内存。请像编写 C 程序一样对待 API 绑定阅读你所使用函数的文档并释放/销毁你通过 API 引用或创建的每一个对象。支持的语言与脚本类型分派从 obs-scripting.c 可以看到支持格式列表由编译期宏决定static const char *supported_formats[] { #if defined(LUAJIT_FOUND) lua, #endif #if defined(Python_FOUND) py, #endif NULL};obs_script_create()obs-scripting.c#L245-L274按文件扩展名分派.lua交给obs_lua_script_create().py交给obs_python_script_create()未知扩展名会记录Unsupported/unknown script type警告。这也解释了为什么 Script Sources 一节标注为Lua Only——注册自定义 source 的能力只在 Luajit 分支中实现obs-scripting-lua-source.c。脚本生命周期函数Script Function Exports脚本可以可选地提供以下全局函数它们构成脚本的完整生命周期。下表逐一说明继承原文档全部条目函数调用时机说明script_description()加载时返回显示在脚本窗口中给用户的描述字符串script_load(settings)脚本启动时携带与脚本关联的 settings 调用。该参数通常不用于用户设置的值而是用于脚本内部的额外设置数据script_unload()脚本卸载时用于清理资源script_save(settings)脚本保存时同样不用于用户设置而是保存脚本内部数据script_defaults(settings)加载早期设置脚本的默认设置如有典型做法是对 settings 调用obs_data_set_default_*系列函数script_update(settings)用户修改脚本设置后响应设置变更script_properties()需要展示设置界面时定义脚本的用户属性返回通过obs_properties_create()创建的obs_properties_t对象script_tick(seconds)每帧用于逐帧处理seconds为距上一帧的秒数。若只需要基础计时功能文档建议使用更高效的定时器见下文在 Python 中不建议使用该函数因为存在全局解释器锁GIL问题C 层的调用顺序可以直接在 Lua 加载流程中得到印证。obs-scripting-lua.c 的load_lua_script()中查找script_properties、script_update、script_save全局函数并保存引用找不到则记为LUA_REFNIL若存在script_defaults(settings)先调用它传入脚本的 settings 对象若存在script_description()调用并缓存返回值到data-base.desc最后调用script_load(settings)。卸载路径同样严谨obs_lua_script_unload() 会先标记所有回调为 removed防止卸载期间回调继续触发再撤销已注册的 source 类型、摘除 tick 钩子、调用script_unload、移除全部回调最后lua_close()解释器状态。获取当前脚本路径script_path()存在一个内置函数可获取当前脚本的绝对路径。该函数在脚本加载之前自动注入到每个脚本中属于脚本自身命名空间的一部分不属于 obslua/obspython 模块。其 Lua 注入模板见 obs-scripting-lua.c#L49-L54static const char *get_script_path_func \ function script_path()\n\ return \%s\\n\ end\n\ package.cpath package.cpath .. \;\ .. script_path() .. \/?. SO_EXT \\n\ package.path package.path .. \;\ .. script_path() .. \/?.lua\\n;即script_path()之外OBS 还会把脚本所在目录加入package.path/package.cpath因此脚本可以require同目录下的其他 Lua 模块或加载同目录的动态库。脚本文件、目录与描述等信息保存在obs_script_t中可通过 obs_script_get_path() 等函数读取obs_lua_script_create()obs-scripting-lua.c#L1111-L1142会从路径中拆出文件名file和目录dir。脚本定时器Script Timers脚本定时器提供了一种高效的计时回调方式无需每帧都锁定脚本解释器。这两个函数属于 obspython/obslua 模块/命名空间timer_add(callback, milliseconds)添加一个每milliseconds毫秒触发一次的计时回调。注意不支持实例方法作为回调必须使用模块级方法module methodstimer_remove(callback)移除一个计时回调。也可以在计时回调内部使用remove_current_callback()终止该定时器。底层实现是理解其高效的关键。每个脚本加载时全局注册一个逐帧钩子obs_lua_load() 末尾执行obs_add_tick_callback(lua_tick, NULL)。lua_tick()obs-scripting-lua.c#L1039-L1091每帧做两件事处理script_tick遍历所有定义了script_tick的脚本链表first_tick_script逐个加锁调用处理定时器遍历所有脚本注册的计时器链表first_timer基于obs_get_video_frame_time()的时间戳做纯 C 层比较elapsed interval只有到期才进入 Lua 解释器调用回调。计时器的注册timer_addobs-scripting-lua.c#L308-L324将毫秒间隔换算为纳秒存于timer-interval初始基准时间取当前视频帧时间注册操作本身通过defer_call_post()投递到脚本子系统自己的延迟线程执行避免在回调上下文中直接修改链表。这就是文档建议优先使用定时器而非script_tick的源码级原因定时器让解释器锁只在回调真正触发时才被获取而script_tick则是每帧都进入解释器。Script SourcesLua 中注册自定义 SourceLua 脚本可以注册自定义 source。做法是创建一个 table并按 C API 中obs_source_info结构体的方式定义其字段local info {} info.id my_source_id info.type obslua.OBS_SOURCE_TYPE_INPUT info.output_flags obslua.OBS_SOURCE_VIDEO info.get_name function() return My Source end info.create function(settings, source) -- 通常把 source 数据保存为一个 table local my_source_data {} [...] return my_source_data end info.video_render function(my_source_data, effect) [...] end info.get_width function(my_source_data) [...] -- 假设 source data 中包含 width 键 return my_source_data.width end info.get_height function(my_source_data) [...] -- 假设 source data 中包含 height 键 return my_source_data.height end -- 注册该 source obs_register_source(info)C 层的obs_lua_register_source()obs-scripting-lua-source.c#L555-L678印证了这段示例的工作方式从 table 中读取必填项id字符串、typeobs_source_type枚举、output_flags整数并调用get_name()得到显示名称通过get_callback宏批量读取可选回调create、destroy、get_width、get_height、get_properties、update、activate、deactivate、show、hide、video_tick、video_render、save、load另加get_defaults每个回调被luaL_ref固定在LUA_REGISTRYINDEX保证在解释器存活期间函数引用不丢失最终填充 C 的obs_source_info并调用obs_register_source(info)同时obs_module_add_source()把该 id 挂到当前模块名下使脚本卸载时能被正确清理支持重定义redefinition若同一 id 已注册且来自已卸载的脚本existing-script NULL会复用旧定义、重新调用各实例的create回调即脚本热重载后已存在的 source 实例可以续接。卸载时的清理由undef_lua_script_sources()obs-scripting-lua-source.c#L713-L725完成对属于该脚本的每个 source 定义obs_enable_source_type(id, false)禁用类型逐一调用各实例的destroy回调并释放注册引用。这也意味着脚本卸载后已创建的该类型 source 实例会被安全销毁而不会留下悬挂引用。注意create回调返回的 Lua table 会被保存在注册表lua_data_ref作为该 source 实例的用户数据后续get_width/video_render等回调都以它作为第一参数——这与示例中my_source_data的用法完全一致。与 C API 的差异回调机制带来的差异函数由于回调的工作方式不同以下函数与 C API 的实现存在差异。它们都属于 obspython/obslua 模块/命名空间原文档 all 条目完整继承obs_enum_sources()枚举所有 source。返回引用计数已增加的 source 数组须用source_list_release()释放obs_scene_enum_items(scene)枚举场景中所有 item。参数为obs_scene_t对象返回 scene item 列表用sceneitem_list_release()释放obs_sceneitem_group_enum_items(group)枚举组内所有 item。参数为obs_sceneitem_t对象返回列表同样用sceneitem_list_release()释放obs_add_main_render_callback(callback)仅 Lua添加主输出渲染回调回调无参数。用obs_remove_main_render_callback()或remove_current_callback()移除obs_remove_main_render_callback(callback)仅 Lua移除主输出渲染回调signal_handler_connect(handler, signal, callback)向信号处理器的特定信号添加回调。回调只有一个参数calldata_t对象。用signal_handler_disconnect()或remove_current_callback()移除signal_handler_disconnect(handler, signal, callback)从信号处理器的特定信号移除回调signal_handler_connect_global(handler, callback)向信号处理器添加全局回调。回调有两个参数信号字符串和calldata_t对象。用signal_handler_disconnect_global()或remove_current_callback()移除signal_handler_disconnect_global(handler, callback)从信号处理器移除全局回调obs_hotkey_register_frontend(name, description, callback)注册前端热键。回调有一个布尔参数pressed。name是热键的唯一名称标识description是展示给用户的描述。用obs_hotkey_unregister()或remove_current_callback()移除回调obs_hotkey_unregister(callback)注销与指定回调关联的热键obs_properties_add_button(properties, setting_name, text, callback)向obs_properties_t对象添加按钮属性。回调有两个参数obs_properties_t对象和按钮的obs_property_t。该回调会被自动清理remove_current_callback()移除当前正在执行的回调若不在回调上下文中调用则什么都不做source_list_release(source_list)释放 source 列表的引用。参数为 source 数组sceneitem_list_release(item_list)释放 scene item 列表的引用。参数为 scene item 数组calldata_source(calldata, name)把calldata_t对象的指针参数转换为obs_source_t对象。返回借用引用borrowed referencecalldata_sceneitem(calldata, name)把calldata_t对象的指针参数转换为obs_sceneitem_t对象。返回借用引用。这些函数在 Lua 侧的注册点集中在add_hook_functions()obs-scripting-lua.c#L988-L1035全部挂到obslua全局表下。几个值得注意的实现细节引用语义enum_sourcesobs-scripting-lua.c#L567-L584在枚举回调里对每个 source 执行obs_source_get_ref()再压栈与文档reference-incremented sources完全对应source_list_release()#L896-L909则遍历 Lua 表逐项obs_source_release()。calldata_source()/calldata_sceneitem()则以ownership false压栈实现借用引用语义——脚本侧不能释放它们remove_current_callback基于线程局部变量current_lua_cb#L235-L236实现回调执行前由运行时设置该指针因此可以在回调内部安全地移除自身典型用途即终止定时器或信号回调热键的延迟注销obs_hotkey_register_frontend()返回热键 id 存入回调元数据当回调被移除如脚本卸载时on_remove_hotkey()会defer_call_post(defer_hotkey_unregister, id)把真正的obs_hotkey_unregister()放到延迟线程执行避免在回调上下文中同步注销导致的重入问题#L650-L704信号连接也是延迟的signal_handler_connect()先在回调元数据calldata中记录 handler 与 signal 字符串再defer_call_post(defer_connect, cb)由延迟线程完成实际的signal_handler_connect()注册#L468-L495。延迟调用线程脚本子系统的核心基础设施上述延迟注册能力来自 obs-scripting.c 中的一套基础设施一个名为scripting: defer的专用线程os_set_thread_name配一个无锁队列deque 互斥锁与信号量。所有脚本触发的、不能安全地即时执行的 OBS 调用注册信号、注册热键、添加 tick/render 回调、注销热键等都被打包成struct defer_call投递到该线程串行执行。obs_scripting_unload()#L171-L226在退出前会统计并日志输出[Scripting] Total detached callbacks: %d——这为排查脚本回调残留提供了日志依据。另外脚本的日志也被接管Lua 的全局print被替换为hook_print写入脚本信息日志、error被替换为hook_error#L988-L1004obslua.script_log(level, message)则支持按级别输出lua_script_log#L952-L984多行消息会逐行拆分写入日志。实战示例官方倒计时脚本仓库自带了完整的参考实现 countdown.lua同目录还有 clock-source.lua、instant-replay.lua、pause-scene.lua 以及 Python 示例 url-text.py。它把前述所有机制组合在一起是一个极好的学习模板script_properties()创建属性集添加duration整数 1–100000 分钟、通过obs_enum_sources()枚举文本 source 填充可编辑下拉列表注意循环结束后调用obs.source_list_release(sources)释放引用添加stop_text文本框以及带回调reset_button_clicked的Reset Timer按钮按钮回调接收props, p两个参数返回布尔值决定是否继续传播script_defaults(settings)obs_data_set_default_int(settings, duration, 5)等即文档中推荐的默认值设置方式script_update(settings)设置变更时重新读取duration/source/stop_text并重置计时器script_save/script_load配对保存热键save 时obs_hotkey_save(hotkey_id)得到数组并存入 settingsload 时obs_hotkey_load()恢复——这正是文档所说script_save用于用户设置之外的内部数据的典型场景用户通过属性界面设置的值会被自动保存信号回调script_load中通过obs_get_signal_handler()获取全局信号处理器signal_handler_connect(sh, source_activate, ...)/source_deactivate监听 source 激活/停用并用calldata_source(cd, source)从 calldata 中取出被操作的 source借用引用用后obs_source_release其查找引用即可calldata 中的引用本身不需释放定时器 自移除timer_callback中秒数归零时调用obs.remove_current_callback()终止定时器——remove_current_callback在回调内使用正是文档推荐的方式热键obs_hotkey_register_frontend(reset_timer_thingy, Reset Timer, reset)回调参数即文档所述布尔pressed。该脚本注释还特别指出这些随脚本生命周期的回调不一定要手动断开脚本卸载时回调会自动销毁——与obs_lua_script_unload()中卸载前统一移除所有回调的实现一致。内存与稳定性像写 C 一样写脚本文档的 WARNING 在源码层面有明确的技术背景SWIG 生成的绑定obslua.i、obspython.i把 libobs 的整个 C API 暴露给了脚本每个 API 返回的引用都需要按 C 的引用计数规则手动释放。对照 obs-scripting.c#L245-L359obs_script_get_settings()与obs_script_save()返回的都是obs_data_addref后的引用调用方必须obs_data_release。官方示例中每一处obs_get_source_by_name都严格配对obs_source_release如 countdown.lua 的set_time_text()countdown.lua#L25-L32。编写脚本时的实践要点均来自文档与源码印证的事实每次obs_get_source_by_name/obs_enum_sources/obs_scene_enum_items获得的所有 source、scene item 引用用完必须 release枚举返回列表用对应的*_list_release()整体释放从calldata_source/calldata_sceneitem获得的是借用引用不要释放定时器/信号/热键回调若需长期存活交给脚本卸载时的自动清理即可若需提前终止用remove_current_callback()或对应的*_remove/*_disconnect/*_unregister需要周期性任务优先timer_add而非script_tickPython 脚本尤其避免script_tickGIL 开销观察日志中的内存泄漏计数器验证脚本没有引用泄漏。小结OBS Studio 的脚本子系统21.0以frontend-tools插件为入口以 shared/obs-scripting 为运行时核心按扩展名分派 Python/Lua 解释器为每个脚本提供独立的解释状态与互斥锁用专门的 defer 线程串行化回调注册等敏感操作并在全局 tick 中调度script_tick与定时器。脚本作者通过script_description/script_load/script_defaults/script_properties/script_update/script_save/script_unload/script_tick八个可选函数控制生命周期通过timer_add实现低开销计时Lua 脚本还可注册完整的自定义 source 类型。理解 scripting.rst 所列的差异函数及其引用语义再配合 countdown.lua 这类官方示例即可开始编写安全、可维护、可热重载的 OBS 脚本。【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考