Browse Source

deps/obs-scripting: Add scripting support

Allows Lua/Python scripting support.
jp9000 7 years ago
parent
commit
9eabfdbf1e

+ 3 - 0
.gitignore

@@ -45,6 +45,9 @@ install-sh
 Makefile.in
 Makefile
 
+#python
+__pycache__
+
 #sphinx
 /docs/sphinx/_build/*
 !/docs/sphinx/_build/.gitignore

+ 1 - 0
CMakeLists.txt

@@ -20,6 +20,7 @@ if(WIN32)
 endif()
 
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
+set(ENABLE_SCRIPTING OFF CACHE BOOL "" FORCE)
 
 include(ObsHelpers)
 include(ObsCpack)

+ 1 - 0
deps/CMakeLists.txt

@@ -12,6 +12,7 @@ endif()
 
 add_subdirectory(media-playback)
 add_subdirectory(file-updater)
+add_subdirectory(obs-scripting)
 
 if(WIN32)
 	add_subdirectory(blake2)

+ 173 - 0
deps/obs-scripting/CMakeLists.txt

@@ -0,0 +1,173 @@
+cmake_minimum_required(VERSION 2.8)
+project(obs-scripting)
+
+if(MSVC)
+	set(obs-scripting_PLATFORM_DEPS
+		w32-pthreads)
+endif()
+
+find_package(Luajit QUIET)
+find_package(PythonLibs QUIET 3.4)
+find_package(SWIG QUIET 2)
+
+set(COMPILE_PYTHON FALSE CACHE BOOL "" FORCE)
+set(COMPILE_LUA FALSE CACHE BOOL "" FORCE)
+
+if(NOT SWIG_FOUND)
+	message(STATUS "Scripting: SWIG not found; scripting disabled")
+	return()
+endif()
+
+if(NOT PYTHONLIBS_FOUND AND NOT LUAJIT_FOUND)
+	message(STATUS "Scripting: Neither Python 3 nor Luajit was found; scripting plugin disabled")
+	return()
+endif()
+
+if(NOT LUAJIT_FOUND)
+	message(STATUS "Scripting: Luajit not found; Luajit support disabled")
+else()
+	message(STATUS "Scripting: Luajit supported")
+	set(COMPILE_LUA TRUE CACHE BOOL "" FORCE)
+endif()
+
+if(NOT PYTHONLIBS_FOUND)
+	message(STATUS "Scripting: Python 3 not found; Python support disabled")
+	set(PYTHON_FOUND FALSE)
+	set(PYTHONLIBS_FOUND FALSE)
+else()
+	message(STATUS "Scripting: Python 3 supported")
+	set(PYTHON_FOUND TRUE)
+	set(COMPILE_PYTHON TRUE CACHE BOOL "" FORCE)
+
+	get_filename_component(PYTHON_LIB "${PYTHON_LIBRARIES}" NAME)
+	string(REGEX REPLACE "\\.[^.]*$" "" PYTHON_LIB ${PYTHON_LIB})
+	if(WIN32)
+		string(REGEX REPLACE "_d" "" PYTHON_LIB "${PYTHON_LIB}")
+	endif()
+endif()
+
+set(ENABLE_SCRIPTING ON CACHE BOOL "" FORCE)
+
+if(UI_ENABLED)
+	set(EXTRA_LIBS obs-frontend-api)
+	include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/UI/obs-frontend-api")
+endif()
+
+configure_file(
+	"${CMAKE_CURRENT_SOURCE_DIR}/obs-scripting-config.h.in"
+	"${CMAKE_CURRENT_BINARY_DIR}/obs-scripting-config.h")
+
+include(${SWIG_USE_FILE})
+
+include_directories(${CMAKE_SOURCE_DIR}/libobs)
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+include_directories(${CMAKE_CURRENT_BINARY_DIR})
+
+if(PYTHONLIBS_FOUND)
+	include_directories(${PYTHON_INCLUDE_DIR})
+
+	set(obs-scripting-python_SOURCES
+		obs-scripting-python.c
+		)
+	set(obs-scripting-python_HEADERS
+		obs-scripting-python.h
+		obs-scripting-python-import.h
+		)
+
+	if(UI_ENABLED)
+		set(obs-scripting-python_SOURCES
+			${obs-scripting-python_SOURCES}
+			obs-scripting-python-frontend.c
+			)
+	endif()
+	if(WIN32 OR APPLE)
+		set(obs-scripting-python_SOURCES
+			${obs-scripting-python_SOURCES}
+			obs-scripting-python-import.c
+			)
+	else()
+		set(EXTRA_LIBS ${EXTRA_LIBS} ${PYTHON_LIBRARIES})
+	endif()
+endif()
+
+if(LUAJIT_FOUND)
+	include_directories(${LUAJIT_INCLUDE_DIR})
+
+	set(obs-scripting-lua_SOURCES
+		obs-scripting-lua.c
+		obs-scripting-lua-source.c
+		)
+	set(obs-scripting-lua_HEADERS
+		obs-scripting-lua.h
+		)
+	if(UI_ENABLED)
+		set(obs-scripting-lua_SOURCES
+			${obs-scripting-lua_SOURCES}
+			obs-scripting-lua-frontend.c
+			)
+	endif()
+endif()
+
+set(obs-scripting_SOURCES
+	obs-scripting.c
+	obs-scripting-logging.c
+	cstrcache.cpp
+	)
+set(obs-scripting_HEADERS
+	${CMAKE_CURRENT_BINARY_DIR}/obs-scripting-config.h
+	obs-scripting.h
+	obs-scripting-callback.h
+	cstrcache.h
+	)
+
+file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/swig)
+
+if(PYTHONLIBS_FOUND)
+	set(SWIG_PY_RUNTIME swig/swigpyrun.h)
+	add_custom_command(OUTPUT ${SWIG_PY_RUNTIME}
+		WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+		PRE_BUILD
+		COMMAND ${SWIG_EXECUTABLE} -python -external-runtime ${SWIG_PY_RUNTIME}
+		COMMENT "Scripting plugin: Building Python SWIG interface header"
+		)
+	set_source_files_properties(${SWIG_PY_RUNTIME} PROPERTIES GENERATED TRUE)
+endif()
+
+if(LUAJIT_FOUND)
+	set(SWIG_LUA_RUNTIME swig/swigluarun.h)
+	add_custom_command(OUTPUT ${SWIG_LUA_RUNTIME}
+		WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+		PRE_BUILD
+		COMMAND ${SWIG_EXECUTABLE} -lua -external-runtime ${SWIG_LUA_RUNTIME}
+		COMMENT "Scripting: Building Lua SWIG interface header"
+		)
+	set_source_files_properties(${SWIG_LUA_RUNTIME} PROPERTIES GENERATED TRUE)
+endif()
+
+add_library(obs-scripting SHARED
+	${obs-scripting_SOURCES}
+	${obs-scripting_HEADERS}
+	${obs-scripting-python_SOURCES}
+	${obs-scripting-python_HEADERS}
+	${obs-scripting-lua_SOURCES}
+	${obs-scripting-lua_HEADERS}
+	${SWIG_PY_RUNTIME}
+	${SWIG_LUA_RUNTIME}
+	)
+
+target_link_libraries(obs-scripting
+	libobs
+	${LUAJIT_LIBRARIES}
+	${EXTRA_LIBS}
+	${obs-scripting_PLATFORM_DEPS}
+	)
+
+if(PYTHONLIBS_FOUND)
+	add_subdirectory(obspython)
+endif()
+
+if(LUAJIT_FOUND)
+	add_subdirectory(obslua)
+endif()
+
+install_obs_core(obs-scripting)

+ 28 - 0
deps/obs-scripting/cstrcache.cpp

@@ -0,0 +1,28 @@
+#include <unordered_map>
+#include <string>
+
+#include "cstrcache.h"
+
+using namespace std;
+
+struct const_string_table {
+	unordered_map<string, string> strings;
+};
+
+static struct const_string_table table;
+
+const char *cstrcache_get(const char *str)
+{
+	if (!str || !*str)
+		return "";
+
+	auto &strings = table.strings;
+	auto pair = strings.find(str);
+
+	if (pair == strings.end()) {
+		strings[str] = str;
+		pair = strings.find(str);
+	}
+
+	return pair->second.c_str();
+}

+ 13 - 0
deps/obs-scripting/cstrcache.h

@@ -0,0 +1,13 @@
+#pragma once
+
+/* simple constant string cache table using STL unordered_map as storage */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern const char *cstrcache_get(const char *str);
+
+#ifdef __cplusplus
+}
+#endif

+ 91 - 0
deps/obs-scripting/obs-scripting-callback.h

@@ -0,0 +1,91 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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/>.
+******************************************************************************/
+
+#pragma once
+
+#include <callback/calldata.h>
+#include <util/threading.h>
+#include <util/bmem.h>
+#include "obs-scripting-internal.h"
+
+extern pthread_mutex_t detach_mutex;
+extern struct script_callback *detached_callbacks;
+
+struct script_callback {
+	struct script_callback *next;
+	struct script_callback **p_prev_next;
+
+	void (*on_remove)(void *p_cb);
+	obs_script_t *script;
+	calldata_t extra;
+
+	bool removed;
+};
+
+static inline void *add_script_callback(
+		struct script_callback **first,
+		obs_script_t *script,
+		size_t extra_size)
+{
+	struct script_callback *cb = bzalloc(sizeof(*cb) + extra_size);
+	cb->script = script;
+
+	struct script_callback *next = *first;
+	cb->next = next;
+	cb->p_prev_next = first;
+	if (next) next->p_prev_next = &cb->next;
+	*first = cb;
+
+	return cb;
+}
+
+static inline void remove_script_callback(struct script_callback *cb)
+{
+	cb->removed = true;
+
+	struct script_callback *next = cb->next;
+	if (next) next->p_prev_next = cb->p_prev_next;
+	*cb->p_prev_next = cb->next;
+
+	pthread_mutex_lock(&detach_mutex);
+	next = detached_callbacks;
+	cb->next = next;
+	if (next) next->p_prev_next = &cb->next;
+	cb->p_prev_next = &detached_callbacks;
+	detached_callbacks = cb;
+	pthread_mutex_unlock(&detach_mutex);
+
+	if (cb->on_remove)
+		cb->on_remove(cb);
+}
+
+static inline void just_free_script_callback(struct script_callback *cb)
+{
+	calldata_free(&cb->extra);
+	bfree(cb);
+}
+
+static inline void free_script_callback(struct script_callback *cb)
+{
+	pthread_mutex_lock(&detach_mutex);
+	struct script_callback *next = cb->next;
+	if (next) next->p_prev_next = cb->p_prev_next;
+	*cb->p_prev_next = cb->next;
+	pthread_mutex_unlock(&detach_mutex);
+
+	just_free_script_callback(cb);
+}

+ 22 - 0
deps/obs-scripting/obs-scripting-config.h.in

@@ -0,0 +1,22 @@
+#pragma once
+
+#ifndef TRUE
+#define TRUE 1
+#endif
+
+#ifndef ON
+#define ON 1
+#endif
+
+#ifndef FALSE
+#define FALSE 0
+#endif
+
+#ifndef OFF
+#define OFF 0
+#endif
+
+#define PYTHON_LIB     "@PYTHON_LIB@"
+#define COMPILE_LUA    @LUAJIT_FOUND@
+#define COMPILE_PYTHON @PYTHON_FOUND@
+#define UI_ENABLED     @UI_ENABLED@

+ 51 - 0
deps/obs-scripting/obs-scripting-internal.h

