Răsfoiți Sursa

UI: Add front-end API library

Allows manipulating and modifying the front-end via plugins.
jp9000 9 ani în urmă
părinte
comite
8836592d92

+ 9 - 2
UI/CMakeLists.txt

@@ -1,5 +1,3 @@
-project(obs)
-
 option(ENABLE_UI "Enables the OBS user interfaces" ON)
 if(DISABLE_UI)
 	message(STATUS "UI disabled")
@@ -10,6 +8,12 @@ else()
 	set(FIND_MODE QUIET)
 endif()
 
+add_subdirectory(obs-frontend-api)
+
+# ----------------------------------------------------------------------------
+
+project(obs)
+
 if(DEFINED QTDIR${_lib_suffix})
 	list(APPEND CMAKE_PREFIX_PATH "${QTDIR${_lib_suffix}}")
 elseif(DEFINED QTDIR)
@@ -40,6 +44,7 @@ if(NOT Qt5Widgets_FOUND)
 	endif()
 endif()
 
+include_directories(SYSTEM "obs-frontend-api")
 include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
 
 find_package(Libcurl REQUIRED)
@@ -97,6 +102,7 @@ endif()
 set(obs_SOURCES
 	${obs_PLATFORM_SOURCES}
 	obs-app.cpp
+	api-interface.cpp
 	window-basic-main.cpp
 	window-basic-filters.cpp
 	window-basic-settings.cpp
@@ -219,6 +225,7 @@ target_link_libraries(obs
 	libobs
 	libff
 	Qt5::Widgets
+	obs-frontend-api
 	${LIBCURL_LIBRARIES}
 	${obs_PLATFORM_LIBRARIES})
 

+ 362 - 0
UI/api-interface.cpp

@@ -0,0 +1,362 @@
+#include <obs-frontend-internal.hpp>
+#include "obs-app.hpp"
+#include "qt-wrappers.hpp"
+#include "window-basic-main.hpp"
+#include "window-basic-main-outputs.hpp"
+
+#include <functional>
+
+using namespace std;
+
+Q_DECLARE_METATYPE(OBSScene);
+Q_DECLARE_METATYPE(OBSSource);
+
+template <typename T>
+static T GetOBSRef(QListWidgetItem *item)
+{
+	return item->data(static_cast<int>(QtDataRole::OBSRef)).value<T>();
+}
+
+void EnumProfiles(function<bool (const char *, const char *)> &&cb);
+void EnumSceneCollections(function<bool (const char *, const char *)> &&cb);
+
+/* ------------------------------------------------------------------------- */
+
+template<typename T> struct OBSStudioCallback {
+	T callback;
+	void *private_data;
+
+	inline OBSStudioCallback(T cb, void *p) :
+		callback(cb), private_data(p)
+	{}
+};
+
+template <typename T> inline size_t GetCallbackIdx(
+		vector<OBSStudioCallback<T>> &callbacks,
+		T callback, void *private_data)
+{
+	for (size_t i = 0; i < callbacks.size(); i++) {
+		OBSStudioCallback<T> curCB = callbacks[i];
+		if (curCB.callback     == callback &&
+		    curCB.private_data == private_data)
+			return i;
+	}
+
+	return (size_t)-1;
+}
+
+struct OBSStudioAPI : obs_frontend_callbacks {
+	OBSBasic *main;
+	vector<OBSStudioCallback<obs_frontend_event_cb>> callbacks;
+	vector<OBSStudioCallback<obs_frontend_save_cb>> saveCallbacks;
+
+	inline OBSStudioAPI(OBSBasic *main_) : main(main_) {}
+
+	void *obs_frontend_get_main_window(void) override
+	{
+		return (void*)main;
+	}
+
+	void *obs_frontend_get_main_window_handle(void) override
+	{
+		return (void*)main->winId();
+	}
+
+	void obs_frontend_get_scenes(
+			struct obs_frontend_source_list *sources) override
+	{
+		for (int i = 0; i < main->ui->scenes->count(); i++) {
+			QListWidgetItem *item = main->ui->scenes->item(i);
+			OBSScene scene = GetOBSRef<OBSScene>(item);
+			obs_source_t *source = obs_scene_get_source(scene);
+
+			obs_source_addref(source);
+			da_push_back(sources->sources, &source);
+		}
+	}
+
+	obs_source_t *obs_frontend_get_current_scene(void) override
+	{
+		OBSSource source;
+
+		if (main->IsPreviewProgramMode()) {
+			source = obs_weak_source_get_source(main->programScene);
+		} else {
+			source = main->GetCurrentSceneSource();
+			obs_source_addref(source);
+		}
+		return source;
+	}
+
+	void obs_frontend_set_current_scene(obs_source_t *scene) override
+	{
+		if (main->IsPreviewProgramMode()) {
+			QMetaObject::invokeMethod(main, "TransitionToScene",
+					Q_ARG(OBSSource, OBSSource(scene)));
+		} else {
+			QMetaObject::invokeMethod(main, "SetCurrentScene",
+					Q_ARG(OBSSource, OBSSource(scene)),
+					Q_ARG(bool, false));
+		}
+	}
+
+	void obs_frontend_get_transitions(
+			struct obs_frontend_source_list *sources) override
+	{
+		for (int i = 0; i < main->ui->transitions->count(); i++) {
+			OBSSource tr = main->ui->transitions->itemData(i)
+				.value<OBSSource>();
+
+			obs_source_addref(tr);
+			da_push_back(sources->sources, &tr);
+		}
+	}
+
+	obs_source_t *obs_frontend_get_current_transition(void) override
+	{
+		OBSSource tr = main->GetCurrentTransition();
+
+		obs_source_addref(tr);
+		return tr;
+	}
+
+	void obs_frontend_set_current_transition(
+			obs_source_t *transition) override
+	{
+		QMetaObject::invokeMethod(main, "SetTransition",
+				Q_ARG(OBSSource, OBSSource(transition)));
+	}
+
+	void obs_frontend_get_scene_collections(
+			std::vector<std::string> &strings) override
+	{
+		auto addCollection = [&](const char *name, const char *)
+		{
+			strings.emplace_back(name);
+			return true;
+		};
+
+		EnumSceneCollections(addCollection);
+	}
+
+	char *obs_frontend_get_current_scene_collection(void) override
+	{
+		const char *cur_name = config_get_string(App()->GlobalConfig(),
+				"Basic", "SceneCollection");
+		return bstrdup(cur_name);
+	}
+
+	void obs_frontend_set_current_scene_collection(
+			const char *collection) override
+	{
+		QList<QAction*> menuActions =
+			main->ui->sceneCollectionMenu->actions();
+		QString qstrCollection = QT_UTF8(collection);
+
+		for (int i = 0; i < menuActions.count(); i++) {
+			QAction *action = menuActions[i];
+			QVariant v = action->property("file_name");
+
+			if (v.typeName() != nullptr) {
+				if (action->text() == qstrCollection) {
+					action->trigger();
+					break;
+				}
+			}
+		}
+	}
+
+	void obs_frontend_get_profiles(
+			std::vector<std::string> &strings) override
+	{
+		auto addProfile = [&](const char *name, const char *)
+		{
+			strings.emplace_back(name);
+			return true;
+		};
+
+		EnumProfiles(addProfile);
+	}
+
+	char *obs_frontend_get_current_profile(void) override
+	{
+		const char *name = config_get_string(App()->GlobalConfig(),
+				"Basic", "Profile");
+		return bstrdup(name);
+	}
+
+	void obs_frontend_set_current_profile(const char *profile) override
+	{
+		QList<QAction*> menuActions =
+			main->ui->profileMenu->actions();
+		QString qstrProfile = QT_UTF8(profile);
+
+		for (int i = 0; i < menuActions.count(); i++) {
+			QAction *action = menuActions[i];
+			QVariant v = action->property("file_name");
+
+			if (v.typeName() != nullptr) {
+				if (action->text() == qstrProfile) {
+					action->trigger();
+					break;
+				}
+			}
+		}
+	}
+
+	void obs_frontend_streaming_start(void) override
+	{
+		QMetaObject::invokeMethod(main, "StartStreaming");
+	}
+
+	void obs_frontend_streaming_stop(void) override
+	{
+		QMetaObject::invokeMethod(main, "StopStreaming");
+	}
+
+	bool obs_frontend_streaming_active(void) override
+	{
+		return main->outputHandler->StreamingActive();
+	}
+
+	void obs_frontend_recording_start(void) override
+	{
+		QMetaObject::invokeMethod(main, "StartRecording");
+	}
+
+	void obs_frontend_recording_stop(void) override
+	{
+		QMetaObject::invokeMethod(main, "StopRecording");
+	}
+
+	bool obs_frontend_recording_active(void) override
+	{
+		return main->outputHandler->StreamingActive();
+	}
+
+	void *obs_frontend_add_tools_menu_qaction(const char *name) override
+	{
+		main->ui->menuTools->setEnabled(true);
+		return (void*)main->ui->menuTools->addAction(QT_UTF8(name));
+	}
+
+	void obs_frontend_add_tools_menu_item(const char *name,
+			obs_frontend_cb callback, void *private_data) override
+	{
+		main->ui->menuTools->setEnabled(true);
+
+		auto func = [private_data, callback] ()
+		{
+			callback(private_data);
+		};
+
+		QAction *action = main->ui->menuTools->addAction(QT_UTF8(name));
+		QObject::connect(action, &QAction::triggered, func);
+	}
+
+	void obs_frontend_add_event_callback(obs_frontend_event_cb callback,
+			void *private_data) override
+	{
+		size_t idx = GetCallbackIdx(callbacks, callback, private_data);
+		if (idx == (size_t)-1)
+			callbacks.emplace_back(callback, private_data);
+	}
+
+	void obs_frontend_remove_event_callback(obs_frontend_event_cb callback,
+			void *private_data) override
+	{
+		size_t idx = GetCallbackIdx(callbacks, callback, private_data);
+		if (idx == (size_t)-1)
+			return;
+
+		callbacks.erase(callbacks.begin() + idx);
+	}
+
+	obs_output_t *obs_frontend_get_streaming_output(void) override
+	{
+		OBSOutput output = main->outputHandler->streamOutput;
+		obs_output_addref(output);
+		return output;
+	}
+
+	obs_output_t *obs_frontend_get_recording_output(void) override
+	{
+		OBSOutput out = main->outputHandler->fileOutput;
+		obs_output_addref(out);
+		return out;
+	}
+
+	config_t *obs_frontend_get_profile_config(void) override
+	{
+		return main->basicConfig;
+	}
+
+	config_t *obs_frontend_get_global_config(void) override
+	{
+		return App()->GlobalConfig();
+	}
+
+	void obs_frontend_save(void) override
+	{
+		main->SaveProject();
+	}
+
+	void obs_frontend_add_save_callback(obs_frontend_save_cb callback,
+			void *private_data) override
+	{
+		size_t idx = GetCallbackIdx(saveCallbacks, callback,
+				private_data);
+		if (idx == (size_t)-1)
+			saveCallbacks.emplace_back(callback, private_data);
+	}
+
+	void obs_frontend_remove_save_callback(obs_frontend_save_cb callback,
+			void *private_data) override
+	{
+		size_t idx = GetCallbackIdx(saveCallbacks, callback,
+				private_data);
+		if (idx == (size_t)-1)
+			return;
+
+		saveCallbacks.erase(saveCallbacks.begin() + idx);
+	}
+
+	void obs_frontend_push_ui_translation(
+			obs_frontend_translate_ui_cb translate) override
+	{
+		App()->PushUITranslation(translate);
+	}
+
+	void obs_frontend_pop_ui_translation(void) override
+	{
+		App()->PopUITranslation();
+	}
+
+	void on_load(obs_data_t *settings) override
+	{
+		for (auto cb : saveCallbacks)
+			cb.callback(settings, false, cb.private_data);
+	}
+
+	void on_save(obs_data_t *settings) override
+	{
+		for (auto cb : saveCallbacks)
+			cb.callback(settings, true, cb.private_data);
+	}
+
+	void on_event(enum obs_frontend_event event) override
+	{
+		if (main->disableSaving)
+			return;
+
+		for (auto cb : callbacks)
+			cb.callback(event, cb.private_data);
+	}
+};
+
+obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main)
+{
+	obs_frontend_callbacks *api = new OBSStudioAPI(main);
+	obs_frontend_set_callbacks_internal(api);
+	return api;
+}

