From 8836592d92284c454f2e2d00b512837d9c53cbe9 Mon Sep 17 00:00:00 2001 From: jp9000 Date: Sun, 28 Aug 2016 14:24:14 -0700 Subject: [PATCH] UI: Add front-end API library Allows manipulating and modifying the front-end via plugins. --- UI/CMakeLists.txt | 11 +- UI/api-interface.cpp | 362 ++++++++++++++++++ UI/data/locale/en-US.ini | 3 + UI/forms/OBSBasic.ui | 11 +- UI/obs-app.cpp | 14 +- UI/obs-app.hpp | 16 + UI/obs-frontend-api/CMakeLists.txt | 20 + UI/obs-frontend-api/obs-frontend-api.cpp | 295 ++++++++++++++ UI/obs-frontend-api/obs-frontend-api.h | 134 +++++++ UI/obs-frontend-api/obs-frontend-internal.hpp | 73 ++++ UI/window-basic-main-profiles.cpp | 18 + UI/window-basic-main-scene-collections.cpp | 18 + UI/window-basic-main-transitions.cpp | 15 + UI/window-basic-main.cpp | 60 ++- UI/window-basic-main.hpp | 5 + 15 files changed, 1049 insertions(+), 6 deletions(-) create mode 100644 UI/api-interface.cpp create mode 100644 UI/obs-frontend-api/CMakeLists.txt create mode 100644 UI/obs-frontend-api/obs-frontend-api.cpp create mode 100644 UI/obs-frontend-api/obs-frontend-api.h create mode 100644 UI/obs-frontend-api/obs-frontend-internal.hpp diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 418518806..e66a26d35 100644 --- a/UI/CMakeLists.txt +++ b/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}) diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp new file mode 100644 index 000000000..390c1da05 --- /dev/null +++ b/UI/api-interface.cpp @@ -0,0 +1,362 @@ +#include +#include "obs-app.hpp" +#include "qt-wrappers.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" + +#include + +using namespace std; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSource); + +template +static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +void EnumProfiles(function &&cb); +void EnumSceneCollections(function &&cb); + +/* ------------------------------------------------------------------------- */ + +template struct OBSStudioCallback { + T callback; + void *private_data; + + inline OBSStudioCallback(T cb, void *p) : + callback(cb), private_data(p) + {} +}; + +template inline size_t GetCallbackIdx( + vector> &callbacks, + T callback, void *private_data) +{ + for (size_t i = 0; i < callbacks.size(); i++) { + OBSStudioCallback 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> callbacks; + vector> 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(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(); + + 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 &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 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 &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 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; +} diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 0c47e9deb..35c06fc17 100644 --- a/UI/data/locale/en-US.ini +++ b/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" diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 76d8e5517..5b922bcf8 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -229,7 +229,7 @@ 0 0 - 201 + 215 16 @@ -913,11 +913,20 @@ + + + false + + + Basic.MainMenu.Tools + + + diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index ca0deda29..eba032ae9 100644 --- a/UI/obs-app.cpp +++ b/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); diff --git a/UI/obs-app.hpp b/UI/obs-app.hpp index a341a821e..4964f5f9a 100644 --- a/UI/obs-app.hpp +++ b/UI/obs-app.hpp @@ -25,9 +25,11 @@ #include #include #include +#include #include #include #include +#include #include "window-main.hpp" @@ -71,6 +73,8 @@ private: os_inhibit_t *sleepInhibitor = nullptr; int sleepInhibitRefs = 0; + std::deque 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); diff --git a/UI/obs-frontend-api/CMakeLists.txt b/UI/obs-frontend-api/CMakeLists.txt new file mode 100644 index 000000000..38eefb69e --- /dev/null +++ b/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) diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/UI/obs-frontend-api/obs-frontend-api.cpp new file mode 100644 index 000000000..5d01b64d8 --- /dev/null +++ b/UI/obs-frontend-api/obs-frontend-api.cpp @@ -0,0 +1,295 @@ +#include "obs-frontend-internal.hpp" +#include + +using namespace std; + +static unique_ptr 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 &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 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 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 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(); +} diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/UI/obs-frontend-api/obs-frontend-api.h new file mode 100644 index 000000000..8a6272a2a --- /dev/null +++ b/UI/obs-frontend-api/obs-frontend-api.h @@ -0,0 +1,134 @@ +#pragma once + +#include +#include + +#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 diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/UI/obs-frontend-api/obs-frontend-internal.hpp new file mode 100644 index 000000000..cb8da9647 --- /dev/null +++ b/UI/obs-frontend-api/obs-frontend-internal.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "obs-frontend-api.h" + +#include +#include + +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 &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 &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); diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index fb04f1979..9637fca34 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/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); } diff --git a/UI/window-basic-main-scene-collections.cpp b/UI/window-basic-main-scene-collections.cpp index d1ce8596a..c35b0b101 100644 --- a/UI/window-basic-main-scene-collections.cpp +++ b/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); } diff --git a/UI/window-basic-main-transitions.cpp b/UI/window-basic-main-transitions.cpp index 65460f1d5..3a69b24ce 100644 --- a/UI/window-basic-main-transitions.cpp +++ b/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() diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 11a801478..eae28ffc9 100644 --- a/UI/window-basic-main.cpp +++ b/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(); } diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index a3376df2e..426f0d1b5 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -30,6 +30,8 @@ #include "window-basic-adv-audio.hpp" #include "window-basic-filters.hpp" +#include + #include #include #include @@ -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 volumes; std::vector signalHandlers;