diff --git a/CMakeLists.txt b/CMakeLists.txt index 5db3f442..6ab0861b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ configure_file( set(obs-websocket_SOURCES src/obs-websocket.cpp src/Config.cpp + src/WebSocketApi.cpp src/websocketserver/WebSocketServer.cpp src/websocketserver/WebSocketServer_Protocol.cpp src/websocketserver/WebSocketServer_RequestBatchProcessing.cpp @@ -124,6 +125,7 @@ set(obs-websocket_SOURCES set(obs-websocket_HEADERS src/obs-websocket.h src/Config.h + src/WebSocketApi.h src/websocketserver/WebSocketServer.h src/websocketserver/rpc/WebSocketSession.h src/eventhandler/EventHandler.h @@ -140,6 +142,7 @@ set(obs-websocket_HEADERS src/utils/Platform.h src/utils/Compat.h src/utils/Utils.h + lib/obs-websocket-api.h deps/qr/cpp/QrCode.hpp) diff --git a/lib/obs-websocket-api.h b/lib/obs-websocket-api.h index b5e095ef..974cbebc 100644 --- a/lib/obs-websocket-api.h +++ b/lib/obs-websocket-api.h @@ -27,7 +27,12 @@ extern "C" { #endif typedef void* obs_websocket_vendor; -typedef void (*obs_websocket_request_callback)(obs_data_t *request_data, obs_data_t *response_data, void *priv); +typedef void (*obs_websocket_request_callback_function)(obs_data_t*, obs_data_t*, void*); + +struct obs_websocket_request_callback { + obs_websocket_request_callback_function callback; + void *priv_data; +}; inline proc_handler_t *ph; @@ -47,7 +52,7 @@ inline proc_handler_t *obs_websocket_get_ph(void) inline bool obs_websocket_run_simple_proc(obs_websocket_vendor vendor, const char *proc_name, calldata_t *cd) { - if (!ph || !vendor || !proc_name || !*proc_name || !cd) + if (!ph || !vendor || !proc_name || !strlen(proc_name) || !cd) return false; calldata_set_ptr(cd, "vendor", vendor); @@ -57,6 +62,7 @@ inline bool obs_websocket_run_simple_proc(obs_websocket_vendor vendor, const cha } // ALWAYS CALL VIA `obs_module_post_load()` CALLBACK! +// Registers a new "vendor" (Example: obs-ndi) inline obs_websocket_vendor obs_websocket_register_vendor(const char *vendor_name) { ph = obs_websocket_get_ph(); @@ -65,22 +71,27 @@ inline obs_websocket_vendor obs_websocket_register_vendor(const char *vendor_nam calldata_t cd = {0}; - calldata_set_string(&cd, "vendor_name", vendor_name); + calldata_set_string(&cd, "name", vendor_name); - proc_handler_call(ph, "vendor_create", &cd); + proc_handler_call(ph, "vendor_register", &cd); obs_websocket_vendor ret = calldata_ptr(&cd, "vendor"); calldata_free(&cd); return ret; } -inline bool obs_websocket_register_request(obs_websocket_vendor vendor, const char *request_name, obs_websocket_request_callback request_callback, void* priv_data) +// Registers a new request for a vendor +inline bool obs_websocket_register_request(obs_websocket_vendor vendor, const char *request_type, obs_websocket_request_callback_function request_callback, void* priv_data) { calldata_t cd = {0}; - calldata_set_string(&cd, "name", request_name); - calldata_set_ptr(&cd, "callback", (void*)request_callback); - calldata_set_ptr(&cd, "callback_priv_data", priv_data); + struct obs_websocket_request_callback cb = { + .callback = request_callback, + .priv_data = priv_data, + }; + + calldata_set_string(&cd, "type", request_type); + calldata_set_ptr(&cd, "callback", &cb); bool success = obs_websocket_run_simple_proc(vendor, "vendor_request_register", &cd); calldata_free(&cd); @@ -88,13 +99,12 @@ inline bool obs_websocket_register_request(obs_websocket_vendor vendor, const ch return success; } -inline bool obs_websocket_unregister_request(obs_websocket_vendor vendor, const char *request_name, obs_websocket_request_callback request_callback, void* priv_data) +// Unregisters an existing vendor request +inline bool obs_websocket_unregister_request(obs_websocket_vendor vendor, const char *request_type) { calldata_t cd = {0}; - calldata_set_string(&cd, "name", request_name); - calldata_set_ptr(&cd, "callback", (void*)request_callback); - calldata_set_ptr(&cd, "callback_priv_data", priv_data); + calldata_set_string(&cd, "type", request_type); bool success = obs_websocket_run_simple_proc(vendor, "vendor_request_unregister", &cd); calldata_free(&cd); @@ -102,12 +112,14 @@ inline bool obs_websocket_unregister_request(obs_websocket_vendor vendor, const return success; } -inline bool obs_websocket_api_emit_event(obs_websocket_vendor vendor, const char *event_name, obs_data_t *event_data) +// Does not affect event_data refcount. +// Emits an event under the vendor's name +inline bool obs_websocket_emit_event(obs_websocket_vendor vendor, const char *event_name, obs_data_t *event_data) { calldata_t cd = {0}; - calldata_set_string(&cd, "event_name", event_name); - calldata_set_ptr(&cd, "event_data", (void*)event_data); + calldata_set_string(&cd, "type", event_name); + calldata_set_ptr(&cd, "data", (void*)event_data); bool success = obs_websocket_run_simple_proc(vendor, "vendor_event_emit", &cd); calldata_free(&cd); diff --git a/src/WebSocketApi.cpp b/src/WebSocketApi.cpp new file mode 100644 index 00000000..5e5be3b6 --- /dev/null +++ b/src/WebSocketApi.cpp @@ -0,0 +1,214 @@ +#include "WebSocketApi.h" +#include "obs-websocket.h" + +#define RETURN_STATUS(status) { calldata_set_bool(cd, "success", status); return; } +#define RETURN_SUCCESS() RETURN_STATUS(true); +#define RETURN_FAILURE() RETURN_STATUS(false); + +WebSocketApi::Vendor *get_vendor(calldata_t *cd) +{ + void *voidVendor; + if (!calldata_get_ptr(cd, "vendor", &voidVendor)) { + blog(LOG_WARNING, "[WebSocketApi: get_vendor] Failed due to missing `vendor` pointer."); + return nullptr; + } + + return static_cast(voidVendor); +} + +WebSocketApi::WebSocketApi(EventCallback cb) : + _eventCallback(cb) +{ + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::WebSocketApi] Setting up..."); + + _procHandler = proc_handler_create(); + + proc_handler_add(_procHandler, "bool vendor_register(in string name, out ptr vendor)", &vendor_register_cb, this); + proc_handler_add(_procHandler, "bool vendor_request_register(in ptr vendor, in string type, in ptr callback)", &vendor_request_register_cb, this); + proc_handler_add(_procHandler, "bool vendor_request_unregister(in ptr vendor, in string type)", &vendor_request_unregister_cb, this); + proc_handler_add(_procHandler, "bool vendor_event_emit(in ptr vendor, in string type, in ptr data)", &vendor_event_emit_cb, this); + + proc_handler_t *ph = obs_get_proc_handler(); + assert(ph != NULL); + + proc_handler_add(ph, "bool obs_websocket_api_get_ph(out ptr ph)", &get_ph_cb, this); + + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::WebSocketApi] Finished."); +} + +WebSocketApi::~WebSocketApi() +{ + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::~WebSocketApi] Shutting down..."); + + proc_handler_destroy(_procHandler); + + for (auto vendor : _vendors) { + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::~WebSocketApi] Deleting vendor: %s", vendor.first.c_str()); + delete vendor.second; + } + + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::~WebSocketApi] Finished."); +} + +enum WebSocketApi::RequestReturnCode WebSocketApi::PerformVendorRequest(std::string vendorName, std::string requestType, obs_data_t *requestData, obs_data_t *responseData) +{ + std::shared_lock l(_mutex); + + if (_vendors.count(vendorName) == 0) + return WEBSOCKET_API_REQUEST_RETURN_CODE_NO_VENDOR; + + auto v = _vendors[vendorName]; + + l.unlock(); + + std::shared_lock v_l(v->_mutex); + + if (v->_requests.count(requestType) == 0) + return WEBSOCKET_API_REQUEST_RETURN_CODE_NO_VENDOR_REQUEST; + + auto cb = v->_requests[requestType]; + + v_l.unlock(); + + cb.callback(requestData, responseData, cb.priv_data); + + return WEBSOCKET_API_REQUEST_RETURN_CODE_NORMAL; +} + +void WebSocketApi::get_ph_cb(void *priv_data, calldata_t *cd) +{ + auto c = static_cast(priv_data); + + calldata_set_ptr(cd, "ph", (void*)c->_procHandler); + + RETURN_SUCCESS(); +} + +void WebSocketApi::vendor_register_cb(void *priv_data, calldata_t *cd) +{ + auto c = static_cast(priv_data); + + const char *vendorName; + if (!calldata_get_string(cd, "name", &vendorName) || strlen(vendorName) == 0) { + blog(LOG_WARNING, "[WebSocketApi::vendor_register_cb] Failed due to missing `name` string."); + RETURN_FAILURE(); + } + + // Theoretically doesn't need a mutex, but it's good to be safe. + std::unique_lock l(c->_mutex); + + if (c->_vendors.count(vendorName)) { + blog(LOG_WARNING, "[WebSocketApi::vendor_register_cb] Failed because `%s` is already a registered vendor.", vendorName); + RETURN_FAILURE(); + } + + Vendor* v = new Vendor(); + v->_name = vendorName; + + c->_vendors[vendorName] = v; + + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::vendor_register_cb] [vendorName: %s] Registered new vendor.", v->_name.c_str()); + + calldata_set_ptr(cd, "vendor", static_cast(v)); + + RETURN_SUCCESS(); +} + +void WebSocketApi::vendor_request_register_cb(void *priv_data, calldata_t *cd) +{ + auto c = static_cast(priv_data); + + Vendor *v = get_vendor(cd); + if (!v) + RETURN_FAILURE(); + + const char *requestType; + if (!calldata_get_string(cd, "type", &requestType) || strlen(requestType) == 0) { + blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed due to missing or empty `type` string.", v->_name.c_str()); + RETURN_FAILURE(); + } + + void *voidCallback; + if (!calldata_get_ptr(cd, "callback", &voidCallback) || !voidCallback) { + blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed due to missing `callback` pointer.", v->_name.c_str()); + RETURN_FAILURE(); + } + + auto cb = static_cast(voidCallback); + + std::unique_lock l(v->_mutex); + + if (v->_requests.count(requestType)) { + blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed because `%s` is already a registered request.", v->_name.c_str(), requestType); + RETURN_FAILURE(); + } + + v->_requests[requestType] = *cb; + + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Registered new vendor request: %s", v->_name.c_str(), requestType); + + RETURN_SUCCESS(); +} + +void WebSocketApi::vendor_request_unregister_cb(void *priv_data, calldata_t *cd) +{ + auto c = static_cast(priv_data); + + Vendor *v = get_vendor(cd); + if (!v) + RETURN_FAILURE(); + + const char *requestType; + if (!calldata_get_string(cd, "type", &requestType) || strlen(requestType) == 0) { + blog(LOG_WARNING, "[WebSocketApi::vendor_request_unregister_cb] [vendorName: %s] Failed due to missing `type` string.", v->_name.c_str()); + RETURN_FAILURE(); + } + + std::unique_lock l(v->_mutex); + + if (!v->_requests.count(requestType)) { + blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed because `%s` is not a registered request.", v->_name.c_str(), requestType); + RETURN_FAILURE(); + } + + v->_requests.erase(requestType); + + if (IsDebugEnabled()) + blog(LOG_INFO, "[WebSocketApi::vendor_request_unregister_cb] [vendorName: %s] Unregistered vendor request: %s", v->_name.c_str(), requestType); + + RETURN_SUCCESS(); +} + +void WebSocketApi::vendor_event_emit_cb(void *priv_data, calldata_t *cd) +{ + auto c = static_cast(priv_data); + + Vendor *v = get_vendor(cd); + if (!v) + RETURN_FAILURE(); + + const char *eventType; + if (!calldata_get_string(cd, "type", &eventType) || strlen(eventType) == 0) { + blog(LOG_WARNING, "[WebSocketApi::vendor_event_emit_cb] [vendorName: %s] Failed due to missing `type` string.", v->_name.c_str()); + RETURN_FAILURE(); + } + + void *voidEventData; + if (!calldata_get_ptr(cd, "data", &voidEventData)) { + blog(LOG_WARNING, "[WebSocketApi::vendor_event_emit_cb] [vendorName: %s] Failed due to missing `data` pointer.", v->_name.c_str()); + RETURN_FAILURE(); + } + + auto eventData = static_cast(voidEventData); + + c->_eventCallback(v->_name, eventType, eventData); + + RETURN_SUCCESS(); +} diff --git a/src/WebSocketApi.h b/src/WebSocketApi.h new file mode 100644 index 00000000..6a9be8c5 --- /dev/null +++ b/src/WebSocketApi.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../lib/obs-websocket-api.h" + +class WebSocketApi { + public: + enum RequestReturnCode { + WEBSOCKET_API_REQUEST_RETURN_CODE_NORMAL, + WEBSOCKET_API_REQUEST_RETURN_CODE_NO_VENDOR, + WEBSOCKET_API_REQUEST_RETURN_CODE_NO_VENDOR_REQUEST, + }; + + typedef std::function EventCallback; + + struct Vendor { + std::shared_mutex _mutex; + std::string _name; + std::map _requests; + }; + + WebSocketApi(EventCallback cb); + ~WebSocketApi(); + + enum RequestReturnCode PerformVendorRequest(std::string vendorName, std::string requestName, obs_data_t *requestData, obs_data_t *responseData); + + static void get_ph_cb(void *priv_data, calldata_t *cd); + static void vendor_register_cb(void *priv_data, calldata_t *cd); + static void vendor_request_register_cb(void *priv_data, calldata_t *cd); + static void vendor_request_unregister_cb(void *priv_data, calldata_t *cd); + static void vendor_event_emit_cb(void *priv_data, calldata_t *cd); + + private: + std::shared_mutex _mutex; + EventCallback _eventCallback; + proc_handler_t *_procHandler; + std::map _vendors; +}; diff --git a/src/eventhandler/EventHandler.cpp b/src/eventhandler/EventHandler.cpp index 54c36146..4e28f911 100644 --- a/src/eventhandler/EventHandler.cpp +++ b/src/eventhandler/EventHandler.cpp @@ -26,7 +26,8 @@ EventHandler::EventHandler() : _inputShowStateChangedRef(0), _sceneItemTransformChangedRef(0) { - blog(LOG_INFO, "[EventHandler::EventHandler] Setting up event handlers..."); + if (IsDebugEnabled()) + blog(LOG_INFO, "[EventHandler::EventHandler] Setting up..."); obs_frontend_add_event_callback(OnFrontendEvent, this); @@ -40,12 +41,14 @@ EventHandler::EventHandler() : blog(LOG_ERROR, "[EventHandler::EventHandler] Unable to get libobs signal handler!"); } - blog(LOG_INFO, "[EventHandler::EventHandler] Finished."); + if (IsDebugEnabled()) + blog(LOG_INFO, "[EventHandler::EventHandler] Finished."); } EventHandler::~EventHandler() { - blog(LOG_INFO, "[EventHandler::~EventHandler] Removing event handlers..."); + if (IsDebugEnabled()) + blog(LOG_INFO, "[EventHandler::~EventHandler] Shutting down..."); obs_frontend_remove_event_callback(OnFrontendEvent, this); @@ -59,7 +62,8 @@ EventHandler::~EventHandler() blog(LOG_ERROR, "[EventHandler::~EventHandler] Unable to get libobs signal handler!"); } - blog(LOG_INFO, "[EventHandler::~EventHandler] Finished."); + if (IsDebugEnabled()) + blog(LOG_INFO, "[EventHandler::~EventHandler] Finished."); } void EventHandler::SetBroadcastCallback(EventHandler::BroadcastCallback cb) diff --git a/src/eventhandler/types/EventSubscription.h b/src/eventhandler/types/EventSubscription.h index c2e4023d..7542c5a9 100644 --- a/src/eventhandler/types/EventSubscription.h +++ b/src/eventhandler/types/EventSubscription.h @@ -41,8 +41,6 @@ namespace EventSubscription { SceneItems = (1 << 7), // Receive events in the `MediaInputs` category MediaInputs = (1 << 8), - // Receive all event categories - All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs), // InputVolumeMeters event (high-volume) InputVolumeMeters = (1 << 9), // InputActiveStateChanged event (high-volume) @@ -51,5 +49,10 @@ namespace EventSubscription { InputShowStateChanged = (1 << 11), // SceneItemTransformChanged event (high-volume) SceneItemTransformChanged = (1 << 12), + // Receive events from external OBS plugins + ExternalPlugins = (1 << 13), + + // Receive all event categories (exclude high-volume) + All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs | ExternalPlugins), }; }; diff --git a/src/obs-websocket.cpp b/src/obs-websocket.cpp index 91a6aed2..cf3b6400 100644 --- a/src/obs-websocket.cpp +++ b/src/obs-websocket.cpp @@ -26,6 +26,7 @@ with this program. If not, see #include "obs-websocket.h" #include "Config.h" +#include "WebSocketApi.h" #include "websocketserver/WebSocketServer.h" #include "eventhandler/EventHandler.h" #include "forms/SettingsDialog.h" @@ -34,6 +35,7 @@ OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US") ConfigPtr _config; +WebSocketApiPtr _webSocketApi; WebSocketServerPtr _webSocketServer; EventHandlerPtr _eventHandler; SettingsDialog *_settingsDialog = nullptr; @@ -49,6 +51,8 @@ void ___data_item_dummy_addref(obs_data_item_t*) {}; void ___data_item_release(obs_data_item_t* dataItem){ obs_data_item_release(&dataItem); }; void ___properties_dummy_addref(obs_properties_t*) {}; +void WebSocketApiEventCallback(std::string vendorName, std::string eventType, obs_data_t *obsEventData); + bool obs_module_load(void) { blog(LOG_INFO, "[obs_module_load] you can haz websockets (Version: %s | RPC Version: %d)", OBS_WEBSOCKET_VERSION, OBS_WEBSOCKET_RPC_VERSION); @@ -61,6 +65,8 @@ bool obs_module_load(void) // Initialize event handler before server, as the server configures the event handler. _eventHandler = EventHandlerPtr(new EventHandler()); + _webSocketApi = WebSocketApiPtr(new WebSocketApi(WebSocketApiEventCallback)); + _webSocketServer = WebSocketServerPtr(new WebSocketServer()); obs_frontend_push_ui_translation(obs_module_get_string); @@ -92,6 +98,8 @@ void obs_module_unload() _eventHandler.reset(); + _webSocketApi.reset(); + _config->Save(); _config.reset(); @@ -105,6 +113,11 @@ ConfigPtr GetConfig() return _config; } +WebSocketApiPtr GetWebSocketApi() +{ + return _webSocketApi; +} + WebSocketServerPtr GetWebSocketServer() { return _webSocketServer; @@ -120,7 +133,19 @@ os_cpu_usage_info_t* GetCpuUsageInfo() return _cpuUsageInfo; } -bool IsDebugMode() +bool IsDebugEnabled() { return !_config || _config->DebugEnabled; -} \ No newline at end of file +} + +void WebSocketApiEventCallback(std::string vendorName, std::string eventType, obs_data_t *obsEventData) +{ + json eventData = Utils::Json::ObsDataToJson(obsEventData); + + json broadcastEventData; + broadcastEventData["vendorName"] = vendorName; + broadcastEventData["eventType"] = eventType; + broadcastEventData["eventData"] = eventData; + + _webSocketServer->BroadcastEvent(EventSubscription::ExternalPlugins, "ExternalPluginEvent", broadcastEventData); +} diff --git a/src/obs-websocket.h b/src/obs-websocket.h index e1819af0..b72030e4 100644 --- a/src/obs-websocket.h +++ b/src/obs-websocket.h @@ -54,6 +54,9 @@ using OBSPropertiesAutoDestroy = OBSRef ConfigPtr; +class WebSocketApi; +typedef std::shared_ptr WebSocketApiPtr; + class WebSocketServer; typedef std::shared_ptr WebSocketServerPtr; @@ -62,10 +65,12 @@ typedef std::shared_ptr EventHandlerPtr; ConfigPtr GetConfig(); +WebSocketApiPtr GetWebSocketApi(); + WebSocketServerPtr GetWebSocketServer(); EventHandlerPtr GetEventHandler(); os_cpu_usage_info_t* GetCpuUsageInfo(); -bool IsDebugMode(); +bool IsDebugEnabled(); diff --git a/src/utils/Json.cpp b/src/utils/Json.cpp index bf8b5a03..65a0c7c0 100644 --- a/src/utils/Json.cpp +++ b/src/utils/Json.cpp @@ -142,6 +142,9 @@ json Utils::Json::ObsDataToJson(obs_data_t *d, bool includeDefault) json j = json::object(); obs_data_item_t *item = nullptr; + if (!d) + return j; + for (item = obs_data_first(d); item; obs_data_item_next(&item)) { enum obs_data_type type = obs_data_item_gettype(item); const char *name = obs_data_item_get_name(item); diff --git a/src/websocketserver/WebSocketServer_RequestBatchProcessing.cpp b/src/websocketserver/WebSocketServer_RequestBatchProcessing.cpp index 929f42fa..964e4476 100644 --- a/src/websocketserver/WebSocketServer_RequestBatchProcessing.cpp +++ b/src/websocketserver/WebSocketServer_RequestBatchProcessing.cpp @@ -81,13 +81,13 @@ bool PreProcessVariables(const json &variables, const json &inputVariables, json std::string key = it.key(); if (!variables.contains(key)) { - if (IsDebugMode()) + if (IsDebugEnabled()) blog(LOG_WARNING, "[WebSocketServer::ProcessRequestBatch] inputVariables requested variable `%s`, but it does not exist. Skipping!", key.c_str()); continue; } if (!it.value().is_string()) { - if (IsDebugMode()) + if (IsDebugEnabled()) blog(LOG_WARNING, "[WebSocketServer::ProcessRequestBatch] Value of item `%s` in inputVariables is not a string. Skipping!", key.c_str()); continue; } @@ -108,13 +108,13 @@ void PostProcessVariables(json &variables, const json &outputVariables, const js std::string key = it.key(); if (!responseData.contains(key)) { - if (IsDebugMode()) + if (IsDebugEnabled()) blog(LOG_WARNING, "[WebSocketServer::ProcessRequestBatch] outputVariables requested responseData item `%s`, but it does not exist. Skipping!", key.c_str()); continue; } if (!it.value().is_string()) { - if (IsDebugMode()) + if (IsDebugEnabled()) blog(LOG_WARNING, "[WebSocketServer::ProcessRequestBatch] Value of item `%s` in outputVariables is not a string. Skipping!", key.c_str()); continue; }