+ 3 - 0
UI/data/locale/en-US.ini

@@ -354,6 +354,9 @@ Basic.MainMenu.View.StatusBar="&Status Bar"
 Basic.MainMenu.SceneCollection="&Scene Collection"
 Basic.MainMenu.Profile="&Profile"
 
+# basic mode help menu
+Basic.MainMenu.Tools="&Tools"
+
 # basic mode help menu
 Basic.MainMenu.Help="&Help"
 Basic.MainMenu.Help.Website="Visit &Website"

+ 10 - 1
UI/forms/OBSBasic.ui

@@ -229,7 +229,7 @@
             <rect>
              <x>0</x>
              <y>0</y>
-             <width>201</width>
+             <width>215</width>
              <height>16</height>
             </rect>
            </property>
@@ -913,11 +913,20 @@
     <addaction name="toggleSceneTransitions"/>
     <addaction name="toggleStatusBar"/>
    </widget>
+   <widget class="QMenu" name="menuTools">
+    <property name="enabled">
+     <bool>false</bool>
+    </property>
+    <property name="title">
+     <string>Basic.MainMenu.Tools</string>
+    </property>
+   </widget>
    <addaction name="menu_File"/>
    <addaction name="menuBasic_MainMenu_Edit"/>
    <addaction name="viewMenu"/>
    <addaction name="profileMenu"/>
    <addaction name="sceneCollectionMenu"/>
