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