@@ -0,0 +1,51 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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/>.
+******************************************************************************/
+
+#pragma once
+
+#include <util/dstr.h>
+#include <callback/calldata.h>
+#include "obs-scripting.h"
+
+struct obs_script {
+	enum obs_script_lang type;
+	bool loaded;
+
+	obs_data_t *settings;
+
+	struct dstr path;
+	struct dstr file;
+	struct dstr desc;
+};
+
+struct script_callback;
+typedef void (*defer_call_cb)(void *param);
+
+extern void defer_call_post(defer_call_cb call, void *cb);
+
+extern void script_log(obs_script_t *script, int level, const char *format, ...);
+extern void script_log_va(obs_script_t *script, int level, const char *format,
+		va_list args);
+
+#define script_error(script, format, ...) \
+	script_log(script, LOG_ERROR, format, ##__VA_ARGS__)
+#define script_warn(script, format, ...) \
+	script_log(script, LOG_WARNING, format, ##__VA_ARGS__)
+#define script_info(script, format, ...) \
+	script_log(script, LOG_INFO, format, ##__VA_ARGS__)
+#define script_debug(script, format, ...) \
+	script_log(script, LOG_DEBUG, format, ##__VA_ARGS__)

+ 59 - 0
deps/obs-scripting/obs-scripting-logging.c

@@ -0,0 +1,59 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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-internal.h"
+#include <util/platform.h>
+
+static scripting_log_handler_t callback = NULL;
+static void *param = NULL;
+
+void script_log_va(obs_script_t *script, int level, const char *format,
+		va_list args)
+{
+	char msg[2048];
+	const char *lang = "(Unknown)";
+	size_t start_len;
+
+	switch (script->type) {
+	case OBS_SCRIPT_LANG_UNKNOWN: lang = "(Unknown language)"; break;
+	case OBS_SCRIPT_LANG_LUA:     lang = "Lua"; break;
+	case OBS_SCRIPT_LANG_PYTHON:  lang = "Python"; break;
+	}
+
+	start_len = snprintf(msg, sizeof(msg), "[%s: %s] ",
+			lang, script->file.array);
+	vsnprintf(msg + start_len, sizeof(msg) - start_len, format, args);
+
+	if (callback)
+		callback(param, script, level, msg + start_len);
+	blog(level, "%s", msg);
+}
+
+void script_log(obs_script_t *script, int level, const char *format, ...)
+{
+	va_list args;
+	va_start(args, format);
+	script_log_va(script, level, format, args);
+	va_end(args);
+}
+
+void obs_scripting_set_log_callback(scripting_log_handler_t handler,
+		void *log_param)
+{
+	callback = handler;
+	param = log_param;
+}

+ 315 - 0
deps/obs-scripting/obs-scripting-lua-frontend.c

@@ -0,0 +1,315 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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-module.h>
+#include <obs-frontend-api.h>
+
+#include "obs-scripting-lua.h"
+
+#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(func, args, rets) \
+	call_func_(script, cb->reg_idx, args, rets, #func, "frontend API")
+
+/* ----------------------------------- */
+
+static int get_scene_names(lua_State *script)
+{
+	char **names = obs_frontend_get_scene_names();
+	char **name = names;
+	int i = 0;
+
+	lua_newtable(script);
+
+	while (name && *name) {
+		lua_pushstring(script, *name);
+		lua_rawseti(script, -2, i++);
+		name++;
+	}
+
+	bfree(names);
+	return 1;
+}
+
+static int get_scenes(lua_State *script)
+{
+	struct obs_frontend_source_list list = {0};
+	obs_frontend_get_scenes(&list);
+
+	lua_createtable(script, (int)list.sources.num, 0);
+
+	for (size_t i = 0; i < list.sources.num; i++) {
+		obs_source_t *source = list.sources.array[i];
+		ls_push_libobs_obj(obs_source_t, source, false);
+		lua_rawseti(script, -2, (int)i++);
+	}
+
+	da_free(list.sources);
+	return 1;
+}
+
+static int get_current_scene(lua_State *script)
+{
+	obs_source_t *source = obs_frontend_get_current_scene();
+	ls_push_libobs_obj(obs_source_t, source, false);
+	return 1;
+}
+
+static int set_current_scene(lua_State *script)
+{
+	obs_source_t *source = NULL;
+	ls_get_libobs_obj(obs_source_t, 1, &source);
+	obs_frontend_set_current_scene(source);
+	return 0;
+}
+
+static int get_transitions(lua_State *script)
+{
+	struct obs_frontend_source_list list = {0};
+	obs_frontend_get_transitions(&list);
+
+	lua_createtable(script, (int)list.sources.num, 0);
+
+	for (size_t i = 0; i < list.sources.num; i++) {
+		obs_source_t *source = list.sources.array[i];
+		ls_push_libobs_obj(obs_source_t, source, false);
+		lua_rawseti(script, -2, (int)i++);
+	}
+
+	da_free(list.sources);
+	return 1;
+}
+
+static int get_current_transition(lua_State *script)
+{
+	obs_source_t *source = obs_frontend_get_current_transition();
+	ls_push_libobs_obj(obs_source_t, source, false);
+	return 1;
+}
+
+static int set_current_transition(lua_State *script)
+{
+	obs_source_t *source = NULL;
+	ls_get_libobs_obj(obs_source_t, 1, &source);
+	obs_frontend_set_current_transition(source);
+	return 0;
+}
+
+static int get_scene_collections(lua_State *script)
+{
+	char **names = obs_frontend_get_scene_collections();
+	char **name = names;
+	int i = 0;
+
+	lua_newtable(script);
+
+	while (name && *name) {
+		lua_pushstring(script, *name);
+		lua_rawseti(script, -2, i++);
+		name++;
+	}
+
+	bfree(names);
+	return 1;
+}
+
+static int get_current_scene_collection(lua_State *script)
+{
+	char *name = obs_frontend_get_current_scene_collection();
+	lua_pushstring(script, name);
+	bfree(name);
+	return 1;
+}
+
+static int set_current_scene_collection(lua_State *script)
+{
+	if (lua_isstring(script, 1)) {
+		const char *name = lua_tostring(script, 1);
+		obs_frontend_set_current_scene_collection(name);
+	}
+	return 0;
+}
+
+static int get_profiles(lua_State *script)
+{
+	char **names = obs_frontend_get_profiles();
+	char **name = names;
+	int i = 0;
+
+	lua_newtable(script);
+
+	while (name && *name) {
+		lua_pushstring(script, *name);
+		lua_rawseti(script, -2, i++);
+		name++;
+	}
+
+	bfree(names);
+	return 1;
+}
+
+static int get_current_profile(lua_State *script)
+{
+	char *name = obs_frontend_get_current_profile();
+	lua_pushstring(script, name);
+	bfree(name);
+	return 1;
+}
+
+static int set_current_profile(lua_State *script)
+{
+	if (lua_isstring(script, 1)) {
+		const char *name = lua_tostring(script, 1);
+		obs_frontend_set_current_profile(name);
+	}
+	return 0;
+}
+
+/* ----------------------------------- */
+
+static void frontend_event_callback(enum obs_frontend_event event, void *priv)
+{
+	struct lua_obs_callback *cb = priv;
+	lua_State *script = cb->script;
+
+	if (cb->base.removed) {
+		obs_frontend_remove_event_callback(frontend_event_callback, cb);		
+		return;
+	}
+
+	lock_callback();
+
+	lua_pushinteger(script, (int)event);
+	call_func(frontend_event_callback, 1, 0);
+
+	unlock_callback();
+}
+
+static int remove_event_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 add_event_callback_defer(void *cb)
+{
+	obs_frontend_add_event_callback(frontend_event_callback, cb);
+}
+
+static int add_event_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(add_event_callback_defer, cb);
+	return 0;
+}
+
+/* ----------------------------------- */
+
+static void frontend_save_callback(obs_data_t *save_data, bool saving,
+		void *priv)
+{
+	struct lua_obs_callback *cb = priv;
+	lua_State *script = cb->script;
+
+	if (cb->base.removed) {
+		obs_frontend_remove_save_callback(frontend_save_callback, cb);
+		return;
+	}
+
+	lock_callback();
+
+	ls_push_libobs_obj(obs_data_t, save_data, false);
+	lua_pushboolean(script, saving);
+	call_func(frontend_save_callback, 2, 0);
+
+	unlock_callback();
+}
+
+static int remove_save_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 add_save_callback_defer(void *cb)
+{
+	obs_frontend_add_save_callback(frontend_save_callback, cb);
+}
+
+static int add_save_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(add_save_callback_defer, cb);
+	return 0;
+}
+
+/* ----------------------------------- */
+
+void add_lua_frontend_funcs(lua_State *script)
+{
+	lua_getglobal(script, "obslua");
+
+#define add_func(name) \
+	do { \
+		lua_pushstring(script, "obs_frontend_" #name); \
+		lua_pushcfunction(script, name); \
+		lua_rawset(script, -3); \
+	} while (false)
+
+	add_func(get_scene_names);
+	add_func(get_scenes);
+	add_func(get_current_scene);
+	add_func(set_current_scene);
+	add_func(get_transitions);
+	add_func(get_current_transition);
+	add_func(set_current_transition);
+	add_func(get_scene_collections);
+	add_func(get_current_scene_collection);
+	add_func(set_current_scene_collection);
+	add_func(get_profiles);
+	add_func(get_current_profile);
+	add_func(set_current_profile);
+	add_func(remove_event_callback);
+	add_func(add_event_callback);
+	add_func(remove_save_callback);
+	add_func(add_save_callback);
+#undef add_func
+
+	lua_pop(script, 1);
+}

+ 762 - 0
deps/obs-scripting/obs-scripting-lua-source.c

@@ -0,0 +1,762 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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 "cstrcache.h"
+
+#include <obs-module.h>
+
+/* ========================================================================= */
+
+static inline const char *get_table_string_(lua_State *script, int idx,
+		const char *name, const char *func)
+{
+	const char *str = "";
+
+	lua_pushstring(script, name);
+	lua_gettable(script, idx - 1);
+	if (!lua_isstring(script, -1))
+		warn("%s: no item '%s' of type %s", func, name, "string");
+	else
+		str = cstrcache_get(lua_tostring(script, -1));
+	lua_pop(script, 1);
+
+	return str;
+}
+
+static inline int get_table_int_(lua_State *script, int idx,
+		const char *name, const char *func)
+{
+	int val = 0;
+
+	lua_pushstring(script, name);
+	lua_gettable(script, idx - 1);
+	val = (int)lua_tointeger(script, -1);
+	lua_pop(script, 1);
+
+	UNUSED_PARAMETER(func);
+
+	return val;
+}
+
+static inline void get_callback_from_table_(lua_State *script, int idx,
+		const char *name, int *p_reg_idx, const char *func)
+{
+	*p_reg_idx = LUA_REFNIL;
+
+	lua_pushstring(script, name);
+	lua_gettable(script, idx - 1);
+	if (!lua_isfunction(script, -1)) {
+		if (!lua_isnil(script, -1)) {
+			warn("%s: item '%s' is not a function", func, name);
+		}
+		lua_pop(script, 1);
+	} else {
+		*p_reg_idx = luaL_ref(script, LUA_REGISTRYINDEX);
+	}
+}
+
+#define get_table_string(script, idx, name) \
+	get_table_string_(script, idx, name, __FUNCTION__)
+#define get_table_int(script, idx, name) \
+	get_table_int_(script, idx, name, __FUNCTION__)
+#define get_callback_from_table(script, idx, name, p_reg_idx) \
+	get_callback_from_table_(script, idx, name, p_reg_idx, __FUNCTION__)
+
+bool ls_get_libobs_obj_(lua_State * script,
+                        const char *type,
+                        int         lua_idx,
+                        void *      libobs_out,
+                        const char *id,
+                        const char *func,
+                        int         line)
+{
+	swig_type_info *info = SWIG_TypeQuery(script, type);
+	if (info == NULL) {
+		warn("%s:%d: SWIG could not find type: %s%s%s",
+		     func,
+		     line,
+		     id ? id : "",
+		     id ? "::" : "",
+		     type);
+		return false;
+	}
+
+	int ret = SWIG_ConvertPtr(script, lua_idx, libobs_out, info, 0);
+	if (!SWIG_IsOK(ret)) {
+		warn("%s:%d: SWIG failed to convert lua object to obs "
+		     "object: %s%s%s",
+		     func,
+		     line,
+		     id ? id : "",
+		     id ? "::" : "",
+		     type);
+		return false;
+	}
+
+	return true;
+}
+
+#define ls_get_libobs_obj(type, lua_index, obs_obj) \
+	ls_get_libobs_obj_(ls->script, #type " *", lua_index, obs_obj, \
+			ls->id, __FUNCTION__, __LINE__)
+
+bool ls_push_libobs_obj_(lua_State * script,
+                         const char *type,
+                         void *      libobs_in,
+                         bool        ownership,
+                         const char *id,
+                         const char *func,
+                         int         line)
+{
+	swig_type_info *info = SWIG_TypeQuery(script, type);
+	if (info == NULL) {
+		warn("%s:%d: SWIG could not find type: %s%s%s",
+		     func,
+		     line,
+		     id ? id : "",
+		     id ? "::" : "",
+		     type);
+		return false;
+	}
+
+	SWIG_NewPointerObj(script, libobs_in, info, (int)ownership);
+	return true;
+}
+
+#define ls_push_libobs_obj(type, obs_obj, ownership) \
+	ls_push_libobs_obj_(ls->script, #type " *", obs_obj, ownership, \
+			ls->id, __FUNCTION__, __LINE__)
+
+/* ========================================================================= */
+
+struct obs_lua_data;
+
+struct obs_lua_source {
+	struct obs_lua_script *data;
+
+	lua_State * script;
+	const char *id;
+	const char *display_name;
+	int         func_create;
+	int         func_destroy;
+	int         func_get_width;
+	int         func_get_height;
+	int         func_get_defaults;
+	int         func_get_properties;
+	int         func_update;
+	int         func_activate;
+	int         func_deactivate;
+	int         func_show;
+	int         func_hide;
+	int         func_video_tick;
+	int         func_video_render;
+	int         func_save;
+	int         func_load;
+
+	pthread_mutex_t definition_mutex;
+	struct obs_lua_data *first_source;
+
+	struct obs_lua_source *next;
+	struct obs_lua_source **p_prev_next;
+};
+
+extern pthread_mutex_t lua_source_def_mutex;
+struct obs_lua_source *first_source_def = NULL;
+
+struct obs_lua_data {
+	obs_source_t *         source;
+	struct obs_lua_source *ls;
+	int                    lua_data_ref;
+
+	struct obs_lua_data *next;
+	struct obs_lua_data **p_prev_next;
+};
+
+#define call_func(name, args, rets) \
+	call_func_(ls->script, ls->func_ ## name, args, rets, #name, \
+			ls->display_name)
+#define have_func(name) \
+	(ls->func_ ## name != LUA_REFNIL)
+#define ls_push_data() \
+	lua_rawgeti(ls->script, LUA_REGISTRYINDEX, ld->lua_data_ref)
+#define ls_pop(count) \
+	lua_pop(ls->script, count)
+#define lock_script() \
+	struct obs_lua_script *__data = ls->data; \
+	struct obs_lua_script *__prev_script = current_lua_script; \
+	current_lua_script = __data; \
+	pthread_mutex_lock(&__data->mutex);
+#define unlock_script() \
+	pthread_mutex_unlock(&__data->mutex); \
+	current_lua_script = __prev_script;
+
+static const char *obs_lua_source_get_name(void *type_data)
+{
+	struct obs_lua_source *ls = type_data;
+	return ls->display_name;
+}
+
+static void *obs_lua_source_create(obs_data_t *settings, obs_source_t *source)
+{
+	struct obs_lua_source *ls = obs_source_get_type_data(source);
+	struct obs_lua_data *data = NULL;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(create))
+		goto fail;
+
+	lock_script();
+
+	ls_push_libobs_obj(obs_data_t, settings, false);
+	ls_push_libobs_obj(obs_source_t, source, false);
+	call_func(create, 2, 1);
+
+	int lua_data_ref = luaL_ref(ls->script, LUA_REGISTRYINDEX);
+	if (lua_data_ref != LUA_REFNIL) {
+		data               = bmalloc(sizeof(*data));
+		data->source       = source;
+		data->ls           = ls;
+		data->lua_data_ref = lua_data_ref;
+	}
+
+	unlock_script();
+
+	if (data) {
+		struct obs_lua_data *next = ls->first_source;
+		data->next = next;
+		data->p_prev_next = &ls->first_source;
+		if (next) next->p_prev_next = &data->next;
+		ls->first_source = data;
+	}
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+	return data;
+}
+
+static void call_destroy(struct obs_lua_data *ld)
+{
+	struct obs_lua_source *ls = ld->ls;
+
+	ls_push_data();
+	call_func(destroy, 1, 0);
+	luaL_unref(ls->script, LUA_REGISTRYINDEX, ld->lua_data_ref);
+	ld->lua_data_ref = LUA_REFNIL;
+}
+
+static void obs_lua_source_destroy(void *data)
+{
+	struct obs_lua_data *  ld = data;
+	struct obs_lua_source *ls = ld->ls;
+	struct obs_lua_data *  next;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(destroy))
+		goto fail;
+
+	lock_script();
+	call_destroy(ld);
+	unlock_script();
+
+fail:
+	next = ld->next;
+	*ld->p_prev_next = next;
+	if (next) next->p_prev_next = ld->p_prev_next;
+
+	bfree(data);
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+static uint32_t obs_lua_source_get_width(void *data)
+{
+	struct obs_lua_data *  ld    = data;
+	struct obs_lua_source *ls    = ld->ls;
+	uint32_t               width = 0;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(get_width))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	if (call_func(get_width, 1, 1)) {
+		width = (uint32_t)lua_tointeger(ls->script, -1);
+		ls_pop(1);
+	}
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+	return width;
+}
+
+static uint32_t obs_lua_source_get_height(void *data)
+{
+	struct obs_lua_data *  ld     = data;
+	struct obs_lua_source *ls     = ld->ls;
+	uint32_t               height = 0;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(get_height))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	if (call_func(get_height, 1, 1)) {
+		height = (uint32_t)lua_tointeger(ls->script, -1);
+		ls_pop(1);
+	}
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+	return height;
+}
+
+static void obs_lua_source_get_defaults(void *type_data, obs_data_t *settings)
+{
+	struct obs_lua_source *ls = type_data;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(get_defaults))
+		goto fail;
+
+	lock_script();
+
+	ls_push_libobs_obj(obs_data_t, settings, false);
+	call_func(get_defaults, 1, 0);
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+static obs_properties_t *obs_lua_source_get_properties(void *data)
+{
+	struct obs_lua_data *  ld    = data;
+	struct obs_lua_source *ls    = ld->ls;
+	obs_properties_t *     props = NULL;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(get_properties))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	if (call_func(get_properties, 1, 1)) {
+		ls_get_libobs_obj(obs_properties_t, -1, &props);
+		ls_pop(1);
+	}
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+	return props;
+}
+
+static void obs_lua_source_update(void *data, obs_data_t *settings)
+{
+	struct obs_lua_data *  ld = data;
+	struct obs_lua_source *ls = ld->ls;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(update))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	ls_push_libobs_obj(obs_data_t, settings, false);
+	call_func(update, 2, 0);
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+#define DEFINE_VOID_DATA_CALLBACK(name) \
+	static void obs_lua_source_ ## name(void *data) \
+	{ \
+		struct obs_lua_data *  ld = data; \
+		struct obs_lua_source *ls = ld->ls; \
+		if (!have_func(name)) \
+			return; \
+		lock_script(); \
+		ls_push_data(); \
+		call_func(name, 1, 0); \
+		unlock_script(); \
+	}
+DEFINE_VOID_DATA_CALLBACK(activate)
+DEFINE_VOID_DATA_CALLBACK(deactivate)
+DEFINE_VOID_DATA_CALLBACK(show)
+DEFINE_VOID_DATA_CALLBACK(hide)
+#undef DEFINE_VOID_DATA_CALLBACK
+
+static void obs_lua_source_video_tick(void *data, float seconds)
+{
+	struct obs_lua_data *  ld = data;
+	struct obs_lua_source *ls = ld->ls;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(video_tick))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	lua_pushnumber(ls->script, (double)seconds);
+	call_func(video_tick, 2, 0);
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+static void obs_lua_source_video_render(void *data, gs_effect_t *effect)
+{
+	struct obs_lua_data *  ld = data;
+	struct obs_lua_source *ls = ld->ls;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(video_render))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	ls_push_libobs_obj(gs_effect_t, effect, false);
+	call_func(video_render, 2, 0);
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+static void obs_lua_source_save(void *data, obs_data_t *settings)
+{
+	struct obs_lua_data *  ld = data;
+	struct obs_lua_source *ls = ld->ls;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(save))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	ls_push_libobs_obj(obs_data_t, settings, false);
+	call_func(save, 2, 0);
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+static void obs_lua_source_load(void *data, obs_data_t *settings)
+{
+	struct obs_lua_data *  ld = data;
+	struct obs_lua_source *ls = ld->ls;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+	if (!ls->script)
+		goto fail;
+	if (!have_func(load))
+		goto fail;
+
+	lock_script();
+
+	ls_push_data();
+	ls_push_libobs_obj(obs_data_t, settings, false);
+	call_func(load, 2, 0);
+
+	unlock_script();
+
+fail:
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+static void source_type_unload(struct obs_lua_source *ls)
+{
+#define unref(name) \
+	luaL_unref(ls->script, LUA_REGISTRYINDEX, name); \
+	name = LUA_REFNIL
+
+	unref(ls->func_create);
+	unref(ls->func_destroy);
+	unref(ls->func_get_width);
+	unref(ls->func_get_height);
+	unref(ls->func_get_defaults);
+	unref(ls->func_get_properties);
+	unref(ls->func_update);
+	unref(ls->func_activate);
+	unref(ls->func_deactivate);
+	unref(ls->func_show);
+	unref(ls->func_hide);
+	unref(ls->func_video_tick);
+	unref(ls->func_video_render);
+	unref(ls->func_save);
+	unref(ls->func_load);
+#undef unref
+}
+
+static void obs_lua_source_free_type_data(void *type_data)
+{
+	struct obs_lua_source *ls = type_data;
+
+	pthread_mutex_lock(&ls->definition_mutex);
+
+	if (ls->script) {
+		lock_script();
+		source_type_unload(ls);
+		unlock_script();
+		ls->script = NULL;
+	}
+
+	pthread_mutex_unlock(&ls->definition_mutex);
+	pthread_mutex_destroy(&ls->definition_mutex);
+	bfree(ls);
+}
+
+EXPORT void obs_enable_source_type(const char *name, bool enable);
+
+static inline struct obs_lua_source *find_existing(const char *id)
+{
+	struct obs_lua_source *existing = NULL;
+
+	pthread_mutex_lock(&lua_source_def_mutex);
+	struct obs_lua_source *ls = first_source_def;
+	while (ls) {
+		/* can compare pointers here due to string table */
+		if (ls->id == id) {
+			existing = ls;
+			break;
+		}
+
+		ls = ls->next;
+	}
+	pthread_mutex_unlock(&lua_source_def_mutex);
+
+	return existing;
+}
+
+static int obs_lua_register_source(lua_State *script)
+{
+	struct obs_lua_source ls = {0};
+	struct obs_lua_source *existing = NULL;
+	struct obs_lua_source *v = NULL;
+	struct obs_source_info info = {0};
+	const char *id;
+
+	if (!verify_args1(script, is_table))
+		goto fail;
+
+	id = get_table_string(script, -1, "id");
+	if (!id || !*id)
+		goto fail;
+
+	/* redefinition */
+	existing = find_existing(id);
+	if (existing) {
+		if (existing->script) {
+			existing = NULL;
+			goto fail;
+		}
+
+		pthread_mutex_lock(&existing->definition_mutex);
+	}
+
+	v = existing ? existing : &ls;
+
+	v->script = script;
+	v->id     = id;
+
+	info.id   = v->id;
+	info.type = (enum obs_source_type)get_table_int(script, -1, "type");
+
+	info.output_flags = get_table_int(script, -1, "output_flags");
+
+	lua_pushstring(script, "get_name");
+	lua_gettable(script, -2);
+	if (lua_pcall(script, 0, 1, 0) == 0) {
+		v->display_name = cstrcache_get(lua_tostring(script, -1));
+		lua_pop(script, 1);
+	}
+
+	if (!v->display_name ||
+	    !*v->display_name ||
+	    !*info.id ||
+	    !info.output_flags)
+		goto fail;
+
+#define get_callback(val) \
+	do { \
+		get_callback_from_table(script, -1, #val, &v->func_ ## val); \
+		info.val = obs_lua_source_ ## val; \
+	} while (false)
+
+	get_callback(create);
+	get_callback(destroy);
+	get_callback(get_width);
+	get_callback(get_height);
+	get_callback(get_properties);
+	get_callback(update);
+	get_callback(activate);
+	get_callback(deactivate);
+	get_callback(show);
+	get_callback(hide);
+	get_callback(video_tick);
+	get_callback(video_render);
+	get_callback(save);
+	get_callback(load);
+#undef get_callback
+
+	get_callback_from_table(script, -1, "get_defaults",
+			&v->func_get_defaults);
+
+	if (!existing) {
+		ls.data = current_lua_script;
+
+		pthread_mutex_init(&ls.definition_mutex, NULL);
+		info.type_data      = bmemdup(&ls, sizeof(ls));
+		info.free_type_data = obs_lua_source_free_type_data;
+		info.get_name       = obs_lua_source_get_name;
+		info.get_defaults2  = obs_lua_source_get_defaults;
+		obs_register_source(&info);
+
+		pthread_mutex_lock(&lua_source_def_mutex);
+		v = info.type_data;
+
+		struct obs_lua_source *next = first_source_def;
+		v->next = next;
+		if (next) next->p_prev_next = &v->next;
+		v->p_prev_next = &first_source_def;
+		first_source_def = v;
+
+		pthread_mutex_unlock(&lua_source_def_mutex);
+	} else {
+		existing->script = script;
+		existing->data = current_lua_script;
+		obs_enable_source_type(id, true);
+
+		struct obs_lua_data *ld = v->first_source;
+		while (ld) {
+			struct obs_lua_source *ls = v;
+
+			if (have_func(create)) {
+				obs_source_t *source = ld->source;
+				obs_data_t *settings = obs_source_get_settings(
+						source);
+
+				ls_push_libobs_obj(obs_data_t, settings, false);
+				ls_push_libobs_obj(obs_source_t, source, false);
+				call_func(create, 2, 1);
+
+				ld->lua_data_ref = luaL_ref(ls->script,
+						LUA_REGISTRYINDEX);
+				obs_data_release(settings);
+			}
+
+			ld = ld->next;
+		}
+	}
+
+fail:
+	if (existing) {
+		pthread_mutex_unlock(&existing->definition_mutex);
+	}
+	return 0;
+}
+
+/* ========================================================================= */
+
+void add_lua_source_functions(lua_State *script)
+{
+	lua_getglobal(script, "obslua");
+
+	lua_pushstring(script, "obs_register_source");
+	lua_pushcfunction(script, obs_lua_register_source);
+	lua_rawset(script, -3);
+
+	lua_pop(script, 1);
+}
+
+static inline void undef_source_type(struct obs_lua_script *data,
+		struct obs_lua_source *ls)
+{
+	pthread_mutex_lock(&ls->definition_mutex);
+	pthread_mutex_lock(&data->mutex);
+
+	obs_enable_source_type(ls->id, false);
+
+	struct obs_lua_data *ld = ls->first_source;
+	while (ld) {
+		call_destroy(ld);
+		ld = ld->next;
+	}
+
+	source_type_unload(ls);
+	ls->script = NULL;
+
+	pthread_mutex_unlock(&data->mutex);
+	pthread_mutex_unlock(&ls->definition_mutex);
+}
+
+void undef_lua_script_sources(struct obs_lua_script *data)
+{
+	pthread_mutex_lock(&lua_source_def_mutex);
+
+	struct obs_lua_source *def = first_source_def;
+	while (def) {
+		if (def->script == data->script)
+			undef_source_type(data, def);
+		def = def->next;
+	}
+
+	pthread_mutex_unlock(&lua_source_def_mutex);
+}

+ 1287 - 0
deps/obs-scripting/obs-scripting-lua.c

@@ -0,0 +1,1287 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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 "obs-scripting-config.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
+
+static const char *startup_script_template = "\
+for val in pairs(package.preload) do\n\
+	package.preload[val] = nil\n\
+end\n\
+require \"obslua\"\n\
+package.path = package.path .. \"%s\"\n";
+
+static const char *get_script_path_func = "\
+function script_path()\n\
+	 return \"%s\"\n\
+end\n\
+package.path = package.path .. \";\" .. script_path() .. \"/?.lua\"\n";
+
+static char *startup_script = NULL;
+
+static pthread_mutex_t tick_mutex = PTHREAD_MUTEX_INITIALIZER;
+static struct obs_lua_script *first_tick_script = NULL;
+
+pthread_mutex_t lua_source_def_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+#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);
+
+#if UI_ENABLED
+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;
+
+	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);
+
+	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;
+	}
+
+	add_lua_source_functions(script);
+	add_hook_functions(script);
+#if UI_ENABLED
+	add_lua_frontend_funcs(script);
+#endif
+
+	if (luaL_loadfile(script, data->base.path.array) != 0) {
+		script_warn(&data->base, "Error loading file: %s",
+				lua_tostring(script, -1));
+		goto fail;
+	}
+
+	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_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);
+	}
+
+	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;
+
+	current_lua_script = data;
+
+	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;
+	success = true;
+
+fail:
+	if (script) {
+		lua_settop(script, 0);
+		pthread_mutex_unlock(&data->mutex);
+	}
+
+	if (!success) {
+		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 = PTHREAD_MUTEX_INITIALIZER;
+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 (p_cb->removed)
+		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 (cb->base.removed) {
+		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 (cb->base.removed) {
+		obs_remove_tick_callback(obs_lua_tick_callback, cb);
+		return;
+	}
+
+	lock_callback();
+
+	lua_pushnumber(script, (lua_Number)seconds);
+	call_func(obs_lua_tick_callback, 2, 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 (cb->base.removed) {
+		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 (cb->base.removed) {
+		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 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 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 (cb->base.removed)
+		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 (cb->base.removed)
+		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 (cb->base.removed)
+		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 (cb->base.removed)
+		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_scene_enum_items", scene_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;
+
+		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;
+	}
+	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 (cb->base.removed) {
+			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)
+			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_mutexattr_t attr;
+	pthread_mutexattr_init(&attr);
+	pthread_mutex_init_value(&data->mutex);
+	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
+
+	if (pthread_mutex_init(&data->mutex, &attr) != 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;
+
+	/* ---------------------------- */
+	/* 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);
+
+	lua_getglobal(script, "script_unload");
+	lua_pcall(script, 0, 0, 0);
+
+	/* ---------------------------- */
+	/* remove all callbacks         */
+
+	struct lua_obs_callback *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;
+}
+
+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;
+}
+
+/* -------------------------------------------- */
+
+void obs_lua_load(void)
+{
+	struct dstr dep_paths = {0};
+	struct dstr tmp = {0};
+
+	pthread_mutexattr_t attr;
+	pthread_mutexattr_init(&attr);
+	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
+
+	pthread_mutex_init(&tick_mutex, NULL);
+	pthread_mutex_init(&timer_mutex, &attr);
+	pthread_mutex_init(&lua_source_def_mutex, NULL);
+
+	/* ---------------------------------------------- */
+	/* Initialize Lua startup script                  */
+
+	dstr_printf(&tmp, startup_script_template,
+			dep_paths.array);
+	startup_script = tmp.array;
+
+	dstr_free(&dep_paths);
+
+	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);
+}

+ 262 - 0
deps/obs-scripting/obs-scripting-lua.h

@@ -0,0 +1,262 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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/>.
+******************************************************************************/
+
+#pragma once
+
+/* ---------------------------- */
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4244)
+#pragma warning(disable : 4267)
+#endif
+
+#define SWIG_TYPE_TABLE obslua
+#include "swig/swigluarun.h"
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+/* ---------------------------- */
+
+#include <util/threading.h>
+#include <util/base.h>
+#include <util/bmem.h>
+
+#include "obs-scripting-internal.h"
+#include "obs-scripting-callback.h"
+
+#define do_log(level, format, ...) \
+	blog(level, "[Lua] " format, ##__VA_ARGS__)
+
+#define warn(format, ...)  do_log(LOG_WARNING, format, ##__VA_ARGS__)
+#define info(format, ...)  do_log(LOG_INFO,    format, ##__VA_ARGS__)
+#define debug(format, ...) do_log(LOG_DEBUG,   format, ##__VA_ARGS__)
+
+/* ------------------------------------------------------------ */
+
+struct obs_lua_script;
+struct lua_obs_callback;
+
+extern THREAD_LOCAL struct lua_obs_callback *current_lua_cb;
+extern THREAD_LOCAL struct obs_lua_script *current_lua_script;
+
+/* ------------------------------------------------------------ */
+
+struct lua_obs_callback;
+
+struct obs_lua_script {
+	obs_script_t base;
+
+	struct dstr dir;
+	struct dstr log_chunk;
+
+	pthread_mutex_t mutex;
+	lua_State *script;
+
+	struct script_callback *first_callback;
+
+	int update;
+	int get_properties;
+	int save;
+
+	int tick;
+	struct obs_lua_script *next_tick;
+	struct obs_lua_script **p_prev_next_tick;
+
+	bool defined_sources;
+};
+
+#define lock_callback() \
+	struct obs_lua_script *__last_script = current_lua_script; \
+	struct lua_obs_callback *__last_callback = current_lua_cb; \
+	current_lua_cb = cb; \
+	current_lua_script = (struct obs_lua_script *)cb->base.script; \
+	pthread_mutex_lock(&current_lua_script->mutex);
+#define unlock_callback() \
+	pthread_mutex_unlock(&current_lua_script->mutex); \
+	current_lua_script = __last_script; \
+	current_lua_cb = __last_callback;
+
+/* ------------------------------------------------ */
+
+struct lua_obs_callback {
+	struct script_callback base;
+
+	lua_State *script;
+	int reg_idx;
+};
+
+static inline struct lua_obs_callback *add_lua_obs_callback_extra(
+		lua_State *script,
+		int stack_idx,
+		size_t extra_size)
+{
+	struct obs_lua_script *data = current_lua_script;
+	struct lua_obs_callback *cb = add_script_callback(
+			&data->first_callback,
+			(obs_script_t *)data,
+			sizeof(*cb) + extra_size);
+
+	lua_pushvalue(script, stack_idx);
+	cb->reg_idx = luaL_ref(script, LUA_REGISTRYINDEX);
+	cb->script = script;
+	return cb;
+}
+
+static inline struct lua_obs_callback *add_lua_obs_callback(
+		lua_State *script, int stack_idx)
+{
+	return add_lua_obs_callback_extra(script, stack_idx, 0);
+}
+
+static inline void *lua_obs_callback_extra_data(struct lua_obs_callback *cb)
+{
+	return (void*)&cb[1];
+}
+
+static inline struct obs_lua_script *lua_obs_callback_script(
+		struct lua_obs_callback *cb)
+{
+	return (struct obs_lua_script *)cb->base.script;
+}
+
+static inline struct lua_obs_callback *find_next_lua_obs_callback(
+		lua_State *script, struct lua_obs_callback *cb, int stack_idx)
+{
+	struct obs_lua_script *data = current_lua_script;
+
+	cb = cb ? (struct lua_obs_callback *)cb->base.next
+		: (struct lua_obs_callback *)data->first_callback;
+
+	while (cb) {
+		lua_rawgeti(script, LUA_REGISTRYINDEX, cb->reg_idx);
+		bool match = lua_rawequal(script, -1, stack_idx);
+		lua_pop(script, 1);
+
+		if (match)
+			break;
+
+		cb = (struct lua_obs_callback *)cb->base.next;
+	}
+
+	return cb;
+}
+
+static inline struct lua_obs_callback *find_lua_obs_callback(
+		lua_State *script, int stack_idx)
+{
+	return find_next_lua_obs_callback(script, NULL, stack_idx);
+}
+
+static inline void remove_lua_obs_callback(struct lua_obs_callback *cb)
+{
+	remove_script_callback(&cb->base);
+	luaL_unref(cb->script, LUA_REGISTRYINDEX, cb->reg_idx);
+}
+
+static inline void just_free_lua_obs_callback(struct lua_obs_callback *cb)
+{
+	just_free_script_callback(&cb->base);
+}
+
+static inline void free_lua_obs_callback(struct lua_obs_callback *cb)
+{
+	free_script_callback(&cb->base);
+}
+
+/* ------------------------------------------------ */
+
+static int is_ptr(lua_State *script, int idx)
+{
+	return lua_isuserdata(script, idx) || lua_isnil(script, idx);
+}
+
+static int is_table(lua_State *script, int idx)
+{
+	return lua_istable(script, idx);
+}
+
+static int is_function(lua_State *script, int idx)
+{
+	return lua_isfunction(script, idx);
+}
+
+typedef int (*param_cb)(lua_State *script, int idx);
+
+static inline bool verify_args1_(lua_State *script,
+		param_cb param1_check,
+		const char *func)
+{
+	if (lua_gettop(script) != 1) {
+		warn("Wrong number of parameters for %s", func);
+		return false;
+	}
+	if (!param1_check(script, 1)) {
+		warn("Wrong parameter type for parameter %d of %s", 1, func);
+		return false;
+	}
+
+	return true;
+}
+
+#define verify_args1(script, param1_check) \
+	verify_args1_(script, param1_check, __FUNCTION__)
+
+static inline bool call_func_(lua_State *script,
+		int reg_idx, int args, int rets,
+		const char *func, const char *display_name)
+{
+	if (reg_idx == LUA_REFNIL)
+		return false;
+
+	struct obs_lua_script *data = current_lua_script;
+
+	lua_rawgeti(script, LUA_REGISTRYINDEX, reg_idx);
+	lua_insert(script, -1 - args);
+
+	if (lua_pcall(script, args, rets, 0) != 0) {
+		script_warn(&data->base, "Failed to call %s for %s: %s", func,
+				display_name,
+				lua_tostring(script, -1));
+		lua_pop(script, 1);
+		return false;
+	}
+
+	return true;
+}
+
+bool ls_get_libobs_obj_(lua_State * script,
+                        const char *type,
+                        int         lua_idx,
+                        void *      libobs_out,
+                        const char *id,
+                        const char *func,
+                        int         line);
+bool ls_push_libobs_obj_(lua_State * script,
+                         const char *type,
+                         void *      libobs_in,
+                         bool        ownership,
+                         const char *id,
+                         const char *func,
+                         int         line);
+
+extern void add_lua_source_functions(lua_State *script);

+ 361 - 0
deps/obs-scripting/obs-scripting-python-frontend.c

@@ -0,0 +1,361 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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-module.h>
+#include <obs-frontend-api.h>
+
+#include "obs-scripting-python.h"
+
+#define libobs_to_py(type, obs_obj, ownership, py_obj) \
+	libobs_to_py_(#type " *", obs_obj, ownership, py_obj, \
+			NULL, __func__, __LINE__)
+#define py_to_libobs(type, py_obj, libobs_out) \
+	py_to_libobs_(#type " *", py_obj, libobs_out, \
+			NULL, __func__, __LINE__)
+
+/* ----------------------------------- */
+
+static PyObject *get_scene_names(PyObject *self, PyObject *args)
+{
+	char **names = obs_frontend_get_scene_names();
+	char **name = names;
+
+	PyObject *list = PyList_New(0);
+
+	while (name && *name) {
+		PyObject *py_name = PyUnicode_FromString(*name);
+		if (py_name) {
+			PyList_Append(list, py_name);
+			Py_DECREF(py_name);
+		}
+		name++;
+	}
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+
+	bfree(names);
+	return list;
+}
+
+static PyObject *get_scenes(PyObject *self, PyObject *args)
+{
+	struct obs_frontend_source_list list = {0};
+	obs_frontend_get_scenes(&list);
+
+	PyObject *ret = PyList_New(0);
+
+	for (size_t i = 0; i < list.sources.num; i++) {
+		obs_source_t *source = list.sources.array[i];
+		PyObject *py_source;
+
+		if (libobs_to_py(obs_source_t, source, false, &py_source)) {
+			PyList_Append(ret, py_source);
+			Py_DECREF(py_source);
+		}
+	}
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+
+	da_free(list.sources);
+	return ret;
+}
+
+static PyObject *get_current_scene(PyObject *self, PyObject *args)
+{
+	obs_source_t *source = obs_frontend_get_current_scene();
+
+	PyObject *py_source;
+	if (!libobs_to_py(obs_source_t, source, false, &py_source)) {
+		obs_source_release(source);
+		return python_none();
+	}
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+	return py_source;
+}
+
+static PyObject *set_current_scene(PyObject *self, PyObject *args)
+{
+	PyObject *py_source;
+	obs_source_t *source = NULL;
+
+	if (!parse_args(args, "O", &py_source))
+		return python_none();
+	if (!py_to_libobs(obs_source_t, py_source, &source))
+		return python_none();
+
+	UNUSED_PARAMETER(self);
+
+	obs_frontend_set_current_scene(source);
+	return python_none();
+}
+
+static PyObject *get_transitions(PyObject *self, PyObject *args)
+{
+	struct obs_frontend_source_list list = {0};
+	obs_frontend_get_transitions(&list);
+
+	PyObject *ret = PyList_New(0);
+
+	for (size_t i = 0; i < list.sources.num; i++) {
+		obs_source_t *source = list.sources.array[i];
+		PyObject *py_source;
+
+		if (libobs_to_py(obs_source_t, source, false, &py_source)) {
+			PyList_Append(ret, py_source);
+			Py_DECREF(py_source);
+		}
+	}
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+
+	da_free(list.sources);
+	return ret;
+}
+
+static PyObject *get_current_transition(PyObject *self, PyObject *args)
+{
+	obs_source_t *source = obs_frontend_get_current_transition();
+
+	PyObject *py_source;
+	if (!libobs_to_py(obs_source_t, source, false, &py_source)) {
+		obs_source_release(source);
+		return python_none();
+	}
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+	return py_source;
+}
+
+static PyObject *set_current_transition(PyObject *self, PyObject *args)
+{
+	PyObject *py_source;
+	obs_source_t *source = NULL;
+
+	if (!parse_args(args, "O", &py_source))
+		return python_none();
+	if (!py_to_libobs(obs_source_t, py_source, &source))
+		return python_none();
+
+	UNUSED_PARAMETER(self);
+
+	obs_frontend_set_current_transition(source);
+	return python_none();
+}
+
+static PyObject *get_scene_collections(PyObject *self, PyObject *args)
+{
+	char **names = obs_frontend_get_scene_collections();
+	char **name = names;
+
+	PyObject *list = PyList_New(0);
+
+	while (name && *name) {
+		PyObject *py_name = PyUnicode_FromString(*name);
+		if (py_name) {
+			PyList_Append(list, py_name);
+			Py_DECREF(py_name);
+		}
+		name++;
+	}
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+
+	bfree(names);
+	return list;
+}
+
+static PyObject *get_current_scene_collection(PyObject *self, PyObject *args)
+{
+	char *name = obs_frontend_get_current_scene_collection();
+	PyObject *ret = PyUnicode_FromString(name);
+	bfree(name);
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+	return ret;
+}
+
+static PyObject *set_current_scene_collection(PyObject *self, PyObject *args)
+{
+	const char *name;
+	if (!parse_args(args, "s", &name))
+		return python_none();
+
+	UNUSED_PARAMETER(self);
+
+	obs_frontend_set_current_scene_collection(name);
+	return python_none();
+}
+
+static PyObject *get_profiles(PyObject *self, PyObject *args)
+{
+	char **names = obs_frontend_get_profiles();
+	char **name = names;
+
+	PyObject *list = PyList_New(0);
+
+	while (name && *name) {
+		PyObject *py_name = PyUnicode_FromString(*name);
+		if (py_name) {
+			PyList_Append(list, py_name);
+			Py_DECREF(py_name);
+		}
+		name++;
+	}
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+
+	bfree(names);
+	return list;
+}
+
+static PyObject *get_current_profile(PyObject *self, PyObject *args)
+{
+	char *name = obs_frontend_get_current_profile();
+	PyObject *ret = PyUnicode_FromString(name);
+	bfree(name);
+
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+	return ret;
+}
+
+static PyObject *set_current_profile(PyObject *self, PyObject *args)
+{
+	const char *name;
+	if (!parse_args(args, "s", &name))
+		return python_none();
+
+	UNUSED_PARAMETER(self);
+
+	obs_frontend_set_current_profile(name);
+	return python_none();
+}
+
+/* ----------------------------------- */
+
+static void frontend_save_callback(obs_data_t *save_data, bool saving,
+		void *priv)
+{
+	struct python_obs_callback *cb = priv;
+
+	if (cb->base.removed) {
+		obs_frontend_remove_save_callback(frontend_save_callback, cb);
+		return;
+	}
+
+	lock_python();
+
+	PyObject *py_save_data;
+
+	if (libobs_to_py(obs_data_t, save_data, false, &py_save_data)) {
+		PyObject *args = Py_BuildValue("(Op)", py_save_data, saving);
+
+		struct python_obs_callback *last_cb = cur_python_cb;
+		cur_python_cb = cb;
+		cur_python_script = (struct obs_python_script *)cb->base.script;
+
+		PyObject *py_ret = PyObject_CallObject(cb->func, args);
+		Py_XDECREF(py_ret);
+		py_error();
+
+		cur_python_script = NULL;
+		cur_python_cb = last_cb;
+
+		Py_XDECREF(args);
+		Py_XDECREF(py_save_data);
+	}
+
+	unlock_python();
+}
+
+static PyObject *remove_save_callback(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_cb = NULL;
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "O", &py_cb))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = find_python_obs_callback(script, py_cb);
+	if (cb) remove_python_obs_callback(cb);
+	return python_none();
+}
+
+static void add_save_callback_defer(void *cb)
+{
+	obs_frontend_add_save_callback(frontend_save_callback, cb);
+}
+
+static PyObject *add_save_callback(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_cb = NULL;
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "O", &py_cb))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = add_python_obs_callback(script, py_cb);
+	defer_call_post(add_save_callback_defer, cb);
+	return python_none();
+}
+
+/* ----------------------------------- */
+
+void add_python_frontend_funcs(PyObject *module)
+{
+	static PyMethodDef funcs[] = {
+#define DEF_FUNC(c) {"obs_frontend_" #c, c, METH_VARARGS, NULL}
+
+		DEF_FUNC(get_scene_names),
+		DEF_FUNC(get_scenes),
+		DEF_FUNC(get_current_scene),
+		DEF_FUNC(set_current_scene),
+		DEF_FUNC(get_transitions),
+		DEF_FUNC(get_current_transition),
+		DEF_FUNC(set_current_transition),
+		DEF_FUNC(get_scene_collections),
+		DEF_FUNC(get_current_scene_collection),
+		DEF_FUNC(set_current_scene_collection),
+		DEF_FUNC(get_profiles),
+		DEF_FUNC(get_current_profile),
+		DEF_FUNC(set_current_profile),
+		DEF_FUNC(remove_save_callback),
+		DEF_FUNC(add_save_callback),
+
+#undef DEF_FUNC
+		{0}
+	};
+
+	add_functions_to_py_module(module, funcs);
+}