+   <addaction name="menuTools"/>
    <addaction name="menuBasic_MainMenu_Help"/>
   </widget>
   <widget class="OBSBasicStatusBar" name="statusbar"/>

+ 12 - 2
UI/obs-app.cpp

@@ -937,12 +937,22 @@ const char *OBSApp::GetCurrentLog() const
 	return currentLogFile.c_str();
 }
 
+bool OBSApp::TranslateString(const char *lookupVal, const char **out) const
+{
+	for (obs_frontend_translate_ui_cb cb : translatorHooks) {
+		if (cb(lookupVal, out))
+			return true;
+	}
+
+	return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out);
+}
+
 QString OBSTranslator::translate(const char *context, const char *sourceText,
 		const char *disambiguation, int n) const
 {
 	const char *out = nullptr;
-	if (!text_lookup_getstr(App()->GetTextLookup(), sourceText, &out))
-		return QString();
+	if (!App()->TranslateString(sourceText, &out))
+		return QString(sourceText);
 
 	UNUSED_PARAMETER(context);
 	UNUSED_PARAMETER(disambiguation);

+ 16 - 0
UI/obs-app.hpp

@@ -25,9 +25,11 @@
 #include <util/profiler.h>
 #include <util/util.hpp>
 #include <util/platform.h>
+#include <obs-frontend-api.h>
 #include <string>
 #include <memory>
 #include <vector>
+#include <deque>
 
 #include "window-main.hpp"
 
@@ -71,6 +73,8 @@ private:
 	os_inhibit_t                   *sleepInhibitor = nullptr;
 	int                            sleepInhibitRefs = 0;
 
+	std::deque<obs_frontend_translate_ui_cb> translatorHooks;
+
 	bool InitGlobalConfig();
 	bool InitGlobalConfigDefaults();
 	bool InitLocale();
@@ -102,6 +106,8 @@ public:
 		return textLookup.GetString(lookupVal);
 	}
 
+	bool TranslateString(const char *lookupVal, const char **out) const;
+
 	profiler_name_store_t *GetProfilerNameStore() const
 	{
 		return profilerNameStore;
@@ -131,6 +137,16 @@ public:
 		if (--sleepInhibitRefs == 0)
 			os_inhibit_sleep_set_active(sleepInhibitor, false);
 	}
+
+	inline void PushUITranslation(obs_frontend_translate_ui_cb cb)
+	{
+		translatorHooks.emplace_front(cb);
+	}
+
+	inline void PopUITranslation()
+	{
+		translatorHooks.pop_front();
+	}
 };
 
 int GetConfigPath(char *path, size_t size, const char *name);

