Files
obs-studio/shared/obs-scripting/obs-scripting-lua.c
PatTheMav 35c7a5a173 cmake: Fix frontend feature flag mismatch
The actual frontend provided an "ENABLE_FRONTEND" build flag, whereas
the global project still provided an "ENABLE_UI" flag, which itself was
only read and respected by the scripting module.

This change retains the global flag and makes both the actual frontend
as well as the scripting module respect its value.
2026-06-05 16:00:18 -04:00

1379 lines
33 KiB
C

/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "obs-scripting-lua.h"
#include <util/platform.h>
#include <util/base.h>
#include <util/dstr.h>
#include <obs.h>
/* ========================================================================= */
#if ARCH_BITS == 64
#define ARCH_DIR "64bit"
#else
#define ARCH_DIR "32bit"
#endif
#ifdef __APPLE__
#define SO_EXT "so"
#elif _WIN32
#include <windows.h>
#define SO_EXT "dll"
#else
#define SO_EXT "so"
#endif
static const char *startup_script_template = "\
for val in pairs(package.preload) do\n\
package.preload[val] = nil\n\
end\n\
%s\
require \"obslua\"\n";
static 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";
static char *startup_script = NULL;
static pthread_mutex_t tick_mutex;
static struct obs_lua_script *first_tick_script = NULL;
pthread_mutex_t lua_source_def_mutex;
#define ls_get_libobs_obj(type, lua_index, obs_obj) \
ls_get_libobs_obj_(script, #type " *", lua_index, obs_obj, NULL, __FUNCTION__, __LINE__)
#define ls_push_libobs_obj(type, obs_obj, ownership) \
ls_push_libobs_obj_(script, #type " *", obs_obj, ownership, NULL, __FUNCTION__, __LINE__)
#define call_func(name, args, rets) call_func_(script, cb->reg_idx, args, rets, #name, __FUNCTION__)
/* ========================================================================= */
static void add_hook_functions(lua_State *script);
static int obs_lua_remove_tick_callback(lua_State *script);
static int obs_lua_remove_main_render_callback(lua_State *script);
#ifdef ENABLE_FRONTEND
void add_lua_frontend_funcs(lua_State *script);
#endif
static bool load_lua_script(struct obs_lua_script *data)
{
struct dstr str = {0};
bool success = false;
int ret;
char *file_data;
lua_State *script = luaL_newstate();
if (!script) {
script_warn(&data->base, "Failed to create new lua state");
goto fail;
}
pthread_mutex_lock(&data->mutex);
luaL_openlibs(script);
luaopen_ffi(script);
if (luaL_dostring(script, startup_script) != 0) {
script_warn(&data->base, "Error executing startup script 1: %s", lua_tostring(script, -1));
goto fail;
}
dstr_printf(&str, get_script_path_func, data->dir.array);
ret = luaL_dostring(script, str.array);
dstr_free(&str);
if (ret != 0) {
script_warn(&data->base, "Error executing startup script 2: %s", lua_tostring(script, -1));
goto fail;
}
current_lua_script = data;
add_lua_source_functions(script);
add_hook_functions(script);
#ifdef ENABLE_FRONTEND
add_lua_frontend_funcs(script);
#endif
file_data = os_quick_read_utf8_file(data->base.path.array);
if (!file_data) {
script_warn(&data->base, "Error opening file: %s", lua_tostring(script, -1));
goto fail;
}
if (luaL_loadbuffer(script, file_data, strlen(file_data), data->base.path.array) != 0) {
script_warn(&data->base, "Error loading file: %s", lua_tostring(script, -1));
bfree(file_data);
goto fail;
}
bfree(file_data);
if (lua_pcall(script, 0, LUA_MULTRET, 0) != 0) {
script_warn(&data->base, "Error running file: %s", lua_tostring(script, -1));
goto fail;
}
ret = lua_gettop(script);
if (ret == 1 && lua_isboolean(script, -1)) {
bool success = lua_toboolean(script, -1);
if (!success) {
goto fail;
}
}
lua_getglobal(script, "script_properties");
if (lua_isfunction(script, -1))
data->get_properties = luaL_ref(script, LUA_REGISTRYINDEX);
else
data->get_properties = LUA_REFNIL;
lua_getglobal(script, "script_update");
if (lua_isfunction(script, -1))
data->update = luaL_ref(script, LUA_REGISTRYINDEX);
else
data->update = LUA_REFNIL;
lua_getglobal(script, "script_save");
if (lua_isfunction(script, -1))
data->save = luaL_ref(script, LUA_REGISTRYINDEX);
else
data->save = LUA_REFNIL;
lua_getglobal(script, "script_defaults");
if (lua_isfunction(script, -1)) {
ls_push_libobs_obj(obs_data_t, data->base.settings, false);
if (lua_pcall(script, 1, 0, 0) != 0) {
script_warn(&data->base,
"Error calling "
"script_defaults: %s",
lua_tostring(script, -1));
}
}
lua_getglobal(script, "script_description");
if (lua_isfunction(script, -1)) {
if (lua_pcall(script, 0, 1, 0) != 0) {
script_warn(&data->base,
"Error calling "
"script_defaults: %s",
lua_tostring(script, -1));
} else {
const char *desc = lua_tostring(script, -1);
dstr_copy(&data->base.desc, desc);
}
}
lua_getglobal(script, "script_load");
if (lua_isfunction(script, -1)) {
ls_push_libobs_obj(obs_data_t, data->base.settings, false);
if (lua_pcall(script, 1, 0, 0) != 0) {
script_warn(&data->base,
"Error calling "
"script_load: %s",
lua_tostring(script, -1));
}
}
data->script = script;
lua_getglobal(script, "script_tick");
if (lua_isfunction(script, -1)) {
pthread_mutex_lock(&tick_mutex);
struct obs_lua_script *next = first_tick_script;
data->next_tick = next;
data->p_prev_next_tick = &first_tick_script;
if (next)
next->p_prev_next_tick = &data->next_tick;
first_tick_script = data;
data->tick = luaL_ref(script, LUA_REGISTRYINDEX);
pthread_mutex_unlock(&tick_mutex);
}
success = true;
fail:
if (script) {
lua_settop(script, 0);
pthread_mutex_unlock(&data->mutex);
}
if (!success && script) {
lua_close(script);
}
current_lua_script = NULL;
return success;
}
/* -------------------------------------------- */
THREAD_LOCAL struct lua_obs_callback *current_lua_cb = NULL;
THREAD_LOCAL struct obs_lua_script *current_lua_script = NULL;
/* -------------------------------------------- */
struct lua_obs_timer {
struct lua_obs_timer *next;
struct lua_obs_timer **p_prev_next;
uint64_t last_ts;
uint64_t interval;
};
static pthread_mutex_t timer_mutex;
static struct lua_obs_timer *first_timer = NULL;
static inline void lua_obs_timer_init(struct lua_obs_timer *timer)
{
pthread_mutex_lock(&timer_mutex);
struct lua_obs_timer *next = first_timer;
timer->next = next;
timer->p_prev_next = &first_timer;
if (next)
next->p_prev_next = &timer->next;
first_timer = timer;
pthread_mutex_unlock(&timer_mutex);
}
static inline void lua_obs_timer_remove(struct lua_obs_timer *timer)
{
struct lua_obs_timer *next = timer->next;
if (next)
next->p_prev_next = timer->p_prev_next;
*timer->p_prev_next = timer->next;
}
static inline struct lua_obs_callback *lua_obs_timer_cb(struct lua_obs_timer *timer)
{
return &((struct lua_obs_callback *)timer)[-1];
}
static int timer_remove(lua_State *script)
{
if (!is_function(script, 1))
return 0;
struct lua_obs_callback *cb = find_lua_obs_callback(script, 1);
if (cb)
remove_lua_obs_callback(cb);
return 0;
}
static void timer_call(struct script_callback *p_cb)
{
struct lua_obs_callback *cb = (struct lua_obs_callback *)p_cb;
if (script_callback_removed(p_cb))
return;
lock_callback();
call_func_(cb->script, cb->reg_idx, 0, 0, "timer_cb", __FUNCTION__);
unlock_callback();
}
static void defer_timer_init(void *p_cb)
{
struct lua_obs_callback *cb = p_cb;
struct lua_obs_timer *timer = lua_obs_callback_extra_data(cb);
lua_obs_timer_init(timer);
}
static int timer_add(lua_State *script)
{
if (!is_function(script, 1))
return 0;
int ms = (int)lua_tointeger(script, 2);
if (!ms)
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback_extra(script, 1, sizeof(struct lua_obs_timer));
struct lua_obs_timer *timer = lua_obs_callback_extra_data(cb);
timer->interval = (uint64_t)ms * 1000000ULL;
timer->last_ts = obs_get_video_frame_time();
defer_call_post(defer_timer_init, cb);
return 0;
}
/* -------------------------------------------- */
static void obs_lua_main_render_callback(void *priv, uint32_t cx, uint32_t cy)
{
struct lua_obs_callback *cb = priv;
lua_State *script = cb->script;
if (script_callback_removed(&cb->base)) {
obs_remove_main_render_callback(obs_lua_main_render_callback, cb);
return;
}
lock_callback();
lua_pushinteger(script, (lua_Integer)cx);
lua_pushinteger(script, (lua_Integer)cy);
call_func(obs_lua_main_render_callback, 2, 0);
unlock_callback();
}
static int obs_lua_remove_main_render_callback(lua_State *script)
{
if (!verify_args1(script, is_function))
return 0;
struct lua_obs_callback *cb = find_lua_obs_callback(script, 1);
if (cb)
remove_lua_obs_callback(cb);
return 0;
}
static void defer_add_render(void *cb)
{
obs_add_main_render_callback(obs_lua_main_render_callback, cb);
}
static int obs_lua_add_main_render_callback(lua_State *script)
{
if (!verify_args1(script, is_function))
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback(script, 1);
defer_call_post(defer_add_render, cb);
return 0;
}
/* -------------------------------------------- */
static void obs_lua_tick_callback(void *priv, float seconds)
{
struct lua_obs_callback *cb = priv;
lua_State *script = cb->script;
if (script_callback_removed(&cb->base)) {
obs_remove_tick_callback(obs_lua_tick_callback, cb);
return;
}
lock_callback();
lua_pushnumber(script, (lua_Number)seconds);
call_func(obs_lua_tick_callback, 1, 0);
unlock_callback();
}
static int obs_lua_remove_tick_callback(lua_State *script)
{
if (!verify_args1(script, is_function))
return 0;
struct lua_obs_callback *cb = find_lua_obs_callback(script, 1);
if (cb)
remove_lua_obs_callback(cb);
return 0;
}
static void defer_add_tick(void *cb)
{
obs_add_tick_callback(obs_lua_tick_callback, cb);
}
static int obs_lua_add_tick_callback(lua_State *script)
{
if (!verify_args1(script, is_function))
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback(script, 1);
defer_call_post(defer_add_tick, cb);
return 0;
}
/* -------------------------------------------- */
static void calldata_signal_callback(void *priv, calldata_t *cd)
{
struct lua_obs_callback *cb = priv;
lua_State *script = cb->script;
if (script_callback_removed(&cb->base)) {
signal_handler_remove_current();
return;
}
lock_callback();
ls_push_libobs_obj(calldata_t, cd, false);
call_func(calldata_signal_callback, 1, 0);
unlock_callback();
}
static int obs_lua_signal_handler_disconnect(lua_State *script)
{
signal_handler_t *handler;
const char *signal;
if (!ls_get_libobs_obj(signal_handler_t, 1, &handler))
return 0;
signal = lua_tostring(script, 2);
if (!signal)
return 0;
if (!is_function(script, 3))
return 0;
struct lua_obs_callback *cb = find_lua_obs_callback(script, 3);
while (cb) {
signal_handler_t *cb_handler = calldata_ptr(&cb->base.extra, "handler");
const char *cb_signal = calldata_string(&cb->base.extra, "signal");
if (cb_signal && strcmp(signal, cb_signal) == 0 && handler == cb_handler)
break;
cb = find_next_lua_obs_callback(script, cb, 3);
}
if (cb)
remove_lua_obs_callback(cb);
return 0;
}
static void defer_connect(void *p_cb)
{
struct script_callback *cb = p_cb;
signal_handler_t *handler = calldata_ptr(&cb->extra, "handler");
const char *signal = calldata_string(&cb->extra, "signal");
signal_handler_connect(handler, signal, calldata_signal_callback, cb);
}
static int obs_lua_signal_handler_connect(lua_State *script)
{
signal_handler_t *handler;
const char *signal;
if (!ls_get_libobs_obj(signal_handler_t, 1, &handler))
return 0;
signal = lua_tostring(script, 2);
if (!signal)
return 0;
if (!is_function(script, 3))
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback(script, 3);
calldata_set_ptr(&cb->base.extra, "handler", handler);
calldata_set_string(&cb->base.extra, "signal", signal);
defer_call_post(defer_connect, cb);
return 0;
}
/* -------------------------------------------- */
static void calldata_signal_callback_global(void *priv, const char *signal, calldata_t *cd)
{
struct lua_obs_callback *cb = priv;
lua_State *script = cb->script;
if (script_callback_removed(&cb->base)) {
signal_handler_remove_current();
return;
}
lock_callback();
lua_pushstring(script, signal);
ls_push_libobs_obj(calldata_t, cd, false);
call_func(calldata_signal_callback_global, 2, 0);
unlock_callback();
}
static int obs_lua_signal_handler_disconnect_global(lua_State *script)
{
signal_handler_t *handler;
if (!ls_get_libobs_obj(signal_handler_t, 1, &handler))
return 0;
if (!is_function(script, 2))
return 0;
struct lua_obs_callback *cb = find_lua_obs_callback(script, 3);
while (cb) {
signal_handler_t *cb_handler = calldata_ptr(&cb->base.extra, "handler");
if (handler == cb_handler)
break;
cb = find_next_lua_obs_callback(script, cb, 3);
}
if (cb)
remove_lua_obs_callback(cb);
return 0;
}
static void defer_connect_global(void *p_cb)
{
struct script_callback *cb = p_cb;
signal_handler_t *handler = calldata_ptr(&cb->extra, "handler");
signal_handler_connect_global(handler, calldata_signal_callback_global, cb);
}
static int obs_lua_signal_handler_connect_global(lua_State *script)
{
signal_handler_t *handler;
if (!ls_get_libobs_obj(signal_handler_t, 1, &handler))
return 0;
if (!is_function(script, 2))
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback(script, 2);
calldata_set_ptr(&cb->base.extra, "handler", handler);
defer_call_post(defer_connect_global, cb);
return 0;
}
/* -------------------------------------------- */
static bool enum_sources_proc(void *param, obs_source_t *source)
{
lua_State *script = param;
obs_source_get_ref(source);
ls_push_libobs_obj(obs_source_t, source, false);
size_t idx = lua_rawlen(script, -2);
lua_rawseti(script, -2, (int)idx + 1);
return true;
}
static int enum_sources(lua_State *script)
{
lua_newtable(script);
obs_enum_sources(enum_sources_proc, script);
return 1;
}
/* -------------------------------------------- */
static void source_enum_filters_proc(obs_source_t *source, obs_source_t *filter, void *param)
{
UNUSED_PARAMETER(source);
lua_State *script = param;
obs_source_get_ref(filter);
ls_push_libobs_obj(obs_source_t, filter, false);
size_t idx = lua_rawlen(script, -2);
lua_rawseti(script, -2, (int)idx + 1);
}
static int source_enum_filters(lua_State *script)
{
obs_source_t *source;
if (!ls_get_libobs_obj(obs_source_t, 1, &source))
return 0;
lua_newtable(script);
obs_source_enum_filters(source, source_enum_filters_proc, script);
return 1;
}
/* -------------------------------------------- */
static bool enum_items_proc(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
{
lua_State *script = param;
UNUSED_PARAMETER(scene);
obs_sceneitem_addref(item);
ls_push_libobs_obj(obs_sceneitem_t, item, false);
lua_rawseti(script, -2, (int)lua_rawlen(script, -2) + 1);
return true;
}
static int scene_enum_items(lua_State *script)
{
obs_scene_t *scene;
if (!ls_get_libobs_obj(obs_scene_t, 1, &scene))
return 0;
lua_newtable(script);
obs_scene_enum_items(scene, enum_items_proc, script);
return 1;
}
static int sceneitem_group_enum_items(lua_State *script)
{
obs_sceneitem_t *sceneitem;
if (!ls_get_libobs_obj(obs_sceneitem_t, 1, &sceneitem))
return 0;
lua_newtable(script);
obs_sceneitem_group_enum_items(sceneitem, enum_items_proc, script);
return 1;
}
/* -------------------------------------------- */
static void defer_hotkey_unregister(void *p_cb)
{
obs_hotkey_unregister((obs_hotkey_id)(uintptr_t)p_cb);
}
static void on_remove_hotkey(void *p_cb)
{
struct lua_obs_callback *cb = p_cb;
obs_hotkey_id id = (obs_hotkey_id)calldata_int(&cb->base.extra, "id");
if (id != OBS_INVALID_HOTKEY_ID)
defer_call_post(defer_hotkey_unregister, (void *)(uintptr_t)id);
}
static void hotkey_pressed(void *p_cb, bool pressed)
{
struct lua_obs_callback *cb = p_cb;
lua_State *script = cb->script;
if (script_callback_removed(&cb->base))
return;
lock_callback();
lua_pushboolean(script, pressed);
call_func(hotkey_pressed, 1, 0);
unlock_callback();
}
static void defer_hotkey_pressed(void *p_cb)
{
hotkey_pressed(p_cb, true);
}
static void defer_hotkey_unpressed(void *p_cb)
{
hotkey_pressed(p_cb, false);
}
static void hotkey_callback(void *p_cb, obs_hotkey_id id, obs_hotkey_t *hotkey, bool pressed)
{
struct lua_obs_callback *cb = p_cb;
if (script_callback_removed(&cb->base))
return;
if (pressed)
defer_call_post(defer_hotkey_pressed, cb);
else
defer_call_post(defer_hotkey_unpressed, cb);
UNUSED_PARAMETER(hotkey);
UNUSED_PARAMETER(id);
}
static int hotkey_unregister(lua_State *script)
{
if (!verify_args1(script, is_function))
return 0;
struct lua_obs_callback *cb = find_lua_obs_callback(script, 1);
if (cb)
remove_lua_obs_callback(cb);
return 0;
}
static int hotkey_register_frontend(lua_State *script)
{
obs_hotkey_id id;
const char *name = lua_tostring(script, 1);
if (!name)
return 0;
const char *desc = lua_tostring(script, 2);
if (!desc)
return 0;
if (!is_function(script, 3))
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback(script, 3);
cb->base.on_remove = on_remove_hotkey;
id = obs_hotkey_register_frontend(name, desc, hotkey_callback, cb);
calldata_set_int(&cb->base.extra, "id", id);
lua_pushinteger(script, (lua_Integer)id);
if (id == OBS_INVALID_HOTKEY_ID)
remove_lua_obs_callback(cb);
return 1;
}
/* -------------------------------------------- */
static bool button_prop_clicked(obs_properties_t *props, obs_property_t *p, void *p_cb)
{
struct lua_obs_callback *cb = p_cb;
lua_State *script = cb->script;
bool ret = false;
if (script_callback_removed(&cb->base))
return false;
lock_callback();
if (!ls_push_libobs_obj(obs_properties_t, props, false))
goto fail;
if (!ls_push_libobs_obj(obs_property_t, p, false)) {
lua_pop(script, 1);
goto fail;
}
call_func(button_prop_clicked, 2, 1);
if (lua_isboolean(script, -1))
ret = lua_toboolean(script, -1);
fail:
unlock_callback();
return ret;
}
static int properties_add_button(lua_State *script)
{
obs_properties_t *props;
obs_property_t *p;
if (!ls_get_libobs_obj(obs_properties_t, 1, &props))
return 0;
const char *name = lua_tostring(script, 2);
if (!name)
return 0;
const char *text = lua_tostring(script, 3);
if (!text)
return 0;
if (!is_function(script, 4))
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback(script, 4);
p = obs_properties_add_button2(props, name, text, button_prop_clicked, cb);
if (!p || !ls_push_libobs_obj(obs_property_t, p, false))
return 0;
return 1;
}
/* -------------------------------------------- */
static bool modified_callback(void *p_cb, obs_properties_t *props, obs_property_t *p, obs_data_t *settings)
{
struct lua_obs_callback *cb = p_cb;
lua_State *script = cb->script;
bool ret = false;
if (script_callback_removed(&cb->base))
return false;
lock_callback();
if (!ls_push_libobs_obj(obs_properties_t, props, false)) {
goto fail;
}
if (!ls_push_libobs_obj(obs_property_t, p, false)) {
lua_pop(script, 1);
goto fail;
}
if (!ls_push_libobs_obj(obs_data_t, settings, false)) {
lua_pop(script, 2);
goto fail;
}
call_func(modified_callback, 3, 1);
if (lua_isboolean(script, -1))
ret = lua_toboolean(script, -1);
fail:
unlock_callback();
return ret;
}
static int property_set_modified_callback(lua_State *script)
{
obs_property_t *p;
if (!ls_get_libobs_obj(obs_property_t, 1, &p))
return 0;
if (!is_function(script, 2))
return 0;
struct lua_obs_callback *cb = add_lua_obs_callback(script, 2);
obs_property_set_modified_callback2(p, modified_callback, cb);
return 0;
}
/* -------------------------------------------- */
static int remove_current_callback(lua_State *script)
{
UNUSED_PARAMETER(script);
if (current_lua_cb)
remove_lua_obs_callback(current_lua_cb);
return 0;
}
/* -------------------------------------------- */
static int calldata_source(lua_State *script)
{
calldata_t *cd;
const char *str;
int ret = 0;
if (!ls_get_libobs_obj(calldata_t, 1, &cd))
goto fail;
str = lua_tostring(script, 2);
if (!str)
goto fail;
obs_source_t *source = calldata_ptr(cd, str);
if (ls_push_libobs_obj(obs_source_t, source, false))
++ret;
fail:
return ret;
}
static int calldata_sceneitem(lua_State *script)
{
calldata_t *cd;
const char *str;
int ret = 0;
if (!ls_get_libobs_obj(calldata_t, 1, &cd))
goto fail;
str = lua_tostring(script, 2);
if (!str)
goto fail;
obs_sceneitem_t *sceneitem = calldata_ptr(cd, str);
if (ls_push_libobs_obj(obs_sceneitem_t, sceneitem, false))
++ret;
fail:
return ret;
}
/* -------------------------------------------- */
static int source_list_release(lua_State *script)
{
size_t count = lua_rawlen(script, 1);
for (size_t i = 0; i < count; i++) {
obs_source_t *source;
lua_rawgeti(script, 1, (int)i + 1);
ls_get_libobs_obj(obs_source_t, -1, &source);
lua_pop(script, 1);
obs_source_release(source);
}
return 0;
}
static int sceneitem_list_release(lua_State *script)
{
size_t count = lua_rawlen(script, 1);
for (size_t i = 0; i < count; i++) {
obs_sceneitem_t *item;
lua_rawgeti(script, 1, (int)i + 1);
ls_get_libobs_obj(obs_sceneitem_t, -1, &item);
lua_pop(script, 1);
obs_sceneitem_release(item);
}
return 0;
}
/* -------------------------------------------- */
static int hook_print(lua_State *script)
{
struct obs_lua_script *data = current_lua_script;
const char *msg = lua_tostring(script, 1);
if (!msg)
return 0;
script_info(&data->base, "%s", msg);
return 0;
}
static int hook_error(lua_State *script)
{
struct obs_lua_script *data = current_lua_script;
const char *msg = lua_tostring(script, 1);
if (!msg)
return 0;
script_error(&data->base, "%s", msg);
return 0;
}
/* -------------------------------------------- */
static int lua_script_log(lua_State *script)
{
struct obs_lua_script *data = current_lua_script;
int log_level = (int)lua_tointeger(script, 1);
const char *msg = lua_tostring(script, 2);
if (!msg)
return 0;
/* ------------------- */
dstr_copy(&data->log_chunk, msg);
const char *start = data->log_chunk.array;
char *endl = strchr(start, '\n');
while (endl) {
*endl = 0;
script_log(&data->base, log_level, "%s", start);
*endl = '\n';
start = endl + 1;
endl = strchr(start, '\n');
}
if (start && *start)
script_log(&data->base, log_level, "%s", start);
dstr_resize(&data->log_chunk, 0);
/* ------------------- */
return 0;
}
/* -------------------------------------------- */
static void add_hook_functions(lua_State *script)
{
#define add_func(name, func) \
do { \
lua_pushstring(script, name); \
lua_pushcfunction(script, func); \
lua_rawset(script, -3); \
} while (false)
lua_getglobal(script, "_G");
add_func("print", hook_print);
add_func("error", hook_error);
lua_pop(script, 1);
/* ------------- */
lua_getglobal(script, "obslua");
add_func("script_log", lua_script_log);
add_func("timer_remove", timer_remove);
add_func("timer_add", timer_add);
add_func("obs_enum_sources", enum_sources);
add_func("obs_source_enum_filters", source_enum_filters);
add_func("obs_scene_enum_items", scene_enum_items);
add_func("obs_sceneitem_group_enum_items", sceneitem_group_enum_items);
add_func("source_list_release", source_list_release);
add_func("sceneitem_list_release", sceneitem_list_release);
add_func("calldata_source", calldata_source);
add_func("calldata_sceneitem", calldata_sceneitem);
add_func("obs_add_main_render_callback", obs_lua_add_main_render_callback);
add_func("obs_remove_main_render_callback", obs_lua_remove_main_render_callback);
add_func("obs_add_tick_callback", obs_lua_add_tick_callback);
add_func("obs_remove_tick_callback", obs_lua_remove_tick_callback);
add_func("signal_handler_connect", obs_lua_signal_handler_connect);
add_func("signal_handler_disconnect", obs_lua_signal_handler_disconnect);
add_func("signal_handler_connect_global", obs_lua_signal_handler_connect_global);
add_func("signal_handler_disconnect_global", obs_lua_signal_handler_disconnect_global);
add_func("obs_hotkey_unregister", hotkey_unregister);
add_func("obs_hotkey_register_frontend", hotkey_register_frontend);
add_func("obs_properties_add_button", properties_add_button);
add_func("obs_property_set_modified_callback", property_set_modified_callback);
add_func("remove_current_callback", remove_current_callback);
lua_pop(script, 1);
#undef add_func
}
/* -------------------------------------------- */
static void lua_tick(void *param, float seconds)
{
struct obs_lua_script *data;
struct lua_obs_timer *timer;
uint64_t ts = obs_get_video_frame_time();
/* --------------------------------- */
/* process script_tick calls */
pthread_mutex_lock(&tick_mutex);
data = first_tick_script;
while (data) {
lua_State *script = data->script;
current_lua_script = data;
pthread_mutex_lock(&data->mutex);
lua_pushnumber(script, (double)seconds);
call_func_(script, data->tick, 1, 0, "tick", __FUNCTION__);
pthread_mutex_unlock(&data->mutex);
data = data->next_tick;
}
current_lua_script = NULL;
pthread_mutex_unlock(&tick_mutex);
/* --------------------------------- */
/* process timers */
pthread_mutex_lock(&timer_mutex);
timer = first_timer;
while (timer) {
struct lua_obs_timer *next = timer->next;
struct lua_obs_callback *cb = lua_obs_timer_cb(timer);
if (script_callback_removed(&cb->base)) {
lua_obs_timer_remove(timer);
} else {
uint64_t elapsed = ts - timer->last_ts;
if (elapsed >= timer->interval) {
timer_call(&cb->base);
timer->last_ts += timer->interval;
}
}
timer = next;
}
pthread_mutex_unlock(&timer_mutex);
UNUSED_PARAMETER(param);
}
/* -------------------------------------------- */
void obs_lua_script_update(obs_script_t *script, obs_data_t *settings);
bool obs_lua_script_load(obs_script_t *s)
{
struct obs_lua_script *data = (struct obs_lua_script *)s;
if (!data->base.loaded) {
data->base.loaded = load_lua_script(data);
if (data->base.loaded) {
blog(LOG_INFO, "[obs-scripting]: Loaded lua script: %s", data->base.file.array);
obs_lua_script_update(s, NULL);
}
}
return data->base.loaded;
}
obs_script_t *obs_lua_script_create(const char *path, obs_data_t *settings)
{
struct obs_lua_script *data = bzalloc(sizeof(*data));
data->base.type = OBS_SCRIPT_LANG_LUA;
data->tick = LUA_REFNIL;
pthread_mutex_init_value(&data->mutex);
if (pthread_mutex_init_recursive(&data->mutex) != 0) {
bfree(data);
return NULL;
}
dstr_copy(&data->base.path, path);
char *slash = path && *path ? strrchr(path, '/') : NULL;
if (slash) {
slash++;
dstr_copy(&data->base.file, slash);
dstr_left(&data->dir, &data->base.path, slash - path);
} else {
dstr_copy(&data->base.file, path);
}
data->base.settings = obs_data_create();
if (settings)
obs_data_apply(data->base.settings, settings);
obs_lua_script_load((obs_script_t *)data);
return (obs_script_t *)data;
}
extern void undef_lua_script_sources(struct obs_lua_script *data);
void obs_lua_script_unload(obs_script_t *s)
{
struct obs_lua_script *data = (struct obs_lua_script *)s;
if (!s->loaded)
return;
lua_State *script = data->script;
/* ---------------------------- */
/* mark callbacks as removed */
pthread_mutex_lock(&data->mutex);
/* XXX: scripts can potentially make callbacks when this happens, so
* this probably still isn't ideal as we can't predict how the
* processor or operating system is going to schedule things. a more
* ideal method would be to reference count the script objects and
* atomically share ownership with callbacks when they're called. */
struct lua_obs_callback *cb = (struct lua_obs_callback *)data->first_callback;
while (cb) {
os_atomic_set_bool(&cb->base.removed, true);
cb = (struct lua_obs_callback *)cb->base.next;
}
pthread_mutex_unlock(&data->mutex);
/* ---------------------------- */
/* undefine source types */
undef_lua_script_sources(data);
/* ---------------------------- */
/* unhook tick function */
if (data->p_prev_next_tick) {
pthread_mutex_lock(&tick_mutex);
struct obs_lua_script *next = data->next_tick;
if (next)
next->p_prev_next_tick = data->p_prev_next_tick;
*data->p_prev_next_tick = next;
pthread_mutex_unlock(&tick_mutex);
data->p_prev_next_tick = NULL;
data->next_tick = NULL;
}
/* ---------------------------- */
/* call script_unload */
pthread_mutex_lock(&data->mutex);
current_lua_script = data;
lua_getglobal(script, "script_unload");
lua_pcall(script, 0, 0, 0);
current_lua_script = NULL;
/* ---------------------------- */
/* remove all callbacks */
cb = (struct lua_obs_callback *)data->first_callback;
while (cb) {
struct lua_obs_callback *next = (struct lua_obs_callback *)cb->base.next;
remove_lua_obs_callback(cb);
cb = next;
}
pthread_mutex_unlock(&data->mutex);
/* ---------------------------- */
/* close script */
lua_close(script);
s->loaded = false;
blog(LOG_INFO, "[obs-scripting]: Unloaded lua script: %s", data->base.file.array);
}
void obs_lua_script_destroy(obs_script_t *s)
{
struct obs_lua_script *data = (struct obs_lua_script *)s;
if (data) {
pthread_mutex_destroy(&data->mutex);
dstr_free(&data->base.path);
dstr_free(&data->base.file);
dstr_free(&data->base.desc);
obs_data_release(data->base.settings);
dstr_free(&data->log_chunk);
dstr_free(&data->dir);
bfree(data);
}
}
void obs_lua_script_update(obs_script_t *s, obs_data_t *settings)
{
struct obs_lua_script *data = (struct obs_lua_script *)s;
lua_State *script = data->script;
if (!s->loaded)
return;
if (data->update == LUA_REFNIL)
return;
if (settings)
obs_data_apply(s->settings, settings);
current_lua_script = data;
pthread_mutex_lock(&data->mutex);
ls_push_libobs_obj(obs_data_t, s->settings, false);
call_func_(script, data->update, 1, 0, "script_update", __FUNCTION__);
pthread_mutex_unlock(&data->mutex);
current_lua_script = NULL;
}
obs_properties_t *obs_lua_script_get_properties(obs_script_t *s)
{
struct obs_lua_script *data = (struct obs_lua_script *)s;
lua_State *script = data->script;
obs_properties_t *props = NULL;
if (!s->loaded)
return NULL;
if (data->get_properties == LUA_REFNIL)
return NULL;
current_lua_script = data;
pthread_mutex_lock(&data->mutex);
call_func_(script, data->get_properties, 0, 1, "script_properties", __FUNCTION__);
ls_get_libobs_obj(obs_properties_t, -1, &props);
pthread_mutex_unlock(&data->mutex);
current_lua_script = NULL;
return props;
}
void obs_lua_script_save(obs_script_t *s)
{
struct obs_lua_script *data = (struct obs_lua_script *)s;
lua_State *script = data->script;
if (!s->loaded)
return;
if (data->save == LUA_REFNIL)
return;
current_lua_script = data;
pthread_mutex_lock(&data->mutex);
ls_push_libobs_obj(obs_data_t, s->settings, false);
call_func_(script, data->save, 1, 0, "script_save", __FUNCTION__);
pthread_mutex_unlock(&data->mutex);
current_lua_script = NULL;
}
/* -------------------------------------------- */
static inline void add_package_cpath(struct dstr *cpath, const char *path)
{
dstr_catf(cpath, " .. \";\" .. \"%s\" .. \"/?." SO_EXT "\"", path);
}
void obs_lua_load(void)
{
struct dstr tmp = {0};
pthread_mutex_init(&tick_mutex, NULL);
pthread_mutex_init_recursive(&timer_mutex);
pthread_mutex_init(&lua_source_def_mutex, NULL);
/* ---------------------------------------------- */
/* Initialize Lua startup script */
#if _WIN32
#define PATH_MAX MAX_PATH
#endif
struct dstr package_cpath = {0};
char import_path[PATH_MAX];
#ifdef __APPLE__
struct dstr bundle_path;
dstr_init_move_array(&bundle_path, os_get_executable_path_ptr(""));
dstr_cat(&bundle_path, "../PlugIns");
char *absolute_plugin_path = os_get_abs_path_ptr(bundle_path.array);
if (absolute_plugin_path != NULL) {
strcpy(import_path, absolute_plugin_path);
bfree(absolute_plugin_path);
}
dstr_free(&bundle_path);
#else
strcpy(import_path, "./");
#endif
dstr_cat(&package_cpath, "package.cpath = package.cpath");
add_package_cpath(&package_cpath, import_path);
#if !defined(_WIN32) && !defined(__APPLE__)
char *relative_script_path = os_get_executable_path_ptr("../" SCRIPT_DIR);
if (relative_script_path)
add_package_cpath(&package_cpath, relative_script_path);
bfree(relative_script_path);
#endif
add_package_cpath(&package_cpath, SCRIPT_DIR);
dstr_cat(&package_cpath, "\n");
dstr_printf(&tmp, startup_script_template, package_cpath.array);
dstr_free(&package_cpath);
startup_script = tmp.array;
obs_add_tick_callback(lua_tick, NULL);
}
void obs_lua_unload(void)
{
obs_remove_tick_callback(lua_tick, NULL);
bfree(startup_script);
pthread_mutex_destroy(&tick_mutex);
pthread_mutex_destroy(&timer_mutex);
pthread_mutex_destroy(&lua_source_def_mutex);
}