diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..d9e2a546
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "deps/asio"]
+ path = deps/asio
+ url = https://github.com/chriskohlhoff/asio.git
+[submodule "deps/websocketpp"]
+ path = deps/websocketpp
+ url = https://github.com/zaphoyd/websocketpp.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 46364b6a..d5b856b0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,7 @@
cmake_minimum_required(VERSION 3.5)
project(advanced-scene-switcher)
+add_definitions(-DASIO_STANDALONE)
# generate version info
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
@@ -52,6 +53,11 @@ if(UNIX AND NOT APPLE)
include_directories(${X11_INCLUDE_DIR})
endif()
+include_directories(
+ "${CMAKE_CURRENT_SOURCE_DIR}/deps/asio/asio/include"
+ "${CMAKE_CURRENT_SOURCE_DIR}/deps/websocketpp"
+ )
+
set(advanced-scene-switcher_HEADERS
${advanced-scene-switcher_HEADERS}
src/headers/advanced-scene-switcher.hpp
@@ -66,6 +72,7 @@ set(advanced-scene-switcher_HEADERS
src/headers/switch-file.hpp
src/headers/switch-idle.hpp
src/headers/switch-media.hpp
+ src/headers/switch-network.hpp
src/headers/switch-pause.hpp
src/headers/switch-random.hpp
src/headers/switch-screen-region.hpp
@@ -94,6 +101,7 @@ set(advanced-scene-switcher_SOURCES
src/switch-file.cpp
src/switch-window.cpp
src/switch-media.cpp
+ src/switch-network.cpp
src/hotkey.cpp
src/general.cpp
src/switch-pause.cpp
@@ -113,6 +121,8 @@ set(advanced-scene-switcher_UI
)
if(WIN32)
+ add_definitions(-D_WEBSOCKETPP_CPP11_STL_)
+
set(advanced-scene-switcher_PLATFORM_SOURCES
src/win/advanced-scene-switcher-win.cpp)
elseif(APPLE)
diff --git a/data/locale/de-DE.ini b/data/locale/de-DE.ini
index 7d508fa3..8fa07bf9 100644
--- a/data/locale/de-DE.ini
+++ b/data/locale/de-DE.ini
@@ -215,6 +215,28 @@ AdvSceneSwitcher.videoTab.ignoreInactiveSource="außer Video Quelle ist inaktiv"
AdvSceneSwitcher.videoTab.entry="Wenn {{videoSources}} {{condition}} {{filePath}} {{browseButton}} für {{duration}} wechsle zu {{scenes}} mit {{transitions}} {{ignoreInactiveSource}}"
AdvSceneSwitcher.videoTab.help="
Dieser Tab ermöglicht es basierend auf der Videoausgabe von Quellen scenen zu wechseln.
Für eine noch bessere Implementierung dieser Funktionalität siehe Pixel Match Switcher.
Klicke auf das markierte Plus Symbol, um einen neuen Eintrag hinzuzufügen..
"
+; Network Tab
+AdvSceneSwitcher.networkTab.title="Netzwerk"
+AdvSceneSwitcher.networkTab.warning="Die Verwendung des Servers außerhalb eines lokalen Netzwerks kann dazu führen, dass die aktive Szene von dritten Personen ausgelesen werden kann."
+AdvSceneSwitcher.networkTab.server="Server starten (Sendet Szenenwechselnachrichten zu allen verbundenen Clients)"
+AdvSceneSwitcher.networkTab.server.port="Port"
+AdvSceneSwitcher.networkTab.server.lockToIPv4="Nur IPv4 verwenden (deaktiviert IPv6)"
+AdvSceneSwitcher.networkTab.server.restrictSendToAutomatedSwitches="Sende nur Szenenwechselnachrichten für automatisierte Szenenwechsel"
+AdvSceneSwitcher.networkTab.startFailed.message="Der WebSocket-Server konnte nicht gestartet werden, mögliche Gründe:\n - Der TCP-Port %1 wird möglicherweise gerade von einem anderen Programm verwendet. Versuchen Sie einen anderen Port in den Websocket-Servereinstellungen zu setzen oder alle Programme zu beenden, die den Port möglicherweise verwenden.\n - Fehler: %2"
+AdvSceneSwitcher.networkTab.server.status.currentStatus="Aktueller Status"
+AdvSceneSwitcher.networkTab.server.status.notRunning="Nicht gestartet"
+AdvSceneSwitcher.networkTab.server.status.starting="Startet"
+AdvSceneSwitcher.networkTab.server.status.running="Server läuft"
+AdvSceneSwitcher.networkTab.server.restart="Server neu starten"
+AdvSceneSwitcher.networkTab.client="Client starten (Empfängt Szenenwechselnachrichten )"
+AdvSceneSwitcher.networkTab.client.address="Hostname oder IP-Adresse"
+AdvSceneSwitcher.networkTab.client.port="Port"
+AdvSceneSwitcher.networkTab.client.status.currentStatus="Aktueller status"
+AdvSceneSwitcher.networkTab.client.status.disconnected="Verbindung getrennt"
+AdvSceneSwitcher.networkTab.client.status.connecting="Verbinde"
+AdvSceneSwitcher.networkTab.client.status.connected="Verbunden"
+AdvSceneSwitcher.networkTab.client.reconnect="Erneutes Verbinden erzwingen"
+
; Scene Group Tab
AdvSceneSwitcher.sceneGroupTab.title="Szenengruppe"
AdvSceneSwitcher.sceneGroupTab.list="Szenengruppen"
diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini
index 05f410f2..1286352a 100644
--- a/data/locale/en-US.ini
+++ b/data/locale/en-US.ini
@@ -215,6 +215,28 @@ AdvSceneSwitcher.videoTab.ignoreInactiveSource="unless source is inactive"
AdvSceneSwitcher.videoTab.entry="When {{videoSources}} {{condition}} {{filePath}} {{browseButton}} for {{duration}} switch to {{scenes}} using {{transitions}} {{ignoreInactiveSource}}"
AdvSceneSwitcher.videoTab.help="This tab will allow you to switch scenes based on the current video output of selected sources.
Make sure to check out Pixel Match Switcher for an even better implementation of this functionality.
Click on the highlighted plus symbol to continue.
"
+; Network Tab
+AdvSceneSwitcher.networkTab.title="Network"
+AdvSceneSwitcher.networkTab.warning="Running the server outside of a local network will allow third parties to read the active scene."
+AdvSceneSwitcher.networkTab.server="Start server (Sends scene switch messages to all connected clients)"
+AdvSceneSwitcher.networkTab.server.port="Port"
+AdvSceneSwitcher.networkTab.server.lockToIPv4="Lock server to only using IPv4"
+AdvSceneSwitcher.networkTab.server.restrictSendToAutomatedSwitches="Only send messages for automated scene switches"
+AdvSceneSwitcher.networkTab.startFailed.message="The WebSockets server failed to start, maybe because:\n - TCP port %1 may currently be in use elsewhere on this system, possibly by another application. Try setting a different TCP port in the WebSocket server settings, or stop any application that could be using this port.\n - Error message: %2"
+AdvSceneSwitcher.networkTab.server.status.currentStatus="Current status"
+AdvSceneSwitcher.networkTab.server.status.notRunning="Not running"
+AdvSceneSwitcher.networkTab.server.status.starting="Starting"
+AdvSceneSwitcher.networkTab.server.status.running="Running"
+AdvSceneSwitcher.networkTab.server.restart="Restart server"
+AdvSceneSwitcher.networkTab.client="Start client (Receives scene switches messages)"
+AdvSceneSwitcher.networkTab.client.address="Hostname or IP address"
+AdvSceneSwitcher.networkTab.client.port="Port"
+AdvSceneSwitcher.networkTab.client.status.currentStatus="Current status"
+AdvSceneSwitcher.networkTab.client.status.disconnected="Disconnected"
+AdvSceneSwitcher.networkTab.client.status.connecting="Connecting"
+AdvSceneSwitcher.networkTab.client.status.connected="Connected"
+AdvSceneSwitcher.networkTab.client.reconnect="Force reconnect"
+
; Scene Group Tab
AdvSceneSwitcher.sceneGroupTab.title="Scene Group"
AdvSceneSwitcher.sceneGroupTab.list="Scene Groups"
diff --git a/deps/asio b/deps/asio
new file mode 160000
index 00000000..93f87fbd
--- /dev/null
+++ b/deps/asio
@@ -0,0 +1 @@
+Subproject commit 93f87fbdbf6111f9ef25ac56bb7e495b7ce83089
diff --git a/deps/websocketpp b/deps/websocketpp
new file mode 160000
index 00000000..1b11fd30
--- /dev/null
+++ b/deps/websocketpp
@@ -0,0 +1 @@
+Subproject commit 1b11fd301531e6df35a6107c1e8665b1e77a2d8e
diff --git a/forms/advanced-scene-switcher.ui b/forms/advanced-scene-switcher.ui
index 0092e0e9..da114138 100644
--- a/forms/advanced-scene-switcher.ui
+++ b/forms/advanced-scene-switcher.ui
@@ -2828,7 +2828,7 @@
-
-
+
Qt::Vertical
@@ -3272,6 +3272,184 @@
+
+
+ AdvSceneSwitcher.networkTab.title
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.warning
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.server
+
+
+ true
+
+
+ false
+
+
+
-
+
+
-
+
+
+ 1024
+
+
+ 65535
+
+
+ 55555
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.server.port
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.server.lockToIPv4
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.server.status.currentStatus
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.server.status.notRunning
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.server.restrictSendToAutomatedSwitches
+
+
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.server.restart
+
+
+
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.client
+
+
+ true
+
+
+ false
+
+
+
-
+
+
-
+
+
+ AdvSceneSwitcher.networkTab.client.port
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.client.address
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ 1024
+
+
+ 65535
+
+
+ 55555
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.client.status.disconnected
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.client.status.currentStatus
+
+
+
+
+
+ -
+
+
+ AdvSceneSwitcher.networkTab.client.reconnect
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
AdvSceneSwitcher.sceneGroupTab.title
diff --git a/src/advanced-scene-switcher.cpp b/src/advanced-scene-switcher.cpp
index 0bd006dc..3b1a75b4 100644
--- a/src/advanced-scene-switcher.cpp
+++ b/src/advanced-scene-switcher.cpp
@@ -60,6 +60,7 @@ void AdvSceneSwitcher::loadUI()
setupTimeTab();
setupAudioTab();
setupVideoTab();
+ setupNetworkTab();
setupSceneGroupTab();
setupTriggerTab();
@@ -546,6 +547,9 @@ void SwitcherData::Thread()
lock.unlock();
if (match) {
+ if (networkConfig.ServerEnabled) {
+ server.sendMessage(scene, transition);
+ }
switchScene(scene, transition,
tansitionOverrideOverride);
}
@@ -652,6 +656,15 @@ void SwitcherData::Start()
// Will be overwritten quickly but might be useful
writeToStatusFile("Advanced Scene Switcher running");
}
+
+ if (networkConfig.ServerEnabled) {
+ server.start(networkConfig.ServerPort,
+ networkConfig.LockToIPv4);
+ }
+
+ if (networkConfig.ClientEnabled) {
+ client.connect(networkConfig.GetClientUri());
+ }
}
void SwitcherData::Stop()
@@ -666,6 +679,9 @@ void SwitcherData::Stop()
writeToStatusFile("Advanced Scene Switcher stopped");
}
+
+ server.stop();
+ client.disconnect();
}
bool SwitcherData::sceneChangedDuringWait()
@@ -714,6 +730,11 @@ void handleSceneChange(SwitcherData *s)
s->checkTriggers();
s->checkDefaultSceneTransitions();
+
+ if (switcher->networkConfig.ServerEnabled &&
+ switcher->networkConfig.SendAll) {
+ switcher->server.sendMessage(ws, nullptr);
+ }
}
void setLiveTime(SwitcherData *s)
diff --git a/src/general.cpp b/src/general.cpp
index ffc4201c..d3ebd247 100644
--- a/src/general.cpp
+++ b/src/general.cpp
@@ -4,7 +4,7 @@
#include "headers/utility.hpp"
#include "headers/version.h"
-constexpr auto tab_count = 16;
+constexpr auto tab_count = 17;
QMetaObject::Connection inactivePluse;
@@ -329,9 +329,12 @@ int findTabIndex(QTabWidget *tabWidget, int pos)
tabName = "videoTab";
break;
case 14:
- tabName = "sceneGroupTab";
+ tabName = "networkTab";
break;
case 15:
+ tabName = "sceneGroupTab";
+ break;
+ case 16:
tabName = "sceneTriggerTab";
break;
}
@@ -400,6 +403,7 @@ void SwitcherData::loadSettings(obs_data_t *obj)
loadTimeSwitches(obj);
loadAudioSwitches(obj);
loadVideoSwitches(obj);
+ loadNetworkSettings(obj);
loadSceneTriggers(obj);
loadGeneralSettings(obj);
loadHotkeys(obj);
@@ -425,6 +429,7 @@ void SwitcherData::saveSettings(obs_data_t *obj)
saveTimeSwitches(obj);
saveAudioSwitches(obj);
saveVideoSwitches(obj);
+ saveNetworkSwitches(obj);
saveSceneTriggers(obj);
saveGeneralSettings(obj);
saveHotkeys(obj);
@@ -497,8 +502,9 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj)
obs_data_set_int(obj, "sequenceTabPos", switcher->tabOrder[11]);
obs_data_set_int(obj, "audioTabPos", switcher->tabOrder[12]);
obs_data_set_int(obj, "videoTabPos", switcher->tabOrder[13]);
- obs_data_set_int(obj, "sceneGroupTabPos", switcher->tabOrder[14]);
- obs_data_set_int(obj, "triggerTabPos", switcher->tabOrder[15]);
+ obs_data_set_int(obj, "networkTabPos", switcher->tabOrder[14]);
+ obs_data_set_int(obj, "sceneGroupTabPos", switcher->tabOrder[15]);
+ obs_data_set_int(obj, "triggerTabPos", switcher->tabOrder[16]);
}
void SwitcherData::loadGeneralSettings(obs_data_t *obj)
@@ -594,8 +600,9 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
obs_data_set_default_int(obj, "sequenceTabPos", 11);
obs_data_set_default_int(obj, "audioTabPos", 12);
obs_data_set_default_int(obj, "videoTabPos", 13);
- obs_data_set_default_int(obj, "sceneGroupTabPos", 14);
- obs_data_set_default_int(obj, "triggerTabPos", 15);
+ obs_data_set_default_int(obj, "networkTabPos", 14);
+ obs_data_set_default_int(obj, "sceneGroupTabPos", 15);
+ obs_data_set_default_int(obj, "triggerTabPos", 16);
switcher->tabOrder.emplace_back(
(int)(obs_data_get_int(obj, "generalTabPos")));
@@ -625,6 +632,8 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
(int)(obs_data_get_int(obj, "audioTabPos")));
switcher->tabOrder.emplace_back(
(int)(obs_data_get_int(obj, "videoTabPos")));
+ switcher->tabOrder.emplace_back(
+ (int)(obs_data_get_int(obj, "networkTabPos")));
switcher->tabOrder.emplace_back(
(int)(obs_data_get_int(obj, "sceneGroupTabPos")));
switcher->tabOrder.emplace_back(
diff --git a/src/headers/advanced-scene-switcher.hpp b/src/headers/advanced-scene-switcher.hpp
index a6f9f989..3810a139 100644
--- a/src/headers/advanced-scene-switcher.hpp
+++ b/src/headers/advanced-scene-switcher.hpp
@@ -60,6 +60,7 @@ public:
void setupSceneGroupTab();
void setupTriggerTab();
void setupVideoTab();
+ void setupNetworkTab();
void setTabOrder();
static bool DisplayMessage(QString msg, bool question = false);
@@ -212,6 +213,18 @@ public slots:
void on_videoDown_clicked();
void on_getScreenshot_clicked();
+ void on_serverSettings_toggled(bool on);
+ void on_serverPort_valueChanged(int value);
+ void on_lockToIPv4_stateChanged(int state);
+ void on_serverRestart_clicked();
+ void updateServerStatus();
+ void on_clientSettings_toggled(bool on);
+ void on_clientHostname_textChanged(const QString &text);
+ void on_clientPort_valueChanged(int value);
+ void on_restrictSend_stateChanged(int state);
+ void on_clientReconnect_clicked();
+ void updateClientStatus();
+
void on_sceneGroupAdd_clicked();
void on_sceneGroupRemove_clicked();
void on_sceneGroupUp_clicked();
diff --git a/src/headers/switch-network.hpp b/src/headers/switch-network.hpp
new file mode 100644
index 00000000..82c5ee52
--- /dev/null
+++ b/src/headers/switch-network.hpp
@@ -0,0 +1,111 @@
+/*
+Most of this code is based on https://github.com/Palakis/obs-websocket
+*/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+using websocketpp::connection_hdl;
+typedef websocketpp::server server;
+typedef websocketpp::client client;
+
+class NetworkConfig {
+public:
+ NetworkConfig();
+ void Load(obs_data_t *obj);
+ void Save(obs_data_t *obj);
+ void SetDefaults(obs_data_t *obj);
+
+ std::string GetClientUri();
+
+ // Server
+ bool ServerEnabled;
+ uint64_t ServerPort;
+ bool LockToIPv4;
+
+ // Client
+ bool ClientEnabled;
+ std::string Address;
+ uint64_t ClientPort;
+ bool SendAll;
+};
+
+class WSServer : public QObject {
+ Q_OBJECT
+
+public:
+ explicit WSServer();
+ virtual ~WSServer();
+ void start(quint16 port, bool lockToIPv4);
+ void stop();
+ void sendMessage(OBSWeakSource scene, OBSWeakSource transition);
+ QThreadPool *threadPool() { return &_threadPool; }
+
+private:
+ void onOpen(connection_hdl hdl);
+ void onMessage(connection_hdl hdl, server::message_ptr message);
+ void onClose(connection_hdl hdl);
+
+ QString getRemoteEndpoint(connection_hdl hdl);
+
+ server _server;
+ quint16 _serverPort;
+ bool _lockToIPv4;
+ std::set> _connections;
+ QMutex _clMutex;
+ QThreadPool _threadPool;
+};
+
+enum class ServerStatus {
+ NOT_RUNNING,
+ STARTING,
+ RUNNING,
+};
+
+class WSClient : public QObject {
+ Q_OBJECT
+
+public:
+ explicit WSClient();
+ virtual ~WSClient();
+ void connect(std::string uri);
+ void disconnect();
+ std::string getFail() { return _failMsg; }
+
+private:
+ void onOpen(connection_hdl hdl);
+ void onFail(connection_hdl hdl);
+ void onMessage(connection_hdl hdl, client::message_ptr message);
+ void onClose(connection_hdl hdl);
+ void connectThread();
+
+ client _client;
+ std::string _uri;
+ connection_hdl _connection;
+ std::thread _thread;
+ bool _retry = false;
+ std::atomic_bool _connected = {false};
+ std::mutex _waitMtx;
+ std::condition_variable _cv;
+ std::string _failMsg;
+};
+
+enum class ClientStatus {
+ DISCONNECTED,
+ CONNECTING,
+ CONNECTED,
+ FAIL,
+};
diff --git a/src/headers/switcher-data-structs.hpp b/src/headers/switcher-data-structs.hpp
index deb0bb22..56bc8e69 100644
--- a/src/headers/switcher-data-structs.hpp
+++ b/src/headers/switcher-data-structs.hpp
@@ -22,6 +22,7 @@
#include "switch-window.hpp"
#include "switch-sequence.hpp"
#include "switch-video.hpp"
+#include "switch-network.hpp"
constexpr auto default_interval = 300;
constexpr auto previous_scene_name = "Previous Scene";
@@ -115,6 +116,12 @@ struct SwitcherData {
std::deque audioSwitches;
AudioSwitchFallback audioFallback;
+ WSServer server;
+ ServerStatus serverStatus = ServerStatus::NOT_RUNNING;
+ WSClient client;
+ ClientStatus clientStatus = ClientStatus::DISCONNECTED;
+ NetworkConfig networkConfig;
+
std::deque videoSwitches;
std::deque sceneGroups;
@@ -226,6 +233,7 @@ struct SwitcherData {
void saveSceneGroups(obs_data_t *obj);
void saveSceneTriggers(obs_data_t *obj);
void saveVideoSwitches(obs_data_t *obj);
+ void saveNetworkSwitches(obs_data_t *obj);
void saveGeneralSettings(obs_data_t *obj);
void saveHotkeys(obs_data_t *obj);
void saveVersion(obs_data_t *obj, std::string currentVersion);
@@ -247,6 +255,7 @@ struct SwitcherData {
void loadSceneGroups(obs_data_t *obj);
void loadSceneTriggers(obs_data_t *obj);
void loadVideoSwitches(obs_data_t *obj);
+ void loadNetworkSettings(obs_data_t *obj);
void loadGeneralSettings(obs_data_t *obj);
void loadHotkeys(obs_data_t *obj);
diff --git a/src/switch-network.cpp b/src/switch-network.cpp
new file mode 100644
index 00000000..16fd9393
--- /dev/null
+++ b/src/switch-network.cpp
@@ -0,0 +1,633 @@
+/*
+Most of this code is based on https://github.com/Palakis/obs-websocket
+*/
+
+#include
+#include
+#include
+#include
+
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+#define PARAM_SERVER_ENABLE "ServerEnabled"
+#define PARAM_SERVER_PORT "ServerPort"
+#define PARAM_LOCKTOIPV4 "LockToIPv4"
+
+#define PARAM_CLIENT_ENABLE "ClientEnabled"
+#define PARAM_CLIENT_PORT "ClientPort"
+#define PARAM_ADDRESS "Address"
+#define PARAM_CLIENT_SENDALL "SendAll"
+
+#define RECONNECT_DELAY 10
+
+#define SCENE_ENTRY "scene"
+#define TRANSITION_ENTRY "transition"
+
+using websocketpp::lib::placeholders::_1;
+using websocketpp::lib::placeholders::_2;
+using websocketpp::lib::bind;
+
+NetworkConfig::NetworkConfig()
+ : ServerEnabled(false),
+ ServerPort(55555),
+ LockToIPv4(false),
+ ClientEnabled(false),
+ Address(""),
+ ClientPort(55555),
+ SendAll(true)
+{
+}
+
+void NetworkConfig::Load(obs_data_t *obj)
+{
+ SetDefaults(obj);
+
+ ServerEnabled = obs_data_get_bool(obj, PARAM_SERVER_ENABLE);
+ ServerPort = obs_data_get_int(obj, PARAM_SERVER_PORT);
+ LockToIPv4 = obs_data_get_bool(obj, PARAM_LOCKTOIPV4);
+
+ ClientEnabled = obs_data_get_bool(obj, PARAM_CLIENT_ENABLE);
+ Address = obs_data_get_string(obj, PARAM_ADDRESS);
+ ClientPort = obs_data_get_int(obj, PARAM_CLIENT_PORT);
+ SendAll = obs_data_get_bool(obj, PARAM_CLIENT_SENDALL);
+}
+
+void NetworkConfig::Save(obs_data_t *obj)
+{
+ obs_data_set_bool(obj, PARAM_SERVER_ENABLE, ServerEnabled);
+ obs_data_set_int(obj, PARAM_SERVER_PORT, ServerPort);
+ obs_data_set_bool(obj, PARAM_LOCKTOIPV4, LockToIPv4);
+
+ obs_data_set_bool(obj, PARAM_CLIENT_ENABLE, ClientEnabled);
+ obs_data_set_string(obj, PARAM_ADDRESS, Address.c_str());
+ obs_data_set_int(obj, PARAM_CLIENT_PORT, ClientPort);
+ obs_data_set_bool(obj, PARAM_CLIENT_SENDALL, SendAll);
+}
+
+void NetworkConfig::SetDefaults(obs_data_t *obj)
+{
+ obs_data_set_default_bool(obj, PARAM_SERVER_ENABLE, ServerEnabled);
+ obs_data_set_default_int(obj, PARAM_SERVER_PORT, ServerPort);
+ obs_data_set_default_bool(obj, PARAM_LOCKTOIPV4, LockToIPv4);
+
+ obs_data_set_default_bool(obj, PARAM_CLIENT_ENABLE, ClientEnabled);
+ obs_data_set_default_string(obj, PARAM_ADDRESS, Address.c_str());
+ obs_data_set_default_int(obj, PARAM_CLIENT_PORT, ClientPort);
+ obs_data_set_default_bool(obj, PARAM_CLIENT_SENDALL, SendAll);
+}
+
+std::string NetworkConfig::GetClientUri()
+{
+ return "ws://" + Address + ":" + std::to_string(ClientPort);
+}
+
+WSServer::WSServer()
+ : QObject(nullptr), _connections(), _clMutex(QMutex::Recursive)
+{
+ _server.get_alog().clear_channels(
+ websocketpp::log::alevel::frame_header |
+ websocketpp::log::alevel::frame_payload |
+ websocketpp::log::alevel::control);
+ _server.init_asio();
+#ifndef _WIN32
+ _server.set_reuse_addr(true);
+#endif
+
+ _server.set_open_handler(bind(&WSServer::onOpen, this, ::_1));
+ _server.set_close_handler(bind(&WSServer::onClose, this, ::_1));
+ _server.set_message_handler(
+ bind(&WSServer::onMessage, this, ::_1, ::_2));
+}
+
+WSServer::~WSServer()
+{
+ stop();
+}
+
+void WSServer::start(quint16 port, bool lockToIPv4)
+{
+ if (_server.is_listening() &&
+ (port == _serverPort && _lockToIPv4 == lockToIPv4)) {
+ blog(LOG_INFO,
+ "WSServer::start: server already on this port and protocol mode. no restart needed");
+ return;
+ }
+
+ if (_server.is_listening()) {
+ stop();
+ }
+
+ _server.reset();
+
+ _serverPort = port;
+ _lockToIPv4 = lockToIPv4;
+
+ websocketpp::lib::error_code errorCode;
+ if (lockToIPv4) {
+ blog(LOG_INFO, "WSServer::start: Locked to IPv4 bindings");
+ _server.listen(websocketpp::lib::asio::ip::tcp::v4(),
+ _serverPort, errorCode);
+ } else {
+ blog(LOG_INFO, "WSServer::start: Not locked to IPv4 bindings");
+ _server.listen(_serverPort, errorCode);
+ }
+
+ if (errorCode) {
+ std::string errorCodeMessage = errorCode.message();
+ blog(LOG_INFO, "server: listen failed: %s",
+ errorCodeMessage.c_str());
+
+ obs_frontend_push_ui_translation(obs_module_get_string);
+ QString errorTitle = tr("AdvSceneSwitcher.windowTitle");
+ QString errorMessage =
+ tr("AdvSceneSwitcher.networkTab.startFailed.message")
+ .arg(_serverPort)
+ .arg(errorCodeMessage.c_str());
+ obs_frontend_pop_ui_translation();
+
+ QMainWindow *mainWindow = reinterpret_cast(
+ obs_frontend_get_main_window());
+ QMessageBox::warning(mainWindow, errorTitle, errorMessage);
+
+ return;
+ }
+ switcher->serverStatus = ServerStatus::STARTING;
+
+ _server.start_accept();
+
+ QtConcurrent::run([=]() {
+ blog(LOG_INFO, "WSServer::start: io thread started");
+ _server.run();
+ blog(LOG_INFO, "WSServer::start: io thread exited");
+ });
+
+ switcher->serverStatus = ServerStatus::RUNNING;
+ blog(LOG_INFO,
+ "WSServer::start: server started successfully on port %d",
+ _serverPort);
+}
+
+void WSServer::stop()
+{
+ if (!_server.is_listening()) {
+ return;
+ }
+
+ _server.stop_listening();
+ for (connection_hdl hdl : _connections) {
+ websocketpp::lib::error_code ec;
+ _server.close(hdl, websocketpp::close::status::going_away,
+ "Server stopping", ec);
+ }
+
+ _threadPool.waitForDone();
+
+ while (_connections.size() > 0) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+
+ switcher->serverStatus = ServerStatus::NOT_RUNNING;
+ blog(LOG_INFO, "server stopped successfully");
+}
+
+void WSServer::sendMessage(OBSWeakSource scene, OBSWeakSource transition)
+{
+ if (!scene) {
+ return;
+ }
+
+ OBSData data = obs_data_create();
+ obs_data_set_string(data, SCENE_ENTRY,
+ GetWeakSourceName(scene).c_str());
+ obs_data_set_string(data, TRANSITION_ENTRY,
+ GetWeakSourceName(transition).c_str());
+ std::string message = obs_data_get_json(data);
+ obs_data_release(data);
+
+ for (connection_hdl hdl : _connections) {
+ websocketpp::lib::error_code ec;
+ _server.send(hdl, message, websocketpp::frame::opcode::text,
+ ec);
+ if (ec) {
+ std::string errorCodeMessage = ec.message();
+ blog(LOG_INFO, "server: send failed: %s",
+ errorCodeMessage.c_str());
+ }
+ }
+
+ if (switcher->verbose) {
+ blog(LOG_INFO, "server sent message:\n%s", message.c_str());
+ }
+}
+
+void WSServer::onOpen(connection_hdl hdl)
+{
+ QMutexLocker locker(&_clMutex);
+ _connections.insert(hdl);
+ locker.unlock();
+
+ QString clientIp = getRemoteEndpoint(hdl);
+ blog(LOG_INFO, "new client connection from %s",
+ clientIp.toUtf8().constData());
+}
+
+std::string processMessage(std::string payload)
+{
+ auto config = switcher->networkConfig;
+ std::string msgContainer(payload);
+ const char *msg = msgContainer.c_str();
+
+ OBSData data = obs_data_create_from_json(msg);
+ if (!data) {
+ blog(LOG_ERROR, "invalid JSON payload received for '%s'", msg);
+ return "invalid JSON payload";
+ }
+
+ if (!obs_data_has_user_value(data, SCENE_ENTRY) ||
+ !obs_data_has_user_value(data, TRANSITION_ENTRY)) {
+ return "missing request parameters";
+ }
+
+ std::string sceneName = obs_data_get_string(data, SCENE_ENTRY);
+ std::string transitionName =
+ obs_data_get_string(data, TRANSITION_ENTRY);
+
+ obs_data_release(data);
+
+ auto scene = GetWeakSourceByName(sceneName.c_str());
+ if (!scene) {
+ return "ignoring request - unknown scene '" + sceneName + "'";
+ }
+
+ std::string ret = "message ok";
+
+ auto transition = GetWeakTransitionByName(transitionName.c_str());
+ if (switcher->verbose && !transition) {
+ ret += " - ignoring invalid transition: '" + transitionName +
+ "'";
+ }
+
+ switchScene(scene, transition, switcher->tansitionOverrideOverride);
+ return ret;
+}
+
+void WSServer::onMessage(connection_hdl hdl, server::message_ptr message)
+{
+ UNUSED_PARAMETER(hdl);
+
+ auto opcode = message->get_opcode();
+ if (opcode != websocketpp::frame::opcode::text) {
+ return;
+ }
+
+ QtConcurrent::run(&_threadPool, [=]() {
+ if (message->get_payload() != "message ok") {
+ blog(LOG_WARNING, "received response: %s",
+ message->get_payload().c_str());
+ }
+ });
+}
+
+void WSServer::onClose(connection_hdl hdl)
+{
+ QMutexLocker locker(&_clMutex);
+ _connections.erase(hdl);
+ locker.unlock();
+
+ auto conn = _server.get_con_from_hdl(hdl);
+ auto localCloseCode = conn->get_local_close_code();
+
+ if (localCloseCode != websocketpp::close::status::going_away) {
+ QString clientIp = getRemoteEndpoint(hdl);
+ blog(LOG_INFO, "client %s disconnected",
+ clientIp.toUtf8().constData());
+ }
+}
+
+QString WSServer::getRemoteEndpoint(connection_hdl hdl)
+{
+ auto conn = _server.get_con_from_hdl(hdl);
+ return QString::fromStdString(conn->get_remote_endpoint());
+}
+
+WSClient::WSClient() : QObject(nullptr)
+{
+ _client.get_alog().clear_channels(
+ websocketpp::log::alevel::frame_header |
+ websocketpp::log::alevel::frame_payload |
+ websocketpp::log::alevel::control);
+ _client.init_asio();
+#ifndef _WIN32
+ _client.set_reuse_addr(true);
+#endif
+
+ _client.set_open_handler(bind(&WSClient::onOpen, this, ::_1));
+ _client.set_fail_handler(bind(&WSClient::onFail, this, ::_1));
+ _client.set_message_handler(
+ bind(&WSClient::onMessage, this, ::_1, ::_2));
+ _client.set_close_handler(bind(&WSClient::onClose, this, ::_1));
+}
+
+WSClient::~WSClient()
+{
+ disconnect();
+}
+
+void WSClient::connectThread()
+{
+ while (_retry) {
+ _client.reset();
+ switcher->clientStatus = ClientStatus::CONNECTING;
+ // Create a connection to the given URI and queue it for connection once
+ // the event loop starts
+ websocketpp::lib::error_code ec;
+ client::connection_ptr con = _client.get_connection(_uri, ec);
+ if (ec) {
+ _failMsg = ec.message();
+ blog(LOG_INFO, "client: connect failed: %s",
+ _failMsg.c_str());
+ switcher->clientStatus = ClientStatus::FAIL;
+ } else {
+ _client.connect(con);
+ _connection = connection_hdl(con);
+
+ // Start the ASIO io_service run loop
+ blog(LOG_INFO, "WSClient::connect: io thread started");
+ _connected = true;
+ _client.run();
+ _connected = false;
+ blog(LOG_INFO, "WSClient::connect: io thread exited");
+ }
+
+ if (_retry) {
+ std::unique_lock lck(_waitMtx);
+ blog(LOG_INFO,
+ "trying to reconnect to %s in %d seconds.",
+ _uri.c_str(), RECONNECT_DELAY);
+ _cv.wait_for(lck,
+ std::chrono::seconds(RECONNECT_DELAY));
+ }
+ }
+}
+
+void WSClient::connect(std::string uri)
+{
+ disconnect();
+ _uri = uri;
+ _retry = true;
+
+ _thread = std::thread(&WSClient::connectThread, this);
+
+ switcher->clientStatus = ClientStatus::DISCONNECTED;
+ blog(LOG_INFO, "WSClient::connect: exited");
+}
+
+void WSClient::disconnect()
+{
+ _retry = false;
+ websocketpp::lib::error_code ec;
+ _client.close(_connection, websocketpp::close::status::normal,
+ "Client stopping", ec);
+
+ {
+ std::unique_lock waitLck(_waitMtx);
+ blog(LOG_INFO, "trying to reconnect to %s in %d seconds.",
+ _uri.c_str(), RECONNECT_DELAY);
+ _cv.notify_all();
+ }
+
+ while (_connected) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ _client.close(_connection, websocketpp::close::status::normal,
+ "Client stopping", ec);
+ }
+
+ if (_thread.joinable()) {
+ _thread.join();
+ }
+}
+
+void WSClient::onOpen(connection_hdl hdl)
+{
+ UNUSED_PARAMETER(hdl);
+ blog(LOG_INFO, "connection to %s opened", _uri.c_str());
+ switcher->clientStatus = ClientStatus::CONNECTED;
+}
+
+void WSClient::onFail(connection_hdl hdl)
+{
+ UNUSED_PARAMETER(hdl);
+ blog(LOG_INFO, "connection to %s failed", _uri.c_str());
+}
+
+void WSClient::onMessage(connection_hdl hdl, client::message_ptr message)
+{
+ auto opcode = message->get_opcode();
+ if (opcode != websocketpp::frame::opcode::text) {
+ return;
+ }
+
+ std::string payload = message->get_payload();
+ std::string response = processMessage(payload);
+ websocketpp::lib::error_code errorCode;
+ _client.send(hdl, response, websocketpp::frame::opcode::text,
+ errorCode);
+
+ if (errorCode) {
+ std::string errorCodeMessage = errorCode.message();
+ blog(LOG_INFO, "client(response): send failed: %s",
+ errorCodeMessage.c_str());
+ }
+
+ if (switcher->verbose) {
+ blog(LOG_INFO, "client sent message:\n%s", response.c_str());
+ }
+}
+
+void WSClient::onClose(connection_hdl hdl)
+{
+ UNUSED_PARAMETER(hdl);
+ blog(LOG_INFO, "client-connection to %s closed.", _uri.c_str());
+ switcher->clientStatus = ClientStatus::DISCONNECTED;
+}
+
+void SwitcherData::loadNetworkSettings(obs_data_t *obj)
+{
+ networkConfig.Load(obj);
+}
+
+void SwitcherData::saveNetworkSwitches(obs_data_t *obj)
+{
+ networkConfig.Save(obj);
+ if (!networkConfig.ServerEnabled) {
+ switcher->server.stop();
+ }
+}
+
+void AdvSceneSwitcher::setupNetworkTab()
+{
+ ui->serverSettings->setChecked(switcher->networkConfig.ServerEnabled);
+ ui->serverPort->setValue(switcher->networkConfig.ServerPort);
+ ui->lockToIPv4->setChecked(switcher->networkConfig.LockToIPv4);
+
+ ui->clientSettings->setChecked(switcher->networkConfig.ClientEnabled);
+ ui->clientHostname->setText(switcher->networkConfig.Address.c_str());
+ ui->clientPort->setValue(switcher->networkConfig.ClientPort);
+ ui->restrictSend->setChecked(!switcher->networkConfig.SendAll);
+
+ QTimer *statusTimer = new QTimer(this);
+ connect(statusTimer, SIGNAL(timeout()), this,
+ SLOT(updateClientStatus()));
+ connect(statusTimer, SIGNAL(timeout()), this,
+ SLOT(updateServerStatus()));
+ statusTimer->start(500);
+}
+
+void AdvSceneSwitcher::on_serverSettings_toggled(bool on)
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->networkConfig.ServerEnabled = on;
+ if (on) {
+ switcher->server.start(switcher->networkConfig.ServerPort,
+ switcher->networkConfig.LockToIPv4);
+ } else {
+ switcher->server.stop();
+ }
+}
+
+void AdvSceneSwitcher::on_serverPort_valueChanged(int value)
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->networkConfig.ServerPort = value;
+}
+
+void AdvSceneSwitcher::on_lockToIPv4_stateChanged(int state)
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->networkConfig.LockToIPv4 = state;
+}
+
+void AdvSceneSwitcher::on_serverRestart_clicked()
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->server.start(switcher->networkConfig.ServerPort,
+ switcher->networkConfig.LockToIPv4);
+}
+
+void AdvSceneSwitcher::updateServerStatus()
+{
+ switch (switcher->serverStatus) {
+ case ServerStatus::NOT_RUNNING:
+ ui->serverStatus->setText(obs_module_text(
+ "AdvSceneSwitcher.networkTab.server.status.notRunning"));
+ break;
+ case ServerStatus::STARTING:
+ ui->serverStatus->setText(obs_module_text(
+ "AdvSceneSwitcher.networkTab.server.status.starting"));
+ break;
+ case ServerStatus::RUNNING:
+ ui->serverStatus->setText(obs_module_text(
+ "AdvSceneSwitcher.networkTab.server.status.running"));
+ break;
+ default:
+ break;
+ }
+}
+
+void AdvSceneSwitcher::on_clientSettings_toggled(bool on)
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->networkConfig.ClientEnabled = on;
+
+ if (on) {
+ switcher->client.connect(
+ switcher->networkConfig.GetClientUri());
+ } else {
+ switcher->client.disconnect();
+ }
+}
+
+void AdvSceneSwitcher::on_clientHostname_textChanged(const QString &text)
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->networkConfig.Address = text.toUtf8().constData();
+}
+
+void AdvSceneSwitcher::on_clientPort_valueChanged(int value)
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->networkConfig.ClientPort = value;
+}
+
+void AdvSceneSwitcher::on_restrictSend_stateChanged(int state)
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->networkConfig.SendAll = !state;
+}
+
+void AdvSceneSwitcher::on_clientReconnect_clicked()
+{
+ if (loading) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switcher->client.connect(switcher->networkConfig.GetClientUri());
+}
+
+void AdvSceneSwitcher::updateClientStatus()
+{
+ switch (switcher->clientStatus) {
+ case ClientStatus::DISCONNECTED:
+ ui->clientStatus->setText(obs_module_text(
+ "AdvSceneSwitcher.networkTab.client.status.disconnected"));
+ break;
+ case ClientStatus::CONNECTING:
+ ui->clientStatus->setText(obs_module_text(
+ "AdvSceneSwitcher.networkTab.client.status.connecting"));
+ break;
+ case ClientStatus::CONNECTED:
+ ui->clientStatus->setText(obs_module_text(
+ "AdvSceneSwitcher.networkTab.client.status.connected"));
+ break;
+ case ClientStatus::FAIL:
+ ui->clientStatus->setText(QString("Error: ") +
+ switcher->client.getFail().c_str());
+ break;
+ default:
+ break;
+ }
+}
diff --git a/src/win/advanced-scene-switcher-win.cpp b/src/win/advanced-scene-switcher-win.cpp
index 02c18dee..2a085547 100644
--- a/src/win/advanced-scene-switcher-win.cpp
+++ b/src/win/advanced-scene-switcher-win.cpp
@@ -1,3 +1,4 @@
+#define WIN32_LEAN_AND_MEAN
#include
#include
#include "../headers/advanced-scene-switcher.hpp"