+ 20 - 0
UI/obs-frontend-api/CMakeLists.txt

@@ -0,0 +1,20 @@
+project(obs-frontend-api)
+
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
+
+add_definitions(-DLIBOBS_EXPORTS)
+
+set(obs-frontend-api_SOURCES
+	obs-frontend-api.cpp)
+
+set(obs-frontend-api_HEADERS
+	obs-frontend-internal.hpp
+	obs-frontend-api.h)
+
+add_library(obs-frontend-api SHARED
+	${obs-frontend-api_SOURCES}
+	${obs-frontend-api_HEADERS})
+target_link_libraries(obs-frontend-api
+	libobs)
+
+install_obs_core(obs-frontend-api)

+ 295 - 0
UI/obs-frontend-api/obs-frontend-api.cpp

@@ -0,0 +1,295 @@
+#include "obs-frontend-internal.hpp"
+#include <memory>
+
+using namespace std;
+
+static unique_ptr<obs_frontend_callbacks> c;
+
+void obs_frontend_set_callbacks_internal(obs_frontend_callbacks *callbacks)
+{
+	c.reset(callbacks);
+}
+
+static inline bool callbacks_valid_(const char *func_name)
+{
+	if (!c) {
+		blog(LOG_WARNING, "Tried to call %s with no callbacks!",
+				func_name);
+		return false;
+	}
+
+	return true;
+}
+
+#define callbacks_valid() callbacks_valid_(__FUNCTION__)
+
+static char **convert_string_list(vector<string> &strings)
+{
+	size_t size = 0;
+	size_t string_data_offset = (strings.size() + 1) * sizeof(char*);
+	uint8_t *out;
+	char **ptr_list;
+	char *string_data;
+
+	size += string_data_offset;
+
+	for (auto &str : strings)
+		size += str.size() + 1;
+
+	if (!size)
+		return 0;
+
+	out = (uint8_t*)bmalloc(size);
+	ptr_list = (char**)out;
+	string_data = (char*)(out + string_data_offset);
+
+	for (auto &str : strings) {
+		*ptr_list = string_data;
+		memcpy(string_data, str.c_str(), str.size() + 1);
+
+		ptr_list++;
+		string_data += str.size() + 1;
+	}
+
+	*ptr_list = nullptr;
+	return (char**)out;
+}
+
+/* ------------------------------------------------------------------------- */
+
+void *obs_frontend_get_main_window(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_main_window()
+		: nullptr;
+}
+
+void *obs_frontend_get_main_window_handle(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_main_window_handle()
+		: nullptr;
+}
+
+char **obs_frontend_get_scene_names(void)
+{
+	if (!callbacks_valid())
+		return NULL;
+
+	struct obs_frontend_source_list sources = {};
+	vector<string> names;
+	c->obs_frontend_get_scenes(&sources);
+
+	for (size_t i = 0; i < sources.sources.num; i++) {
+		obs_source_t *source = sources.sources.array[i];
+		const char *name = obs_source_get_name(source);
+		names.emplace_back(name);
+	}
+
+	obs_frontend_source_list_free(&sources);
+	return convert_string_list(names);
+}
+
+void obs_frontend_get_scenes(struct obs_frontend_source_list *sources)
+{
+	if (callbacks_valid()) c->obs_frontend_get_scenes(sources);
+}
+
+obs_source_t *obs_frontend_get_current_scene(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_current_scene()
+		: nullptr;
+}
+
+void obs_frontend_set_current_scene(obs_source_t *scene)
+{
+	if (callbacks_valid()) c->obs_frontend_set_current_scene(scene);
+}
+
+void obs_frontend_get_transitions(struct obs_frontend_source_list *sources)
+{
+	if (callbacks_valid()) c->obs_frontend_get_transitions(sources);
+}
+
+obs_source_t *obs_frontend_get_current_transition(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_current_transition()
+		: nullptr;
+}
+
+void obs_frontend_set_current_transition(obs_source_t *transition)
+{
+	if (callbacks_valid())
+		c->obs_frontend_set_current_transition(transition);
+}
+
+char **obs_frontend_get_scene_collections(void)
+{
+	if (!callbacks_valid())
+		return nullptr;
+
+	vector<string> strings;
+	c->obs_frontend_get_scene_collections(strings);
+	return convert_string_list(strings);
+}
+
+char *obs_frontend_get_current_scene_collection(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_current_scene_collection()
+		: nullptr;
+}
+
+void obs_frontend_set_current_scene_collection(const char *collection)
+{
+	if (callbacks_valid())
+		c->obs_frontend_set_current_scene_collection(collection);
+}
+
+char **obs_frontend_get_profiles(void)
+{
+	if (!callbacks_valid())
+		return nullptr;
+
+	vector<string> strings;
+	c->obs_frontend_get_profiles(strings);
+	return convert_string_list(strings);
+}
+
+char *obs_frontend_get_current_profile(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_current_profile()
+		: nullptr;
+}
+
+void obs_frontend_set_current_profile(const char *profile)
+{
+	if (callbacks_valid())
+		c->obs_frontend_set_current_profile(profile);
+}
+
+void obs_frontend_streaming_start(void)
+{
+	if (callbacks_valid()) c->obs_frontend_streaming_start();
+}
+
+void obs_frontend_streaming_stop(void)
+{
+	if (callbacks_valid()) c->obs_frontend_streaming_stop();
+}
+
+bool obs_frontend_streaming_active(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_streaming_active()
+		: false;
+}
+
+void obs_frontend_recording_start(void)
+{
+	if (callbacks_valid()) c->obs_frontend_recording_start();
+}
+
+void obs_frontend_recording_stop(void)
+{
+	if (callbacks_valid()) c->obs_frontend_recording_stop();
+}
+
+bool obs_frontend_recording_active(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_recording_active()
+		: false;
+}
+
+void *obs_frontend_add_tools_menu_qaction(const char *name)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_add_tools_menu_qaction(name)
+		: nullptr;
+}
+
+void obs_frontend_add_tools_menu_item(const char *name,
+		obs_frontend_cb callback, void *private_data)
+{
+	if (callbacks_valid())
+		c->obs_frontend_add_tools_menu_item(name, callback,
+				private_data);
+}
+
+void obs_frontend_add_event_callback(obs_frontend_event_cb callback,
+		void *private_data)
+{
+	if (callbacks_valid())
+		c->obs_frontend_add_event_callback(callback, private_data);
+}
+
+void obs_frontend_remove_event_callback(obs_frontend_event_cb callback,
+		void *private_data)
+{
+	if (callbacks_valid())
+		c->obs_frontend_remove_event_callback(callback, private_data);
+}
+
+obs_output_t *obs_frontend_get_streaming_output(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_streaming_output()
+		: nullptr;
+}
+
+obs_output_t *obs_frontend_get_recording_output(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_recording_output()
+		: nullptr;
+}
+
+config_t *obs_frontend_get_profile_config(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_profile_config()
+		: nullptr;
+}
+
+config_t *obs_frontend_get_global_config(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_global_config()
+		: nullptr;
+}
+
+void obs_frontend_save(void)
+{
+	if (callbacks_valid())
+		c->obs_frontend_save();
+}
+
+void obs_frontend_add_save_callback(obs_frontend_save_cb callback,
+		void *private_data)
+{
+	if (callbacks_valid())
+		c->obs_frontend_add_save_callback(callback, private_data);
+}
+
+void obs_frontend_remove_save_callback(obs_frontend_save_cb callback,
+		void *private_data)
+{
+	if (callbacks_valid())
+		c->obs_frontend_remove_save_callback(callback, private_data);
+}
+
+void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate)
+{
+	if (callbacks_valid())
+		c->obs_frontend_push_ui_translation(translate);
+}
+
+void obs_frontend_pop_ui_translation(void)
+{
+	if (callbacks_valid())
+		c->obs_frontend_pop_ui_translation();
+}

