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