+ 149 - 0
deps/obs-scripting/obs-scripting-python-import.c

@@ -0,0 +1,149 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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 <util/dstr.h>
+#include <util/platform.h>
+
+#define NO_REDEFS
+#include "obs-scripting-python-import.h"
+#include "obs-scripting-config.h"
+
+#ifdef _MSC_VER
+#pragma warning(disable : 4152)
+#endif
+
+#ifdef _WIN32
+#define SO_EXT ".dll"
+#elif __APPLE__
+#define SO_EXT ".dylib"
+#endif
+
+bool import_python(const char *python_path)
+{
+	struct dstr lib_path;
+	bool success = false;
+	void *lib;
+
+	if (!python_path)
+		python_path = "";
+
+	dstr_init_copy(&lib_path, python_path);
+	dstr_replace(&lib_path, "\\", "/");
+	if (!dstr_is_empty(&lib_path)) {
+		dstr_cat(&lib_path, "/");
+	}
+	dstr_cat(&lib_path, PYTHON_LIB SO_EXT);
+
+	lib = os_dlopen(lib_path.array);
+	if (!lib) {
+		blog(LOG_WARNING, "[Python] Could not load library: %s",
+				lib_path.array);
+		goto fail;
+	}
+
+#define IMPORT_FUNC(x) \
+	do { \
+		Import_##x = os_dlsym(lib, #x); \
+		if (!Import_##x) { \
+			blog(LOG_WARNING, "[Python] Failed to import: %s", \
+					#x); \
+			goto fail; \
+		} \
+	} while (false)
+
+	IMPORT_FUNC(PyType_Ready);
+	IMPORT_FUNC(PyObject_GenericGetAttr);
+	IMPORT_FUNC(PyObject_IsTrue);
+	IMPORT_FUNC(Py_DecRef);
+	IMPORT_FUNC(PyObject_Malloc);
+	IMPORT_FUNC(PyObject_Free);
+	IMPORT_FUNC(PyObject_Init);
+	IMPORT_FUNC(PyUnicode_FromFormat);
+	IMPORT_FUNC(PyUnicode_Concat);
+	IMPORT_FUNC(PyLong_FromVoidPtr);
+	IMPORT_FUNC(PyBool_FromLong);
+	IMPORT_FUNC(PyGILState_Ensure);
+	IMPORT_FUNC(PyGILState_GetThisThreadState);
+	IMPORT_FUNC(PyErr_SetString);
+	IMPORT_FUNC(PyErr_Occurred);
+	IMPORT_FUNC(PyErr_Fetch);
+	IMPORT_FUNC(PyErr_Restore);
+	IMPORT_FUNC(PyErr_WriteUnraisable);
+	IMPORT_FUNC(PyArg_UnpackTuple);
+	IMPORT_FUNC(Py_BuildValue);
+	IMPORT_FUNC(PyRun_SimpleStringFlags);
+	IMPORT_FUNC(PyErr_Print);
+	IMPORT_FUNC(Py_SetPythonHome);
+	IMPORT_FUNC(Py_Initialize);
+	IMPORT_FUNC(Py_Finalize);
+	IMPORT_FUNC(Py_IsInitialized);
+	IMPORT_FUNC(PyEval_InitThreads);
+	IMPORT_FUNC(PyEval_ThreadsInitialized);
+	IMPORT_FUNC(PyEval_ReleaseThread);
+	IMPORT_FUNC(PySys_SetArgv);
+	IMPORT_FUNC(PyImport_ImportModule);
+	IMPORT_FUNC(PyObject_CallFunctionObjArgs);
+	IMPORT_FUNC(_Py_NotImplementedStruct);
+	IMPORT_FUNC(PyExc_TypeError);
+	IMPORT_FUNC(PyExc_RuntimeError);
+	IMPORT_FUNC(PyObject_GetAttr);
+	IMPORT_FUNC(PyUnicode_FromString);
+	IMPORT_FUNC(PyDict_GetItemString);
+	IMPORT_FUNC(PyDict_SetItemString);
+	IMPORT_FUNC(PyCFunction_NewEx);
+	IMPORT_FUNC(PyModule_GetDict);
+	IMPORT_FUNC(PyModule_GetNameObject);
+	IMPORT_FUNC(PyModule_AddObject);
+	IMPORT_FUNC(PyModule_AddStringConstant);
+	IMPORT_FUNC(PyImport_Import);
+	IMPORT_FUNC(PyObject_CallObject);
+	IMPORT_FUNC(_Py_FalseStruct);
+	IMPORT_FUNC(_Py_TrueStruct);
+	IMPORT_FUNC(PyGILState_Release);
+	IMPORT_FUNC(PyList_Append);
+	IMPORT_FUNC(PySys_GetObject);
+	IMPORT_FUNC(PyImport_ReloadModule);
+	IMPORT_FUNC(PyObject_GetAttrString);
+	IMPORT_FUNC(PyCapsule_New);
+	IMPORT_FUNC(PyCapsule_GetPointer);
+	IMPORT_FUNC(PyArg_ParseTuple);
+	IMPORT_FUNC(PyFunction_Type);
+	IMPORT_FUNC(PyObject_SetAttr);
+	IMPORT_FUNC(_PyObject_New);
+	IMPORT_FUNC(PyCapsule_Import);
+	IMPORT_FUNC(PyErr_Clear);
+	IMPORT_FUNC(PyObject_Call);
+	IMPORT_FUNC(PyList_New);
+	IMPORT_FUNC(PyList_Size);
+	IMPORT_FUNC(PyList_GetItem);
+	IMPORT_FUNC(PyUnicode_AsUTF8String);
+	IMPORT_FUNC(PyLong_FromUnsignedLongLong);
+	IMPORT_FUNC(PyArg_VaParse);
+	IMPORT_FUNC(_Py_NoneStruct);
+
+#undef IMPORT_FUNC
+
+	success = true;
+
+fail:
+	if (!success && lib)
+		os_dlclose(lib);
+
+	dstr_free(&lib_path);
+
+	return success;
+}

+ 198 - 0
deps/obs-scripting/obs-scripting-python-import.h

@@ -0,0 +1,198 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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/>.
+******************************************************************************/
+
+#pragma once
+
+#include <util/c99defs.h>
+
+#if defined(_WIN32) || defined(__APPLE__)
+#define RUNTIME_LINK 1
+#define Py_NO_ENABLE_SHARED
+#else
+#define RUNTIME_LINK 0
+#endif
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4115)
+#endif
+
+#if defined(_WIN32) && defined(_DEBUG)
+# undef _DEBUG
+# include <Python.h>
+# define _DEBUG
+#else
+# include <Python.h>
+#endif
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+#if RUNTIME_LINK
+
+#ifdef NO_REDEFS
+#define PY_EXTERN
+#else
+#define PY_EXTERN extern
+#endif
+
+PY_EXTERN int (*Import_PyType_Ready)(PyTypeObject *);
+PY_EXTERN PyObject *(*Import_PyObject_GenericGetAttr)(PyObject *, PyObject *);
+PY_EXTERN int (*Import_PyObject_IsTrue)(PyObject *);
+PY_EXTERN void (*Import_Py_DecRef)(PyObject *);
+PY_EXTERN void *(*Import_PyObject_Malloc)(size_t size);
+PY_EXTERN void (*Import_PyObject_Free)(void *ptr);
+PY_EXTERN PyObject *(*Import_PyObject_Init)(PyObject *, PyTypeObject *);
+PY_EXTERN PyObject *(*Import_PyUnicode_FromFormat)(const char *format, ...);
+PY_EXTERN PyObject *(*Import_PyUnicode_Concat)(PyObject *left, PyObject *right);
+PY_EXTERN PyObject *(*Import_PyLong_FromVoidPtr)(void *);
+PY_EXTERN PyObject *(*Import_PyBool_FromLong)(long);
+PY_EXTERN PyGILState_STATE (*Import_PyGILState_Ensure)(void);
+PY_EXTERN PyThreadState *(*Import_PyGILState_GetThisThreadState)(void);
+PY_EXTERN void (*Import_PyErr_SetString)(PyObject *exception, const char *string);
+PY_EXTERN PyObject *(*Import_PyErr_Occurred)(void);
+PY_EXTERN void (*Import_PyErr_Fetch)(PyObject **, PyObject **, PyObject **);
+PY_EXTERN void (*Import_PyErr_Restore)(PyObject *, PyObject *, PyObject *);
+PY_EXTERN void (*Import_PyErr_WriteUnraisable)(PyObject *);
+PY_EXTERN int (*Import_PyArg_UnpackTuple)(PyObject *, const char *, Py_ssize_t, Py_ssize_t, ...);
+PY_EXTERN PyObject *(*Import_Py_BuildValue)(const char *, ...);
+PY_EXTERN int (*Import_PyRun_SimpleStringFlags)(const char *, PyCompilerFlags *);
+PY_EXTERN void (*Import_PyErr_Print)(void);
+PY_EXTERN void (*Import_Py_SetPythonHome)(wchar_t *);
+PY_EXTERN void (*Import_Py_Initialize)(void);
+PY_EXTERN void (*Import_Py_Finalize)(void);
+PY_EXTERN int (*Import_Py_IsInitialized)(void);
+PY_EXTERN void (*Import_PyEval_InitThreads)(void);
+PY_EXTERN int (*Import_PyEval_ThreadsInitialized)(void);
+PY_EXTERN void (*Import_PyEval_ReleaseThread)(PyThreadState *tstate);
+PY_EXTERN void (*Import_PySys_SetArgv)(int, wchar_t **);
+PY_EXTERN PyObject *(*Import_PyImport_ImportModule)(const char *name);
+PY_EXTERN PyObject *(*Import_PyObject_CallFunctionObjArgs)(PyObject *callable, ...);
+PY_EXTERN PyObject (*Import__Py_NotImplementedStruct);
+PY_EXTERN PyObject *(*Import_PyExc_TypeError);
+PY_EXTERN PyObject *(*Import_PyExc_RuntimeError);
+PY_EXTERN PyObject *(*Import_PyObject_GetAttr)(PyObject *, PyObject *);
+PY_EXTERN PyObject *(*Import_PyUnicode_FromString)(const char *u);
+PY_EXTERN PyObject *(*Import_PyDict_GetItemString)(PyObject *dp, const char *key);
+PY_EXTERN int (*Import_PyDict_SetItemString)(PyObject *dp, const char *key, PyObject *item);
+PY_EXTERN PyObject *(*Import_PyCFunction_NewEx)(PyMethodDef *, PyObject *, PyObject *);
+PY_EXTERN PyObject *(*Import_PyModule_GetDict)(PyObject *);
+PY_EXTERN PyObject *(*Import_PyModule_GetNameObject)(PyObject *);
+PY_EXTERN int (*Import_PyModule_AddObject)(PyObject *, const char *, PyObject *);
+PY_EXTERN int (*Import_PyModule_AddStringConstant)(PyObject *, const char *, const char *);
+PY_EXTERN PyObject *(*Import_PyImport_Import)(PyObject *name);
+PY_EXTERN PyObject *(*Import_PyObject_CallObject)(PyObject *callable_object, PyObject *args);
+PY_EXTERN struct _longobject (*Import__Py_FalseStruct);
+PY_EXTERN struct _longobject (*Import__Py_TrueStruct);
+PY_EXTERN void (*Import_PyGILState_Release)(PyGILState_STATE);
+PY_EXTERN int (*Import_PyList_Append)(PyObject *, PyObject *);
+PY_EXTERN PyObject *(*Import_PySys_GetObject)(const char *);
+PY_EXTERN PyObject *(*Import_PyImport_ReloadModule)(PyObject *m);
+PY_EXTERN PyObject *(*Import_PyObject_GetAttrString)(PyObject *, const char *);
+PY_EXTERN PyObject *(*Import_PyCapsule_New)(void *pointer, const char *name, PyCapsule_Destructor destructor);
+PY_EXTERN void *(*Import_PyCapsule_GetPointer)(PyObject *capsule, const char *name);
+PY_EXTERN int (*Import_PyArg_ParseTuple)(PyObject *, const char *, ...);
+PY_EXTERN PyTypeObject (*Import_PyFunction_Type);
+PY_EXTERN int (*Import_PyObject_SetAttr)(PyObject *, PyObject *, PyObject *);
+PY_EXTERN PyObject *(*Import__PyObject_New)(PyTypeObject *);
+PY_EXTERN void *(*Import_PyCapsule_Import)(const char *name, int no_block); 
+PY_EXTERN void (*Import_PyErr_Clear)(void);
+PY_EXTERN PyObject *(*Import_PyObject_Call)(PyObject *callable_object, PyObject *args, PyObject *kwargs);
+PY_EXTERN PyObject *(*Import_PyList_New)(Py_ssize_t size);
+PY_EXTERN Py_ssize_t (*Import_PyList_Size)(PyObject *);
+PY_EXTERN PyObject *(*Import_PyList_GetItem)(PyObject *, Py_ssize_t);
+PY_EXTERN PyObject *(*Import_PyUnicode_AsUTF8String)(PyObject *unicode);
+PY_EXTERN PyObject *(*Import_PyLong_FromUnsignedLongLong)(unsigned long long);
+PY_EXTERN int (*Import_PyArg_VaParse)(PyObject *, const char *, va_list);
+PY_EXTERN PyObject (*Import__Py_NoneStruct);
+
+extern bool import_python(const char *python_path);
+
+# ifndef NO_REDEFS
+# define PyType_Ready Import_PyType_Ready
+# define PyObject_GenericGetAttr Import_PyObject_GenericGetAttr
+# define PyObject_IsTrue Import_PyObject_IsTrue
+# define Py_DecRef Import_Py_DecRef
+# define PyObject_Malloc Import_PyObject_Malloc
+# define PyObject_Free Import_PyObject_Free
+# define PyObject_Init Import_PyObject_Init
+# define PyUnicode_FromFormat Import_PyUnicode_FromFormat
+# define PyUnicode_Concat Import_PyUnicode_Concat
+# define PyLong_FromVoidPtr Import_PyLong_FromVoidPtr
+# define PyBool_FromLong Import_PyBool_FromLong
+# define PyGILState_Ensure Import_PyGILState_Ensure
+# define PyGILState_GetThisThreadState Import_PyGILState_GetThisThreadState
+# define PyErr_SetString Import_PyErr_SetString
+# define PyErr_Occurred Import_PyErr_Occurred
+# define PyErr_Fetch Import_PyErr_Fetch
+# define PyErr_Restore Import_PyErr_Restore
+# define PyErr_WriteUnraisable Import_PyErr_WriteUnraisable
+# define PyArg_UnpackTuple Import_PyArg_UnpackTuple
+# define Py_BuildValue Import_Py_BuildValue
+# define PyRun_SimpleStringFlags Import_PyRun_SimpleStringFlags
+# define PyErr_Print Import_PyErr_Print
+# define Py_SetPythonHome Import_Py_SetPythonHome
+# define Py_Initialize Import_Py_Initialize
+# define Py_Finalize Import_Py_Finalize
+# define Py_IsInitialized Import_Py_IsInitialized
+# define PyEval_InitThreads Import_PyEval_InitThreads
+# define PyEval_ThreadsInitialized Import_PyEval_ThreadsInitialized
+# define PyEval_ReleaseThread Import_PyEval_ReleaseThread
+# define PySys_SetArgv Import_PySys_SetArgv
+# define PyImport_ImportModule Import_PyImport_ImportModule
+# define PyObject_CallFunctionObjArgs Import_PyObject_CallFunctionObjArgs
+# define _Py_NotImplementedStruct (*Import__Py_NotImplementedStruct)
+# define PyExc_TypeError (*Import_PyExc_TypeError)
+# define PyExc_RuntimeError (*Import_PyExc_RuntimeError)
+# define PyObject_GetAttr Import_PyObject_GetAttr
+# define PyUnicode_FromString Import_PyUnicode_FromString
+# define PyDict_GetItemString Import_PyDict_GetItemString
+# define PyDict_SetItemString Import_PyDict_SetItemString
+# define PyCFunction_NewEx Import_PyCFunction_NewEx
+# define PyModule_GetDict Import_PyModule_GetDict
+# define PyModule_GetNameObject Import_PyModule_GetNameObject
+# define PyModule_AddObject Import_PyModule_AddObject
+# define PyModule_AddStringConstant Import_PyModule_AddStringConstant
+# define PyImport_Import Import_PyImport_Import
+# define PyObject_CallObject Import_PyObject_CallObject
+# define _Py_FalseStruct (*Import__Py_FalseStruct)
+# define _Py_TrueStruct (*Import__Py_TrueStruct)
+# define PyGILState_Release Import_PyGILState_Release
+# define PyList_Append Import_PyList_Append
+# define PySys_GetObject Import_PySys_GetObject
+# define PyImport_ReloadModule Import_PyImport_ReloadModule
+# define PyObject_GetAttrString Import_PyObject_GetAttrString
+# define PyCapsule_New Import_PyCapsule_New
+# define PyCapsule_GetPointer Import_PyCapsule_GetPointer
+# define PyArg_ParseTuple Import_PyArg_ParseTuple
+# define PyFunction_Type (*Import_PyFunction_Type)
+# define PyObject_SetAttr Import_PyObject_SetAttr
+# define _PyObject_New Import__PyObject_New
+# define PyCapsule_Import Import_PyCapsule_Import
+# define PyErr_Clear Import_PyErr_Clear
+# define PyObject_Call Import_PyObject_Call
+# define PyList_New Import_PyList_New
+# define PyList_Size Import_PyList_Size
+# define PyList_GetItem Import_PyList_GetItem
+# define PyUnicode_AsUTF8String Import_PyUnicode_AsUTF8String
+# define PyLong_FromUnsignedLongLong Import_PyLong_FromUnsignedLongLong
+# define PyArg_VaParse Import_PyArg_VaParse
+# define _Py_NoneStruct (*Import__Py_NoneStruct)
+# endif
+
+#endif