+ 134 - 0
UI/obs-frontend-api/obs-frontend-api.h

@@ -0,0 +1,134 @@
+#pragma once
+
+#include <obs.h>
+#include <util/darray.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct config_data;
+typedef struct config_data config_t;
+
+struct obs_data;
+typedef struct obs_data obs_data_t;
+
+/* ------------------------------------------------------------------------- */
+
+struct obs_frontend_source_list {
+	DARRAY(obs_source_t*) sources;
+};
+
+static inline void obs_frontend_source_list_free(
+		struct obs_frontend_source_list *source_list)
+{
+	size_t num = source_list->sources.num;
+	for (size_t i = 0; i < num; i++)
+		obs_source_release(source_list->sources.array[i]);
+	da_free(source_list->sources);
+}
+
+/* ------------------------------------------------------------------------- */
+
+/* NOTE: Functions that return char** string lists are a single allocation of
+ * memory with pointers to itself.  Free with a single call to bfree on the
+ * base char** pointer. */
+
+/* NOTE: User interface should not use typical Qt locale translation methods,
+ * as the OBS UI bypasses it to use a custom translation implementation.  Use
+ * standard module translation methods, obs_module_text.  For text in a Qt
+ * window, use obs_frontend_push_ui_translation when the text is about to be
+ * translated, and obs_frontend_pop_ui_translation when translation is
+ * complete. */
+
+EXPORT void *obs_frontend_get_main_window(void);
+EXPORT void *obs_frontend_get_main_window_handle(void);
+
+EXPORT char **obs_frontend_get_scene_names(void);
+EXPORT void obs_frontend_get_scenes(struct obs_frontend_source_list *sources);
+EXPORT obs_source_t *obs_frontend_get_current_scene(void);
+EXPORT void obs_frontend_set_current_scene(obs_source_t *scene);
+
+EXPORT void obs_frontend_get_transitions(
+		struct obs_frontend_source_list *sources);
+EXPORT obs_source_t *obs_frontend_get_current_transition(void);
+EXPORT void obs_frontend_set_current_transition(obs_source_t *transition);
+
+EXPORT char **obs_frontend_get_scene_collections(void);
+EXPORT char *obs_frontend_get_current_scene_collection(void);
+EXPORT void obs_frontend_set_current_scene_collection(const char *collection);
+
+EXPORT char **obs_frontend_get_profiles(void);
+EXPORT char *obs_frontend_get_current_profile(void);
+EXPORT void obs_frontend_set_current_profile(const char *profile);
+
+EXPORT void obs_frontend_streaming_start(void);
+EXPORT void obs_frontend_streaming_stop(void);
+EXPORT bool obs_frontend_streaming_active(void);
+
+EXPORT void obs_frontend_recording_start(void);
+EXPORT void obs_frontend_recording_stop(void);
+EXPORT bool obs_frontend_recording_active(void);
+
+typedef void (*obs_frontend_cb)(void *private_data);
+
+EXPORT void *obs_frontend_add_tools_menu_qaction(const char *name);
+EXPORT void obs_frontend_add_tools_menu_item(const char *name,
+		obs_frontend_cb callback, void *private_data);
+
+enum obs_frontend_event {
+	OBS_FRONTEND_EVENT_STREAMING_STARTING,
+	OBS_FRONTEND_EVENT_STREAMING_STARTED,
+	OBS_FRONTEND_EVENT_STREAMING_STOPPING,
+	OBS_FRONTEND_EVENT_STREAMING_STOPPED,
+	OBS_FRONTEND_EVENT_RECORDING_STARTING,
+	OBS_FRONTEND_EVENT_RECORDING_STARTED,
+	OBS_FRONTEND_EVENT_RECORDING_STOPPING,
+	OBS_FRONTEND_EVENT_RECORDING_STOPPED,
+	OBS_FRONTEND_EVENT_SCENE_CHANGED,
+	OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED,
+	OBS_FRONTEND_EVENT_TRANSITION_CHANGED,
+	OBS_FRONTEND_EVENT_TRANSITION_STOPPED,
+	OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED,
+	OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED,
+	OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED,
+	OBS_FRONTEND_EVENT_PROFILE_CHANGED,
+	OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED,
+	OBS_FRONTEND_EVENT_EXIT
+};
+
+typedef void (*obs_frontend_event_cb)(enum obs_frontend_event event,
+		void *private_data);
+
+EXPORT void obs_frontend_add_event_callback(obs_frontend_event_cb callback,
+		void *private_data);
+EXPORT void obs_frontend_remove_event_callback(obs_frontend_event_cb callback,
+		void *private_data);
+
+typedef void (*obs_frontend_save_cb)(obs_data_t *save_data, bool saving,
+		void *private_data);
+
+EXPORT void obs_frontend_save(void);
+EXPORT void obs_frontend_add_save_callback(obs_frontend_save_cb callback,
+		void *private_data);
+EXPORT void obs_frontend_remove_save_callback(obs_frontend_save_cb callback,
+		void *private_data);
+
+EXPORT obs_output_t *obs_frontend_get_streaming_output(void);
+EXPORT obs_output_t *obs_frontend_get_recording_output(void);
+
+EXPORT config_t *obs_frontend_get_profile_config(void);
+EXPORT config_t *obs_frontend_get_global_config(void);
+
+typedef bool (*obs_frontend_translate_ui_cb)(const char *text,
+		const char **out);
+
+EXPORT void obs_frontend_push_ui_translation(
+		obs_frontend_translate_ui_cb translate);
+EXPORT void obs_frontend_pop_ui_translation(void);
+
+/* ------------------------------------------------------------------------- */
+
+#ifdef __cplusplus
+}
+#endif

