From e3ad148c150dada0137a8d4be95027ea1815f3a7 Mon Sep 17 00:00:00 2001 From: yinzara Date: Thu, 6 Jul 2017 04:07:06 -0700 Subject: [PATCH] Request types: get/set/save streaming settings (PR #100) * Adding support for changing streaming server settings * Updates after initial code review for customized rtmp settings * Updating PROTOCOL.MD documentment for streaming service settings * Changes based on code review --- PROTOCOL.md | 73 +++++++++++++++++- Utils.cpp | 57 +++++++++++++- Utils.h | 2 + WSRequestHandler.cpp | 180 ++++++++++++++++++++++++++++++++++++++++++- WSRequestHandler.h | 6 ++ 5 files changed, 311 insertions(+), 7 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 7c8dadcd..f6bef4c2 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -104,6 +104,10 @@ The protocol in general is based on the OBS Remote protocol created by Bill Hami - ["ListSceneCollections"](#listscenecollections) - ["SetCurrentSceneCollection"](#setcurrentscenecollection) - ["GetCurrentSceneCollection"](#getcurrentscenecollection) + - **Streaming Server Settings** + - ["GetStreamSettings"](#getstreamsettings) + - ["SetStreamSettings"](#setstreamsettings) + - ["SaveStreamSettings"](#savestreamsettings) - **Profiles** - ["ListProfiles"](#listprofiles) - ["SetCurrentProfile"](#setcurrentprofile) @@ -489,7 +493,9 @@ __Response__ : always OK. No additional fields. #### "StartStopRecording" Toggles recording on or off. -__Request fields__ : none +__Request fields__ : +- **"stream"** (object; optional) : See 'stream' parameter in 'StartStreaming'. Ignored if stream is already started. + __Response__ : always OK. No additional fields. --- @@ -497,7 +503,23 @@ __Response__ : always OK. No additional fields. #### "StartStreaming" Start streaming. -__Request fields__ : none +__Request fields__ : +- **"stream"** (object; optional) : If specified allows for special configuration of the stream + +The 'stream' object has the following fields: +- **"settings"** (object; optional) : The settings for the stream +- **"type"** (string; optional) : If specified ensures the type of the stream matches the given type (usually 'rtmp\_custom' or 'rtmp\_common'). If the currently configured stream type does not match the given stream type, all settings must be specified in the 'settings' object or an error will occur starting the stream. +- **"metadata"** (object; optional) : Adds the given object parameters as encoded query string parameters to the 'key' of the RTMP stream. Used to pass data to the RTMP service about the stream. + +The 'settings' object has the following fields: +- **"server"** (string; optional) : The publish URL +- **"key"** (string; optional) : The publish key of the stream +- **"use-auth"** (bool; optional) : should authentication be used when connecting to the streaming server +- **"username"** (string; optional) : if authentication is enabled, the username for access to the streaming server. Ignored if 'use-auth' is not specified as 'true'. +- **"password"** (string; optional) : if authentication is enabled, the password for access to the streaming server. Ignored if 'use-auth' is not specified as 'true'. + +The 'metadata' object supports passing any string, numeric or boolean field. + __Response__ : Error if streaming is already active, OK otherwise. No additional fields. --- @@ -746,6 +768,53 @@ __Response__ : OK with these additional fields : --- +#### "GetStreamSettings" +Gets the current streaming server settings + +__Request fields__ : none + +__Response__ : OK with these additional fields : +- **"type"** (string) : The type of streaming service configuration usually 'rtmp\_custom' or 'rtmp\_common' +- **"settings"** (object) : The actual settings of the stream (i.e. server, key, use-auth, username, password) + +The 'settings' object has the following fields however they may vary by 'type': +- **"server"** (string) : The publish URL +- **"key"** (string) : The publish key of the stream +- **"use-auth"** (bool) : should authentication be used when connecting to the streaming server +- **"username"** (string) : if authentication is enabled, the username for access to the streaming server +- **"password"** (string) : if authentication is enabled, the password for access to the streaming server + +-- + +#### "SetStreamSettings" +Sets one or more attributes of the current streaming server settings. Any options not passed will remain unchanged. Returns the updated settings in response. +If 'type' is different than the current streaming service type, all settings are required. +Returns the full settings of the stream (i.e. the same as GetStreamSettings) + +__Request fields__ : +- **"type"** (string) : The type of streaming service configuration usually 'rtmp\_custom' or 'rtmp\_common' +- **"settings"** (object) : The actual settings of the stream (i.e. server, key, use-auth, username, password) +- **"save"** (bool) : If specified as true, saves the settings to disk + +The 'settings' object has the following fields however they may vary by 'type': +- **"server"** (string; optional) : The publish URL +- **"key"** (string; optional) : The publish key of the stream +- **"use-auth"** (bool; optional) : should authentication be used when connecting to the streaming server +- **"username"** (string; optional) : if authentication is enabled, the username for access to the streaming server +- **"password"** (string; optional) : if authentication is enabled, the password for access to the streaming server + +__Response__ : OK with the same fields as the request (except 'save') + +--- + +#### "SaveStreamSettings" +Saves the current streaming server settings to disk + +__Request fields__ : none + +__Response__ : OK + + #### "SetCurrentProfile" Change the current profile. diff --git a/Utils.cpp b/Utils.cpp index 49dfca39..02d2c0e3 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -20,6 +20,7 @@ with this program. If not, see #include #include #include +#include #include "Utils.h" #include "obs-websocket.h" @@ -464,4 +465,58 @@ bool Utils::SetRecordingFolder(const char* path) config_save(profile); return true; -} \ No newline at end of file +} + +QString* Utils::ParseDataToQueryString(obs_data_t * data) +{ + QString* query = nullptr; + if (data) + { + obs_data_item_t* item = obs_data_first(data); + if (item) + { + query = new QString(); + bool isFirst = true; + do + { + if (!obs_data_item_has_user_value(item)) + continue; + + if (!isFirst) + query->append('&'); + else + isFirst = false; + + const char* attrName = obs_data_item_get_name(item); + query->append(attrName).append("="); + switch (obs_data_item_gettype(item)) + { + case OBS_DATA_BOOLEAN: + query->append(obs_data_item_get_bool(item)?"true":"false"); + break; + case OBS_DATA_NUMBER: + switch (obs_data_item_numtype(item)) + { + case OBS_DATA_NUM_DOUBLE: + query->append(QString::number(obs_data_item_get_double(item))); + break; + case OBS_DATA_NUM_INT: + query->append(QString::number(obs_data_item_get_int(item))); + break; + case OBS_DATA_NUM_INVALID: + break; + } + break; + case OBS_DATA_STRING: + query->append(QUrl::toPercentEncoding(QString(obs_data_item_get_string(item)))); + break; + default: + //other types are not supported + break; + } + } while ( obs_data_item_next( &item ) ); + } + } + + return query; +} diff --git a/Utils.h b/Utils.h index 9b428b9e..3a103539 100644 --- a/Utils.h +++ b/Utils.h @@ -76,6 +76,8 @@ class Utils static QString FormatIPAddress(QHostAddress &addr); static const char* GetRecordingFolder(); static bool SetRecordingFolder(const char* path); + + static QString* ParseDataToQueryString(obs_data_t * data); }; #endif // UTILS_H diff --git a/WSRequestHandler.cpp b/WSRequestHandler.cpp index 59916321..e026da16 100644 --- a/WSRequestHandler.cpp +++ b/WSRequestHandler.cpp @@ -17,17 +17,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ +#include #include "WSRequestHandler.h" #include "WSEvents.h" #include "obs-websocket.h" #include "Config.h" #include "Utils.h" +#include bool str_valid(const char* str) { return (str != nullptr && strlen(str) > 0); } + +obs_service_t* WSRequestHandler::_service = nullptr; + WSRequestHandler::WSRequestHandler(QWebSocket* client) : _messageId(0), _requestType(""), @@ -81,6 +86,10 @@ WSRequestHandler::WSRequestHandler(QWebSocket* client) : messageMap["GetCurrentProfile"] = WSRequestHandler::HandleGetCurrentProfile; messageMap["ListProfiles"] = WSRequestHandler::HandleListProfiles; + messageMap["SetStreamSettings"] = WSRequestHandler::HandleSetStreamSettings; + messageMap["GetStreamSettings"] = WSRequestHandler::HandleGetStreamSettings; + messageMap["SaveStreamSettings"] = WSRequestHandler::HandleSaveStreamSettings; + messageMap["GetStudioModeStatus"] = WSRequestHandler::HandleGetStudioModeStatus; messageMap["GetPreviewScene"] = WSRequestHandler::HandleGetPreviewScene; messageMap["SetPreviewScene"] = WSRequestHandler::HandleSetPreviewScene; @@ -91,6 +100,7 @@ WSRequestHandler::WSRequestHandler(QWebSocket* client) : messageMap["SetTextGDIPlusProperties"] = WSRequestHandler::HandleSetTextGDIPlusProperties; messageMap["GetTextGDIPlusProperties"] = WSRequestHandler::HandleGetTextGDIPlusProperties; + messageMap["GetBrowserSourceProperties"] = WSRequestHandler::HandleGetBrowserSourceProperties; messageMap["SetBrowserSourceProperties"] = WSRequestHandler::HandleSetBrowserSourceProperties; @@ -380,11 +390,13 @@ void WSRequestHandler::HandleGetStreamingStatus(WSRequestHandler* req) void WSRequestHandler::HandleStartStopStreaming(WSRequestHandler* req) { if (obs_frontend_streaming_active()) - obs_frontend_streaming_stop(); + { + HandleStopStreaming(req); + } else - obs_frontend_streaming_start(); - - req->SendOKResponse(); + { + HandleStartStreaming(req); + } } void WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req) @@ -401,8 +413,97 @@ void WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) { if (obs_frontend_streaming_active() == false) { + obs_data_t* streamData = obs_data_get_obj(req->data, "stream"); + obs_service_t* currentService = nullptr; + + if (streamData) + { + currentService = obs_frontend_get_streaming_service(); + obs_service_addref(currentService); + + obs_service_t* service = _service; + const char* currentServiceType = obs_service_get_type(currentService); + + const char* requestedType = obs_data_has_user_value(streamData, "type") ? obs_data_get_string(streamData, "type") : currentServiceType; + const char* serviceType = service != nullptr ? obs_service_get_type(service) : currentServiceType; + obs_data_t* settings = obs_data_get_obj(streamData, "settings"); + + + obs_data_t* metadata = obs_data_get_obj(streamData, "metadata"); + QString* query = Utils::ParseDataToQueryString(metadata); + + if (strcmp(requestedType, serviceType) != 0) + { + if (settings) + { + obs_service_release(service); + service = nullptr; //different type so we can't reuse the existing service instance + } + else + { + req->SendErrorResponse("Service type requested does not match currently configured type and no 'settings' were provided"); + return; + } + } + else + { + //if type isn't changing we should overlay the settings we got with the existing settings + obs_data_t* existingSettings = obs_service_get_settings(currentService); + obs_data_t* newSettings = obs_data_create(); //by doing this you can send a request to the websocket that only contains a setting you want to change instead of having to do a get and then change them + + obs_data_apply(newSettings, existingSettings); //first apply the existing settings + + obs_data_apply(newSettings, settings); //then apply the settings from the request should they exist + obs_data_release(settings); + + settings = newSettings; + obs_data_release(existingSettings); + } + + if (service == nullptr) + { //create the new custom service setup by the websocket + service = obs_service_create(requestedType, "websocket_custom_service", settings, nullptr); + } + + //Supporting adding metadata parameters to key query string + if (query && query->length() > 0) { + const char* key = obs_data_get_string(settings, "key"); + int keylen = strlen(key); + bool hasQuestionMark = false; + for (int i = 0; i < keylen; i++) { + if (key[i] == '?') { + hasQuestionMark = true; + break; + } + } + if (hasQuestionMark) { + query->prepend('&'); + } else { + query->prepend('?'); + } + query->prepend(key); + key = query->toUtf8(); + obs_data_set_string(settings, "key", key); + } + + obs_service_update(service, settings); + obs_data_release(settings); + obs_data_release(metadata); + _service = service; + obs_frontend_set_streaming_service(_service); + } else if (_service != nullptr) { + obs_service_release(_service); + _service = nullptr; + } + obs_frontend_streaming_start(); + + if (_service != nullptr) { + obs_frontend_set_streaming_service(currentService); + } + req->SendOKResponse(); + obs_service_release(currentService); } else { @@ -909,6 +1010,77 @@ void WSRequestHandler::HandleGetCurrentProfile(WSRequestHandler* req) obs_data_release(response); } +void WSRequestHandler::HandleSetStreamSettings(WSRequestHandler* req) +{ + obs_service_t* service = obs_frontend_get_streaming_service(); + + obs_data_t* settings = obs_data_get_obj(req->data, "settings"); + if (!settings) + { + req->SendErrorResponse("'settings' are required'"); + return; + } + + const char* serviceType = obs_service_get_type(service); + const char* requestedType = obs_data_get_string(req->data, "type"); + + if (requestedType != nullptr && strcmp(requestedType, serviceType) != 0) + { + obs_data_t* hotkeys = obs_hotkeys_save_service(service); + obs_service_release(service); + service = obs_service_create(requestedType, "websocket_custom_service", settings, hotkeys); + obs_data_release(hotkeys); + } + else + { + obs_data_t* existingSettings = obs_service_get_settings(service); //if type isn't changing we should overlay the settings we got with the existing settings + obs_data_t* newSettings = obs_data_create(); //by doing this you can send a request to the websocket that only contains a setting you want to change instead of having to do a get and then change them + obs_data_apply(newSettings, existingSettings); //first apply the existing settings + obs_data_apply(newSettings, settings); //then apply the settings from the request + obs_data_release(settings); + obs_data_release(existingSettings); + obs_service_update(service, settings); + settings = newSettings; + } + + if (obs_data_get_bool(req->data, "save")) //if save is specified we should immediately save the streaming service + { + obs_frontend_save_streaming_service(); + } + + obs_data_t* response = obs_data_create(); + obs_data_set_string(response, "type", requestedType); + obs_data_set_obj(response, "settings", settings); + + req->SendOKResponse(response); + + obs_data_release(settings); + obs_data_release(response); +} + +void WSRequestHandler::HandleGetStreamSettings(WSRequestHandler* req) +{ + obs_service_t* service = obs_frontend_get_streaming_service(); + + const char* serviceType = obs_service_get_type(service); + obs_data_t* settings = obs_service_get_settings(service); + + obs_data_t* response = obs_data_create(); + obs_data_set_string(response, "type", serviceType); + obs_data_set_obj(response, "settings", settings); + + req->SendOKResponse(response); + + obs_data_release(settings); + obs_data_release(response); +} + +void WSRequestHandler::HandleSaveStreamSettings(WSRequestHandler* req) +{ + obs_frontend_save_streaming_service(); + req->SendOKResponse(); +} + void WSRequestHandler::HandleListProfiles(WSRequestHandler* req) { obs_data_array_t* profiles = Utils::GetProfiles(); diff --git a/WSRequestHandler.h b/WSRequestHandler.h index eaf9e563..5d0accd8 100644 --- a/WSRequestHandler.h +++ b/WSRequestHandler.h @@ -21,6 +21,7 @@ with this program. If not, see #define WSREQUESTHANDLER_H #include + #include #include @@ -35,6 +36,7 @@ class WSRequestHandler : public QObject bool hasField(const char* name); private: + static obs_service_t* _service; QWebSocket* _client; const char* _messageId; const char* _requestType; @@ -90,6 +92,10 @@ class WSRequestHandler : public QObject static void HandleGetCurrentProfile(WSRequestHandler* req); static void HandleListProfiles(WSRequestHandler* req); + static void HandleSetStreamSettings(WSRequestHandler* req); + static void HandleGetStreamSettings(WSRequestHandler* req); + static void HandleSaveStreamSettings(WSRequestHandler* req); + static void HandleSetTransitionDuration(WSRequestHandler* req); static void HandleGetTransitionDuration(WSRequestHandler* req);