+ 1705 - 0
deps/obs-scripting/obs-scripting-python.c

@@ -0,0 +1,1705 @@
+/******************************************************************************
+    Copyright (C) 2015 by Andrew Skinner <[email protected]>
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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-python.h"
+#include "obs-scripting-config.h"
+#include <util/base.h>
+#include <util/platform.h>
+#include <util/darray.h>
+#include <util/dstr.h>
+
+#include <obs.h>
+
+/* ========================================================================= */
+
+// #define DEBUG_PYTHON_STARTUP
+
+static const char *startup_script = "\n\
+import sys\n\
+import os\n\
+import obspython\n\
+class stdout_logger(object):\n\
+	def write(self, message):\n\
+		obspython.script_log(obspython.LOG_INFO, message)\n\
+	def flush(self):\n\
+		pass\n\
+class stderr_logger(object):\n\
+	def write(self, message):\n\
+		obspython.script_log(obspython.LOG_ERROR, message)\n\
+	def flush(self):\n\
+		pass\n\
+os.environ['PYTHONUNBUFFERED'] = '1'\n\
+sys.stdout = stdout_logger()\n\
+sys.stderr = stderr_logger()\n";
+
+#if RUNTIME_LINK
+static wchar_t home_path[1024] = {0};
+#endif
+
+DARRAY(char*) python_paths;
+static bool python_loaded = false;
+
+static pthread_mutex_t tick_mutex = PTHREAD_MUTEX_INITIALIZER;
+static struct obs_python_script *first_tick_script = NULL;
+
+static PyObject *py_obspython = NULL;
+struct obs_python_script *cur_python_script = NULL;
+struct python_obs_callback *cur_python_cb = NULL;
+
+/* -------------------------------------------- */
+
+bool py_to_libobs_(const char *type,
+                   PyObject *  py_in,
+                   void *      libobs_out,
+                   const char *id,
+                   const char *func,
+                   int         line)
+{
+	swig_type_info *info = SWIG_TypeQuery(type);
+	if (info == NULL) {
+		warn("%s:%d: SWIG could not find type: %s%s%s",
+		     func,
+		     line,
+		     id ? id : "",
+		     id ? "::" : "",
+		     type);
+		return false;
+	}
+
+	int ret = SWIG_ConvertPtr(py_in, libobs_out, info, 0);
+	if (!SWIG_IsOK(ret)) {
+		warn("%s:%d: SWIG failed to convert python object to obs "
+		     "object: %s%s%s",
+		     func,
+		     line,
+		     id ? id : "",
+		     id ? "::" : "",
+		     type);
+		return false;
+	}
+
+	return true;
+}
+
+bool libobs_to_py_(const char *type,
+                   void *      libobs_in,
+                   bool        ownership,
+                   PyObject ** py_out,
+                   const char *id,
+                   const char *func,
+                   int         line)
+{
+	swig_type_info *info = SWIG_TypeQuery(type);
+	if (info == NULL) {
+		warn("%s:%d: SWIG could not find type: %s%s%s",
+		     func,
+		     line,
+		     id ? id : "",
+		     id ? "::" : "",
+		     type);
+		return false;
+	}
+
+	*py_out = SWIG_NewPointerObj(libobs_in, info, (int)ownership);
+	if (*py_out == Py_None) {
+		warn("%s:%d: SWIG failed to convert obs object to python "
+		     "object: %s%s%s",
+		     func,
+		     line,
+		     id ? id : "",
+		     id ? "::" : "",
+		     type);
+		return false;
+	}
+
+	return true;
+}
+
+
+#define libobs_to_py(type, obs_obj, ownership, py_obj) \
+	libobs_to_py_(#type " *", obs_obj, ownership, py_obj, \
+			NULL, __func__, __LINE__)
+#define py_to_libobs(type, py_obj, libobs_out) \
+	py_to_libobs_(#type " *", py_obj, libobs_out, \
+			NULL, __func__, __LINE__)
+
+#define lock_callback(cb) \
+	lock_python(); \
+	struct obs_python_script *__last_script = cur_python_script; \
+	struct python_obs_callback *__last_cb = cur_python_cb; \
+	cur_python_script = (struct obs_python_script *)cb->base.script; \
+	cur_python_cb = cb
+#define unlock_callback() \
+	cur_python_cb = __last_cb; \
+	cur_python_script = __last_script; \
+	unlock_python()
+
+/* ========================================================================= */
+
+void add_functions_to_py_module(PyObject *module, PyMethodDef *method_list)
+{
+	PyObject *dict = PyModule_GetDict(module);
+	PyObject *name = PyModule_GetNameObject(module);
+	if (!dict || !name) {
+		return;
+	}
+	for (PyMethodDef *ml = method_list; ml->ml_name != NULL; ml++) {
+		PyObject *func = PyCFunction_NewEx(ml, module, name);
+		if (!func) {
+			continue;
+		}
+		PyDict_SetItemString(dict, ml->ml_name, func);
+		Py_DECREF(func);
+	}
+	Py_DECREF(name);
+}
+
+/* -------------------------------------------- */
+
+static PyObject *py_get_current_script_path(PyObject *self, PyObject *args)
+{
+	UNUSED_PARAMETER(args);
+	return PyDict_GetItemString(PyModule_GetDict(self),
+			"__script_dir__");
+}
+
+static void get_defaults(struct obs_python_script *data, PyObject *get_defs)
+{
+	PyObject *py_settings;
+	if (!libobs_to_py(obs_data_t, data->base.settings, false, &py_settings))
+		return;
+
+	PyObject *args = Py_BuildValue("(O)", py_settings);
+	PyObject *py_ret = PyObject_CallObject(get_defs, args);
+	py_error();
+	Py_XDECREF(py_ret);
+	Py_XDECREF(args);
+	Py_XDECREF(py_settings);
+}
+
+static bool load_python_script(struct obs_python_script *data)
+{
+	PyObject *py_file     = NULL;
+	PyObject *py_module   = NULL;
+	PyObject *py_success  = NULL;
+	PyObject *py_tick     = NULL;
+	PyObject *py_load     = NULL;
+	PyObject *py_defaults = NULL;
+	bool      success     = false;
+	int       ret;
+
+	cur_python_script = data;
+
+	if (!data->module) {
+		py_file   = PyUnicode_FromString(data->name.array);
+		py_module = PyImport_Import(py_file);
+	} else {
+		py_module = PyImport_ReloadModule(data->module);
+	}
+
+	if (!py_module) {
+		py_error();
+		goto fail;
+	}
+
+	Py_XINCREF(py_obspython);
+	ret = PyModule_AddObject(py_module, "obspython", py_obspython);
+	if (py_error() || ret != 0)
+		goto fail;
+
+	ret = PyModule_AddStringConstant(py_module, "__script_dir__",
+			data->dir.array);
+	if (py_error() || ret != 0)
+		goto fail;
+
+	PyObject *py_data = PyCapsule_New(data, NULL, NULL);
+	ret = PyModule_AddObject(py_module, "__script_data__", py_data);
+	if (py_error() || ret != 0)
+		goto fail;
+
+	static PyMethodDef global_funcs[] = {
+		{"script_path",
+		 py_get_current_script_path,
+		 METH_NOARGS,
+		 "Gets the script path"},
+		{0}
+	};
+
+	add_functions_to_py_module(py_module, global_funcs);
+
+	data->update = PyObject_GetAttrString(py_module, "script_update");
+	if (!data->update)
+		PyErr_Clear();
+
+	data->save = PyObject_GetAttrString(py_module, "script_save");
+	if (!data->save)
+		PyErr_Clear();
+
+	data->get_properties = PyObject_GetAttrString(py_module,
+			"script_properties");
+	if (!data->get_properties)
+		PyErr_Clear();
+
+	PyObject *func = PyObject_GetAttrString(py_module, "script_defaults");
+	if (func) {
+		get_defaults(data, func);
+		Py_DECREF(func);
+	} else {
+		PyErr_Clear();
+	}
+
+	func = PyObject_GetAttrString(py_module, "script_description");
+	if (func) {
+		PyObject *py_ret = PyObject_CallObject(func, NULL);
+		py_error();
+		PyObject *py_desc = PyUnicode_AsUTF8String(py_ret);
+		if (py_desc) {
+			const char *desc = PyBytes_AS_STRING(py_desc);
+			if (desc) dstr_copy(&data->base.desc, desc);
+			Py_DECREF(py_desc);
+		}
+		Py_XDECREF(py_ret);
+		Py_DECREF(func);
+	} else {
+		PyErr_Clear();
+	}
+
+	py_tick = PyObject_GetAttrString(py_module, "script_tick");
+	if (py_tick) {
+		pthread_mutex_lock(&tick_mutex);
+
+		struct obs_python_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 = py_tick;
+		py_tick = NULL;
+
+		pthread_mutex_unlock(&tick_mutex);
+	} else {
+		PyErr_Clear();
+	}
+
+	py_load = PyObject_GetAttrString(py_module, "script_load");
+	if (py_load) {
+		PyObject *py_s;
+		libobs_to_py(obs_data_t, data->base.settings, false, &py_s);
+		PyObject *args = Py_BuildValue("(O)", py_s);
+		PyObject *py_ret = PyObject_CallObject(py_load, args);
+		py_error();
+		Py_XDECREF(py_ret);
+		Py_XDECREF(args);
+		Py_XDECREF(py_s);
+	} else {
+		PyErr_Clear();
+	}
+
+	if (data->module)
+		Py_XDECREF(data->module);
+	data->module = py_module;
+	py_module = NULL;
+
+	success = true;
+
+fail:
+	Py_XDECREF(py_load);
+	Py_XDECREF(py_tick);
+	Py_XDECREF(py_defaults);
+	Py_XDECREF(py_success);
+	Py_XDECREF(py_file);
+	if (!success)
+		Py_XDECREF(py_module);
+	cur_python_script = NULL;
+	return success;
+}
+
+static void unload_python_script(struct obs_python_script *data)
+{
+	PyObject *py_module   = data->module;
+	PyObject *py_func     = NULL;
+	PyObject *py_ret      = NULL;
+
+	cur_python_script = data;
+
+	py_func = PyObject_GetAttrString(py_module, "script_unload");
+	if (PyErr_Occurred() || !py_func) {
+		PyErr_Clear();
+		goto fail;
+	}
+
+	py_ret = PyObject_CallObject(py_func, NULL);
+	if (py_error())
+		goto fail;
+
+fail:
+	Py_XDECREF(py_ret);
+	Py_XDECREF(py_func);
+
+	cur_python_script = NULL;
+}
+
+static void add_to_python_path(const char *path)
+{
+	PyObject *py_path_str = NULL;
+	PyObject *py_path     = NULL;
+	int       ret;
+
+	if (!path || !*path)
+		return;
+
+	for (size_t i = 0; i < python_paths.num; i++) {
+		const char *python_path = python_paths.array[i];
+		if (strcmp(path, python_path) == 0)
+			return;
+	}
+
+	ret = PyRun_SimpleString("import sys");
+	if (py_error() || ret != 0)
+		goto fail;
+
+	/* borrowed reference here */
+	py_path = PySys_GetObject("path");
+	if (py_error() || !py_path)
+		goto fail;
+
+	py_path_str = PyUnicode_FromString(path);
+	ret = PyList_Append(py_path, py_path_str);
+	if (py_error() || ret != 0)
+		goto fail;
+
+	char *new_path = bstrdup(path);
+	da_push_back(python_paths, &new_path);
+
+fail:
+	Py_XDECREF(py_path_str);
+}
+
+/* -------------------------------------------- */
+
+struct python_obs_timer {
+	struct python_obs_timer *next;
+	struct python_obs_timer **p_prev_next;
+
+	uint64_t last_ts;
+	uint64_t interval;
+};
+
+static pthread_mutex_t timer_mutex = PTHREAD_MUTEX_INITIALIZER;
+static struct python_obs_timer *first_timer = NULL;
+
+static inline void python_obs_timer_init(struct python_obs_timer *timer)
+{
+	pthread_mutex_lock(&timer_mutex);
+
+	struct python_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 python_obs_timer_remove(struct python_obs_timer *timer)
+{
+	struct python_obs_timer *next = timer->next;
+	if (next) next->p_prev_next = timer->p_prev_next;
+	*timer->p_prev_next = timer->next;
+}
+
+static inline struct python_obs_callback *python_obs_timer_cb(
+		struct python_obs_timer *timer)
+{
+	return &((struct python_obs_callback *)timer)[-1];
+}
+
+static PyObject *timer_remove(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_cb;
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "O", &py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = find_python_obs_callback(script, py_cb);
+	if (cb) remove_python_obs_callback(cb);
+	return python_none();
+}
+
+static void timer_call(struct script_callback *p_cb)
+{
+	struct python_obs_callback *cb = (struct python_obs_callback *)p_cb;
+
+	if (p_cb->removed)
+		return;
+
+	lock_callback(cb);
+	PyObject *py_ret = PyObject_CallObject(cb->func, NULL);
+	py_error();
+	Py_XDECREF(py_ret);
+	unlock_callback();
+}
+
+static void defer_timer_init(void *p_cb)
+{
+	struct python_obs_callback *cb = p_cb;
+	struct python_obs_timer *timer = python_obs_callback_extra_data(cb);
+	python_obs_timer_init(timer);
+}
+
+static PyObject *timer_add(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_cb;
+	int ms;
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "Oi", &py_cb, &ms))
+		return python_none();
+
+	struct python_obs_callback *cb = add_python_obs_callback_extra(
+			script, py_cb, sizeof(struct python_obs_timer));
+	struct python_obs_timer *timer = python_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 python_none();
+}
+
+/* -------------------------------------------- */
+
+static void obs_python_tick_callback(void *priv, float seconds)
+{
+	struct python_obs_callback *cb = priv;
+
+	if (cb->base.removed) {
+		obs_remove_tick_callback(obs_python_tick_callback, cb);
+		return;
+	}
+
+	lock_callback(cb);
+
+	PyObject *args = Py_BuildValue("(f)", seconds);
+	PyObject *py_ret = PyObject_CallObject(cb->func, args);
+	py_error();
+	Py_XDECREF(py_ret);
+	Py_XDECREF(args);
+
+	unlock_callback();
+}
+
+static PyObject *obs_python_remove_tick_callback(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_cb = NULL;
+
+	if (!script) {
+		PyErr_SetString(PyExc_RuntimeError,
+				"No active script, report this to Jim");
+		return NULL;
+	}
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "O", &py_cb))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = find_python_obs_callback(script, py_cb);
+	if (cb) remove_python_obs_callback(cb);
+	return python_none();
+}
+
+static PyObject *obs_python_add_tick_callback(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_cb = NULL;
+
+	if (!script) {
+		PyErr_SetString(PyExc_RuntimeError,
+				"No active script, report this to Jim");
+		return NULL;
+	}
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "O", &py_cb))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = add_python_obs_callback(script, py_cb);
+	obs_add_tick_callback(obs_python_tick_callback, cb);
+	return python_none();
+}
+
+/* -------------------------------------------- */
+
+static void calldata_signal_callback(void *priv, calldata_t *cd)
+{
+	struct python_obs_callback *cb = priv;
+
+	if (cb->base.removed) {
+		signal_handler_remove_current();
+		return;
+	}
+
+	lock_callback(cb);
+
+	PyObject *py_cd;
+
+	if (libobs_to_py(calldata_t, cd, false, &py_cd)) {
+		PyObject *args = Py_BuildValue("(O)", py_cd);
+		PyObject *py_ret = PyObject_CallObject(cb->func, args);
+		py_error();
+		Py_XDECREF(py_ret);
+		Py_XDECREF(args);
+		Py_XDECREF(py_cd);
+	}
+
+	unlock_callback();
+}
+
+static PyObject *obs_python_signal_handler_disconnect(
+		PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_sh = NULL;
+	PyObject *py_cb = NULL;
+	const char *signal;
+
+	if (!script) {
+		PyErr_SetString(PyExc_RuntimeError,
+				"No active script, report this to Jim");
+		return NULL;
+	}
+
+	UNUSED_PARAMETER(self);
+
+	signal_handler_t *handler;
+
+	if (!parse_args(args, "OsO", &py_sh, &signal, &py_cb))
+		return python_none();
+
+	if (!py_to_libobs(signal_handler_t, py_sh, &handler))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = find_python_obs_callback(script, py_cb);
+	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_python_obs_callback(script, cb, py_cb);
+	}
+
+	if (cb) remove_python_obs_callback(cb);
+	return python_none();
+}
+
+static PyObject *obs_python_signal_handler_connect(
+		PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_sh = NULL;
+	PyObject *py_cb = NULL;
+	const char *signal;
+
+	if (!script) {
+		PyErr_SetString(PyExc_RuntimeError,
+				"No active script, report this to Jim");
+		return NULL;
+	}
+
+	UNUSED_PARAMETER(self);
+
+	signal_handler_t *handler;
+
+	if (!parse_args(args, "OsO", &py_sh, &signal, &py_cb))
+		return python_none();
+	if (!py_to_libobs(signal_handler_t, py_sh, &handler))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = add_python_obs_callback(script, py_cb);
+	calldata_set_ptr(&cb->base.extra, "handler", handler);
+	calldata_set_string(&cb->base.extra, "signal", signal);
+	signal_handler_connect(handler, signal, calldata_signal_callback, cb);
+	return python_none();
+}
+
+/* -------------------------------------------- */
+
+static void calldata_signal_callback_global(void *priv, const char *signal,
+		calldata_t *cd)
+{
+	struct python_obs_callback *cb = priv;
+
+	if (cb->base.removed) {
+		signal_handler_remove_current();
+		return;
+	}
+
+	lock_callback(cb);
+
+	PyObject *py_cd;
+
+	if (libobs_to_py(calldata_t, cd, false, &py_cd)) {
+		PyObject *args = Py_BuildValue("(sO)", signal, py_cd);
+		PyObject *py_ret = PyObject_CallObject(cb->func, args);
+		py_error();
+		Py_XDECREF(py_ret);
+		Py_XDECREF(args);
+		Py_XDECREF(py_cd);
+	}
+
+	unlock_callback();
+}
+
+static PyObject *obs_python_signal_handler_disconnect_global(
+		PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_sh = NULL;
+	PyObject *py_cb = NULL;
+
+	if (!script) {
+		PyErr_SetString(PyExc_RuntimeError,
+				"No active script, report this to Jim");
+		return NULL;
+	}
+
+	UNUSED_PARAMETER(self);
+
+	signal_handler_t *handler;
+
+	if (!parse_args(args, "OO", &py_sh, &py_cb))
+		return python_none();
+
+	if (!py_to_libobs(signal_handler_t, py_sh, &handler))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = find_python_obs_callback(script, py_cb);
+	while (cb) {
+		signal_handler_t *cb_handler =
+			calldata_ptr(&cb->base.extra, "handler");
+
+		if (handler == cb_handler)
+			break;
+
+		cb = find_next_python_obs_callback(script, cb, py_cb);
+	}
+
+	if (cb) remove_python_obs_callback(cb);
+	return python_none();
+}
+
+static PyObject *obs_python_signal_handler_connect_global(
+		PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_sh = NULL;
+	PyObject *py_cb = NULL;
+
+	if (!script) {
+		PyErr_SetString(PyExc_RuntimeError,
+				"No active script, report this to Jim");
+		return NULL;
+	}
+
+	UNUSED_PARAMETER(self);
+
+	signal_handler_t *handler;
+
+	if (!parse_args(args, "OO", &py_sh, &py_cb))
+		return python_none();
+
+	if (!py_to_libobs(signal_handler_t, py_sh, &handler))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = add_python_obs_callback(script, py_cb);
+	calldata_set_ptr(&cb->base.extra, "handler", handler);
+	signal_handler_connect_global(handler,
+			calldata_signal_callback_global, cb);
+	return python_none();
+}
+
+/* -------------------------------------------- */
+
+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 python_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 python_obs_callback *cb = p_cb;
+
+	if (cb->base.removed)
+		return;
+
+	lock_callback(cb);
+
+	PyObject *py_pressed = PyBool_FromLong(pressed);
+	PyObject *args = Py_BuildValue("(O)", py_pressed);
+	PyObject *py_ret = PyObject_CallObject(cb->func, args);
+	py_error();
+	Py_XDECREF(py_ret);
+	Py_XDECREF(args);
+	Py_XDECREF(py_pressed);
+
+	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 inline PyObject *py_invalid_hotkey_id()
+{
+	return PyLong_FromUnsignedLongLong(OBS_INVALID_HOTKEY_ID);
+}
+
+static void hotkey_callback(void *p_cb, obs_hotkey_id id,
+		obs_hotkey_t *hotkey, bool pressed)
+{
+	struct python_obs_callback *cb = p_cb;
+
+	if (cb->base.removed)
+		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 PyObject *hotkey_unregister(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_cb = NULL;
+
+	if (!script) {
+		PyErr_SetString(PyExc_RuntimeError,
+				"No active script, report this to Jim");
+		return NULL;
+	}
+
+	if (!parse_args(args, "O", &py_cb))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = find_python_obs_callback(script, py_cb);
+	if (cb) remove_python_obs_callback(cb);
+
+	UNUSED_PARAMETER(self);
+	return python_none();
+}
+
+static PyObject *hotkey_register_frontend(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	const char *name;
+	const char *desc;
+	obs_hotkey_id id;
+	PyObject *py_cb;
+
+	if (!parse_args(args, "ssO", &name, &desc, &py_cb))
+		return py_invalid_hotkey_id();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return py_invalid_hotkey_id();
+
+	struct python_obs_callback *cb = add_python_obs_callback(script, py_cb);
+	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);
+
+	if (id == OBS_INVALID_HOTKEY_ID)
+		remove_python_obs_callback(cb);
+
+	UNUSED_PARAMETER(self);
+	return PyLong_FromUnsignedLongLong(id);
+}
+
+/* -------------------------------------------- */
+
+static bool button_prop_clicked(obs_properties_t *props, obs_property_t *p,
+		void *p_cb)
+{
+	struct python_obs_callback *cb = p_cb;
+	bool ret = false;
+
+	if (cb->base.removed)
+		return false;
+
+	lock_callback(cb);
+
+	PyObject *py_props = NULL;
+	PyObject *py_p = NULL;
+
+	if (libobs_to_py(obs_properties_t, props, false, &py_props) &&
+	    libobs_to_py(obs_property_t, p, false, &py_p)) {
+
+		PyObject *args = Py_BuildValue("(OO)", py_props, py_p);
+		PyObject *py_ret = PyObject_CallObject(cb->func, args);
+		if (!py_error())
+			ret = py_ret == Py_True;
+		Py_XDECREF(py_ret);
+		Py_XDECREF(args);
+	}
+
+	Py_XDECREF(py_p);
+	Py_XDECREF(py_props);
+
+	unlock_callback();
+
+	return ret;
+}
+
+static PyObject *properties_add_button(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	obs_properties_t *props;
+	obs_property_t *p;
+	PyObject *py_props;
+	PyObject *py_ret;
+	const char *name;
+	const char *text;
+	PyObject *py_cb;
+
+	if (!parse_args(args, "OssO", &py_props, &name, &text, &py_cb))
+		return python_none();
+	if (!py_to_libobs(obs_properties_t, py_props, &props))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = add_python_obs_callback(script, py_cb);
+	p = obs_properties_add_button2(props, name, text, button_prop_clicked,
+			cb);
+
+	if (!p || !libobs_to_py(obs_property_t, p, false, &py_ret))
+		return python_none();
+
+	UNUSED_PARAMETER(self);
+	return py_ret;
+}
+
+/* -------------------------------------------- */
+
+static bool modified_callback(void *p_cb, obs_properties_t *props,
+		obs_property_t *p, obs_data_t *settings)
+{
+	struct python_obs_callback *cb = p_cb;
+	bool ret = false;
+
+	if (cb->base.removed)
+		return false;
+
+	lock_callback(cb);
+
+	PyObject *py_props = NULL;
+	PyObject *py_p = NULL;
+	PyObject *py_settings = NULL;
+
+	if (libobs_to_py(obs_properties_t, props, false, &py_props) &&
+	    libobs_to_py(obs_property_t, p, false, &py_p) &&
+	    libobs_to_py(obs_data_t, settings, false, &py_settings)) {
+
+		PyObject *args = Py_BuildValue("(OOO)", py_props, py_p,
+				py_settings);
+		PyObject *py_ret = PyObject_CallObject(cb->func, args);
+		if (!py_error())
+			ret = py_ret == Py_True;
+		Py_XDECREF(py_ret);
+		Py_XDECREF(args);
+	}
+
+	Py_XDECREF(py_settings);
+	Py_XDECREF(py_p);
+	Py_XDECREF(py_props);
+
+	unlock_callback();
+
+	return ret;
+}
+
+static PyObject *property_set_modified_callback(PyObject *self, PyObject *args)
+{
+	struct obs_python_script *script = cur_python_script;
+	PyObject *py_p;
+	PyObject *py_cb;
+	obs_property_t *p;
+
+	if (!parse_args(args, "OO", &py_p, &py_cb))
+		return python_none();
+	if (!py_to_libobs(obs_property_t, py_p, &p))
+		return python_none();
+	if (!py_cb || !PyFunction_Check(py_cb))
+		return python_none();
+
+	struct python_obs_callback *cb = add_python_obs_callback(script, py_cb);
+	obs_property_set_modified_callback2(p, modified_callback, cb);
+
+	UNUSED_PARAMETER(self);
+	return python_none();
+}
+
+/* -------------------------------------------- */
+
+static PyObject *remove_current_callback(PyObject *self, PyObject *args)
+{
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+
+	if (cur_python_cb)
+		remove_python_obs_callback(cur_python_cb);
+	return python_none();
+}
+
+/* -------------------------------------------- */
+
+static PyObject *calldata_source(PyObject *self, PyObject *args)
+{
+	PyObject *py_ret = NULL;
+	PyObject *py_cd  = NULL;
+
+	calldata_t *cd;
+	const char *name;
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "Os", &py_cd, &name))
+		goto fail;
+	if (!py_to_libobs(calldata_t, py_cd, &cd))
+		goto fail;
+
+	obs_source_t *source = calldata_ptr(cd, name);
+	libobs_to_py(obs_source_t, source, false, &py_ret);
+
+fail:
+	return py_ret;
+}
+
+static PyObject *calldata_sceneitem(PyObject *self, PyObject *args)
+{
+	PyObject *py_ret = NULL;
+	PyObject *py_cd  = NULL;
+
+	calldata_t *cd;
+	const char *name;
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "Os", &py_cd, &name))
+		goto fail;
+	if (!py_to_libobs(calldata_t, py_cd, &cd))
+		goto fail;
+
+	obs_sceneitem_t *item = calldata_ptr(cd, name);
+	libobs_to_py(obs_sceneitem_t, item, false, &py_ret);
+
+fail:
+	return py_ret;
+}
+
+/* -------------------------------------------- */
+
+static bool enum_sources_proc(void *param, obs_source_t *source)
+{
+	PyObject *list = param;
+	PyObject *py_source;
+
+	if (libobs_to_py(obs_source_t, source, false, &py_source)) {
+		obs_source_get_ref(source);
+		PyList_Append(list, py_source);
+		Py_DECREF(py_source);
+	}
+	return true;
+}
+
+static PyObject *enum_sources(PyObject *self, PyObject *args)
+{
+	UNUSED_PARAMETER(self);
+	UNUSED_PARAMETER(args);
+
+	PyObject *list = PyList_New(0);
+	obs_enum_sources(enum_sources_proc, list);
+	return list;
+}
+
+/* -------------------------------------------- */
+
+static bool enum_items_proc(obs_scene_t *scene, obs_sceneitem_t *item,
+		void *param)
+{
+	PyObject *list = param;
+	PyObject *py_item;
+
+	UNUSED_PARAMETER(scene);
+
+	if (libobs_to_py(obs_sceneitem_t, item, false, &py_item)) {
+		obs_sceneitem_addref(item);
+		PyList_Append(list, py_item);
+		Py_DECREF(py_item);
+	}
+	return true;
+}
+
+static PyObject *scene_enum_items(PyObject *self, PyObject *args)
+{
+	PyObject *py_scene;
+	obs_scene_t *scene;
+
+	UNUSED_PARAMETER(self);
+
+	if (!parse_args(args, "O", &py_scene))
+		return python_none();
+	if (!py_to_libobs(obs_scene_t, py_scene, &scene))
+		return python_none();
+
+	PyObject *list = PyList_New(0);
+	obs_scene_enum_items(scene, enum_items_proc, list);
+	return list;
+}
+
+/* -------------------------------------------- */
+
+static PyObject *source_list_release(PyObject *self, PyObject *args)
+{
+	PyObject *list;
+	if (!parse_args(args, "O", &list))
+		return python_none();
+
+	Py_ssize_t count = PyList_Size(list);
+	for (Py_ssize_t i = 0; i < count; i++) {
+		PyObject *py_source = PyList_GetItem(list, i);
+		obs_source_t *source;
+
+		if (py_to_libobs(obs_source_t, py_source, &source)) {
+			obs_source_release(source);
+		}
+	}
+
+	UNUSED_PARAMETER(self);
+	return python_none();
+}
+
+static PyObject *sceneitem_list_release(PyObject *self, PyObject *args)
+{
+	PyObject *list;
+	if (!parse_args(args, "O", &list))
+		return python_none();
+
+	Py_ssize_t count = PyList_Size(list);
+	for (Py_ssize_t i = 0; i < count; i++) {
+		PyObject *py_item = PyList_GetItem(list, i);
+		obs_sceneitem_t *item;
+
+		if (py_to_libobs(obs_sceneitem_t, py_item, &item)) {
+			obs_sceneitem_release(item);
+		}
+	}
+
+	UNUSED_PARAMETER(self);
+	return python_none();
+}
+
+/* -------------------------------------------- */
+
+struct dstr cur_py_log_chunk = {0};
+
+static PyObject *py_script_log(PyObject *self, PyObject *args)
+{
+	static bool calling_self = false;
+	int log_level;
+	const char *msg;
+
+	UNUSED_PARAMETER(self);
+
+	if (calling_self)
+		return python_none();
+	calling_self = true;
+
+	/* ------------------- */
+
+	if (!parse_args(args, "is", &log_level, &msg))
+		goto fail;
+	if (!msg || !*msg)
+		goto fail;
+
+	dstr_cat(&cur_py_log_chunk, msg);
+
+	const char *start = cur_py_log_chunk.array;
+	char *endl = strchr(start, '\n');
+
+	while (endl) {
+		*endl = 0;
+		script_log(&cur_python_script->base, log_level, "%s", start);
+		*endl = '\n';
+
+		start = endl + 1;
+		endl = strchr(start, '\n');
+	}
+
+	if (start) {
+		size_t len = strlen(start);
+		if (len) memmove(cur_py_log_chunk.array, start, len);
+		dstr_resize(&cur_py_log_chunk, len);
+	}
+
+	/* ------------------- */
+
+fail:
+	calling_self = false;
+	return python_none();
+}
+
+/* -------------------------------------------- */
+
+static void add_hook_functions(PyObject *module)
+{
+	static PyMethodDef funcs[] = {
+#define DEF_FUNC(n, c) {n, c, METH_VARARGS, NULL}
+
+		DEF_FUNC("script_log", py_script_log),
+		DEF_FUNC("timer_remove", timer_remove),
+		DEF_FUNC("timer_add", timer_add),
+		DEF_FUNC("calldata_source", calldata_source),
+		DEF_FUNC("calldata_sceneitem", calldata_sceneitem),
+		DEF_FUNC("source_list_release", source_list_release),
+		DEF_FUNC("sceneitem_list_release", sceneitem_list_release),
+		DEF_FUNC("obs_enum_sources", enum_sources),
+		DEF_FUNC("obs_scene_enum_items", scene_enum_items),
+		DEF_FUNC("obs_remove_tick_callback",
+		         obs_python_remove_tick_callback),
+		DEF_FUNC("obs_add_tick_callback",
+		         obs_python_add_tick_callback),
+		DEF_FUNC("signal_handler_disconnect",
+		         obs_python_signal_handler_disconnect),
+		DEF_FUNC("signal_handler_connect",
+		         obs_python_signal_handler_connect),
+		DEF_FUNC("signal_handler_disconnect_global",
+		         obs_python_signal_handler_disconnect_global),
+		DEF_FUNC("signal_handler_connect_global",
+		         obs_python_signal_handler_connect_global),
+		DEF_FUNC("obs_hotkey_unregister",
+			hotkey_unregister),
+		DEF_FUNC("obs_hotkey_register_frontend",
+			hotkey_register_frontend),
+		DEF_FUNC("obs_properties_add_button",
+			properties_add_button),
+		DEF_FUNC("obs_property_set_modified_callback",
+			property_set_modified_callback),
+		DEF_FUNC("remove_current_callback",
+		         remove_current_callback),
+
+#undef DEF_FUNC
+		{0}
+	};
+
+	add_functions_to_py_module(module, funcs);
+}
+
+/* -------------------------------------------- */
+
+void obs_python_script_update(obs_script_t *script, obs_data_t *settings);
+
+bool obs_python_script_load(obs_script_t *s)
+{
+	struct obs_python_script *data = (struct obs_python_script *)s;
+	if (python_loaded && !data->base.loaded) {
+		lock_python();
+		if (!data->module)
+			add_to_python_path(data->dir.array);
+		data->base.loaded = load_python_script(data);
+		unlock_python();
+
+		if (data->base.loaded)
+			obs_python_script_update(s, NULL);
+	}
+
+	return data->base.loaded;
+}
+
+obs_script_t *obs_python_script_create(const char *path, obs_data_t *settings)
+{
+	struct obs_python_script *data = bzalloc(sizeof(*data));
+
+	data->base.type = OBS_SCRIPT_LANG_PYTHON;
+
+	dstr_copy(&data->base.path, path);
+	dstr_replace(&data->base.path, "\\", "/");
+	path = data->base.path.array;
+
+	const 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);
+	}
+
+	path = data->base.file.array;
+	dstr_copy_dstr(&data->name, &data->base.file);
+
+	const char *ext = strstr(path, ".py");
+	if (ext)
+		dstr_resize(&data->name, ext - path);
+
+	data->base.settings = obs_data_create();
+	if (settings)
+		obs_data_apply(data->base.settings, settings);
+
+	if (!python_loaded)
+		return (obs_script_t *)data;
+
+	lock_python();
+	add_to_python_path(data->dir.array);
+	data->base.loaded = load_python_script(data);
+	if (data->base.loaded) {
+		cur_python_script = data;
+		obs_python_script_update(&data->base, NULL);
+		cur_python_script = NULL;
+	}
+	unlock_python();
+
+	return (obs_script_t *)data;
+}
+
+void obs_python_script_unload(obs_script_t *s)
+{
+	struct obs_python_script *data = (struct obs_python_script *)s;
+
+	if (!s->loaded || !python_loaded)
+		return;
+
+	/* ---------------------------- */
+	/* unhook tick function         */
+
+	if (data->p_prev_next_tick) {
+		pthread_mutex_lock(&tick_mutex);
+
+		struct obs_python_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;
+	}
+
+	lock_python();
+
+	Py_XDECREF(data->tick);
+	Py_XDECREF(data->save);
+	Py_XDECREF(data->update);
+	Py_XDECREF(data->get_properties);
+	data->tick = NULL;
+	data->save = NULL;
+	data->update = NULL;
+	data->get_properties = NULL;
+
+	/* ---------------------------- */
+	/* remove all callbacks         */
+
+	struct script_callback *cb = data->first_callback;
+	while (cb) {
+		struct script_callback *next = cb->next;
+		remove_script_callback(cb);
+		cb = next;
+	}
+
+	/* ---------------------------- */
+	/* unload                       */
+
+	unload_python_script(data);
+	unlock_python();
+
+	s->loaded = false;
+}
+
+void obs_python_script_destroy(obs_script_t *s)
+{
+	struct obs_python_script *data = (struct obs_python_script *)s;
+
+	if (data) {
+		if (python_loaded) {
+			lock_python();
+			Py_XDECREF(data->module);
+			unlock_python();
+		}
+
+		dstr_free(&data->base.path);
+		dstr_free(&data->base.file);
+		dstr_free(&data->base.desc);
+		obs_data_release(data->base.settings);
+		dstr_free(&data->dir);
+		dstr_free(&data->name);
+		bfree(data);
+	}
+}
+
+void obs_python_script_update(obs_script_t *s, obs_data_t *settings)
+{
+	struct obs_python_script *data = (struct obs_python_script *)s;
+
+	if (!s->loaded || !python_loaded)
+		return;
+	if (!data->update)
+		return;
+
+	if (settings)
+		obs_data_apply(s->settings, settings);
+
+	lock_python();
+	cur_python_script = data;
+
+	PyObject *py_settings;
+	if (libobs_to_py(obs_data_t, s->settings, false, &py_settings)) {
+		PyObject *args = Py_BuildValue("(O)", py_settings);
+		PyObject *ret = PyObject_CallObject(data->update, args);
+		py_error();
+
+		Py_XDECREF(ret);
+		Py_XDECREF(args);
+		Py_XDECREF(py_settings);
+	}
+
+	cur_python_script = NULL;
+	unlock_python();
+}
+
+obs_properties_t *obs_python_script_get_properties(obs_script_t *s)
+{
+	struct obs_python_script *data = (struct obs_python_script *)s;
+	obs_properties_t *props = NULL;
+
+	if (!s->loaded || !python_loaded)
+		return NULL;
+	if (!data->get_properties)
+		return NULL;
+
+	lock_python();
+	cur_python_script = data;
+
+	PyObject *ret = PyObject_CallObject(data->get_properties, NULL);
+	if (!py_error())
+		py_to_libobs(obs_properties_t, ret, &props);
+	Py_XDECREF(ret);
+
+	cur_python_script = NULL;
+	unlock_python();
+
+	return props;
+}
+
+void obs_python_script_save(obs_script_t *s)
+{
+	struct obs_python_script *data = (struct obs_python_script *)s;
+
+	if (!s->loaded || !python_loaded)
+		return;
+	if (!data->save)
+		return;
+
+	lock_python();
+	cur_python_script = data;
+
+	PyObject *py_settings;
+	if (libobs_to_py(obs_data_t, s->settings, false, &py_settings)) {
+		PyObject *args = Py_BuildValue("(O)", py_settings);
+		PyObject *ret = PyObject_CallObject(data->save, args);
+		py_error();
+		Py_XDECREF(ret);
+		Py_XDECREF(args);
+		Py_XDECREF(py_settings);
+	}
+
+	cur_python_script = NULL;
+	unlock_python();
+}
+
+/* -------------------------------------------- */
+
+static void python_tick(void *param, float seconds)
+{
+	struct obs_python_script *data;
+	bool valid;
+	uint64_t ts = obs_get_video_frame_time();
+
+	pthread_mutex_lock(&tick_mutex);
+	valid = !!first_tick_script;
+	pthread_mutex_unlock(&tick_mutex);
+
+	/* --------------------------------- */
+	/* process script_tick calls         */
+
+	if (valid) {
+		lock_python();
+
+		PyObject *args = Py_BuildValue("(f)", seconds);
+
+		pthread_mutex_lock(&tick_mutex);
+		data = first_tick_script;
+		while (data) {
+			cur_python_script = data;
+
+			PyObject *py_ret = PyObject_CallObject(data->tick, args);
+			Py_XDECREF(py_ret);
+			py_error();
+
+			data = data->next_tick;
+		}
+
+		cur_python_script = NULL;
+
+		pthread_mutex_unlock(&tick_mutex);
+
+		Py_XDECREF(args);
+
+		unlock_python();
+	}
+
+	/* --------------------------------- */
+	/* process timers                    */
+
+	pthread_mutex_lock(&timer_mutex);
+	struct python_obs_timer *timer = first_timer;
+	while (timer) {
+		struct python_obs_timer *next = timer->next;
+		struct python_obs_callback *cb = python_obs_timer_cb(timer);
+
+		if (cb->base.removed) {
+			python_obs_timer_remove(timer);
+		} else {
+			uint64_t elapsed = ts - timer->last_ts;
+
+			if (elapsed >= timer->interval) {
+				lock_python();
+				timer_call(&cb->base);
+				unlock_python();
+
+				timer->last_ts += timer->interval;
+			}
+		}
+
+		timer = next;
+	}
+	pthread_mutex_unlock(&timer_mutex);
+
+	UNUSED_PARAMETER(param);
+}
+
+/* -------------------------------------------- */
+
+void obs_python_unload(void);
+
+bool obs_scripting_python_runtime_linked(void)
+{
+	return (bool)RUNTIME_LINK;
+}
+
+bool obs_scripting_python_loaded(void)
+{
+	return python_loaded;
+}
+
+void obs_python_load(void)
+{
+	da_init(python_paths);
+
+	pthread_mutexattr_t attr;
+	pthread_mutexattr_init(&attr);
+	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
+
+	pthread_mutex_init(&tick_mutex, NULL);
+	pthread_mutex_init(&timer_mutex, &attr);
+
+}
+
+extern void add_python_frontend_funcs(PyObject *module);
+
+bool obs_scripting_load_python(const char *python_path)
+{
+	if (python_loaded)
+		return true;
+
+	/* Use external python on windows and mac */
+#if RUNTIME_LINK
+# if 0
+	struct dstr old_path  = {0};
+	struct dstr new_path  = {0};
+# endif
+
+	if (!import_python(python_path))
+		return false;
+
+	if (python_path && *python_path) {
+		os_utf8_to_wcs(python_path, 0, home_path, 1024);
+		Py_SetPythonHome(home_path);
+# if 0
+		dstr_copy(&old_path, getenv("PATH"));
+		_putenv("PYTHONPATH=");
+		_putenv("PATH=");
+# endif
+	}
+#else
+	UNUSED_PARAMETER(python_path);
+#endif
+
+	Py_Initialize();
+	if (!Py_IsInitialized())
+		return false;
+
+#if 0
+# ifdef _DEBUG
+	if (pythondir && *pythondir) {
+		dstr_printf(&new_path, "PATH=%s", old_path.array);
+		_putenv(new_path.array);
+	}
+# endif
+
+	bfree(pythondir);
+	dstr_free(&new_path);
+	dstr_free(&old_path);
+#endif
+
+	PyEval_InitThreads();
+	if (!PyEval_ThreadsInitialized())
+		return false;
+
+	/* ---------------------------------------------- */
+	/* Must set arguments for guis to work            */
+
+	wchar_t *argv[] = {L"", NULL};
+	int      argc   = sizeof(argv) / sizeof(wchar_t*) - 1;
+
+	PySys_SetArgv(argc, argv);
+
+#ifdef DEBUG_PYTHON_STARTUP
+	/* ---------------------------------------------- */
+	/* Debug logging to file if startup is failing    */
+
+	PyRun_SimpleString("import os");
+	PyRun_SimpleString("import sys");
+	PyRun_SimpleString("os.environ['PYTHONUNBUFFERED'] = '1'");
+	PyRun_SimpleString("sys.stdout = open('./stdOut.txt','w',1)");
+	PyRun_SimpleString("sys.stderr = open('./stdErr.txt','w',1)");
+	PyRun_SimpleString("print(sys.version)");
+#endif
+
+	/* ---------------------------------------------- */
+	/* Load main interface module                     */
+
+	py_obspython = PyImport_ImportModule("obspython");
+	bool success = !py_error();
+	if (!success) {
+		warn("Error importing obspython.py', unloading obs-python");
+		goto out;
+	}
+
+	python_loaded = PyRun_SimpleString(startup_script) == 0;
+	py_error();
+
+	add_hook_functions(py_obspython);
+	py_error();
+
+	add_python_frontend_funcs(py_obspython);
+	py_error();
+
+out:
+	/* ---------------------------------------------- */
+	/* Free data                                      */
+
+	PyEval_ReleaseThread(PyGILState_GetThisThreadState());
+
+	if (!success) {
+		warn("Failed to load python plugin");
+		obs_python_unload();
+	}
+
+	if (python_loaded)
+		obs_add_tick_callback(python_tick, NULL);
+
+	return python_loaded;
+}
+
+void obs_python_unload(void)
+{
+	if (python_loaded && Py_IsInitialized()) {
+		PyGILState_Ensure();
+
+		Py_XDECREF(py_obspython);
+		Py_Finalize();
+	}
+
+	/* ---------------------- */
+
+	obs_remove_tick_callback(python_tick, NULL);
+
+	for (size_t i = 0; i < python_paths.num; i++)
+		bfree(python_paths.array[i]);
+	da_free(python_paths);
+
+	pthread_mutex_destroy(&tick_mutex);
+	pthread_mutex_destroy(&timer_mutex);
+	dstr_free(&cur_py_log_chunk);
+}