+ 73 - 0
UI/obs-frontend-api/obs-frontend-internal.hpp

@@ -0,0 +1,73 @@
+#pragma once
+
+#include "obs-frontend-api.h"
+
+#include <vector>
+#include <string>
+
+struct obs_frontend_callbacks {
+	virtual void *obs_frontend_get_main_window(void)=0;
+	virtual void *obs_frontend_get_main_window_handle(void)=0;
+
+	virtual void obs_frontend_get_scenes(
+			struct obs_frontend_source_list *sources)=0;
+	virtual obs_source_t *obs_frontend_get_current_scene(void)=0;
+	virtual void obs_frontend_set_current_scene(obs_source_t *scene)=0;
+
+	virtual void obs_frontend_get_transitions(
+			struct obs_frontend_source_list *sources)=0;
+	virtual obs_source_t *obs_frontend_get_current_transition(void)=0;
+	virtual void obs_frontend_set_current_transition(
+			obs_source_t *transition)=0;
+
+	virtual void obs_frontend_get_scene_collections(
+			std::vector<std::string> &strings)=0;
+	virtual char *obs_frontend_get_current_scene_collection(void)=0;
+	virtual void obs_frontend_set_current_scene_collection(
+			const char *collection)=0;
+
+	virtual void obs_frontend_get_profiles(
+			std::vector<std::string> &strings)=0;
+	virtual char *obs_frontend_get_current_profile(void)=0;
+	virtual void obs_frontend_set_current_profile(const char *profile)=0;
+
+	virtual void obs_frontend_streaming_start(void)=0;
+	virtual void obs_frontend_streaming_stop(void)=0;
+	virtual bool obs_frontend_streaming_active(void)=0;
+
+	virtual void obs_frontend_recording_start(void)=0;
+	virtual void obs_frontend_recording_stop(void)=0;
+	virtual bool obs_frontend_recording_active(void)=0;
+
+	virtual void *obs_frontend_add_tools_menu_qaction(const char *name)=0;
+	virtual void obs_frontend_add_tools_menu_item(const char *name,
+			obs_frontend_cb callback, void *private_data)=0;
+
+	virtual void obs_frontend_add_event_callback(
+			obs_frontend_event_cb callback, void *private_data)=0;
+	virtual void obs_frontend_remove_event_callback(
+			obs_frontend_event_cb callback, void *private_data)=0;
+
+	virtual obs_output_t *obs_frontend_get_streaming_output(void)=0;
+	virtual obs_output_t *obs_frontend_get_recording_output(void)=0;
+
+	virtual config_t *obs_frontend_get_profile_config(void)=0;
+	virtual config_t *obs_frontend_get_global_config(void)=0;
+
+	virtual void obs_frontend_save(void)=0;
+	virtual void obs_frontend_add_save_callback(
+			obs_frontend_save_cb callback, void *private_data)=0;
+	virtual void obs_frontend_remove_save_callback(
+			obs_frontend_save_cb callback, void *private_data)=0;
+
+	virtual void obs_frontend_push_ui_translation(
+			obs_frontend_translate_ui_cb translate)=0;
+	virtual void obs_frontend_pop_ui_translation(void)=0;
+
+	virtual void on_load(obs_data_t *settings)=0;
+	virtual void on_save(obs_data_t *settings)=0;
+	virtual void on_event(enum obs_frontend_event event)=0;
+};
+
+EXPORT void obs_frontend_set_callbacks_internal(
+		obs_frontend_callbacks *callbacks);

