Base: Implement ObsWebSocketApi + cleanup

- Implements a WIP ObsWebSocketApi, for obs-websocket-api.h. Events are
finished, but requests are not.
- Some logging improvements
- A bit of code cleanup around the plugin
This commit is contained in:
tt2468 2021-11-20 01:26:50 -08:00
parent bc1d5386a5
commit 99cbaaf34c
10 changed files with 341 additions and 28 deletions

View File

@ -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)

View File

@ -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);

214
src/WebSocketApi.cpp Normal file
View File

@ -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<WebSocketApi::Vendor*>(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<WebSocketApi*>(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<WebSocketApi*>(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<void*>(v));
RETURN_SUCCESS();
}
void WebSocketApi::vendor_request_register_cb(void *priv_data, calldata_t *cd)
{
auto c = static_cast<WebSocketApi*>(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<obs_websocket_request_callback*>(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<WebSocketApi*>(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<WebSocketApi*>(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<obs_data_t*>(voidEventData);
c->_eventCallback(v->_name, eventType, eventData);
RETURN_SUCCESS();
}

44
src/WebSocketApi.h Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <functional>
#include <string>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <obs.h>
#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<void(std::string, std::string, obs_data_t*)> EventCallback;
struct Vendor {
std::shared_mutex _mutex;
std::string _name;
std::map<std::string, obs_websocket_request_callback> _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<std::string, Vendor*> _vendors;
};

View File

@ -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)

View File

@ -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),
};
};

View File

@ -26,6 +26,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#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;
}
}
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);
}

View File

@ -54,6 +54,9 @@ using OBSPropertiesAutoDestroy = OBSRef<obs_properties_t*, ___properties_dummy_a
class Config;
typedef std::shared_ptr<Config> ConfigPtr;
class WebSocketApi;
typedef std::shared_ptr<WebSocketApi> WebSocketApiPtr;
class WebSocketServer;
typedef std::shared_ptr<WebSocketServer> WebSocketServerPtr;
@ -62,10 +65,12 @@ typedef std::shared_ptr<EventHandler> EventHandlerPtr;
ConfigPtr GetConfig();
WebSocketApiPtr GetWebSocketApi();
WebSocketServerPtr GetWebSocketServer();
EventHandlerPtr GetEventHandler();
os_cpu_usage_info_t* GetCpuUsageInfo();
bool IsDebugMode();
bool IsDebugEnabled();

View File

@ -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);

View File

@ -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;
}