+ 244 - 0
deps/obs-scripting/obs-scripting-python.h

@@ -0,0 +1,244 @@
+/******************************************************************************
+    Copyright (C) 2015 by Andrew Skinner <[email protected]>
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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/>.
+******************************************************************************/
+
+#pragma once
+
+/* ---------------------------- */
+
+#define SWIG_TYPE_TABLE obspython
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4100)
+#pragma warning(disable : 4115)
+#pragma warning(disable : 4204)
+#endif
+
+#include "obs-scripting-python-import.h"
+
+#include <structmember.h>
+#include "swig/swigpyrun.h"
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+/* ---------------------------- */
+
+#include "obs-scripting-internal.h"
+#include "obs-scripting-callback.h"
+
+#ifdef _WIN32
+#define __func__ __FUNCTION__
+#else
+#include <dlfcn.h>
+#endif
+
+#include <callback/calldata.h>
+#include <util/threading.h>
+#include <util/base.h>
+
+#define do_log(level, format, ...) \
+	blog(level, "[Python] " format, ##__VA_ARGS__)
+
+#define warn(format, ...)  do_log(LOG_WARNING, format, ##__VA_ARGS__)
+#define info(format, ...)  do_log(LOG_INFO,    format, ##__VA_ARGS__)
+#define debug(format, ...) do_log(LOG_DEBUG,   format, ##__VA_ARGS__)
+
+/* ------------------------------------------------------------ */
+
+struct python_obs_callback;
+
+struct obs_python_script {
+	obs_script_t base;
+
+	struct dstr dir;
+	struct dstr name;
+
+	PyObject *module;
+
+	PyObject *save;
+	PyObject *update;
+	PyObject *get_properties;
+
+	struct script_callback *first_callback;
+
+	PyObject *tick;
+	struct obs_python_script *next_tick;
+	struct obs_python_script **p_prev_next_tick;
+};
+
+/* ------------------------------------------------------------ */
+
+struct python_obs_callback {
+	struct script_callback base;
+
+	PyObject *func;
+};
+
+static inline struct python_obs_callback *add_python_obs_callback_extra(
+		struct obs_python_script *script,
+		PyObject *func,
+		size_t extra_size)
+{
+	struct python_obs_callback *cb = add_script_callback(
+			&script->first_callback,
+			(obs_script_t *)script,
+			sizeof(*cb) + extra_size);
+
+	Py_XINCREF(func);
+	cb->func = func;
+	return cb;
+}
+
+static inline struct python_obs_callback *add_python_obs_callback(
+		struct obs_python_script *script,
+		PyObject *func)
+{
+	return add_python_obs_callback_extra(script, func, 0);
+}
+
+static inline void *python_obs_callback_extra_data(
+		struct python_obs_callback *cb)
+{
+	return (void*)&cb[1];
+}
+
+static inline struct obs_python_script *python_obs_callback_script(
+		struct python_obs_callback *cb)
+{
+	return (struct obs_python_script *)cb->base.script;
+}
+
+static inline struct python_obs_callback *find_next_python_obs_callback(
+		struct obs_python_script *script,
+		struct python_obs_callback *cb, PyObject *func)
+{
+	cb = cb ? (struct python_obs_callback *)cb->base.next
+		: (struct python_obs_callback *)script->first_callback;
+
+	while (cb) {
+		if (cb->func == func)
+			break;
+		cb = (struct python_obs_callback *)cb->base.next;
+	}
+
+	return cb;
+}
+
+static inline struct python_obs_callback *find_python_obs_callback(
+		struct obs_python_script *script,
+		PyObject *func)
+{
+	return find_next_python_obs_callback(script, NULL, func);
+}
+
+static inline void remove_python_obs_callback(struct python_obs_callback *cb)
+{
+	remove_script_callback(&cb->base);
+
+	Py_XDECREF(cb->func);
+	cb->func = NULL;
+}
+
+static inline void just_free_python_obs_callback(struct python_obs_callback *cb)
+{
+	just_free_script_callback(&cb->base);
+}
+
+static inline void free_python_obs_callback(struct python_obs_callback *cb)
+{
+	free_script_callback(&cb->base);
+}
+
+/* ------------------------------------------------------------ */
+
+static int parse_args_(PyObject *args, const char *func, const char *format, ...)
+{
+	char new_format[128];
+	va_list va_args;
+	int ret;
+
+	snprintf(new_format, sizeof(new_format), "%s:%s", format, func);
+
+	va_start(va_args, format);
+	ret = PyArg_VaParse(args, new_format, va_args);
+	va_end(va_args);
+
+	return ret;
+}
+
+#define parse_args(args, format, ...) \
+	parse_args_(args, __FUNCTION__, format, ##__VA_ARGS__)
+
+static inline bool py_error_(const char *func, int line)
+{
+	if (PyErr_Occurred()) {
+		warn("Python failure in %s:%d:", func, line);
+		PyErr_Print();
+		return true;
+	}
+	return false;
+}
+
+#define py_error() py_error_(__FUNCTION__, __LINE__)
+
+#define lock_python() \
+	PyGILState_STATE gstate = PyGILState_Ensure()
+#define unlock_python() \
+	PyGILState_Release(gstate)
+
+struct py_source;
+typedef struct py_source py_source_t;
+
+extern PyObject* py_libobs;
+extern struct python_obs_callback *cur_python_cb;
+extern struct obs_python_script *cur_python_script;
+
+extern void py_to_obs_source_info(py_source_t *py_info);
+extern PyObject *py_obs_register_source(PyObject *self, PyObject *args);
+extern PyObject *py_obs_get_script_config_path(PyObject *self, PyObject *args);
+extern void add_functions_to_py_module(PyObject *module,
+		PyMethodDef *method_list);
+
+/* ------------------------------------------------------------ */
+/* Warning: the following functions expect python to be locked! */
+
+extern bool py_to_libobs_(const char *type,
+                          PyObject *  py_in,
+                          void *      libobs_out,
+                          const char *id,
+                          const char *func,
+                          int         line);
+
+extern bool libobs_to_py_(const char *type,
+                          void *      libobs_in,
+                          bool        ownership,
+                          PyObject ** py_out,
+                          const char *id,
+                          const char *func,
+                          int         line);
+
+extern bool py_call(PyObject *call, PyObject **ret, const char *arg_def, ...);
+extern bool py_import_script(const char *name);
+
+static inline PyObject *python_none(void)
+{
+	PyObject *none = Py_None;
+	Py_INCREF(none);
+	return none;
+}

+ 455 - 0
deps/obs-scripting/obs-scripting.c

@@ -0,0 +1,455 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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.h>
+#include <util/dstr.h>
+#include <util/platform.h>
+#include <util/threading.h>
+#include <util/circlebuf.h>
+
+#include "obs-scripting-internal.h"
+#include "obs-scripting-callback.h"
+#include "obs-scripting-config.h"
+
+#if COMPILE_LUA
+extern obs_script_t *obs_lua_script_create(const char *path,
+		obs_data_t *settings);
+extern bool obs_lua_script_load(obs_script_t *s);
+extern void obs_lua_script_unload(obs_script_t *s);
+extern void obs_lua_script_destroy(obs_script_t *s);
+extern void obs_lua_load(void);
+extern void obs_lua_unload(void);
+
+extern obs_properties_t *obs_lua_script_get_properties(obs_script_t *script);
+extern void obs_lua_script_update(obs_script_t *script, obs_data_t *settings);
+extern void obs_lua_script_save(obs_script_t *script);
+#endif
+
+#if COMPILE_PYTHON
+extern obs_script_t *obs_python_script_create(const char *path,
+		obs_data_t *settings);
+extern bool obs_python_script_load(obs_script_t *s);
+extern void obs_python_script_unload(obs_script_t *s);
+extern void obs_python_script_destroy(obs_script_t *s);
+extern void obs_python_load(void);
+extern void obs_python_unload(void);
+
+extern obs_properties_t *obs_python_script_get_properties(obs_script_t *script);
+extern void obs_python_script_update(obs_script_t *script, obs_data_t *settings);
+extern void obs_python_script_save(obs_script_t *script);
+#endif
+
+pthread_mutex_t detach_mutex;
+struct script_callback *detached_callbacks;
+
+static struct dstr file_filter = {0};
+static bool scripting_loaded = false;
+
+static const char *supported_formats[] = {
+#if COMPILE_LUA
+	"lua",
+#endif
+#if COMPILE_PYTHON
+	"py",
+#endif
+	NULL
+};
+
+/* -------------------------------------------- */
+
+static pthread_mutex_t defer_call_mutex;
+static struct circlebuf defer_call_queue;
+static bool defer_call_exit = false;
+static os_sem_t *defer_call_semaphore;
+static pthread_t defer_call_thread;
+
+struct defer_call {
+	defer_call_cb call;
+	void *cb;
+};
+
+static void *defer_thread(void *unused)
+{
+	UNUSED_PARAMETER(unused);
+
+	while (os_sem_wait(defer_call_semaphore) == 0) {
+		struct defer_call info;
+
+		pthread_mutex_lock(&defer_call_mutex);
+		if (defer_call_exit) {
+			pthread_mutex_unlock(&defer_call_mutex);
+			return NULL;
+		}
+
+		circlebuf_pop_front(&defer_call_queue, &info, sizeof(info));
+		pthread_mutex_unlock(&defer_call_mutex);
+
+		info.call(info.cb);
+	}
+
+	return NULL;
+}
+
+void defer_call_post(defer_call_cb call, void *cb)
+{
+	struct defer_call info;
+	info.call = call;
+	info.cb = cb;
+
+	pthread_mutex_lock(&defer_call_mutex);
+	if (!defer_call_exit)
+		circlebuf_push_back(&defer_call_queue, &info, sizeof(info));
+	pthread_mutex_unlock(&defer_call_mutex);
+
+	os_sem_post(defer_call_semaphore);
+}
+
+/* -------------------------------------------- */
+
+bool obs_scripting_load(void)
+{
+	circlebuf_init(&defer_call_queue);
+
+	if (pthread_mutex_init(&detach_mutex, NULL) != 0) {
+		return false;
+	}
+	if (pthread_mutex_init(&defer_call_mutex, NULL) != 0) {
+		pthread_mutex_destroy(&detach_mutex);
+		return false;
+	}
+	if (os_sem_init(&defer_call_semaphore, 0) != 0) {
+		pthread_mutex_destroy(&defer_call_mutex);
+		pthread_mutex_destroy(&detach_mutex);
+		return false;
+	}
+
+	if (pthread_create(&defer_call_thread, NULL, defer_thread, NULL) != 0) {
+		os_sem_destroy(defer_call_semaphore);
+		pthread_mutex_destroy(&defer_call_mutex);
+		pthread_mutex_destroy(&detach_mutex);
+		return false;
+	}
+
+#if COMPILE_LUA
+	obs_lua_load();
+#endif
+
+#if COMPILE_PYTHON
+	obs_python_load();
+	obs_scripting_load_python(NULL);
+#endif
+
+	scripting_loaded = true;
+	return true;
+}
+
+void obs_scripting_unload(void)
+{
+	if (!scripting_loaded)
+		return;
+
+	/* ---------------------- */
+
+#if COMPILE_LUA
+	obs_lua_unload();
+#endif
+
+#if COMPILE_PYTHON
+	obs_python_unload();
+#endif
+
+	dstr_free(&file_filter);
+
+	/* ---------------------- */
+
+	int total_detached = 0;
+
+	pthread_mutex_lock(&detach_mutex);
+
+	struct script_callback *cur = detached_callbacks;
+	while (cur) {
+		struct script_callback *next = cur->next;
+		just_free_script_callback(cur);
+		cur = next;
+
+		++total_detached;
+	}
+
+	pthread_mutex_unlock(&detach_mutex);
+	pthread_mutex_destroy(&detach_mutex);
+
+	blog(LOG_INFO, "[Scripting] Total detached callbacks: %d",
+			total_detached);
+
+	/* ---------------------- */
+
+	pthread_mutex_lock(&defer_call_mutex);
+
+	/* TODO */
+
+	defer_call_exit = true;
+	circlebuf_free(&defer_call_queue);
+
+	pthread_mutex_unlock(&defer_call_mutex);
+
+	os_sem_post(defer_call_semaphore);
+	pthread_join(defer_call_thread, NULL);
+
+	pthread_mutex_destroy(&defer_call_mutex);
+	os_sem_destroy(defer_call_semaphore);
+}
+
+const char **obs_scripting_supported_formats(void)
+{
+	return supported_formats;
+}
+
+static inline bool pointer_valid(const void *x, const char *name,
+		const char *func)
+{
+	if (!x) {
+		blog(LOG_WARNING, "obs-scripting: [%s] %s is null",
+				func, name);
+		return false;
+	}
+
+	return true;
+}
+
+#define ptr_valid(x) pointer_valid(x, #x, __FUNCTION__)
+
+obs_script_t *obs_script_create(const char *path, obs_data_t *settings)
+{
+	obs_script_t *script = NULL;
+	const char *ext;
+
+	if (!scripting_loaded)
+		return NULL;
+	if (!ptr_valid(path))
+		return NULL;
+
+	ext = strrchr(path, '.');
+	if (!ext)
+		return NULL;
+
+#if COMPILE_LUA
+	if (strcmp(ext, ".lua") == 0) {
+		script = obs_lua_script_create(path, settings);
+	} else
+#endif
+#if COMPILE_PYTHON
+	if (strcmp(ext, ".py") == 0) {
+		script = obs_python_script_create(path, settings);
+	} else
+#endif
+	{
+		blog(LOG_WARNING, "Unsupported/unknown script type: %s", path);
+	}
+
+	return script;
+}
+
+const char *obs_script_get_description(const obs_script_t *script)
+{
+	return ptr_valid(script) ? script->desc.array : NULL;
+}
+
+const char *obs_script_get_path(const obs_script_t *script)
+{
+	const char *path = ptr_valid(script) ? script->path.array : "";
+	return path ? path : "";
+}
+
+const char *obs_script_get_file(const obs_script_t *script)
+{
+	const char *file = ptr_valid(script) ? script->file.array : "";
+	return file ? file : "";
+}
+
+enum obs_script_lang obs_script_get_lang(const obs_script_t *script)
+{
+	return ptr_valid(script) ? script->type : OBS_SCRIPT_LANG_UNKNOWN;
+}
+
+obs_data_t *obs_script_get_settings(obs_script_t *script)
+{
+	obs_data_t *settings;
+
+	if (!ptr_valid(script))
+		return NULL;
+
+	settings = script->settings;
+	obs_data_addref(settings);
+	return settings;
+}
+
+obs_properties_t *obs_script_get_properties(obs_script_t *script)
+{
+	obs_properties_t *props = NULL;
+
+	if (!ptr_valid(script))
+		return NULL;
+#if COMPILE_LUA
+	if (script->type == OBS_SCRIPT_LANG_LUA) {
+		props = obs_lua_script_get_properties(script);
+		goto out;
+	}
+#endif
+#if COMPILE_PYTHON
+	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
+		props = obs_python_script_get_properties(script);
+		goto out;
+	}
+#endif
+
+out:
+	if (!props)
+		props = obs_properties_create();
+	return props;
+}
+
+obs_data_t *obs_script_save(obs_script_t *script)
+{
+	obs_data_t *settings;
+
+	if (!ptr_valid(script))
+		return NULL;
+
+#if COMPILE_LUA
+	if (script->type == OBS_SCRIPT_LANG_LUA) {
+		obs_lua_script_save(script);
+		goto out;
+	}
+#endif
+#if COMPILE_PYTHON
+	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
+		obs_python_script_save(script);
+		goto out;
+	}
+#endif
+
+out:
+	settings = script->settings;
+	obs_data_addref(settings);
+	return settings;
+}
+
+static void clear_queue_signal(void *p_event)
+{
+	os_event_t *event = p_event;
+	os_event_signal(event);
+}
+
+static void clear_call_queue(void)
+{
+	os_event_t *event;
+	if (os_event_init(&event, OS_EVENT_TYPE_AUTO) != 0)
+		return;
+
+	defer_call_post(clear_queue_signal, event);
+
+	os_event_wait(event);
+	os_event_destroy(event);
+}
+
+void obs_script_update(obs_script_t *script, obs_data_t *settings)
+{
+	if (!ptr_valid(script))
+		return;
+#if COMPILE_LUA
+	if (script->type == OBS_SCRIPT_LANG_LUA) {
+		obs_lua_script_update(script, settings);
+	}
+#endif
+#if COMPILE_PYTHON
+	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
+		obs_python_script_update(script, settings);
+	}
+#endif
+}
+
+bool obs_script_reload(obs_script_t *script)
+{
+	if (!scripting_loaded)
+		return false;
+	if (!ptr_valid(script))
+		return false;
+
+#if COMPILE_LUA
+	if (script->type == OBS_SCRIPT_LANG_LUA) {
+		obs_lua_script_unload(script);
+		clear_call_queue();
+		obs_lua_script_load(script);
+		goto out;
+	}
+#endif
+#if COMPILE_PYTHON
+	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
+		obs_python_script_unload(script);
+		clear_call_queue();
+		obs_python_script_load(script);
+		goto out;
+	}
+#endif
+
+out:
+	return script->loaded;
+}
+
+bool obs_script_loaded(const obs_script_t *script)
+{
+	return ptr_valid(script) ? script->loaded : false;
+}
+
+void obs_script_destroy(obs_script_t *script)
+{
+	if (!script)
+		return;
+
+#if COMPILE_LUA
+	if (script->type == OBS_SCRIPT_LANG_LUA) {
+		obs_lua_script_unload(script);
+		obs_lua_script_destroy(script);
+		return;
+	}
+#endif
+#if COMPILE_PYTHON
+	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
+		obs_python_script_unload(script);
+		obs_python_script_destroy(script);
+		return;
+	}
+#endif
+}
+
+#if !COMPILE_PYTHON
+bool obs_scripting_load_python(const char *python_path)
+{
+	UNUSED_PARAMETER(python_path);
+	return false;
+}
+
+bool obs_scripting_python_loaded(void)
+{
+	return false;
+}
+
+bool obs_scripting_python_runtime_linked(void)
+{
+	return (bool)true;
+}
+#endif