+ 18 - 0
UI/window-basic-main-profiles.cpp

@@ -239,6 +239,11 @@ bool OBSBasic::AddProfile(bool create_new, const char *title, const char *text,
 
 	config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
 	UpdateTitleBar();
+
+	if (api) {
+		api->on_event(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED);
+		api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED);
+	}
 	return true;
 }
 
@@ -363,6 +368,11 @@ void OBSBasic::on_actionRenameProfile_triggered()
 		DeleteProfile(curName.c_str(), curDir.c_str());
 		RefreshProfiles();
 	}
+
+	if (api) {
+		api->on_event(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED);
+		api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED);
+	}
 }
 
 void OBSBasic::on_actionRemoveProfile_triggered()
@@ -431,6 +441,11 @@ void OBSBasic::on_actionRemoveProfile_triggered()
 	blog(LOG_INFO, "------------------------------------------------");
 
 	UpdateTitleBar();
+
+	if (api) {
+		api->on_event(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED);
+		api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED);
+	}
 }
 
 void OBSBasic::ChangeProfile()
@@ -481,4 +496,7 @@ void OBSBasic::ChangeProfile()
 	blog(LOG_INFO, "Switched to profile '%s' (%s)",
 			newName, newDir);
 	blog(LOG_INFO, "------------------------------------------------");
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED);
 }

+ 18 - 0
UI/window-basic-main-scene-collections.cpp

@@ -178,6 +178,11 @@ void OBSBasic::AddSceneCollection(bool create_new)
 	blog(LOG_INFO, "------------------------------------------------");
 
 	UpdateTitleBar();
+
+	if (api) {
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED);
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED);
+	}
 }
 
 void OBSBasic::RefreshSceneCollections()
@@ -267,6 +272,11 @@ void OBSBasic::on_actionRenameSceneCollection_triggered()
 
 	UpdateTitleBar();
 	RefreshSceneCollections();
+
+	if (api) {
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED);
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED);
+	}
 }
 
 void OBSBasic::on_actionRemoveSceneCollection_triggered()
@@ -330,6 +340,11 @@ void OBSBasic::on_actionRemoveSceneCollection_triggered()
 	blog(LOG_INFO, "------------------------------------------------");
 
 	UpdateTitleBar();
+
+	if (api) {
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED);
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED);
+	}
 }
 
 void OBSBasic::ChangeSceneCollection()
@@ -366,4 +381,7 @@ void OBSBasic::ChangeSceneCollection()
 	blog(LOG_INFO, "------------------------------------------------");
 
 	UpdateTitleBar();
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED);
 }

+ 15 - 0
UI/window-basic-main-transitions.cpp

@@ -234,6 +234,9 @@ void OBSBasic::TransitionStopped()
 			SetCurrentScene(scene);
 	}
 
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_TRANSITION_STOPPED);
+
 	swapScene = nullptr;
 }
 
@@ -281,6 +284,9 @@ void OBSBasic::TransitionToScene(OBSSource source, bool force)
 		obs_scene_release(scene);
 
 	obs_source_release(transition);
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_CHANGED);
 }
 
 static inline void SetComboTransition(QComboBox *combo, obs_source_t *tr)
@@ -317,6 +323,9 @@ void OBSBasic::SetTransition(OBSSource transition)
 	bool configurable = obs_source_configurable(transition);
 	ui->transitionRemove->setEnabled(configurable);
 	ui->transitionProps->setEnabled(configurable);
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_TRANSITION_CHANGED);
 }
 
 OBSSource OBSBasic::GetCurrentTransition()
@@ -378,6 +387,9 @@ void OBSBasic::AddTransition()
 		ui->transitions->setCurrentIndex(ui->transitions->count() - 1);
 		CreatePropertiesWindow(source);
 		obs_source_release(source);
+
+		if (api)
+			api->on_event(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED);
 	}
 }
 
@@ -428,6 +440,9 @@ void OBSBasic::on_transitionRemove_clicked()
 	}
 
 	ui->transitions->removeItem(idx);
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED);
 }
 
 void OBSBasic::RenameTransition()

+ 59 - 1
UI/window-basic-main.cpp

@@ -373,6 +373,13 @@ void OBSBasic::Save(const char *file)
 
 	obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked());
 
+	if (api) {
+		obs_data_t *moduleObj = obs_data_create();
+		api->on_save(moduleObj);
+		obs_data_set_obj(saveData, "modules", moduleObj);
+		obs_data_release(moduleObj);
+	}
+
 	if (!obs_data_save_json_safe(saveData, file, "tmp", "bak"))
 		blog(LOG_ERROR, "Could not save scene data to %s", file);
 
@@ -655,6 +662,12 @@ retryScene:
 	ui->preview->SetLocked(previewLocked);
 	ui->actionLockPreview->setChecked(previewLocked);
 
