diff --git a/PROTOCOL.md b/PROTOCOL.md index 51032f98..0c03e8b2 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -21,6 +21,8 @@ The protocol in general is based on the OBS Remote protocol created by Bill Hami - ["TransitionDurationChanged"](#transitiondurationchanged) - ["TransitionListChanged"](#transitionlistchanged) - ["TransitionBegin"](#transitionbegin) + - ["PreviewSceneChanged"](#previewscenechanged) + - ["StudioModeSwitched"](#studiomodeswitched) - ["ProfileChanged"](#profilechanged) - ["ProfileListChanged"](#profilelistchanged) - ["StreamStarting"](#streamstarting) @@ -43,6 +45,12 @@ The protocol in general is based on the OBS Remote protocol created by Bill Hami - ["SetCurrentScene"](#setcurrentscene) - ["GetSceneList"](#getscenelist) - ["SetSourceRender"](#setsourcerender) + - ["GetStudioModeStatus"](#getstudiomodestatus) + - ["SetPreviewScene"](#setpreviewscene) + - ["TransitionToProgram"](#transitiontoprogram) + - ["EnableStudioMode"](#enablestudiomode) + - ["DisableStudioMode"](#disablestudiomode) + - ["ToggleStudioMode"](#togglestudiomode) - ["StartStopStreaming"](#startstopstreaming) - ["StartStopRecording"](#startstoprecording) - ["StartStreaming"](#startstreaming) @@ -86,6 +94,7 @@ Additional fields will be present in the event message depending on the event ty #### "SwitchScenes" OBS is switching to another scene (called at the end of the transition). - **scene-name** (string) : The name of the scene being switched to. +- **sources** (array of objects) : List of sources composing the scene. Same specification as [`GetCurrentScene`](#getcurrentscene). --- @@ -154,6 +163,19 @@ A transition other than "Cut" has begun. --- +#### "PreviewSceneChanged" +The selected Preview scene changed (only in Studio Mode). +- **scene-name** (string) : Name of the scene being previewed. +- **sources** (array of objects) : List of sources composing the scene. Same specification as [`GetCurrentScene`](#getcurrentscene). + +--- + +#### "StudioModeSwitched" +Studio Mode has been switched on or off. +- **"new-state"** (bool) : new state of Studio Mode: true if enabled, false if disabled. + +--- + #### "ProfileChanged" Triggered when switching to another profile or when renaming the current profile. @@ -336,8 +358,73 @@ __Response__ : OK if source exists in the current scene, error otherwise. --- +#### "GetStudioModeStatus" +Tells if Studio Mode is currently enabled or disabled. + +__Request fields__ : none +__Response__ : always OK, with these additional fields : +- **"studio-mode"** (bool) : true if OBS is in Studio Mode, false otherwise. + +--- + +#### "GetPreviewScene" +Studio Mode only. Gets the name of the currently Previewed scene, along with a list of its sources. + +__Request fields__ : none +__Response__ : OK if Studio Mode is enabled, with the same fields as [`GetCurrentScene`](#getcurrentscene), error otherwise. + +--- + +#### "SetPreviewScene" +Studio Mode only. Sets the specified scene as the Previewed scene in Studio Mode. + +__Request fields__ : +- **"scene-name"** (string) : name of the scene to selected as the preview of Studio Mode + +__Response__ : OK if Studio Mode is enabled and specified scene exists, error otherwise. + +--- + +#### "TransitionToProgram" +Studio Mode only. Transitions the currently previewed scene to Program (main output). + +__Request fields__ : +- **"with-transition" (object, optional) : if specified, use this transition when switching from preview to program. This will change the current transition in the frontend to this one. + +__Response__ : OK if studio mode is enabled and optional transition exists, error otherwise. + +An object passed as `"with-transition"` in a request must have the following fields : +- **"name"** (string, optional) : transition name +- **"duration"** (integer, optional) : transition duration in milliseconds + +--- + +#### "EnableStudioMode" +Enables Studio Mode. + +__Request fields__ : none +__Response__ : always OK. No additional fields. + +--- + +#### "DisableStudioMode" +Disables Studio Mode. + +__Request fields__ : none +__Response__ : always OK. No additional fields. + +--- + +#### "ToggleStudioMode" +Toggles Studio Mode on or off. + +__Request fields__ : none +__Response__ : always OK. No additional fields. + +--- + #### "StartStopStreaming" -Toggle streaming on or off. +Toggles streaming on or off. __Request fields__ : none __Response__ : always OK. No additional fields. @@ -345,7 +432,7 @@ __Response__ : always OK. No additional fields. --- #### "StartStopRecording" -Toggle recording on or off. +Toggles recording on or off. __Request fields__ : none __Response__ : always OK. No additional fields. diff --git a/Utils.cpp b/Utils.cpp index 0ac75611..70d0fafe 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -18,10 +18,13 @@ with this program. If not, see #include "Utils.h" #include +#include #include -#include +#include #include "obs-websocket.h" +Q_DECLARE_METATYPE(OBSScene); + obs_data_array_t* string_list_to_array(char** strings, char* key) { if (!strings) @@ -238,6 +241,136 @@ void Utils::SetTransitionDuration(int ms) } } +bool Utils::SetTransitionByName(const char* transition_name) +{ + obs_source_t *transition = GetTransitionFromName(transition_name); + + if (transition) + { + obs_frontend_set_current_transition(transition); + obs_source_release(transition); + + return true; + } + else + { + return false; + } +} + +QPushButton* Utils::GetPreviewModeButtonControl() +{ + QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); + return main->findChild("modeSwitch"); +} + +QListWidget* Utils::GetSceneListControl() +{ + QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); + return main->findChild("scenes"); +} + +obs_scene_t* Utils::SceneListItemToScene(QListWidgetItem* item) +{ + if (!item) + return nullptr; + + QVariant item_data = item->data(static_cast(Qt::UserRole)); + return item_data.value(); +} + +QLayout* Utils::GetPreviewLayout() +{ + QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); + return main->findChild("previewLayout"); +} + +bool Utils::IsPreviewModeActive() +{ + QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); + + // Clue 1 : "Studio Mode" button is toggled on + bool buttonToggledOn = GetPreviewModeButtonControl()->isChecked(); + + // Clue 2 : Preview layout has more than one item + int previewChildCount = GetPreviewLayout()->count(); + blog(LOG_INFO, "preview layout children count : %d", previewChildCount); + + return buttonToggledOn || (previewChildCount >= 2); +} + +void Utils::EnablePreviewMode() +{ + if (!IsPreviewModeActive()) + GetPreviewModeButtonControl()->click(); +} + +void Utils::DisablePreviewMode() +{ + if (IsPreviewModeActive()) + GetPreviewModeButtonControl()->click(); +} + +void Utils::TogglePreviewMode() +{ + GetPreviewModeButtonControl()->click(); +} + +obs_scene_t* Utils::GetPreviewScene() +{ + if (IsPreviewModeActive()) + { + QListWidget* sceneList = GetSceneListControl(); + + QList selected = sceneList->selectedItems(); + + // Qt::UserRole == QtUserRole::OBSRef + obs_scene_t* scene = Utils::SceneListItemToScene(selected.first()); + + obs_scene_addref(scene); + return scene; + } + + return nullptr; +} + +void Utils::SetPreviewScene(const char* name) +{ + if (IsPreviewModeActive()) + { + QListWidget* sceneList = GetSceneListControl(); + QList matchingItems = sceneList->findItems(name, Qt::MatchExactly); + + if (matchingItems.count() > 0) + sceneList->setCurrentItem(matchingItems.first()); + } +} + +void Utils::TransitionToProgram() +{ + if (!IsPreviewModeActive()) + return; + + // WARNING : if the layout created in OBS' CreateProgramOptions() changes + // then this won't work as expected + + QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); + + // The program options widget is the second item in the left-to-right layout + QWidget* programOptions = GetPreviewLayout()->itemAt(1)->widget(); + + // The "Transition" button lies in the mainButtonLayout + // which is the first itemin the program options' layout + QLayout* mainButtonLayout = programOptions->layout()->itemAt(1)->layout(); + QWidget* transitionBtnWidget = mainButtonLayout->itemAt(0)->widget(); + + // Try to cast that widget into a button + QPushButton* transitionBtn = qobject_cast(transitionBtnWidget); + + // Perform a click on that button + transitionBtn->click(); +} + const char* Utils::OBSVersionString() { uint32_t version = obs_get_version(); diff --git a/Utils.h b/Utils.h index 91b22be7..d7e0b0a2 100644 --- a/Utils.h +++ b/Utils.h @@ -20,6 +20,9 @@ with this program. If not, see #define UTILS_H #include +#include +#include +#include #include #include @@ -42,6 +45,22 @@ class Utils static int GetTransitionDuration(); static void SetTransitionDuration(int ms); + static bool SetTransitionByName(const char* transition_name); + + static QPushButton* GetPreviewModeButtonControl(); + static QLayout* GetPreviewLayout(); + static QListWidget* GetSceneListControl(); + static obs_scene_t* SceneListItemToScene(QListWidgetItem* item); + + static bool IsPreviewModeActive(); + static void EnablePreviewMode(); + static void DisablePreviewMode(); + static void TogglePreviewMode(); + + static obs_scene_t* GetPreviewScene(); + static void SetPreviewScene(const char* name); + static void TransitionToProgram(); + static const char* OBSVersionString(); }; diff --git a/WSEvents.cpp b/WSEvents.cpp index ec20852a..90fc617a 100644 --- a/WSEvents.cpp +++ b/WSEvents.cpp @@ -19,6 +19,7 @@ with this program. If not, see #include #include +#include #include "Utils.h" #include "WSEvents.h" #include "obs-websocket.h" @@ -68,6 +69,12 @@ WSEvents::WSEvents(WSServer *srv) connect(statusTimer, SIGNAL(timeout()), this, SLOT(StreamStatus())); statusTimer->start(2000); // equal to frontend's constant BITRATE_UPDATE_SECONDS + QListWidget* sceneList = Utils::GetSceneListControl(); + connect(sceneList, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), this, SLOT(SelectedSceneChanged(QListWidgetItem*, QListWidgetItem*))); + + QPushButton* modeSwitch = Utils::GetPreviewModeButtonControl(); + connect(modeSwitch, SIGNAL(clicked(bool)), this, SLOT(ModeSwitchClicked(bool))); + transition_handler = nullptr; scene_handler = nullptr; @@ -276,14 +283,17 @@ void WSEvents::OnSceneChange() obs_data_t *data = obs_data_create(); obs_source_t* current_scene = obs_frontend_get_current_scene(); + obs_data_array_t* scene_items = Utils::GetSceneItems(current_scene); connectSceneSignals(current_scene); obs_data_set_string(data, "scene-name", obs_source_get_name(current_scene)); - + obs_data_set_array(data, "sources", scene_items); + broadcastUpdate("SwitchScenes", data); - obs_data_release(data); + obs_data_array_release(scene_items); obs_source_release(current_scene); + obs_data_release(data); } void WSEvents::OnSceneListChange() @@ -568,4 +578,35 @@ void WSEvents::OnSceneItemVisibilityChanged(void *param, calldata_t *data) instance->broadcastUpdate("SceneItemVisibilityChanged", fields); obs_data_release(fields); +} + +void WSEvents::SelectedSceneChanged(QListWidgetItem *current, QListWidgetItem *prev) +{ + if (Utils::IsPreviewModeActive()) + { + obs_scene_t* scene = Utils::SceneListItemToScene(current); + if (!scene) return; + + obs_source_t* scene_source = obs_scene_get_source(scene); + obs_data_array_t* scene_items = Utils::GetSceneItems(scene_source); + + obs_data_t* data = obs_data_create(); + obs_data_set_string(data, "scene-name", obs_source_get_name(scene_source)); + obs_data_set_array(data, "sources", scene_items); + + broadcastUpdate("PreviewSceneChanged", data); + + obs_data_array_release(scene_items); + obs_data_release(data); + } +} + +void WSEvents::ModeSwitchClicked(bool checked) +{ + obs_data_t* data = obs_data_create(); + obs_data_set_bool(data, "new-state", checked); + + broadcastUpdate("StudioModeSwitched", data); + + obs_data_release(data); } \ No newline at end of file diff --git a/WSEvents.h b/WSEvents.h index 2c678364..5e9d02bf 100644 --- a/WSEvents.h +++ b/WSEvents.h @@ -21,6 +21,7 @@ with this program. If not, see #define WSEVENTS_H #include +#include #include "WSServer.h" class WSEvents : public QObject @@ -41,9 +42,11 @@ class WSEvents : public QObject const char* GetRecordingTimecode(); private Q_SLOTS: + void deferredInitOperations(); void StreamStatus(); void TransitionDurationChanged(int ms); - void deferredInitOperations(); + void SelectedSceneChanged(QListWidgetItem *current, QListWidgetItem *prev); + void ModeSwitchClicked(bool checked); private: WSServer *_srv; diff --git a/WSRequestHandler.cpp b/WSRequestHandler.cpp index 026a96f5..c2af012f 100644 --- a/WSRequestHandler.cpp +++ b/WSRequestHandler.cpp @@ -71,6 +71,14 @@ WSRequestHandler::WSRequestHandler(QWebSocket *client) : messageMap["GetCurrentProfile"] = WSRequestHandler::HandleGetCurrentProfile; messageMap["ListProfiles"] = WSRequestHandler::HandleListProfiles; + messageMap["GetStudioModeStatus"] = WSRequestHandler::HandleGetStudioModeStatus; + messageMap["GetPreviewScene"] = WSRequestHandler::HandleGetPreviewScene; + messageMap["SetPreviewScene"] = WSRequestHandler::HandleSetPreviewScene; + messageMap["TransitionToProgram"] = WSRequestHandler::HandleTransitionToProgram; + messageMap["EnableStudioMode"] = WSRequestHandler::HandleEnableStudioMode; + messageMap["DisableStudioMode"] = WSRequestHandler::HandleDisableStudioMode; + messageMap["ToggleStudioMode"] = WSRequestHandler::HandleToggleStudioMode; + authNotRequired.insert("GetVersion"); authNotRequired.insert("GetAuthRequired"); authNotRequired.insert("Authenticate"); @@ -429,19 +437,13 @@ void WSRequestHandler::HandleGetCurrentTransition(WSRequestHandler *owner) void WSRequestHandler::HandleSetCurrentTransition(WSRequestHandler *owner) { const char *name = obs_data_get_string(owner->_requestData, "transition-name"); - obs_source_t *transition = Utils::GetTransitionFromName(name); - if (transition) - { - obs_frontend_set_current_transition(transition); + bool success = Utils::SetTransitionByName(name); + + if (success) owner->SendOKResponse(); - - obs_source_release(transition); - } else - { owner->SendErrorResponse("requested transition does not exist"); - } } void WSRequestHandler::HandleSetTransitionDuration(WSRequestHandler *owner) @@ -746,6 +748,120 @@ void WSRequestHandler::HandleListProfiles(WSRequestHandler *owner) obs_data_array_release(profiles); } +void WSRequestHandler::HandleGetStudioModeStatus(WSRequestHandler *owner) +{ + bool previewActive = Utils::IsPreviewModeActive(); + + obs_data_t* response = obs_data_create(); + obs_data_set_bool(response, "studio-mode", previewActive); + + owner->SendOKResponse(response); + + obs_data_release(response); +} + +void WSRequestHandler::HandleGetPreviewScene(WSRequestHandler *owner) +{ + if (!Utils::IsPreviewModeActive()) + { + owner->SendErrorResponse("studio mode not enabled"); + return; + } + + obs_scene_t* preview_scene = Utils::GetPreviewScene(); + obs_source_t* source = obs_scene_get_source(preview_scene); + const char *name = obs_source_get_name(source); + + obs_data_array_t *scene_items = Utils::GetSceneItems(source); + + obs_data_t *data = obs_data_create(); + obs_data_set_string(data, "name", name); + obs_data_set_array(data, "sources", scene_items); + + owner->SendOKResponse(data); + + obs_data_release(data); + obs_data_array_release(scene_items); + + obs_scene_release(preview_scene); +} + +void WSRequestHandler::HandleSetPreviewScene(WSRequestHandler *owner) +{ + if (!Utils::IsPreviewModeActive()) + { + owner->SendErrorResponse("studio mode not enabled"); + return; + } + + if (!obs_data_has_user_value(owner->_requestData, "scene-name")) + { + owner->SendErrorResponse("invalid request parameters"); + return; + } + + const char* scene_name = obs_data_get_string(owner->_requestData, "scene-name"); + Utils::SetPreviewScene(scene_name); + + owner->SendOKResponse(); +} + +void WSRequestHandler::HandleTransitionToProgram(WSRequestHandler *owner) +{ + if (!Utils::IsPreviewModeActive()) + { + owner->SendErrorResponse("studio mode not enabled"); + return; + } + + if (obs_data_has_user_value(owner->_requestData, "with-transition")) + { + obs_data_t* transitionInfo = obs_data_get_obj(owner->_requestData, "with-transition"); + + if (obs_data_has_user_value(transitionInfo, "name")) + { + const char* transitionName = obs_data_get_string(transitionInfo, "name"); + bool success = Utils::SetTransitionByName(transitionName); + + if (!success) + { + owner->SendErrorResponse("specified transition doesn't exist"); + obs_data_release(transitionInfo); + return; + } + } + + if (obs_data_has_user_value(transitionInfo, "duration")) + { + int transitionDuration = obs_data_get_int(transitionInfo, "duration"); + Utils::SetTransitionDuration(transitionDuration); + } + + obs_data_release(transitionInfo); + } + + Utils::TransitionToProgram(); + owner->SendOKResponse(); +} + +void WSRequestHandler::HandleEnableStudioMode(WSRequestHandler *owner) +{ + Utils::EnablePreviewMode(); + owner->SendOKResponse(); +} + +void WSRequestHandler::HandleDisableStudioMode(WSRequestHandler *owner) +{ + Utils::DisablePreviewMode(); + owner->SendOKResponse(); +} + +void WSRequestHandler::HandleToggleStudioMode(WSRequestHandler *owner) +{ + Utils::TogglePreviewMode(); + owner->SendOKResponse(); +} + void WSRequestHandler::ErrNotImplemented(WSRequestHandler *owner) { owner->SendErrorResponse("not implemented"); diff --git a/WSRequestHandler.h b/WSRequestHandler.h index afe696db..c1c67680 100644 --- a/WSRequestHandler.h +++ b/WSRequestHandler.h @@ -87,6 +87,14 @@ class WSRequestHandler : public QObject static void HandleSetTransitionDuration(WSRequestHandler *owner); static void HandleGetTransitionDuration(WSRequestHandler *owner); + + static void HandleGetStudioModeStatus(WSRequestHandler *owner); + static void HandleGetPreviewScene(WSRequestHandler *owner); + static void HandleSetPreviewScene(WSRequestHandler *owner); + static void HandleTransitionToProgram(WSRequestHandler *owner); + static void HandleEnableStudioMode(WSRequestHandler *owner); + static void HandleDisableStudioMode(WSRequestHandler *owner); + static void HandleToggleStudioMode(WSRequestHandler *owner); }; #endif // WSPROTOCOL_H