+ 73 - 0
deps/obs-scripting/obs-scripting.h

@@ -0,0 +1,73 @@
+/******************************************************************************
+    Copyright (C) 2017 by Hugh Bailey <[email protected]>
+
+    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/>.
+******************************************************************************/
+
+#pragma once
+
+#include <stdarg.h>
+#include <util/c99defs.h>
+#include <obs-data.h>
+#include <obs-properties.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct obs_script;
+typedef struct obs_script obs_script_t;
+
+enum obs_script_lang {
+	OBS_SCRIPT_LANG_UNKNOWN,
+	OBS_SCRIPT_LANG_LUA,
+	OBS_SCRIPT_LANG_PYTHON
+};
+
+EXPORT bool obs_scripting_load(void);
+EXPORT void obs_scripting_unload(void);
+EXPORT const char **obs_scripting_supported_formats(void);
+
+typedef void (*scripting_log_handler_t)(
+		void *p,
+		obs_script_t *script,
+		int lvl,
+		const char *msg);
+
+EXPORT void obs_scripting_set_log_callback(
+		scripting_log_handler_t handler, void *param);
+
+EXPORT bool obs_scripting_python_runtime_linked(void);
+EXPORT bool obs_scripting_python_loaded(void);
+EXPORT bool obs_scripting_load_python(const char *python_path);
+
+EXPORT obs_script_t *obs_script_create(const char *path, obs_data_t *settings);
+EXPORT void obs_script_destroy(obs_script_t *script);
+
+EXPORT const char *obs_script_get_description(const obs_script_t *script);
+EXPORT const char *obs_script_get_path(const obs_script_t *script);
+EXPORT const char *obs_script_get_file(const obs_script_t *script);
+EXPORT enum obs_script_lang obs_script_get_lang(const obs_script_t *script);
+
+EXPORT obs_properties_t *obs_script_get_properties(obs_script_t *script);
+EXPORT obs_data_t *obs_script_save(obs_script_t *script);
+EXPORT obs_data_t *obs_script_get_settings(obs_script_t *script);
+EXPORT void obs_script_update(obs_script_t *script, obs_data_t *settings);
+
+EXPORT bool obs_script_loaded(const obs_script_t *script);
+EXPORT bool obs_script_reload(obs_script_t *script);
+
+#ifdef __cplusplus
+}
+#endif