+	if (api) {
+		obs_data_t *modulesObj = obs_data_get_obj(data, "modules");
+		api->on_load(modulesObj);
+		obs_data_release(modulesObj);
+	}
+
 	obs_data_release(data);
 
 	if (!opt_starting_scene.empty())
@@ -1019,6 +1032,8 @@ void OBSBasic::ResetOutputs()
 #define SHUTDOWN_SEPARATOR \
 	"==== Shutting down =================================================="
 
+extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main);
+
 void OBSBasic::OBSInit()
 {
 	ProfileScope("OBSBasic::OBSInit");
@@ -1065,6 +1080,8 @@ void OBSBasic::OBSInit()
 	InitOBSCallbacks();
 	InitHotkeys();
 
+	api = InitializeAPIInterface(this);
+
 	AddExtraModulePaths();
 	blog(LOG_INFO, "---------------------------------");
 	obs_load_all_modules();
@@ -2067,6 +2084,9 @@ void OBSBasic::DuplicateSelectedScene()
 		AddScene(source);
 		SetCurrentScene(source, true);
 		obs_scene_release(scene);
+
+		if (api)
+			api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
 		break;
 	}
 }
@@ -2076,8 +2096,12 @@ void OBSBasic::RemoveSelectedScene()
 	OBSScene scene = GetCurrentScene();
 	if (scene) {
 		obs_source_t *source = obs_scene_get_source(scene);
-		if (QueryRemoveSource(source))
+		if (QueryRemoveSource(source)) {
 			obs_source_remove(source);
+
+			if (api)
+				api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
+		}
 	}
 }
 
@@ -2639,6 +2663,10 @@ void OBSBasic::closeEvent(QCloseEvent *event)
 	signalHandlers.clear();
 
 	SaveProjectNow();
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_EXIT);
+
 	disableSaving++;
 
 	/* Clear all scene data (dialogs, widgets, widget sub-items, scenes,
@@ -3487,6 +3515,9 @@ void OBSBasic::SceneNameEdited(QWidget *editor,
 	obs_source_t *source = obs_scene_get_source(scene);
 	RenameListItem(this, ui->scenes, source, text);
 
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED);
+
 	UNUSED_PARAMETER(endHint);
 }
 
@@ -3537,6 +3568,12 @@ void OBSBasic::OpenSceneFilters()
 
 void OBSBasic::StartStreaming()
 {
+	if (outputHandler->StreamingActive())
+		return;
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTING);
+
 	SaveProject();
 
 	ui->streamButton->setEnabled(false);
@@ -3684,6 +3721,9 @@ void OBSBasic::StreamingStart()
 	sysTrayStream->setText(ui->streamButton->text());
 	sysTrayStream->setEnabled(true);
 
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTED);
+
 	OnActivate();
 
 	blog(LOG_INFO, STREAMING_START);
@@ -3693,6 +3733,9 @@ void OBSBasic::StreamStopping()
 {
 	ui->streamButton->setText(QTStr("Basic.Main.StoppingStreaming"));
 	sysTrayStream->setText(ui->streamButton->text());
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPING);
 }
 
 void OBSBasic::StreamingStop(int code)
@@ -3730,6 +3773,9 @@ void OBSBasic::StreamingStop(int code)
 	sysTrayStream->setText(ui->streamButton->text());
 	sysTrayStream->setEnabled(true);
 
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_STREAMING_STOPPED);
+
 	OnDeactivate();
 
 	blog(LOG_INFO, STREAMING_STOP);
@@ -3754,6 +3800,9 @@ void OBSBasic::StartRecording()
 	if (outputHandler->RecordingActive())
 		return;
 
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTING);
+
 	SaveProject();
 	outputHandler->StartRecording();
 }
@@ -3762,6 +3811,9 @@ void OBSBasic::RecordStopping()
 {
 	ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording"));
 	sysTrayRecord->setText(ui->recordButton->text());
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STOPPING);
 }
 
 void OBSBasic::StopRecording()
@@ -3780,6 +3832,9 @@ void OBSBasic::RecordingStart()
 	ui->recordButton->setText(QTStr("Basic.Main.StopRecording"));
 	sysTrayRecord->setText(ui->recordButton->text());
 
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTED);
+
 	OnActivate();
 
 	blog(LOG_INFO, RECORDING_START);
@@ -3820,6 +3875,9 @@ void OBSBasic::RecordingStop(int code)
 			QSystemTrayIcon::Warning);
 	}
 
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STOPPED);
+
 	OnDeactivate();
 }
 

+ 5 - 0
UI/window-basic-main.hpp

@@ -30,6 +30,8 @@
 #include "window-basic-adv-audio.hpp"
 #include "window-basic-filters.hpp"
 
+#include <obs-frontend-internal.hpp>
+
 #include <util/platform.h>
 #include <util/threading.h>
 #include <util/util.hpp>
@@ -83,6 +85,7 @@ class OBSBasic : public OBSMainWindow {
 	friend class OBSBasicPreview;
 	friend class OBSBasicStatusBar;
 	friend class OBSBasicSourceSelect;
+	friend struct OBSStudioAPI;
 
 	enum class MoveDir {
 		Up,
@@ -92,6 +95,8 @@ class OBSBasic : public OBSMainWindow {
 	};
 
 private:
+	obs_frontend_callbacks *api = nullptr;
+
 	std::vector<VolControl*> volumes;
 
 	std::vector<OBSSignal> signalHandlers;