+ 41 - 0
deps/obs-scripting/obslua/CMakeLists.txt

@@ -0,0 +1,41 @@
+cmake_minimum_required(VERSION 2.8)
+project(obslua)
+
+find_package(SWIG 2 REQUIRED)
+include(${SWIG_USE_FILE})
+
+add_definitions(-DSWIG_TYPE_TABLE=obslua -DSWIG_LUA_INTERPRETER_NO_DEBUG)
+
+if(MSVC)
+	add_compile_options("/wd4054")
+	add_compile_options("/wd4197")
+	add_compile_options("/wd4244")
+	add_compile_options("/wd4267")
+endif()
+
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+
+SWIG_ADD_MODULE(obslua lua obslua.i ../cstrcache.cpp ../cstrcache.h)
+SWIG_LINK_LIBRARIES(obslua obs-scripting libobs ${LUA_LIBRARIES} ${EXTRA_LIBS})
+
+function(install_plugin_bin_swig target additional_target)
+	if(APPLE)
+		set(_bit_suffix "")
+	elseif(CMAKE_SIZEOF_VOID_P EQUAL 8)
+		set(_bit_suffix "64bit/")
+	else()
+		set(_bit_suffix "32bit/")
+	endif()
+
+	set_target_properties(${additional_target} PROPERTIES
+		PREFIX "")
+
+	add_custom_command(TARGET ${additional_target} POST_BUILD
+		COMMAND "${CMAKE_COMMAND}" -E copy
+			"$<TARGET_FILE:${additional_target}>"
+			"${OBS_OUTPUT_DIR}/$<CONFIGURATION>/bin/${_bit_suffix}$<TARGET_FILE_NAME:${additional_target}>"
+		VERBATIM)
+endfunction()
+
+install_plugin_bin_swig(obs-scripting obslua)

+ 99 - 0
deps/obs-scripting/obslua/obslua.i

@@ -0,0 +1,99 @@
+%module obslua
+%{
+#define SWIG_FILE_WITH_INIT
+#define DEPRECATED_START
+#define DEPRECATED_END
+#include <graphics/graphics.h>
+#include <graphics/vec4.h>
+#include <graphics/vec3.h>
+#include <graphics/vec2.h>
+#include <graphics/quat.h>
+#include <obs.h>
+#include <obs-hotkey.h>
+#include <obs-source.h>
+#include <obs-data.h>
+#include <obs-properties.h>
+#include <obs-interaction.h>
+#include <callback/calldata.h>
+#include <callback/proc.h>
+#include <callback/signal.h>
+#include <util/bmem.h>
+#include <util/base.h>
+#include "cstrcache.h"
+#include "obs-scripting-config.h"
+
+#if UI_ENABLED
+#include "obs-frontend-api.h"
+#endif
+
+%}
+
+#define DEPRECATED_START
+#define DEPRECATED_END
+#define EXPORT
+
+%rename(blog) wrap_blog;
+%inline %{
+static inline void wrap_blog(int log_level, const char *message)
+{
+        blog(log_level, "%s", message);
+}
+%}
+
+%include "stdint.i"
+
+/* Used to free when using %newobject functions.  E.G.:
+ * %newobject obs_module_get_config_path; */
+%typemap(newfree) char * "bfree($1);";
+
+%ignore blog;
+%ignore blogva;
+%ignore bcrash;
+%ignore obs_source_info;
+%ignore obs_register_source_s(const struct obs_source_info *info, size_t size);
+%ignore obs_output_set_video(obs_output_t *output, video_t *video);
+%ignore obs_output_video(const obs_output_t *output);
+%ignore obs_add_tick_callback;
+%ignore obs_remove_tick_callback;
+%ignore obs_add_main_render_callback;
+%ignore obs_remove_main_render_callback;
+%ignore obs_enum_sources;
+%ignore obs_properties_add_button;
+%ignore obs_property_set_modified_callback;
+%ignore signal_handler_connect;
+%ignore signal_handler_disconnect;
+%ignore signal_handler_connect_global;
+%ignore signal_handler_disconnect_global;
+%ignore signal_handler_remove_current;
+%ignore obs_hotkey_register_frontend;
+%ignore obs_hotkey_register_encoder;
+%ignore obs_hotkey_register_output;
+%ignore obs_hotkey_register_service;
+%ignore obs_hotkey_register_source;
+%ignore obs_hotkey_pair_register_frontend;
+%ignore obs_hotkey_pair_register_encoder;
+%ignore obs_hotkey_pair_register_output;
+%ignore obs_hotkey_pair_register_service;
+%ignore obs_hotkey_pair_register_source;
+
+%include "graphics/graphics.h"
+%include "graphics/vec4.h"
+%include "graphics/vec3.h"
+%include "graphics/vec2.h"
+%include "graphics/quat.h"
+%include "obs-data.h"
+%include "obs-source.h"
+%include "obs-properties.h"
+%include "obs-interaction.h"
+%include "obs-hotkey.h"
+%include "obs.h"
+%include "callback/calldata.h"
+%include "callback/proc.h"
+%include "callback/signal.h"
+%include "util/bmem.h"
+%include "util/base.h"
+%include "obs-scripting-config.h"
+
+#if UI_ENABLED
+%include "obs-frontend-api.h"
+#endif

+ 60 - 0
deps/obs-scripting/obspython/CMakeLists.txt

@@ -0,0 +1,60 @@
+cmake_minimum_required(VERSION 2.8)
+project(obspython)
+
+find_package(SWIG 2 REQUIRED)
+include(${SWIG_USE_FILE})
+
+add_definitions(-DSWIG_TYPE_TABLE=obspython -DMS_NO_COREDLL -DPy_ENABLE_SHARED=1 -DSWIG_PYTHON_INTERPRETER_NO_DEBUG)
+
+if(MSVC)
+	add_compile_options("/wd4054")
+	add_compile_options("/wd4100")
+	add_compile_options("/wd4115")
+	add_compile_options("/wd4197")
+	add_compile_options("/wd4701")
+endif()
+
+include_directories(${PYTHON_INCLUDE_DIR})
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+
+#add_definitions( -DSWIG_TYPE_TABLE=libobs )
+SET_SOURCE_FILES_PROPERTIES(obspython.i PROPERTIES SWIG_FLAGS "-modern")
+SET_SOURCE_FILES_PROPERTIES(obspython.i PROPERTIES SWIG_FLAGS "-builtin")
+SET_SOURCE_FILES_PROPERTIES(obspython.i PROPERTIES SWIG_FLAGS "-modernargs")
+SET_SOURCE_FILES_PROPERTIES(obspython.i PROPERTIES SWIG_FLAGS "-includeall")
+SET_SOURCE_FILES_PROPERTIES(obspython.i PROPERTIES SWIG_FLAGS "-importall")
+SET_SOURCE_FILES_PROPERTIES(obspython.i PROPERTIES SWIG_FLAGS "-py3")
+
+if(WIN32)
+	string(REGEX REPLACE "_d" "" PYTHON_LIBRARIES "${PYTHON_LIBRARIES}")
+endif()
+
+SWIG_ADD_MODULE(obspython python obspython.i ../cstrcache.cpp ../cstrcache.h)
+SWIG_LINK_LIBRARIES(obspython obs-scripting libobs ${PYTHON_LIBRARIES})
+
+function(install_plugin_bin_swig target additional_target)
+	if(APPLE)
+		set(_bit_suffix "")
+	elseif(CMAKE_SIZEOF_VOID_P EQUAL 8)
+		set(_bit_suffix "64bit/")
+	else()
+		set(_bit_suffix "32bit/")
+	endif()
+
+	set_target_properties(${additional_target} PROPERTIES
+		PREFIX "")
+
+	add_custom_command(TARGET ${additional_target} POST_BUILD
+		COMMAND "${CMAKE_COMMAND}" -E copy
+			"${CMAKE_CURRENT_BINARY_DIR}/obspython.py"
+			"${OBS_OUTPUT_DIR}/$<CONFIGURATION>/bin/${_bit_suffix}/obspython.py"
+		VERBATIM)
+	add_custom_command(TARGET ${additional_target} POST_BUILD
+		COMMAND "${CMAKE_COMMAND}" -E copy
+			"$<TARGET_FILE:${additional_target}>"
+			"${OBS_OUTPUT_DIR}/$<CONFIGURATION>/bin/${_bit_suffix}$<TARGET_FILE_NAME:${additional_target}>"
+		VERBATIM)
+endfunction()
+
+install_plugin_bin_swig(obs-scripting _obspython)

+ 106 - 0
deps/obs-scripting/obspython/obspython.i

@@ -0,0 +1,106 @@
+%module(threads="1") obspython
+%nothread;
+%{
+#define SWIG_FILE_WITH_INIT
+#define DEPRECATED_START
+#define DEPRECATED_END
+#include <graphics/graphics.h>
+#include <graphics/vec4.h>
+#include <graphics/vec3.h>
+#include <graphics/vec2.h>
+#include <graphics/quat.h>
+#include <obs.h>
+#include <obs-hotkey.h>
+#include <obs-source.h>
+#include <obs-data.h>
+#include <obs-properties.h>
+#include <obs-interaction.h>
+#include <callback/calldata.h>
+#include <callback/decl.h>
+#include <callback/proc.h>
+#include <callback/signal.h>
+#include <util/bmem.h>
+#include <util/base.h>
+#include "obs-scripting-config.h"
+
+#if UI_ENABLED
+#include "obs-frontend-api.h"
+#endif
+
+%}
+
+#define DEPRECATED_START
+#define DEPRECATED_END
+#define EXPORT
+
+%rename(blog) wrap_blog;
+%inline %{
+static inline void wrap_blog(int log_level, const char *message)
+{
+        blog(log_level, "%s", message);
+}
+%}
+
+%include "stdint.i"
+
+/* Used to free when using %newobject functions.  E.G.:
+ * %newobject obs_module_get_config_path; */
+%typemap(newfree) char * "bfree($1);";
+
+%ignore blog;
+%ignore blogva;
+%ignore bcrash;
+%ignore obs_source_info;
+%ignore obs_register_source_s(const struct obs_source_info *info, size_t size);
+%ignore obs_output_set_video(obs_output_t *output, video_t *video);
+%ignore obs_output_video(const obs_output_t *output);
+%ignore obs_add_tick_callback;
+%ignore obs_remove_tick_callback;
+%ignore obs_add_main_render_callback;
+%ignore obs_remove_main_render_callback;
+%ignore obs_enum_sources;
+%ignore obs_properties_add_button;
+%ignore obs_property_set_modified_callback;
+%ignore signal_handler_connect;
+%ignore signal_handler_disconnect;
+%ignore signal_handler_connect_global;
+%ignore signal_handler_disconnect_global;
+%ignore signal_handler_remove_current;
+%ignore obs_hotkey_register_frontend;
+%ignore obs_hotkey_register_encoder;
+%ignore obs_hotkey_register_output;
+%ignore obs_hotkey_register_service;
+%ignore obs_hotkey_register_source;
+%ignore obs_hotkey_pair_register_frontend;
+%ignore obs_hotkey_pair_register_encoder;
+%ignore obs_hotkey_pair_register_output;
+%ignore obs_hotkey_pair_register_service;
+%ignore obs_hotkey_pair_register_source;
+
+%include "graphics/graphics.h"
+%include "graphics/vec4.h"
+%include "graphics/vec3.h"
+%include "graphics/vec2.h"
+%include "graphics/quat.h"
+%include "obs-data.h"
+%include "obs-source.h"
+%include "obs-properties.h"
+%include "obs-interaction.h"
+%include "obs-hotkey.h"
+%include "obs.h"
+%include "callback/calldata.h"
+%include "callback/proc.h"
+%include "callback/signal.h"
+%include "util/bmem.h"
+%include "util/base.h"
+%include "obs-scripting-config.h"
+
+#if UI_ENABLED
+%include "obs-frontend-api.h"
+#endif
+
+/* declare these manually because mutex + GIL = deadlocks */
+%thread;
+void obs_enter_graphics(void); //Should only block on entering mutex
+%nothread;
+%include "obs.h"