Compare commits

..

33 Commits
1.9 ... 1.11

Author SHA1 Message Date
WarmUpTill
dfccc4b16d Add network tab (#149)
The network tab allows you to open a web-socket or connect to web-socket to synchronize the active scene between two or more instances of OBS.
The functionality is mostly based on https://github.com/Palakis/obs-websocket.

An OBS instance running a server will send its currently active scene to all connected clients on scene change.
By default all scene switches will trigger a message to be sent to all clients, but you also have the option to limit sending these messages only when an automated scene change is triggered by the advanced scene switcher.
2021-03-24 20:51:22 +01:00
WarmUpTill
b0ef1fee71 Fix window check always testing for focus (#152) 2021-03-22 23:06:44 +01:00
WarmUpTill
1ef8ce4b4a Resolve Qt runtime warning by renaming function. (#151) 2021-03-22 22:31:29 +01:00
WarmUpTill
1bf4bff014 Merge pull request #150 from WarmUpTill/CI
CI: Improve format check and init submodules for build
2021-03-21 21:48:21 +01:00
WarmUpTill
4b6e00b2b2 CI: Init submodules for build 2021-03-21 21:39:11 +01:00
WarmUpTill
c99cbb0c0f CI: Improve format check.
The format check used to check too many files, which would be worsened
when introducing additional submodules.
2021-03-21 21:37:10 +01:00
WarmUpTill
2a87334d38 Init noMatchDelay to zero. (#148)
A non-zero value might be confusing for users which set up the plugin
for the first time.
2021-03-21 14:44:42 +01:00
WarmUpTill
1a4724ff21 Fix audio tab not supporting mono audio output. (#145) 2021-03-19 20:01:15 +01:00
WarmUpTill
59d8fb5d28 Fix MacOS build. (#146) 2021-03-19 19:43:58 +01:00
WarmUpTill
8823c05aeb Fix scene groups not being selected when using more than one entry on the Random tab. (#143) 2021-03-18 21:22:33 +01:00
WarmUpTill
2d0a52f8d1 Enable use of Scene Groups for Random tab (#141) 2021-03-17 20:00:06 +01:00
WarmUpTill
1273446ce5 Add option to configure default transition delay. (#140) 2021-03-17 18:21:42 +01:00
WarmUpTill
177ca4aea2 CI: Build on Ubuntu 18.04, use newer clang-format. (#139) 2021-03-13 17:02:04 +01:00
WarmUpTill
7526cbe1a3 Add option to check volume of 'inactive' audio sources. (#137) 2021-03-12 18:48:06 +01:00
WarmUpTill
e4bb034303 Add video tab (#131)
This tab will allow switching scenes based on the video output of sources.
2021-03-05 14:08:17 +01:00
WarmUpTill
78e14df620 Add german locale for extended sequences (#135) 2021-03-01 20:25:18 +01:00
WarmUpTill
34f07bfb07 Clean up extended sequence (#134)
* Update original switch widget status if changed in extend edit

* Clean up

* Add more comments

* Clean up unecessary code.

Sleep no longer happens inside the sequence switch check so we don't
need to stop / start the switcher after importing new entries or deleting
existing entries.
2021-02-28 15:38:33 +01:00
WarmUpTill
ee74d39424 Fixes and Minor rework (#132)
* Fix warning in scene trigger log functions.
* Use GetWeakSourceName() when saving scene trigger sources.
* Fix prioFuncsValid().
2021-02-26 19:21:02 +01:00
WarmUpTill
33667a1f23 Fix crash on advancing to unset scene for extended sequence (#130) 2021-02-24 18:53:52 +01:00
WarmUpTill
7947ad2608 Add option to extend scene sequences (#121)
Extending a scene sequence allows for more complex switching setups, but should also simplify some setups.
Sequences like "A -> B -> A -> C", which previously were only possible by creating a copy of A, can now be specified in a single entry.
To extend a sequence either select the sequence you want to modify and click the extend sequence button or simply double click the entry.
2021-02-24 18:19:23 +01:00
WarmUpTill
920c188213 Add scene trigger for starting and stopping the scene switcher (#129) 2021-02-23 21:00:25 +01:00
WarmUpTill
28fbff1adc Allow specifying transition when reading scenes from file. (#128)
The transition can be specified on the second line of the file.
For example:
Scene 1
Slide
2021-02-21 20:17:32 +01:00
WarmUpTill
326617aad7 Fix checkSwitchInfoFromFile ignoring file switch puase (#127) 2021-02-21 20:16:51 +01:00
WarmUpTill
499ba7dd21 Update the UI's status element if the status changed externally (#126) 2021-02-20 18:58:07 +01:00
WarmUpTill
f4902933ab Fix getCurrentScene() returning garbage if scene group has type random (#125) 2021-02-20 18:08:47 +01:00
WarmUpTill
9aa0b31bff Add scene trigger to unmute source (#124) 2021-02-20 18:08:35 +01:00
WarmUpTill
6d5086b971 Add option to select 'current transition' (#123)
* move addSelectionEntry to AdvSceneSwitcher class and allow select entry to be 'selectable'

Can be useful for implementing 'current transition' as an option or just allowing to select 'None' for screen region ignore

* Add option to select 'current transition'
2021-02-19 19:03:38 +01:00
WarmUpTill
f32f47db4b Install qtbase5-private-dev on Linux (#122) 2021-02-13 19:28:24 +01:00
WarmUpTill
74c0cd1736 Adjust sleep log message and comment for sceneChangedDuringWait() (#120) 2021-02-13 19:14:28 +01:00
WarmUpTill
a74a952b1d Fix log messages containing two '[adv-ss]' tags (#118) 2021-02-07 18:41:47 +01:00
WarmUpTill
5bbfef81e0 Improve scene trigger handling (#117) 2021-02-06 18:23:57 +01:00
WarmUpTill
c31d1cd975 Delay setting of default scene transitions (#116) 2021-02-06 18:23:41 +01:00
WarmUpTill
81d91a9139 resovle name conflict (#115) 2021-02-04 07:24:52 +01:00
40 changed files with 3208 additions and 363 deletions

View File

@@ -27,6 +27,7 @@ jobs:
uses: actions/checkout@v2.3.4
with:
path: UI/frontend-plugins/${{ env.PLUGIN_NAME }}
submodules: 'recursive'
- name: Fetch Git Tags
run: |
cd UI/frontend-plugins/${{ env.PLUGIN_NAME }}
@@ -75,8 +76,9 @@ jobs:
if: success()
shell: bash
run: |
curl -L -O https://s3-us-west-2.amazonaws.com/obs-nightly/Packages.pkg
sudo installer -pkg ./Packages.pkg -target /
curl -L -O http://s.sudre.free.fr/Software/files/Packages.dmg
sudo hdiutil attach ./Packages.dmg
sudo installer -pkg /Volumes/Packages\ 1.2.9/Install\ Packages.pkg -target /
- name: Package
if: success()
shell: bash
@@ -102,7 +104,7 @@ jobs:
path: ./nightly/*.pkg
ubuntu64:
name: 'Linux/Ubuntu 64-bit'
runs-on: [ubuntu-latest]
runs-on: [ubuntu-18.04]
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
@@ -113,6 +115,7 @@ jobs:
uses: actions/checkout@v2.3.4
with:
path: UI/frontend-plugins/${{ env.PLUGIN_NAME }}
submodules: 'recursive'
- name: Add plugin to obs cmake
shell: bash
run: echo "add_subdirectory(${{ env.PLUGIN_NAME }})" >> UI/frontend-plugins/CMakeLists.txt
@@ -161,6 +164,7 @@ jobs:
pkg-config \
python3-dev \
qtbase5-dev \
qtbase5-private-dev \
libqt5svg5-dev \
swig \
libxss-dev
@@ -209,6 +213,7 @@ jobs:
uses: actions/checkout@v2.3.4
with:
path: UI/frontend-plugins/${{ env.PLUGIN_NAME}}
submodules: 'recursive'
- name: Add plugin to obs cmake
shell: cmd
run: echo add_subdirectory(${{ env.PLUGIN_NAME }}) >> UI/frontend-plugins/CMakeLists.txt
@@ -270,7 +275,7 @@ jobs:
release:
needs: [macos64, ubuntu64, windows]
name: 'Create Release'
runs-on: [ubuntu-latest]
runs-on: [ubuntu-18.04]
steps:
- name: "Checkout plugin"
uses: actions/checkout@v2.3.4

View File

@@ -8,23 +8,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: 'recursive'
- name: Install clang format
run: |
# gets us newer clang
sudo bash -c "cat >> /etc/apt/sources.list" << LLVMAPT
# 3.8
deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-8 main
deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial-8 main
LLVMAPT
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add -
sudo apt-get -qq update
sudo apt-get install -y clang-format-8
sudo apt-get install -y clang-format-10
- name: Check the Formatting
run: |

6
.gitmodules vendored Normal file
View File

@@ -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

View File

@@ -6,6 +6,8 @@ set -o errexit
set -o pipefail
set -o nounset
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
# Runs the Clang Formatter in parallel on the code base.
# Return codes:
# - 1 there are files to be formatted
@@ -19,13 +21,15 @@ if [[ $OS = "Linux" || $OS = "Darwin" ]] ; then
fi
# Discover clang-format
if type clang-format-8 2> /dev/null ; then
if type clang-format-10 2> /dev/null ; then
CLANG_FORMAT=clang-format-10
elif type clang-format-8 2> /dev/null ; then
CLANG_FORMAT=clang-format-8
else
CLANG_FORMAT=clang-format
fi
find .. -type d \( -path ./deps \
-o -path ./cmake \
-o -path ./build \) -prune -type f -o -name '*.h' -or -name '*.hpp' -or -name '*.m' -or -name '*.mm' -or -name '*.c' -or -name '*.cpp' \
find $SCRIPTPATH/.. -type d \( -path $SCRIPTPATH/../deps \
-o -path $SCRIPTPATH/../cmake \
-o -path $SCRIPTPATH/../build \) -prune -type f -o -name '*.h' -or -name '*.hpp' -or -name '*.m' -or -name '*.mm' -or -name '*.c' -or -name '*.cpp' \
| xargs -L100 -P${NPROC} ${CLANG_FORMAT} -i -style=file -fallback-style=none

View File

@@ -1,12 +1,20 @@
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/")
include(GetGitRevisionDescription)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/version.cpp" @ONLY)
if(WIN32)
get_filename_component(ISS_PLUGIN_FILES_DIR "${CMAKE_BINARY_DIR}\\..\\package" ABSOLUTE)
file(TO_NATIVE_PATH "${ISS_PLUGIN_FILES_DIR}" ISS_PLUGIN_FILES_DIR)
get_filename_component(ISS_MSVC_REDIST_HELPER_DIR "${CMAKE_PACKAGE_PREFIX}\\.." ABSOLUTE)
file(TO_NATIVE_PATH "${ISS_MSVC_REDIST_HELPER_DIR}" ISS_MSVC_REDIST_HELPER_DIR)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/CI/windows/setup.iss.in" "${CMAKE_CURRENT_BINARY_DIR}/CI/windows/setup.iss" @ONLY)
endif()
if(WIN32)
get_filename_component(ISS_PLUGIN_FILES_DIR "${CMAKE_BINARY_DIR}\\..\\package" ABSOLUTE)
@@ -45,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
@@ -59,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
@@ -66,6 +80,7 @@ set(advanced-scene-switcher_HEADERS
src/headers/switch-transitions.hpp
src/headers/switch-window.hpp
src/headers/switch-sequence.hpp
src/headers/switch-video.hpp
src/headers/switch-generic.hpp
src/headers/version.h
)
@@ -86,12 +101,14 @@ 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
src/switch-random.cpp
src/switch-time.cpp
src/switch-audio.cpp
src/switch-video.cpp
src/switch-generic.cpp
src/curl-helper.cpp
src/volume-control.cpp
@@ -104,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)

View File

@@ -49,6 +49,7 @@ AdvSceneSwitcher.generalTab.priority.windowTitle="Fenstername"
AdvSceneSwitcher.generalTab.priority.media="Medien"
AdvSceneSwitcher.generalTab.priority.time="Zeit"
AdvSceneSwitcher.generalTab.priority.audio="Audio"
AdvSceneSwitcher.generalTab.priority.video="Video"
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Szenenübergänge"
@@ -59,6 +60,8 @@ AdvSceneSwitcher.transitionTab.defaultTransition="Ändere den Szenenübergang we
AdvSceneSwitcher.transitionTab.entry="Wechsle von {{scenes}} zu {{scenes2}} mit {{transitions}}"
AdvSceneSwitcher.transitionTab.defaultTransitionEntry="Wenn {{scenes}} aktiv ist ändere den Szenenübergang zu {{transitions}}"
AdvSceneSwitcher.transitionTab.defaultTransitionsHelp="Klicke auf das Plus Symbol, um einen neuen Eintrag hinzuzufügen."
AdvSceneSwitcher.transitionTab.defaultTransition.delay="Wechsle Szenenübergang {{defTransitionDelay}} nach Szenenwechsel."
AdvSceneSwitcher.transitionTab.defaultTransition.delay.help="Diese Verzögerung dient dazu Szenenwechsel zu unterbrechen.\nDies kann passieren, wenn der Szenenübergangtyp gewechselt wird während ein Szenenübergang grade noch im Gang ist."
; Pause Scenes Tab
AdvSceneSwitcher.pauseTab.title="Pause"
@@ -185,6 +188,8 @@ AdvSceneSwitcher.sceneSequenceTab.loadSuccess="Laden der Einstellungen war erfol
AdvSceneSwitcher.sceneSequenceTab.fileType="Text Dateien (*.txt)"
AdvSceneSwitcher.sceneSequenceTab.interruptible="kann unterbrochen werden"
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Andere Szenenwechselmethoden können diese Sequenz unterbrechen"
AdvSceneSwitcher.sceneSequenceTab.extendEdit="Szenenfolge erweitern"
AdvSceneSwitcher.sceneSequenceTab.extendEntry="Nach {{delay}} {{delayUnits}} wechsle zu {{scenes}} mit {{transitions}}"
AdvSceneSwitcher.sceneSequenceTab.entry="Wenn {{startScenes}} aktiv ist wechsle zu {{scenes}} nach {{delay}} {{delayUnits}} mit {{transitions}} {{interruptible}}"
AdvSceneSwitcher.sceneSequenceTab.help="Dieser Tab ermöglicht es automatisiert zu einer anderen Szene zu wechseln, nachdem eine Szene für die konfigurierte Zeit aktiv war.\nZum Beispiel kann so automatisch zwischen zwei Szenen hin und her gewechselt werden.\n\nKlicke auf das markierte Plus Symbol, um einen neuen Eintrag hinzuzufügen."
@@ -192,11 +197,46 @@ AdvSceneSwitcher.sceneSequenceTab.help="Dieser Tab ermöglicht es automatisiert
AdvSceneSwitcher.audioTab.title="Audio"
AdvSceneSwitcher.audioTab.condition.above="über"
AdvSceneSwitcher.audioTab.condition.below="unter"
AdvSceneSwitcher.audioTab.entry="Wenn die Lautstärke von {{audioSources}} {{condition}} {{volumeWidget}} ist für {{duration}} wechsle zu {{scenes}} mit {{transitions}}"
AdvSceneSwitcher.audioTab.ignoreInactiveSource="außer Audio Quelle ist inaktiv"
AdvSceneSwitcher.audioTab.entry="Wenn die Lautstärke von {{audioSources}} {{condition}} {{volumeWidget}} ist für {{duration}} wechsle zu {{scenes}} mit {{transitions}} {{ignoreInactiveSource}}"
AdvSceneSwitcher.audioTab.multiMatchfallbackCondition="Wenn mehrere Einträge zutreffen ..."
AdvSceneSwitcher.audioTab.multiMatchfallback="... für {{duration}} wechsle zu {{scenes}} mit {{transitions}}"
AdvSceneSwitcher.audioTab.help="Dieser Tab ermöglicht es basierend auf der Lautstärke von Audioquellen Szenen zu wechseln.\nSo kann zum Beispiel automatisch zu einer Szene gewechselt werden, wenn die Lautstärke eines Mikrofons eine konfigurierte Schwelle überschreitet.\n\nKlicke auf das markierte Plus Symbol, um einen neuen Eintrag hinzuzufügen."
; Video Tab
AdvSceneSwitcher.videoTab.title="Video"
AdvSceneSwitcher.videoTab.getScreenshot="Screenshot für ausgewählten Eintrag erstellen"
AdvSceneSwitcher.videoTab.getScreenshotHelp="Erstellt einen Screenshot für die Video Quelle des ausgewählten Eintrag und setzt diesen als Ziel Bild"
AdvSceneSwitcher.videoTab.condition.match="genau übereinstimmt mit"
AdvSceneSwitcher.videoTab.condition.match.tooltip="Eine genaue Übereinstimmung setzt voraus, dass sowohl Zielquelle als auch das Bild die gleiche Auflösung besitzen.\nZusätzlich muss jeder einzelne Pixel zwischen beiden Bildquellen übereinstimmen weshalb Bildformate, welche das Bild komprimieren (z. B. .JPG) nicht zu empfehlen sind."
AdvSceneSwitcher.videoTab.condition.differ="nicht übereinstimmt mit"
AdvSceneSwitcher.videoTab.condition.hasNotChanged="sich nicht verändert"
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="<html><head/><body><p>Dieser Tab ermöglicht es basierend auf der Videoausgabe von Quellen scenen zu wechseln.<br/>Für eine noch bessere Implementierung dieser Funktionalität siehe <a href=\"https://obsproject.com/forum/resources/pixel-match-switcher.1202/\"><span style=\" text-decoration: underline; color:#268bd2;\">Pixel Match Switcher</span></a>.<br/><br/>Klicke auf das markierte Plus Symbol, um einen neuen Eintrag hinzuzufügen..</p></body></html>"
; 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"
@@ -232,7 +272,10 @@ AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopStreaming="Streamen stop
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startStreaming="Streamen starten"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startReplayBuffer="Replay Buffer starten"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopReplayBuffer="Replay Buffer stoppen"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.muteSource="Quelle stumm schalten"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.muteSource="Audio Quelle stumm schalten"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.unmuteSource="Audio Quelle aktiv schalten"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startSwitcher="Szenenwechsler starten"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopSwitcher="Szenenwechsler stoppen"
AdvSceneSwitcher.sceneTriggerTab.entry="Wenn {{scenes}} {{triggers}} {{actions}} {{audioSources}} nach {{duration}} "
AdvSceneSwitcher.sceneTriggerTab.help="Dieser Tab ermöglicht es automatisiert Handlungen, wie etwa das Stoppen von einer Aufnahme oder des Streamens, bei einem Szenenwechsel auszuführen."
@@ -249,9 +292,11 @@ AdvSceneSwitcher.browse="Öffnen"
AdvSceneSwitcher.selectScene="--Szene auswählen--"
AdvSceneSwitcher.selectPreviousScene="Vorhergehende Szene"
AdvSceneSwitcher.currentTransition="Aktueller Szenenübergang"
AdvSceneSwitcher.selectTransition="--Szenenübergang auswählen--"
AdvSceneSwitcher.selectWindow="--Fenster auswählen--"
AdvSceneSwitcher.selectAudioSource="--Audio Quelle auswählen--"
AdvSceneSwitcher.selectVideoSource="--Video Quelle auswählen--"
AdvSceneSwitcher.selectMediaSource="--Medien Quelle auswählen--"
AdvSceneSwitcher.selectProcess="--Prozess auswählen--"
AdvSceneSwitcher.enterPath="--Pfad eingeben--"

View File

@@ -49,6 +49,7 @@ AdvSceneSwitcher.generalTab.priority.windowTitle="Window Title"
AdvSceneSwitcher.generalTab.priority.media="Media"
AdvSceneSwitcher.generalTab.priority.time="Time"
AdvSceneSwitcher.generalTab.priority.audio="Audio"
AdvSceneSwitcher.generalTab.priority.video="Video"
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"
@@ -59,6 +60,8 @@ AdvSceneSwitcher.transitionTab.defaultTransition="Change transition if scene is
AdvSceneSwitcher.transitionTab.entry="Switch from {{scenes}} to {{scenes2}} using {{transitions}}"
AdvSceneSwitcher.transitionTab.defaultTransitionEntry="When scene {{scenes}} is active change default scene transition to {{transitions}}"
AdvSceneSwitcher.transitionTab.defaultTransitionsHelp="Click on the plus symbol to add an entry."
AdvSceneSwitcher.transitionTab.defaultTransition.delay="Switch transition {{defTransitionDelay}} after scene change."
AdvSceneSwitcher.transitionTab.defaultTransition.delay.help="The delay is used to avoid cancelled scene switches, which can happen if the transition type is changed while a transition is still ongoing."
; Pause Scenes Tab
AdvSceneSwitcher.pauseTab.title="Pause"
@@ -186,17 +189,54 @@ AdvSceneSwitcher.sceneSequenceTab.fileType="Text files (*.txt)"
AdvSceneSwitcher.sceneSequenceTab.interruptible="interruptible"
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Other switching methods are allowed to interrupt this scene sequence"
AdvSceneSwitcher.sceneSequenceTab.entry="When {{startScenes}} is active switch to {{scenes}} after {{delay}} {{delayUnits}} using {{transitions}} {{interruptible}}"
AdvSceneSwitcher.sceneSequenceTab.extendEdit="Extend Sequence"
AdvSceneSwitcher.sceneSequenceTab.extendEntry="After {{delay}} {{delayUnits}} switch to {{scenes}} using {{transitions}}"
AdvSceneSwitcher.sceneSequenceTab.help="This tab will allow you to automatically switch to a different scene if a scene was active for a configured period of time.\nFor example, you could automatically cycle back and forth between two scenes automatically.\n\nClick on the highlighted plus symbol to continue."
; Audio Tab
AdvSceneSwitcher.audioTab.title="Audio"
AdvSceneSwitcher.audioTab.condition.above="above"
AdvSceneSwitcher.audioTab.condition.below="below"
AdvSceneSwitcher.audioTab.entry="When the volume of {{audioSources}} is {{condition}} {{volumeWidget}} for {{duration}} switch to {{scenes}} using {{transitions}}"
AdvSceneSwitcher.audioTab.ignoreInactiveSource="unless source is inactive"
AdvSceneSwitcher.audioTab.entry="When the volume of {{audioSources}} is {{condition}} {{volumeWidget}} for {{duration}} switch to {{scenes}} using {{transitions}} {{ignoreInactiveSource}}"
AdvSceneSwitcher.audioTab.multiMatchfallbackCondition="If multiple entries match ..."
AdvSceneSwitcher.audioTab.multiMatchfallback="... for {{duration}} switch to {{scenes}} using {{transitions}}"
AdvSceneSwitcher.audioTab.help="This tab will allow you to switch scenes based on the volume of sources.\nFor example, you could automatically switch to a different scene if the volume of your microphone reaches a certain threshold.\n\nClick on the highlighted plus symbol to continue."
; Video Tab
AdvSceneSwitcher.videoTab.title="Video"
AdvSceneSwitcher.videoTab.getScreenshot="Get screenshot for selected entry"
AdvSceneSwitcher.videoTab.getScreenshotHelp="Get Screenshot of the currently selected entry's video source and automatically set it as the target image"
AdvSceneSwitcher.videoTab.condition.match="exactly matches"
AdvSceneSwitcher.videoTab.condition.match.tooltip="An exact match requires the target and the source image to be of the same resolution.\nAdditionally every single pixel needs to match, which is why use of image formats which use compression (e.g. .JPG) is not recommended!"
AdvSceneSwitcher.videoTab.condition.differ="does not match"
AdvSceneSwitcher.videoTab.condition.hasNotChanged="has not changed"
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="<html><head/><body><p>This tab will allow you to switch scenes based on the current video output of selected sources.<br/>Make sure to check out <a href=\"https://obsproject.com/forum/resources/pixel-match-switcher.1202/\"><span style=\" text-decoration: underline; color:#268bd2;\">Pixel Match Switcher</span></a> for an even better implementation of this functionality.<br/><br/> Click on the highlighted plus symbol to continue.</p></body></html>"
; 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"
@@ -233,6 +273,9 @@ AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startStreaming="start stream
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startReplayBuffer="start replay buffer"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopReplayBuffer="stop replay buffer"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.muteSource="mute source"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.unmuteSource="unmute source"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startSwitcher="start the scene switcher"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopSwitcher="stop the scene switcher"
AdvSceneSwitcher.sceneTriggerTab.entry="When {{scenes}} {{triggers}} {{actions}} {{audioSources}} after {{duration}}"
AdvSceneSwitcher.sceneTriggerTab.help="This tab allows you to trigger actions on scene changes, like stopping recording or streaming."
@@ -248,9 +291,11 @@ AdvSceneSwitcher.browse="Browse"
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.currentTransition="Current Transition"
AdvSceneSwitcher.selectTransition="--select transition--"
AdvSceneSwitcher.selectWindow="--select window--"
AdvSceneSwitcher.selectAudioSource="--select audio source--"
AdvSceneSwitcher.selectVideoSource="--select video source--"
AdvSceneSwitcher.selectMediaSource="--select media source--"
AdvSceneSwitcher.selectProcess="--select process--"
AdvSceneSwitcher.enterPath="--enter path--"

1
deps/asio vendored Submodule

Submodule deps/asio added at 93f87fbdbf

1
deps/websocketpp vendored Submodule

Submodule deps/websocketpp added at 1b11fd3015

View File

@@ -737,6 +737,9 @@
<string>AdvSceneSwitcher.transitionTab.defaultTransition</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<layout class="QHBoxLayout" name="defTransitionDelayLayout"/>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_10">
<item row="0" column="0">
@@ -2824,6 +2827,20 @@
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sequenceEdit">
<property name="text">
<string>AdvSceneSwitcher.sceneSequenceTab.extendEdit</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<property name="orientation">
@@ -3070,6 +3087,369 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="videoTab">
<attribute name="title">
<string>AdvSceneSwitcher.videoTab.title</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_29">
<item>
<layout class="QGridLayout" name="gridLayout_22">
<item row="0" column="0">
<widget class="QListWidget" name="videoSwitches">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="videoHelp">
<property name="text">
<string>AdvSceneSwitcher.videoTab.help</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_58">
<item>
<widget class="QPushButton" name="videoAdd">
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">addIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="videoRemove">
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">removeIconSmall</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_124">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_43">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_125">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="videoUp">
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>../../../forms/images/up.svg</normaloff>../../../forms/images/up.svg</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string>upArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="videoDown">
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>../../../forms/images/down.svg</normaloff>../../../forms/images/down.svg</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string>downArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="getScreenshot">
<property name="text">
<string>AdvSceneSwitcher.videoTab.getScreenshot</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_62">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="networkTab">
<attribute name="title">
<string>AdvSceneSwitcher.networkTab.title</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_30">
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>AdvSceneSwitcher.networkTab.warning</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="serverSettings">
<property name="title">
<string>AdvSceneSwitcher.networkTab.server</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_31">
<item>
<layout class="QGridLayout" name="gridLayout_24">
<item row="0" column="1">
<widget class="QSpinBox" name="serverPort">
<property name="minimum">
<number>1024</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>55555</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>AdvSceneSwitcher.networkTab.server.port</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="lockToIPv4">
<property name="text">
<string>AdvSceneSwitcher.networkTab.server.lockToIPv4</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>AdvSceneSwitcher.networkTab.server.status.currentStatus</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="serverStatus">
<property name="text">
<string>AdvSceneSwitcher.networkTab.server.status.notRunning</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="restrictSend">
<property name="text">
<string>AdvSceneSwitcher.networkTab.server.restrictSendToAutomatedSwitches</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="serverRestart">
<property name="text">
<string>AdvSceneSwitcher.networkTab.server.restart</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="clientSettings">
<property name="title">
<string>AdvSceneSwitcher.networkTab.client</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_23">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_21">
<item row="1" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>AdvSceneSwitcher.networkTab.client.port</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>AdvSceneSwitcher.networkTab.client.address</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="clientHostname">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="clientPort">
<property name="minimum">
<number>1024</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>55555</number>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="clientStatus">
<property name="text">
<string>AdvSceneSwitcher.networkTab.client.status.disconnected</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>AdvSceneSwitcher.networkTab.client.status.currentStatus</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="clientReconnect">
<property name="text">
<string>AdvSceneSwitcher.networkTab.client.reconnect</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="sceneGroupTab">
<attribute name="title">
<string>AdvSceneSwitcher.sceneGroupTab.title</string>

View File

@@ -59,6 +59,8 @@ void AdvSceneSwitcher::loadUI()
setupFileTab();
setupTimeTab();
setupAudioTab();
setupVideoTab();
setupNetworkTab();
setupSceneGroupTab();
setupTriggerTab();
@@ -92,8 +94,9 @@ bool AdvSceneSwitcher::DisplayMessage(QString msg, bool question)
return false;
}
void addSelectionEntry(QComboBox *sel, const char *description,
const char *tooltip = "")
void AdvSceneSwitcher::addSelectionEntry(QComboBox *sel,
const char *description,
bool selectable, const char *tooltip)
{
sel->addItem(description);
@@ -106,26 +109,28 @@ void addSelectionEntry(QComboBox *sel, const char *description,
QModelIndex firstIndex =
model->index(0, sel->modelColumn(), sel->rootModelIndex());
QStandardItem *firstItem = model->itemFromIndex(firstIndex);
firstItem->setSelectable(false);
firstItem->setEnabled(false);
if (!selectable) {
firstItem->setSelectable(false);
firstItem->setEnabled(false);
}
}
void AdvSceneSwitcher::populateSceneSelection(QComboBox *sel, bool addPrevious,
bool addSceneGroup,
bool addSelect,
std::string selectText)
std::string selectText,
bool selectable)
{
sel->clear();
if (addSelect) {
if (selectText.empty()) {
addSelectionEntry(
sel,
obs_module_text("AdvSceneSwitcher.selectScene"),
selectable,
obs_module_text(
"AdvSceneSwitcher.invaildEntriesWillNotBeSaved"));
} else {
addSelectionEntry(sel, selectText.c_str());
addSelectionEntry(sel, selectText.c_str(), selectable);
}
}
@@ -150,12 +155,20 @@ void AdvSceneSwitcher::populateSceneSelection(QComboBox *sel, bool addPrevious,
}
void AdvSceneSwitcher::populateTransitionSelection(QComboBox *sel,
bool addSelect)
bool addCurrent,
bool addSelect,
bool selectable)
{
if (addSelect) {
addSelectionEntry(
sel,
obs_module_text("AdvSceneSwitcher.selectTransition"));
obs_module_text("AdvSceneSwitcher.selectTransition"),
selectable);
}
if (addCurrent) {
sel->addItem(
obs_module_text("AdvSceneSwitcher.currentTransition"));
}
obs_frontend_source_list *transitions = new obs_frontend_source_list();
@@ -197,6 +210,7 @@ void AdvSceneSwitcher::populateAudioSelection(QComboBox *sel, bool addSelect)
addSelectionEntry(
sel,
obs_module_text("AdvSceneSwitcher.selectAudioSource"),
false,
obs_module_text(
"AdvSceneSwitcher.invaildEntriesWillNotBeSaved"));
}
@@ -221,12 +235,49 @@ void AdvSceneSwitcher::populateAudioSelection(QComboBox *sel, bool addSelect)
}
}
void AdvSceneSwitcher::populateVideoSelection(QComboBox *sel, bool addScenes,
bool addSelect)
{
if (addSelect) {
addSelectionEntry(
sel,
obs_module_text("AdvSceneSwitcher.selectVideoSource"),
false,
obs_module_text(
"AdvSceneSwitcher.invaildEntriesWillNotBeSaved"));
}
auto sourceEnum = [](void *data, obs_source_t *source) -> bool /* -- */
{
std::vector<std::string> *list =
reinterpret_cast<std::vector<std::string> *>(data);
uint32_t flags = obs_source_get_output_flags(source);
std::string test = obs_source_get_name(source);
if ((flags & (OBS_SOURCE_VIDEO | OBS_SOURCE_ASYNC)) != 0) {
list->push_back(obs_source_get_name(source));
}
return true;
};
std::vector<std::string> videoSources;
obs_enum_sources(sourceEnum, &videoSources);
sort(videoSources.begin(), videoSources.end());
for (std::string &source : videoSources) {
sel->addItem(source.c_str());
}
if (addScenes) {
populateSceneSelection(sel, false, false, false);
}
}
void AdvSceneSwitcher::populateMediaSelection(QComboBox *sel, bool addSelect)
{
if (addSelect) {
addSelectionEntry(
sel,
obs_module_text("AdvSceneSwitcher.selectMediaSource"),
false,
obs_module_text(
"AdvSceneSwitcher.invaildEntriesWillNotBeSaved"));
}
@@ -428,9 +479,6 @@ void SwitcherData::Thread()
OBSWeakSource scene;
OBSWeakSource transition;
bool defTransitionMatch = false;
OBSWeakSource defTransition;
endTime = std::chrono::high_resolution_clock::now();
auto runTime =
@@ -449,7 +497,7 @@ void SwitcherData::Thread()
}
}
vblog(LOG_INFO, "sleep for %ld", duration.count());
vblog(LOG_INFO, "try to sleep for %ld", duration.count());
cv.wait_for(lock, duration);
startTime = std::chrono::high_resolution_clock::now();
@@ -457,63 +505,17 @@ void SwitcherData::Thread()
linger = 0;
switcher->Prune();
if (switcher->stop) {
break;
}
if (checkPause()) {
continue;
}
checkDefaultSceneTransitions(defTransitionMatch, defTransition);
for (int switchFuncName : functionNamesByPriority) {
switch (switchFuncName) {
case read_file_func:
checkSwitchInfoFromFile(match, scene,
transition);
checkFileContent(match, scene, transition);
break;
case idle_func:
checkIdleSwitch(match, scene, transition);
break;
case exe_func:
checkExeSwitch(match, scene, transition);
break;
case screen_region_func:
checkScreenRegionSwitch(match, scene,
transition);
break;
case window_title_func:
checkWindowTitleSwitch(match, scene,
transition);
break;
case round_trip_func:
checkSceneSequence(match, scene, transition,
linger);
break;
case media_func:
checkMediaSwitch(match, scene, transition);
break;
case time_func:
checkTimeSwitch(match, scene, transition);
break;
case audio_func:
checkAudioSwitch(match, scene, transition);
break;
}
if (switcher->stop) {
goto endLoop;
}
if (match) {
break;
}
match = checkForMatch(scene, transition, linger);
if (switcher->stop) {
break;
}
checkNoMatchSwitch(match, scene, transition, sleep);
checkSwitchCooldown(match);
if (linger) {
@@ -544,21 +546,77 @@ void SwitcherData::Thread()
// leading to a deadlock, so we have to unlock()
lock.unlock();
if (!match && defTransitionMatch) {
setCurrentDefTransition(defTransition);
}
if (match) {
if (networkConfig.ServerEnabled) {
server.sendMessage(scene, transition);
}
switchScene(scene, transition,
tansitionOverrideOverride);
}
writeSceneInfoToFile();
}
endLoop:
blog(LOG_INFO, "stopped");
}
bool SwitcherData::checkForMatch(OBSWeakSource &scene,
OBSWeakSource &transition, int &linger)
{
bool match = false;
if (uninterruptibleSceneSequenceActive) {
checkSceneSequence(match, scene, transition, linger);
if (match) {
return match;
}
}
for (int switchFuncName : functionNamesByPriority) {
switch (switchFuncName) {
case read_file_func:
checkSwitchInfoFromFile(match, scene, transition);
checkFileContent(match, scene, transition);
break;
case idle_func:
checkIdleSwitch(match, scene, transition);
break;
case exe_func:
checkExeSwitch(match, scene, transition);
break;
case screen_region_func:
checkScreenRegionSwitch(match, scene, transition);
break;
case window_title_func:
checkWindowTitleSwitch(match, scene, transition);
break;
case round_trip_func:
checkSceneSequence(match, scene, transition, linger);
break;
case media_func:
checkMediaSwitch(match, scene, transition);
break;
case time_func:
checkTimeSwitch(match, scene, transition);
break;
case audio_func:
checkAudioSwitch(match, scene, transition);
break;
case video_func:
checkVideoSwitch(match, scene, transition);
break;
}
if (switcher->stop) {
return false;
}
if (match) {
break;
}
}
return match;
}
void switchScene(OBSWeakSource &scene, OBSWeakSource &transition,
bool &transitionOverrideOverride)
{
@@ -598,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()
@@ -612,6 +679,9 @@ void SwitcherData::Stop()
writeToStatusFile("Advanced Scene Switcher stopped");
}
server.stop();
client.disconnect();
}
bool SwitcherData::sceneChangedDuringWait()
@@ -643,7 +713,7 @@ extern "C" void FreeSceneSwitcher()
void handleSceneChange(SwitcherData *s)
{
// Stop waiting if scene was manually changed
// Stop waiting if scene was changed
if (s->sceneChangedDuringWait()) {
s->cv.notify_one();
}
@@ -658,12 +728,13 @@ void handleSceneChange(SwitcherData *s)
s->previousSceneHelper = ws;
}
switcher->checkTriggers();
}
s->checkTriggers();
s->checkDefaultSceneTransitions();
void handleTransitionStop(SwitcherData *s)
{
s->checkedDefTransition = false;
if (switcher->networkConfig.ServerEnabled &&
switcher->networkConfig.SendAll) {
switcher->server.sendMessage(ws, nullptr);
}
}
void setLiveTime(SwitcherData *s)
@@ -702,9 +773,6 @@ static void OBSEvent(enum obs_frontend_event event, void *switcher)
case OBS_FRONTEND_EVENT_SCENE_CHANGED:
handleSceneChange((SwitcherData *)switcher);
break;
case OBS_FRONTEND_EVENT_TRANSITION_STOPPED:
handleTransitionStop((SwitcherData *)switcher);
break;
case OBS_FRONTEND_EVENT_RECORDING_STARTED:
setLiveTime((SwitcherData *)switcher);
checkAutoStartRecording((SwitcherData *)switcher);

View File

@@ -4,7 +4,7 @@
#include "headers/utility.hpp"
#include "headers/version.h"
constexpr auto tab_count = 15;
constexpr auto tab_count = 17;
QMetaObject::Connection inactivePluse;
@@ -129,6 +129,7 @@ void AdvSceneSwitcher::SetStarted()
ui->pluginRunningText->setText(
obs_module_text("AdvSceneSwitcher.status.active"));
ui->pluginRunningText->disconnect(inactivePluse);
currentStatusActive = true;
}
void AdvSceneSwitcher::SetStopped()
@@ -139,6 +140,7 @@ void AdvSceneSwitcher::SetStopped()
obs_module_text("AdvSceneSwitcher.status.inactive"));
inactivePluse = PulseWidget(ui->pluginRunningText, QColor(Qt::red),
QColor(0, 0, 0, 0), "QLabel ");
currentStatusActive = false;
}
void AdvSceneSwitcher::on_toggleStartButton_clicked()
@@ -324,9 +326,15 @@ int findTabIndex(QTabWidget *tabWidget, int pos)
tabName = "audioTab";
break;
case 13:
tabName = "sceneGroupTab";
tabName = "videoTab";
break;
case 14:
tabName = "networkTab";
break;
case 15:
tabName = "sceneGroupTab";
break;
case 16:
tabName = "sceneTriggerTab";
break;
}
@@ -381,22 +389,24 @@ void SwitcherData::loadSettings(obs_data_t *obj)
return;
}
switcher->loadSceneGroups(obj);
switcher->loadWindowTitleSwitches(obj);
switcher->loadScreenRegionSwitches(obj);
switcher->loadPauseSwitches(obj);
switcher->loadSceneSequenceSwitches(obj);
switcher->loadSceneTransitions(obj);
switcher->loadIdleSwitches(obj);
switcher->loadExecutableSwitches(obj);
switcher->loadRandomSwitches(obj);
switcher->loadFileSwitches(obj);
switcher->loadMediaSwitches(obj);
switcher->loadTimeSwitches(obj);
switcher->loadAudioSwitches(obj);
switcher->loadSceneTriggers(obj);
switcher->loadGeneralSettings(obj);
switcher->loadHotkeys(obj);
loadSceneGroups(obj);
loadWindowTitleSwitches(obj);
loadScreenRegionSwitches(obj);
loadPauseSwitches(obj);
loadSceneSequenceSwitches(obj);
loadSceneTransitions(obj);
loadIdleSwitches(obj);
loadExecutableSwitches(obj);
loadRandomSwitches(obj);
loadFileSwitches(obj);
loadMediaSwitches(obj);
loadTimeSwitches(obj);
loadAudioSwitches(obj);
loadVideoSwitches(obj);
loadNetworkSettings(obj);
loadSceneTriggers(obj);
loadGeneralSettings(obj);
loadHotkeys(obj);
}
void SwitcherData::saveSettings(obs_data_t *obj)
@@ -418,6 +428,8 @@ void SwitcherData::saveSettings(obs_data_t *obj)
saveMediaSwitches(obj);
saveTimeSwitches(obj);
saveAudioSwitches(obj);
saveVideoSwitches(obj);
saveNetworkSwitches(obj);
saveSceneTriggers(obj);
saveGeneralSettings(obj);
saveHotkeys(obj);
@@ -465,15 +477,15 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj)
switcher->functionNamesByPriority[7]);
obs_data_set_int(obj, "priority8",
switcher->functionNamesByPriority[8]);
obs_data_set_int(obj, "priority9",
switcher->functionNamesByPriority[9]);
obs_data_set_int(obj, "threadPriority", switcher->threadPriority);
// After fresh install of OBS the vector can be empty
// as save() might be called before first load()
if (switcher->tabOrder.size() < tab_count) {
switcher->tabOrder = std::vector<int>(tab_count);
std::iota(switcher->tabOrder.begin(), switcher->tabOrder.end(),
0);
if (tabOrder.size() < tab_count) {
resetTabOrder();
}
obs_data_set_int(obj, "generalTabPos", switcher->tabOrder[0]);
@@ -489,8 +501,10 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj)
obs_data_set_int(obj, "idleTabPos", switcher->tabOrder[10]);
obs_data_set_int(obj, "sequenceTabPos", switcher->tabOrder[11]);
obs_data_set_int(obj, "audioTabPos", switcher->tabOrder[12]);
obs_data_set_int(obj, "sceneGroupTabPos", switcher->tabOrder[13]);
obs_data_set_int(obj, "triggerTabPos", switcher->tabOrder[14]);
obs_data_set_int(obj, "videoTabPos", switcher->tabOrder[13]);
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)
@@ -553,6 +567,8 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
(obs_data_get_int(obj, "priority7"));
switcher->functionNamesByPriority[8] =
(obs_data_get_int(obj, "priority8"));
switcher->functionNamesByPriority[9] =
(obs_data_get_int(obj, "priority9"));
if (!switcher->prioFuncsValid()) {
switcher->functionNamesByPriority[0] = (default_priority_0);
switcher->functionNamesByPriority[1] = (default_priority_1);
@@ -563,6 +579,7 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
switcher->functionNamesByPriority[6] = (default_priority_6);
switcher->functionNamesByPriority[7] = (default_priority_7);
switcher->functionNamesByPriority[8] = (default_priority_8);
switcher->functionNamesByPriority[9] = (default_priority_9);
}
obs_data_set_default_int(obj, "threadPriority",
@@ -582,8 +599,10 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
obs_data_set_default_int(obj, "idleTabPos", 10);
obs_data_set_default_int(obj, "sequenceTabPos", 11);
obs_data_set_default_int(obj, "audioTabPos", 12);
obs_data_set_default_int(obj, "sceneGroupTabPos", 13);
obs_data_set_default_int(obj, "triggerTabPos", 14);
obs_data_set_default_int(obj, "videoTabPos", 13);
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")));
@@ -611,10 +630,32 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
(int)(obs_data_get_int(obj, "sequenceTabPos")));
switcher->tabOrder.emplace_back(
(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(
(int)(obs_data_get_int(obj, "triggerTabPos")));
if (!tabOrderValid()) {
resetTabOrder();
}
}
bool SwitcherData::tabOrderValid()
{
auto tmp = tabOrder;
std::sort(tmp.begin(), tmp.end());
auto it = std::unique(tmp.begin(), tmp.end());
return it == tmp.end();
}
void SwitcherData::resetTabOrder()
{
tabOrder = std::vector<int>(tab_count);
std::iota(switcher->tabOrder.begin(), switcher->tabOrder.end(), 0);
}
void SwitcherData::checkNoMatchSwitch(bool &match, OBSWeakSource &scene,
@@ -681,6 +722,21 @@ void populateAutoStartEventSelection(QComboBox *cb)
"AdvSceneSwitcher.generalTab.status.autoStart.recordingAndStreaming"));
}
void AdvSceneSwitcher::updateStatus()
{
if (switcher->th && switcher->th->isRunning()) {
if (currentStatusActive) {
return;
}
SetStarted();
} else {
if (!currentStatusActive) {
return;
}
SetStopped();
}
}
void AdvSceneSwitcher::setupGeneralTab()
{
populateSceneSelection(ui->noMatchSwitchScene, false);
@@ -748,6 +804,10 @@ void AdvSceneSwitcher::setupGeneralTab()
s = obs_module_text(
"AdvSceneSwitcher.generalTab.priority.audio");
break;
case video_func:
s = obs_module_text(
"AdvSceneSwitcher.generalTab.priority.video");
break;
}
QString text(s.c_str());
QListWidgetItem *item =
@@ -780,4 +840,10 @@ void AdvSceneSwitcher::setupGeneralTab()
} else {
SetStopped();
}
// Updates the UI status element if the status changed externally
// (e.g. via hotkeys)
QTimer *statusTimer = new QTimer(this);
connect(statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus()));
statusTimer->start(1000);
}

View File

@@ -7,9 +7,9 @@
#include "switcher-data-structs.hpp"
#define blog(level, msg, ...) blog(level, "[adv-ss] " msg, ##__VA_ARGS__)
#define vblog(level, msg, ...) \
if (verbose) { \
blog(level, "[adv-ss] " msg, ##__VA_ARGS__); \
#define vblog(level, msg, ...) \
if (verbose) { \
blog(level, msg, ##__VA_ARGS__); \
}
class QCloseEvent;
@@ -23,6 +23,7 @@ class AdvSceneSwitcher : public QDialog {
public:
std::unique_ptr<Ui_AdvSceneSwitcher> ui;
bool loading = true;
bool currentStatusActive = false;
AdvSceneSwitcher(QWidget *parent);
@@ -39,6 +40,7 @@ public:
int IgnoreIdleWindowsFindByData(const QString &window);
void UpdateNonMatchingScene(const QString &name);
void OpenSequenceExtendEdit(SequenceWidget *sw);
void SetEditSceneGroup(SceneGroup &sg);
void loadUI();
@@ -57,23 +59,34 @@ public:
void setupAudioTab();
void setupSceneGroupTab();
void setupTriggerTab();
void setupVideoTab();
void setupNetworkTab();
void setTabOrder();
static bool DisplayMessage(QString msg, bool question = false);
static void AskBackup(obs_data_t *obj);
static void addSelectionEntry(QComboBox *sel, const char *description,
bool selectable = false,
const char *tooltip = "");
static void populateSceneSelection(QComboBox *sel,
bool addPrevious = false,
bool addSceneGroup = false,
bool addSelect = true,
std::string selectText = "");
std::string selectText = "",
bool selectable = false);
static void populateTransitionSelection(QComboBox *sel,
bool addSelect = true);
bool addCurrent = true,
bool addSelect = true,
bool selectable = false);
static void populateWindowSelection(QComboBox *sel,
bool addSelect = true);
static void populateAudioSelection(QComboBox *sel,
bool addSelect = true);
static void populateVideoSelection(QComboBox *sel,
bool addScenes = true,
bool addSelect = true);
static void populateMediaSelection(QComboBox *sel,
bool addSelect = true);
static void populateProcessSelection(QComboBox *sel,
@@ -129,10 +142,12 @@ public slots:
void on_sceneSequenceAdd_clicked();
void on_sceneSequenceRemove_clicked();
void on_sceneSequenceSave_clicked();
void on_sceneSequenceLoad_clicked();
void on_sceneSequenceUp_clicked();
void on_sceneSequenceDown_clicked();
void on_sceneSequenceSave_clicked();
void on_sceneSequenceLoad_clicked();
void on_sequenceEdit_clicked();
void on_sceneSequenceSwitches_itemDoubleClicked(QListWidgetItem *item);
void on_verboseLogging_stateChanged(int state);
void on_uiHintsDisable_stateChanged(int state);
@@ -149,6 +164,7 @@ public slots:
void on_defaultTransitionsUp_clicked();
void on_defaultTransitionsDown_clicked();
void on_transitionOverridecheckBox_stateChanged(int state);
void defTransitionDelayValueChanged(int value);
void on_browseButton_clicked();
void on_readFileCheckBox_stateChanged(int state);
@@ -191,6 +207,24 @@ public slots:
void on_audioDown_clicked();
void on_audioFallback_toggled(bool on);
void on_videoAdd_clicked();
void on_videoRemove_clicked();
void on_videoUp_clicked();
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();
@@ -213,6 +247,7 @@ public slots:
void on_threadPriority_currentTextChanged(const QString &text);
void updateScreenRegionCursorPos();
void updateStatus();
void on_close_clicked();

View File

@@ -37,7 +37,7 @@ struct SceneGroup {
int currentCount = -1;
std::chrono::high_resolution_clock::time_point lastAdvTime;
OBSWeakSource lastRandomScene = nullptr;
int lastRandomScene = -1;
inline SceneGroup(){};
inline SceneGroup(std::string name_) : name(name_){};

View File

@@ -23,6 +23,10 @@ enum class sceneTriggerAction {
STOP_REPLAY_BUFFER,
MUTE_SOURCE,
UNMUTE_SOURCE,
START_SWITCHER,
STOP_SWITCHER,
};
struct SceneTrigger : SceneSwitcherEntry {
@@ -36,7 +40,8 @@ struct SceneTrigger : SceneSwitcherEntry {
void save(obs_data_t *obj);
void load(obs_data_t *obj);
bool checkMatch(OBSWeakSource previousScene);
bool checkMatch(OBSWeakSource currentScene,
OBSWeakSource previousScene);
void performAction();
void logMatch();
};

View File

@@ -1,5 +1,6 @@
#pragma once
#include <QSpinBox>
#include <limits>
#include "switch-generic.hpp"
#include "volume-control.hpp"
@@ -18,8 +19,9 @@ struct AudioSwitch : virtual SceneSwitcherEntry {
int volumeThreshold = 0;
audioCondition condition = ABOVE;
double duration = 0;
bool ignoreInactiveSource = true;
unsigned int matchCount = 0;
float peak = -1;
float peak = -std::numeric_limits<float>::infinity();
obs_volmeter_t *volmeter = nullptr;
const char *getType() { return "audio"; }
@@ -69,12 +71,14 @@ private slots:
void VolumeThresholdChanged(int vol);
void ConditionChanged(int cond);
void DurationChanged(double dur);
void IgnoreInactiveChanged(int state);
private:
QComboBox *audioSources;
QComboBox *condition;
QSpinBox *audioVolumeThreshold;
QDoubleSpinBox *duration;
QCheckBox *ignoreInactiveSource;
VolControl *volMeter;
AudioSwitch *switchData;

View File

@@ -8,6 +8,7 @@ struct SceneSwitcherEntry {
OBSWeakSource scene = nullptr;
OBSWeakSource transition = nullptr;
bool usePreviousScene = false;
bool useCurrentTransition = false;
virtual const char *getType() = 0;
virtual bool initialized();
@@ -64,14 +65,15 @@ class SwitchWidget : public QWidget {
public:
SwitchWidget(QWidget *parent, SceneSwitcherEntry *s,
bool usePreviousScene = true, bool addSceneGroup = false);
bool usePreviousScene = true, bool addSceneGroup = false,
bool addCurrentTransition = true);
virtual SceneSwitcherEntry *getSwitchData();
virtual void setSwitchData(SceneSwitcherEntry *s);
void showSwitchData();
static void swapSwitchData(SwitchWidget *s1, SwitchWidget *s2);
private slots:
protected slots:
void SceneChanged(const QString &text);
void TransitionChanged(const QString &text);
void SceneGroupAdd(const QString &name);

View File

@@ -0,0 +1,111 @@
/*
Most of this code is based on https://github.com/Palakis/obs-websocket
*/
#pragma once
#include <set>
#include <QtCore/QObject>
#include <QtCore/QMutex>
#include <QtCore/QSharedPointer>
#include <QtCore/QVariantHash>
#include <QtCore/QThreadPool>
#include <mutex>
#include <condition_variable>
#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include <websocketpp/client.hpp>
using websocketpp::connection_hdl;
typedef websocketpp::server<websocketpp::config::asio> server;
typedef websocketpp::client<websocketpp::config::asio_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<connection_hdl, std::owner_less<connection_hdl>> _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,
};

View File

@@ -19,6 +19,7 @@ enum class PauseTarget {
Idle,
Sequence,
Audio,
Video,
};
struct PauseEntry : SceneSwitcherEntry {

View File

@@ -1,4 +1,6 @@
#pragma once
#include <QVBoxLayout>
#include "switch-generic.hpp"
constexpr auto round_trip_func = 1;
@@ -12,42 +14,77 @@ typedef enum {
struct SceneSequenceSwitch : SceneSwitcherEntry {
static bool pause;
SwitchTargetType startTargetType = SwitchTargetType::Scene;
OBSWeakSource startScene = nullptr;
double delay = 0;
int delayMultiplier = 1;
bool interruptible = false;
unsigned int matchCount = 0;
// nullptr marks start point and reaching end of extended sequence
SceneSequenceSwitch *activeSequence = nullptr;
std::unique_ptr<SceneSequenceSwitch> extendedSequence = nullptr;
const char *getType() { return "sequence"; }
bool initialized();
bool valid();
void save(obs_data_t *obj);
void load(obs_data_t *obj);
void save(obs_data_t *obj, bool saveExt = true);
void load(obs_data_t *obj, bool saveExt = true);
bool reduce();
SceneSequenceSwitch *extend();
bool checkMatch(OBSWeakSource currentScene, int &linger,
SceneSequenceSwitch *root = nullptr);
bool checkDurationMatchInterruptible();
void prepareUninterruptibleMatch(OBSWeakSource currentScene,
int &linger);
void advanceActiveSequence();
void logAdvanceSequence();
void logSequenceCanceled();
};
class SequenceWidget : public SwitchWidget {
Q_OBJECT
public:
SequenceWidget(QWidget *parent, SceneSequenceSwitch *s);
SequenceWidget(QWidget *parent, SceneSequenceSwitch *s,
bool extendSequence = false, bool editExtendMode = false,
bool showExtendText = true);
SceneSequenceSwitch *getSwitchData();
void setSwitchData(SceneSequenceSwitch *s);
static void swapSwitchData(SequenceWidget *s1, SequenceWidget *s2);
void UpdateDelay();
void UpdateWidgetStatus(bool showExtendText);
void setExtendedSequenceStartScene();
private slots:
void SceneChanged(const QString &text);
void DelayChanged(double delay);
void DelayUnitsChanged(int idx);
void StartSceneChanged(const QString &text);
void InterruptibleChanged(int state);
void ExtendClicked();
void ReduceClicked();
private:
protected:
QDoubleSpinBox *delay;
QComboBox *delayUnits;
QComboBox *startScenes;
QCheckBox *interruptible;
QVBoxLayout *extendSequenceLayout;
QPushButton *extend;
QPushButton *reduce;
// I would prefer having a list of only widgets of type editExtendMode
// but I am not sure how to best implement that using a QListWidget.
//
// So use edit button to bring up edit widget and
// add a label to disaplay current extended sequence state.
QLabel *extendText;
SceneSequenceSwitch *switchData;
};

View File

@@ -18,8 +18,11 @@ struct SceneTransition : SceneSwitcherEntry {
struct DefaultSceneTransition : SceneSwitcherEntry {
static bool pause;
static unsigned int delay;
const char *getType() { return "def_transition"; }
bool checkMatch(OBSWeakSource currentScene);
void setTransition();
inline DefaultSceneTransition(){};
inline DefaultSceneTransition(OBSWeakSource scene_,

View File

@@ -0,0 +1,98 @@
#pragma once
#include <QSpinBox>
#include "switch-generic.hpp"
constexpr auto video_func = 9;
constexpr auto default_priority_9 = video_func;
enum class videoSwitchType {
MATCH,
DIFFER,
HAS_NOT_CHANGED,
};
class AdvSSScreenshotObj {
public:
AdvSSScreenshotObj(obs_source_t *source);
~AdvSSScreenshotObj();
void Screenshot();
void Download();
void Copy();
void MarkDone();
gs_texrender_t *texrender = nullptr;
gs_stagesurf_t *stagesurf = nullptr;
OBSWeakSource weakSource;
std::string path;
QImage image;
uint32_t cx = 0;
uint32_t cy = 0;
int stage = 0;
bool done = false;
std::chrono::high_resolution_clock::time_point time;
};
struct VideoSwitch : virtual SceneSwitcherEntry {
static bool pause;
videoSwitchType condition = videoSwitchType::MATCH;
OBSWeakSource videoSource = nullptr;
std::string file = obs_module_text("AdvSceneSwitcher.enterPath");
double duration = 0;
bool ignoreInactiveSource = false;
std::unique_ptr<AdvSSScreenshotObj> screenshotData = nullptr;
std::chrono::high_resolution_clock::time_point previousTime{};
QImage matchImage;
std::chrono::milliseconds currentMatchDuration{};
const char *getType() { return "video"; }
bool initialized();
bool valid();
void save(obs_data_t *obj);
void load(obs_data_t *obj);
void getScreenshot();
bool loadImageFromFile();
bool checkMatch();
VideoSwitch(){};
friend void swap(VideoSwitch &first, VideoSwitch &second);
};
class VideoSwitchWidget : public SwitchWidget {
Q_OBJECT
public:
VideoSwitchWidget(QWidget *parent, VideoSwitch *s);
VideoSwitch *getSwitchData();
void setSwitchData(VideoSwitch *s);
static void swapSwitchData(VideoSwitchWidget *as1,
VideoSwitchWidget *as2);
void UpdatePreviewTooltip();
void SetFilePath(const QString &text);
private slots:
void SourceChanged(const QString &text);
void ConditionChanged(int cond);
void DurationChanged(double dur);
void FilePathChanged();
void BrowseButtonClicked();
void IgnoreInactiveChanged(int state);
private:
QComboBox *videoSources;
QComboBox *condition;
QDoubleSpinBox *duration;
QLineEdit *filePath;
QPushButton *browseButton;
QCheckBox *ignoreInactiveSource;
VideoSwitch *switchData;
};

View File

@@ -21,9 +21,12 @@
#include "switch-transitions.hpp"
#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";
constexpr auto current_transition_name = "Current Transition";
typedef enum { NO_SWITCH = 0, SWITCH = 1, RANDOM_SWITCH = 2 } NoMatch;
typedef enum { PERSIST = 0, START = 1, STOP = 2 } StartupBehavior;
@@ -66,9 +69,10 @@ struct SwitcherData {
OBSWeakSource previousScene = nullptr;
OBSWeakSource previousSceneHelper = nullptr;
OBSWeakSource lastRandomScene;
SceneGroup *lastRandomSceneGroup;
OBSWeakSource nonMatchingScene;
NoMatch switchIfNotMatching = NO_SWITCH;
double noMatchDelay;
double noMatchDelay = 0;
double noMatchCount = 0;
StartupBehavior startupBehavior = PERSIST;
AutoStartEvent autoStartEvent = AutoStartEvent::NEVER;
@@ -84,6 +88,7 @@ struct SwitcherData {
std::vector<std::string> ignoreWindowsSwitches;
bool uninterruptibleSceneSequenceActive = false;
std::deque<SceneSequenceSwitch> sceneSequenceSwitches;
std::deque<RandomSwitch> randomSwitches;
@@ -100,7 +105,6 @@ struct SwitcherData {
std::deque<SceneTransition> sceneTransitions;
std::deque<DefaultSceneTransition> defaultSceneTransitions;
bool checkedDefTransition = false;
std::deque<MediaSwitch> mediaSwitches;
@@ -112,12 +116,21 @@ struct SwitcherData {
std::deque<AudioSwitch> audioSwitches;
AudioSwitchFallback audioFallback;
WSServer server;
ServerStatus serverStatus = ServerStatus::NOT_RUNNING;
WSClient client;
ClientStatus clientStatus = ClientStatus::DISCONNECTED;
NetworkConfig networkConfig;
std::deque<VideoSwitch> videoSwitches;
std::deque<SceneGroup> sceneGroups;
std::vector<int> functionNamesByPriority = std::vector<int>{
default_priority_0, default_priority_1, default_priority_2,
default_priority_3, default_priority_4, default_priority_5,
default_priority_6, default_priority_7, default_priority_8};
default_priority_6, default_priority_7, default_priority_8,
default_priority_9};
struct ThreadPrio {
std::string name;
@@ -160,13 +173,17 @@ struct SwitcherData {
void Stop();
bool sceneChangedDuringWait();
bool prioFuncsValid();
bool tabOrderValid();
void resetTabOrder();
void writeSceneInfoToFile();
void writeToStatusFile(QString status);
bool checkPause();
void checkDefaultSceneTransitions(bool &match,
OBSWeakSource &transition);
void setCurrentDefTransition(OBSWeakSource &transition);
bool checkForMatch(OBSWeakSource &scene, OBSWeakSource &transition,
int &linger);
void checkSceneSequence(bool &match, OBSWeakSource &scene,
OBSWeakSource &transition, int &linger);
void checkIdleSwitch(bool &match, OBSWeakSource &scene,
@@ -191,10 +208,14 @@ struct SwitcherData {
OBSWeakSource &transition);
void checkAudioSwitchFallback(OBSWeakSource &scene,
OBSWeakSource &transition);
void checkVideoSwitch(bool &match, OBSWeakSource &scene,
OBSWeakSource &transition);
void checkNoMatchSwitch(bool &match, OBSWeakSource &scene,
OBSWeakSource &transition, int &sleep);
void checkSwitchCooldown(bool &match);
void checkTriggers();
bool checkPause();
void checkDefaultSceneTransitions();
void saveSettings(obs_data_t *obj);
void saveWindowTitleSwitches(obs_data_t *obj);
@@ -211,6 +232,8 @@ struct SwitcherData {
void saveAudioSwitches(obs_data_t *obj);
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);
@@ -231,6 +254,8 @@ struct SwitcherData {
void loadAudioSwitches(obs_data_t *obj);
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);

View File

@@ -51,27 +51,26 @@ OBSWeakSource SceneGroup::getNextSceneTime()
OBSWeakSource SceneGroup::getNextSceneRandom()
{
if (scenes.size() == 1) {
return *scenes.begin();
currentIdx = 0;
return scenes[currentIdx];
}
std::vector<OBSWeakSource> rs(scenes);
std::random_device rng;
std::mt19937 urng(rng());
std::shuffle(rs.begin(), rs.end(), urng);
for (OBSWeakSource &s : rs) {
if (s == lastRandomScene) {
continue;
}
int rIdx = lastRandomScene;
do {
rIdx = rand() % scenes.size();
} while (rIdx == lastRandomScene);
lastRandomScene = s;
lastRandomScene = rIdx;
currentIdx = rIdx;
return s;
}
return nullptr;
return scenes[currentIdx];
}
OBSWeakSource SceneGroup::getCurrentScene()
{
if (scenes.size() < currentIdx + 1) {
return nullptr;
}
return scenes[currentIdx];
}

View File

@@ -133,6 +133,15 @@ void SceneTrigger::logMatch()
case sceneTriggerAction::MUTE_SOURCE:
actionName = "MUTE (" + GetWeakSourceName(audioSource) + ")";
break;
case sceneTriggerAction::UNMUTE_SOURCE:
actionName = "UNMUTE (" + GetWeakSourceName(audioSource) + ")";
break;
case sceneTriggerAction::START_SWITCHER:
actionName = "START SCENE SWITCHER";
break;
case sceneTriggerAction::STOP_SWITCHER:
actionName = "STOP SCENE SWITCHER";
break;
default:
actionName = "UNKOWN";
break;
@@ -176,24 +185,35 @@ void frontEndActionThread(sceneTriggerAction action, double delay)
case sceneTriggerAction::STOP_REPLAY_BUFFER:
obs_frontend_replay_buffer_stop();
break;
case sceneTriggerAction::MUTE_SOURCE:
obs_frontend_replay_buffer_stop();
break;
default:
blog(LOG_WARNING, "ignoring unexpected frontend action '%d'",
static_cast<int>(action));
break;
}
}
void muteThread(OBSWeakSource source, double delay)
void muteThread(OBSWeakSource source, double delay, bool mute)
{
long long mil = delay * 1000;
std::this_thread::sleep_for(std::chrono::milliseconds(mil));
auto s = obs_weak_source_get_source(source);
obs_source_set_muted(s, true);
obs_source_set_muted(s, mute);
obs_source_release(s);
}
void statusThread(double delay, bool stop)
{
long long mil = delay * 1000;
std::this_thread::sleep_for(std::chrono::milliseconds(mil));
if (stop) {
switcher->Stop();
} else {
switcher->Start();
}
}
bool isFrontendAction(sceneTriggerAction triggerAction)
{
return triggerAction == sceneTriggerAction::START_RECORDING ||
@@ -206,6 +226,18 @@ bool isFrontendAction(sceneTriggerAction triggerAction)
triggerAction == sceneTriggerAction::STOP_REPLAY_BUFFER;
}
bool isAudioAction(sceneTriggerAction t)
{
return t == sceneTriggerAction::MUTE_SOURCE ||
t == sceneTriggerAction::UNMUTE_SOURCE;
}
bool isSwitcherStatusAction(sceneTriggerAction t)
{
return t == sceneTriggerAction::START_SWITCHER ||
t == sceneTriggerAction::STOP_SWITCHER;
}
void SceneTrigger::performAction()
{
if (triggerAction == sceneTriggerAction::NONE) {
@@ -216,20 +248,23 @@ void SceneTrigger::performAction()
if (isFrontendAction(triggerAction)) {
t = std::thread(frontEndActionThread, triggerAction, duration);
} else if (isAudioAction(triggerAction)) {
bool mute = triggerAction == sceneTriggerAction::MUTE_SOURCE;
t = std::thread(muteThread, audioSource, duration, mute);
} else if (isSwitcherStatusAction(triggerAction)) {
bool stop = triggerAction == sceneTriggerAction::STOP_SWITCHER;
t = std::thread(statusThread, duration, stop);
} else {
t = std::thread(muteThread, audioSource, duration);
blog(LOG_WARNING, "ignoring unkown action '%d'",
static_cast<int>(triggerAction));
}
t.detach();
}
bool SceneTrigger::checkMatch(OBSWeakSource previousScene)
bool SceneTrigger::checkMatch(OBSWeakSource currentScene,
OBSWeakSource previousScene)
{
OBSSource source = obs_frontend_get_current_scene();
OBSWeakSource currentScene = obs_source_get_weak_source(source);
obs_source_release(source);
obs_weak_source_release(currentScene);
switch (triggerType) {
case sceneTriggerType::NONE:
return false;
@@ -245,16 +280,26 @@ bool SceneTrigger::checkMatch(OBSWeakSource previousScene)
void SwitcherData::checkTriggers()
{
if (SceneTrigger::pause || stop) {
if (SceneTrigger::pause) {
return;
}
OBSSource source = obs_frontend_get_current_scene();
OBSWeakSource currentScene = obs_source_get_weak_source(source);
for (auto &t : sceneTriggers) {
if (t.checkMatch(previousScene)) {
if (stop && !isSwitcherStatusAction(t.triggerAction)) {
continue;
}
if (t.checkMatch(currentScene, previousScene)) {
t.logMatch();
t.performAction();
}
}
obs_source_release(source);
obs_weak_source_release(currentScene);
}
void SwitcherData::saveSceneTriggers(obs_data_t *obj)
@@ -395,19 +440,12 @@ void AdvSceneSwitcher::setupTriggerTab()
void SceneTrigger::save(obs_data_t *obj)
{
obs_source_t *sceneSource = obs_weak_source_get_source(scene);
const char *sceneName = obs_source_get_name(sceneSource);
obs_data_set_string(obj, "scene", sceneName);
obs_source_release(sceneSource);
obs_data_set_string(obj, "scene", GetWeakSourceName(scene).c_str());
obs_data_set_int(obj, "triggerType", static_cast<int>(triggerType));
obs_data_set_int(obj, "triggerAction", static_cast<int>(triggerAction));
obs_data_set_double(obj, "duration", duration);
obs_source_t *source = obs_weak_source_get_source(audioSource);
const char *audioSourceName = obs_source_get_name(source);
obs_data_set_string(obj, "audioSource", audioSourceName);
obs_source_release(source);
obs_data_set_string(obj, "audioSource",
GetWeakSourceName(audioSource).c_str());
}
void SceneTrigger::load(obs_data_t *obj)
@@ -425,10 +463,13 @@ void SceneTrigger::load(obs_data_t *obj)
audioSource = GetWeakSourceByName(audioSourceName);
}
inline void populateTriggers(QComboBox *list)
static inline void populateTriggers(QComboBox *list)
{
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.none"));
AdvSceneSwitcher::addSelectionEntry(
list,
obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.none"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.sceneActive"));
list->addItem(obs_module_text(
@@ -439,8 +480,11 @@ inline void populateTriggers(QComboBox *list)
inline void populateActions(QComboBox *list)
{
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.none"));
AdvSceneSwitcher::addSelectionEntry(
list,
obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.none"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startRecording"));
list->addItem(obs_module_text(
@@ -459,6 +503,12 @@ inline void populateActions(QComboBox *list)
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopReplayBuffer"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.muteSource"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.unmuteSource"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startSwitcher"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopSwitcher"));
}
SceneTriggerWidget::SceneTriggerWidget(QWidget *parent, SceneTrigger *s)
@@ -495,7 +545,7 @@ SceneTriggerWidget::SceneTriggerWidget(QWidget *parent, SceneTrigger *s)
audioSources->setCurrentText(
GetWeakSourceName(s->audioSource).c_str());
if (s->triggerAction == sceneTriggerAction::MUTE_SOURCE) {
if (isAudioAction(s->triggerAction)) {
audioSources->show();
} else {
audioSources->hide();
@@ -560,7 +610,7 @@ void SceneTriggerWidget::TriggerActionChanged(int index)
static_cast<sceneTriggerAction>(index);
}
if (switchData->triggerAction == sceneTriggerAction::MUTE_SOURCE) {
if (isAudioAction(switchData->triggerAction)) {
audioSources->show();
} else {
audioSources->hide();

View File

@@ -125,9 +125,16 @@ void SwitcherData::checkAudioSwitch(bool &match, OBSWeakSource &scene,
continue;
}
obs_source_t *as = obs_weak_source_get_source(s.audioSource);
bool audioActive = obs_source_active(as);
obs_source_release(as);
if (s.ignoreInactiveSource) {
obs_source_t *as =
obs_weak_source_get_source(s.audioSource);
bool audioActive = obs_source_active(as);
obs_source_release(as);
if (!audioActive) {
continue;
}
}
// peak will have a value from -60 db to 0 db
bool volumeThresholdreached = false;
@@ -154,7 +161,7 @@ void SwitcherData::checkAudioSwitch(bool &match, OBSWeakSource &scene,
1000.0 >=
s.duration;
if (volumeThresholdreached && durationReached && audioActive) {
if (volumeThresholdreached && durationReached) {
if (match) {
checkAudioSwitchFallback(scene, transition);
fallbackChecked = true;
@@ -250,7 +257,7 @@ void AudioSwitch::setVolumeLevel(void *data,
UNUSED_PARAMETER(inputPeak);
AudioSwitch *s = static_cast<AudioSwitch *>(data);
for (int i = 1; i < MAX_AUDIO_CHANNELS; i++) {
for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) {
if (peak[i] > s->peak) {
s->peak = peak[i];
}
@@ -301,6 +308,7 @@ void AudioSwitch::save(obs_data_t *obj)
obs_data_set_int(obj, "volume", volumeThreshold);
obs_data_set_int(obj, "condition", condition);
obs_data_set_double(obj, "duration", duration);
obs_data_set_bool(obj, "ignoreInactiveSource", ignoreInactiveSource);
}
// To be removed in future version
@@ -346,6 +354,7 @@ void AudioSwitch::load(obs_data_t *obj)
volumeThreshold = obs_data_get_int(obj, "volume");
condition = (audioCondition)obs_data_get_int(obj, "condition");
duration = obs_data_get_double(obj, "duration");
ignoreInactiveSource = obs_data_get_bool(obj, "ignoreInactiveSource");
volmeter = AddVolmeterToSource(this, audioSource);
}
@@ -438,7 +447,7 @@ void swap(AudioSwitch &first, AudioSwitch &second)
second.resetVolmeter();
}
void populateConditionSelection(QComboBox *list)
static inline void populateConditionSelection(QComboBox *list)
{
list->addItem(
obs_module_text("AdvSceneSwitcher.audioTab.condition.above"));
@@ -453,6 +462,8 @@ AudioSwitchWidget::AudioSwitchWidget(QWidget *parent, AudioSwitch *s)
condition = new QComboBox();
audioVolumeThreshold = new QSpinBox();
duration = new QDoubleSpinBox();
ignoreInactiveSource = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.audioTab.ignoreInactiveSource"));
obs_source_t *source = nullptr;
if (s) {
@@ -482,6 +493,8 @@ AudioSwitchWidget::AudioSwitchWidget(QWidget *parent, AudioSwitch *s)
QWidget::connect(audioSources,
SIGNAL(currentTextChanged(const QString &)), this,
SLOT(SourceChanged(const QString &)));
QWidget::connect(ignoreInactiveSource, SIGNAL(stateChanged(int)), this,
SLOT(IgnoreInactiveChanged(int)));
AdvSceneSwitcher::populateAudioSelection(audioSources);
populateConditionSelection(condition);
@@ -492,6 +505,7 @@ AudioSwitchWidget::AudioSwitchWidget(QWidget *parent, AudioSwitch *s)
audioVolumeThreshold->setValue(s->volumeThreshold);
condition->setCurrentIndex(s->condition);
duration->setValue(s->duration);
ignoreInactiveSource->setChecked(s->ignoreInactiveSource);
}
QHBoxLayout *switchLayout = new QHBoxLayout;
@@ -500,6 +514,7 @@ AudioSwitchWidget::AudioSwitchWidget(QWidget *parent, AudioSwitch *s)
{"{{volumeWidget}}", audioVolumeThreshold},
{"{{condition}}", condition},
{"{{duration}}", duration},
{"{{ignoreInactiveSource}}", ignoreInactiveSource},
{"{{scenes}}", scenes},
{"{{transitions}}", transitions}};
placeWidgets(obs_module_text("AdvSceneSwitcher.audioTab.entry"),
@@ -599,6 +614,16 @@ void AudioSwitchWidget::DurationChanged(double dur)
switchData->duration = dur;
}
void AudioSwitchWidget::IgnoreInactiveChanged(int state)
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switchData->ignoreInactiveSource = state;
}
AudioSwitchFallbackWidget::AudioSwitchFallbackWidget(QWidget *parent,
AudioSwitchFallback *s)
: SwitchWidget(parent, s, true, true)

View File

@@ -118,29 +118,37 @@ void SwitcherData::writeToStatusFile(QString msg)
void SwitcherData::checkSwitchInfoFromFile(bool &match, OBSWeakSource &scene,
OBSWeakSource &transition)
{
if (!fileIO.readEnabled || fileIO.readPath.empty()) {
if (!fileIO.readEnabled || fileIO.readPath.empty() ||
FileSwitch::pause) {
return;
}
QFile file(QString::fromStdString(fileIO.readPath));
if (file.open(QIODevice::ReadOnly)) {
QTextStream in(&file);
QString sceneStr = in.readLine();
obs_source_t *sceneRead =
obs_get_source_by_name(sceneStr.toUtf8().constData());
if (sceneRead) {
obs_weak_source_t *sceneReadWs =
obs_source_get_weak_source(sceneRead);
match = true;
scene = sceneReadWs;
transition = nullptr;
obs_weak_source_release(sceneReadWs);
obs_source_release(sceneRead);
}
file.close();
if (!file.open(QIODevice::ReadOnly)) {
return;
}
QTextStream in(&file);
QString sceneStr = in.readLine();
OBSWeakSource sceneRead = GetWeakSourceByQString(sceneStr);
QString transitionStr = in.readLine();
OBSWeakSource transitionRead =
GetWeakTransitionByQString(transitionStr);
if (sceneRead) {
match = true;
scene = sceneRead;
transition = transitionRead;
vblog(LOG_INFO,
"match for 'file' - read scene '%s' and transition '%s' from file '%s'",
sceneStr.toUtf8().constData(),
transitionStr.toUtf8().constData(),
fileIO.readPath.c_str());
}
file.close();
}
static size_t WriteCallback(void *contents, size_t size, size_t nmemb,

View File

@@ -5,14 +5,15 @@ bool SceneSwitcherEntry::initialized()
{
return (usePreviousScene || WeakSourceValid(scene) ||
SceneGroupValid(group)) &&
transition;
(useCurrentTransition || transition);
}
bool SceneSwitcherEntry::valid()
{
return !initialized() || ((usePreviousScene || WeakSourceValid(scene) ||
SceneGroupValid(group)) &&
WeakSourceValid(transition));
return !initialized() ||
((usePreviousScene || WeakSourceValid(scene) ||
SceneGroupValid(group)) &&
(useCurrentTransition || WeakSourceValid(transition)));
}
void SceneSwitcherEntry::logMatchScene()
@@ -82,8 +83,11 @@ void SceneSwitcherEntry::save(obs_data_t *obj, const char *targetTypeSaveName,
obs_data_set_string(obj, targetSaveName, targetName.c_str());
obs_data_set_string(obj, transitionSaveName,
GetWeakSourceName(transition).c_str());
std::string transitionName = current_transition_name;
if (!useCurrentTransition) {
transitionName = GetWeakSourceName(transition);
}
obs_data_set_string(obj, transitionSaveName, transitionName.c_str());
}
void SceneSwitcherEntry::load(obs_data_t *obj, const char *targetTypeLoadName,
@@ -104,11 +108,14 @@ void SceneSwitcherEntry::load(obs_data_t *obj, const char *targetTypeLoadName,
group = GetSceneGroupByName(targetName);
}
usePreviousScene = strcmp(targetName, previous_scene_name) == 0;
const char *transitionName =
obs_data_get_string(obj, transitionLoadName);
transition = GetWeakTransitionByName(transitionName);
usePreviousScene = strcmp(targetName, previous_scene_name) == 0;
useCurrentTransition =
strcmp(transitionName, current_transition_name) == 0;
}
void SwitchWidget::SceneGroupAdd(const QString &name)
@@ -166,7 +173,8 @@ void SwitchWidget::SceneGroupRename(const QString &oldName,
}
SwitchWidget::SwitchWidget(QWidget *parent, SceneSwitcherEntry *s,
bool usePreviousScene, bool addSceneGroup)
bool usePreviousScene, bool addSceneGroup,
bool addCurrentTransition)
{
scenes = new QComboBox();
transitions = new QComboBox();
@@ -194,7 +202,8 @@ SwitchWidget::SwitchWidget(QWidget *parent, SceneSwitcherEntry *s,
AdvSceneSwitcher::populateSceneSelection(scenes, usePreviousScene,
addSceneGroup);
AdvSceneSwitcher::populateTransitionSelection(transitions);
AdvSceneSwitcher::populateTransitionSelection(transitions,
addCurrentTransition);
switchData = s;
showSwitchData();
@@ -218,6 +227,10 @@ void SwitchWidget::showSwitchData()
transitions->setCurrentText(
GetWeakSourceName(switchData->transition).c_str());
if (switchData->useCurrentTransition) {
transitions->setCurrentText(
obs_module_text("AdvSceneSwitcher.currentTransition"));
}
if (switchData->usePreviousScene) {
scenes->setCurrentText(obs_module_text(
@@ -278,4 +291,5 @@ void SwitchWidget::TransitionChanged(const QString &text)
std::lock_guard<std::mutex> lock(switcher->m);
switchData->transition = GetWeakTransitionByQString(text);
switchData->useCurrentTransition = switchData->transition == nullptr;
}

633
src/switch-network.cpp Normal file
View File

@@ -0,0 +1,633 @@
/*
Most of this code is based on https://github.com/Palakis/obs-websocket
*/
#include <QtWidgets/QMainWindow>
#include <QtConcurrent/QtConcurrent>
#include <QTime>
#include <QMessageBox>
#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<QMainWindow *>(
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(switcher->m);
switcher->networkConfig.ServerPort = value;
}
void AdvSceneSwitcher::on_lockToIPv4_stateChanged(int state)
{
if (loading) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switcher->networkConfig.LockToIPv4 = state;
}
void AdvSceneSwitcher::on_serverRestart_clicked()
{
if (loading) {
return;
}
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(switcher->m);
switcher->networkConfig.Address = text.toUtf8().constData();
}
void AdvSceneSwitcher::on_clientPort_valueChanged(int value)
{
if (loading) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switcher->networkConfig.ClientPort = value;
}
void AdvSceneSwitcher::on_restrictSend_stateChanged(int state)
{
if (loading) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switcher->networkConfig.SendAll = !state;
}
void AdvSceneSwitcher::on_clientReconnect_clicked()
{
if (loading) {
return;
}
std::lock_guard<std::mutex> 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;
}
}

View File

@@ -87,6 +87,7 @@ void resetPause()
IdleData::pause = false;
SceneSequenceSwitch::pause = false;
AudioSwitch::pause = false;
VideoSwitch::pause = false;
}
void setPauseTarget(PauseTarget &target, bool &verbose)
@@ -139,6 +140,10 @@ void setPauseTarget(PauseTarget &target, bool &verbose)
vblog(LOG_INFO, "pause audio switching");
AudioSwitch::pause = true;
break;
case PauseTarget::Video:
vblog(LOG_INFO, "pause video switching");
VideoSwitch::pause = true;
break;
}
}
@@ -340,6 +345,7 @@ void populatePauseTargets(QComboBox *list)
list->addItem(
obs_module_text("AdvSceneSwitcher.sceneSequenceTab.title"));
list->addItem(obs_module_text("AdvSceneSwitcher.audioTab.title"));
list->addItem(obs_module_text("AdvSceneSwitcher.VideoTab.title"));
}
PauseEntryWidget::PauseEntryWidget(QWidget *parent, PauseEntry *s)

View File

@@ -55,15 +55,18 @@ void AdvSceneSwitcher::on_priorityDown_clicked()
bool SwitcherData::prioFuncsValid()
{
auto it = std::unique(functionNamesByPriority.begin(),
functionNamesByPriority.end());
bool wasUnique = (it == functionNamesByPriority.end());
auto fNBPCopy = functionNamesByPriority;
std::sort(fNBPCopy.begin(), fNBPCopy.end());
auto it = std::unique(fNBPCopy.begin(), fNBPCopy.end());
bool wasUnique = (it == fNBPCopy.end());
if (!wasUnique) {
return false;
}
for (int p : functionNamesByPriority) {
if (p < 0 || p > 8) {
if (p < 0 || p > 9) {
return false;
}
}

View File

@@ -52,8 +52,15 @@ void SwitcherData::checkRandom(bool &match, OBSWeakSource &scene,
continue;
}
if (r.scene == lastRandomScene && randomSwitches.size() != 1) {
continue;
if (randomSwitches.size() != 1) {
if (r.targetType == SwitchTargetType::Scene &&
r.scene == lastRandomScene) {
continue;
} else if (r.targetType ==
SwitchTargetType::SceneGroup &&
r.group && r.group == lastRandomSceneGroup) {
continue;
}
}
scene = r.getScene();
@@ -61,6 +68,7 @@ void SwitcherData::checkRandom(bool &match, OBSWeakSource &scene,
delay = (int)r.delay * 1000;
match = true;
lastRandomScene = r.scene;
lastRandomSceneGroup = r.group;
if (verbose) {
r.logMatch();
@@ -142,7 +150,7 @@ void RandomSwitch::load(obs_data_t *obj)
}
RandomSwitchWidget::RandomSwitchWidget(QWidget *parent, RandomSwitch *s)
: SwitchWidget(parent, s, false, false)
: SwitchWidget(parent, s, false, true)
{
delay = new QDoubleSpinBox();

View File

@@ -353,7 +353,8 @@ ScreenRegionWidget::ScreenRegionWidget(QWidget *parent, ScreenRegionSwitch *s)
AdvSceneSwitcher::populateSceneSelection(
excludeScenes, false, false, true,
obs_module_text(
"AdvSceneSwitcher.screenRegionTab.excludeScenes.None"));
"AdvSceneSwitcher.screenRegionTab.excludeScenes.None"),
true);
if (s) {
excludeScenes->setCurrentText(

View File

@@ -4,6 +4,8 @@
#include "headers/advanced-scene-switcher.hpp"
#include "headers/utility.hpp"
constexpr auto max_extend_text_size = 150;
bool SceneSequenceSwitch::pause = false;
static QMetaObject::Connection addPulse;
@@ -28,20 +30,10 @@ void AdvSceneSwitcher::on_sceneSequenceRemove_clicked()
}
{
// Might be in waiting state of sequence
// causing invalid access after wakeup
// thus we need to stop the main thread before delete
bool wasRunning = !switcher->stop;
switcher->Stop();
std::lock_guard<std::mutex> lock(switcher->m);
int idx = ui->sceneSequenceSwitches->currentRow();
auto &switches = switcher->sceneSequenceSwitches;
switches.erase(switches.begin() + idx);
if (wasRunning) {
switcher->Start();
}
}
delete item;
@@ -140,18 +132,8 @@ void AdvSceneSwitcher::on_sceneSequenceLoad_clicked()
return;
}
// Might be in waiting state of sequence
// causing invalid access after wakeup
// thus we need to stop the main thread before delete
bool wasRunning = !switcher->stop;
switcher->Stop();
switcher->loadSceneSequenceSwitches(obj);
if (wasRunning) {
switcher->Start();
}
obs_data_release(obj);
DisplayMessage(obs_module_text(
@@ -159,30 +141,42 @@ void AdvSceneSwitcher::on_sceneSequenceLoad_clicked()
close();
}
bool matchInterruptible(SwitcherData *switcher, SceneSequenceSwitch &s)
void AdvSceneSwitcher::OpenSequenceExtendEdit(SequenceWidget *sw)
{
bool durationReached = s.matchCount * (switcher->interval / 1000.0) >=
s.delay;
QDialog edit;
SequenceWidget editWidget(this, sw->getSwitchData(), false, true,
false);
QHBoxLayout layout;
layout.setSizeConstraint(QLayout::SetFixedSize);
layout.addWidget(&editWidget);
edit.setLayout(&layout);
edit.setWindowTitle(obs_module_text(
"AdvSceneSwitcher.sceneSequenceTab.extendEdit"));
edit.exec();
s.matchCount++;
if (durationReached) {
return true;
}
return false;
sw->UpdateWidgetStatus(true);
}
bool matchUninterruptible(SwitcherData *switcher, SceneSequenceSwitch &s,
obs_source_t *currentSource, int &linger)
void AdvSceneSwitcher::on_sequenceEdit_clicked()
{
// scene was already active for the previous cycle so remove this time
int dur = s.delay * 1000 - switcher->interval;
if (dur > 0) {
switcher->waitScene = currentSource;
linger = dur;
int index = ui->sceneSequenceSwitches->currentRow();
if (index == -1) {
return;
}
return true;
SequenceWidget *currentWidget =
(SequenceWidget *)ui->sceneSequenceSwitches->itemWidget(
ui->sceneSequenceSwitches->item(index));
OpenSequenceExtendEdit(currentWidget);
}
void AdvSceneSwitcher::on_sceneSequenceSwitches_itemDoubleClicked(
QListWidgetItem *item)
{
SequenceWidget *currentWidget =
(SequenceWidget *)ui->sceneSequenceSwitches->itemWidget(item);
OpenSequenceExtendEdit(currentWidget);
}
void SwitcherData::checkSceneSequence(bool &match, OBSWeakSource &scene,
@@ -192,38 +186,53 @@ void SwitcherData::checkSceneSequence(bool &match, OBSWeakSource &scene,
return;
}
obs_source_t *currentSource = obs_frontend_get_current_scene();
obs_weak_source_t *ws = obs_source_get_weak_source(currentSource);
obs_source_t *currentSceneSource = obs_frontend_get_current_scene();
obs_weak_source_t *currentScene =
obs_source_get_weak_source(currentSceneSource);
for (SceneSequenceSwitch &s : sceneSequenceSwitches) {
if (!s.initialized()) {
// Continue the active uninterruptible sequence and skip others
if (uninterruptibleSceneSequenceActive &&
s.activeSequence == nullptr) {
continue;
}
if (s.startScene == ws) {
if (!match) {
if (s.interruptible) {
match = matchInterruptible(switcher, s);
} else {
match = matchUninterruptible(
switcher, s, currentSource,
linger);
}
bool matched = s.checkMatch(currentScene, linger);
if (match) {
scene = s.getScene();
transition = s.transition;
if (switcher->verbose) {
s.logMatch();
}
if (!match && matched) {
match = matched;
if (s.activeSequence) {
scene = s.activeSequence->getScene();
transition = s.activeSequence->transition;
} else {
scene = s.getScene();
transition = s.transition;
if (verbose) {
s.logMatch();
}
}
} else {
s.matchCount = 0;
s.advanceActiveSequence();
if (verbose) {
s.logAdvanceSequence();
}
// Ignore other switching methods if sequence is not
// interruptible and has not reached its end
if (s.activeSequence) {
uninterruptibleSceneSequenceActive =
!s.interruptible;
}
}
}
obs_source_release(currentSource);
obs_weak_source_release(ws);
if (!match) {
uninterruptibleSceneSequenceActive = false;
}
obs_source_release(currentSceneSource);
obs_weak_source_release(currentScene);
}
void SwitcherData::saveSceneSequenceSwitches(obs_data_t *obj)
@@ -291,18 +300,32 @@ bool SceneSequenceSwitch::valid()
(SceneSwitcherEntry::valid() && WeakSourceValid(startScene));
}
void SceneSequenceSwitch::save(obs_data_t *obj)
void SceneSequenceSwitch::save(obs_data_t *obj, bool saveExt)
{
SceneSwitcherEntry::save(obj);
obs_data_set_int(obj, "startTargetType",
static_cast<int>(startTargetType));
obs_data_set_string(obj, "startScene",
GetWeakSourceName(startScene).c_str());
obs_data_set_double(obj, "delay", delay);
obs_data_set_int(obj, "delayMultiplier", delayMultiplier);
obs_data_set_bool(obj, "interruptible", interruptible);
if (saveExt) {
auto cur = extendedSequence.get();
obs_data_array_t *extendScenes = obs_data_array_create();
while (cur) {
obs_data_t *array_obj = obs_data_create();
cur->save(array_obj, false);
obs_data_array_push_back(extendScenes, array_obj);
obs_data_release(array_obj);
cur = cur->extendedSequence.get();
}
obs_data_set_array(obj, "extendScenes", extendScenes);
obs_data_array_release(extendScenes);
}
}
// To be removed in future version
@@ -341,14 +364,15 @@ bool loadOldScequence(obs_data_t *obj, SceneSequenceSwitch *s)
return true;
}
void SceneSequenceSwitch::load(obs_data_t *obj)
void SceneSequenceSwitch::load(obs_data_t *obj, bool saveExt)
{
if (loadOldScequence(obj, this)) {
return;
}
SceneSwitcherEntry::load(obj);
startTargetType = static_cast<SwitchTargetType>(
obs_data_get_int(obj, "startTargetType"));
const char *scene = obs_data_get_string(obj, "startScene");
startScene = GetWeakSourceByName(scene);
@@ -360,6 +384,220 @@ void SceneSequenceSwitch::load(obs_data_t *obj)
delayMultiplier = 1;
interruptible = obs_data_get_bool(obj, "interruptible");
if (saveExt) {
auto cur = this;
obs_data_array_t *extendScenes =
obs_data_get_array(obj, "extendScenes");
size_t count = obs_data_array_count(extendScenes);
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj =
obs_data_array_item(extendScenes, i);
cur->extendedSequence =
std::make_unique<SceneSequenceSwitch>();
cur->extendedSequence->load(array_obj, false);
cur = cur->extendedSequence.get();
obs_data_release(array_obj);
}
obs_data_array_release(extendScenes);
}
}
bool SceneSequenceSwitch::reduce()
{
// Reset activeSequence just in case it was one of the deleted entries
activeSequence = nullptr;
if (!extendedSequence) {
return true;
}
if (extendedSequence->reduce()) {
extendedSequence.reset(nullptr);
}
return false;
}
SceneSequenceSwitch *SceneSequenceSwitch::extend()
{
if (extendedSequence) {
return extendedSequence->extend();
}
extendedSequence = std::make_unique<SceneSequenceSwitch>();
extendedSequence->startScene = scene;
return extendedSequence.get();
}
bool SceneSequenceSwitch::checkMatch(OBSWeakSource currentScene, int &linger,
SceneSequenceSwitch *root)
{
if (!initialized()) {
if (root) {
root->activeSequence = nullptr;
}
return false;
}
bool match = false;
if (activeSequence) {
return activeSequence->checkMatch(currentScene, linger, this);
}
if (startScene == currentScene) {
if (interruptible) {
match = checkDurationMatchInterruptible();
} else {
match = true;
prepareUninterruptibleMatch(currentScene, linger);
}
} else {
matchCount = 0;
if (root) {
root->activeSequence = nullptr;
logSequenceCanceled();
}
}
return match;
}
bool SceneSequenceSwitch::checkDurationMatchInterruptible()
{
bool durationReached = matchCount * (switcher->interval / 1000.0) >=
delay;
matchCount++;
if (durationReached) {
matchCount = 0;
return true;
}
return false;
}
void SceneSequenceSwitch::prepareUninterruptibleMatch(
OBSWeakSource currentScene, int &linger)
{
int dur = delay * 1000;
if (dur > 0) {
switcher->waitScene = obs_weak_source_get_source(currentScene);
obs_source_release(switcher->waitScene);
linger = dur;
}
}
void SceneSequenceSwitch::advanceActiveSequence()
{
// Set start Scene
OBSWeakSource currentSceneGroupScene = nullptr;
if (targetType == SwitchTargetType::SceneGroup && group) {
currentSceneGroupScene = group->getCurrentScene();
}
if (activeSequence) {
activeSequence = activeSequence->extendedSequence.get();
} else {
activeSequence = extendedSequence.get();
}
if (activeSequence) {
if (activeSequence->startTargetType ==
SwitchTargetType::SceneGroup) {
activeSequence->startScene = currentSceneGroupScene;
}
if (activeSequence->targetType == SwitchTargetType::Scene &&
!activeSequence->scene) {
blog(LOG_WARNING,
"cannot advance sequence - null scene set");
activeSequence = nullptr;
return;
}
if (activeSequence->targetType ==
SwitchTargetType::SceneGroup &&
activeSequence->group &&
activeSequence->group->scenes.empty()) {
blog(LOG_WARNING,
"cannot advance sequence - no scenes specified in '%s'",
activeSequence->group->name.c_str());
activeSequence = nullptr;
return;
}
// Reinit old matchCount value in case it was previously set
activeSequence->matchCount = 0;
}
}
void SceneSequenceSwitch::logAdvanceSequence()
{
if (activeSequence) {
std::string targetName =
GetWeakSourceName(activeSequence->scene);
if (activeSequence->targetType ==
SwitchTargetType::SceneGroup &&
activeSequence->group) {
targetName = activeSequence->group->name;
}
blog(LOG_INFO, "continuing sequence with '%s' -> '%s'",
GetWeakSourceName(activeSequence->startScene).c_str(),
targetName.c_str());
}
}
void SceneSequenceSwitch::logSequenceCanceled()
{
blog(LOG_INFO, "unexpected scene change - cancel sequence");
}
QString delayMultiplierToString(int delayMultiplier)
{
switch (delayMultiplier) {
case 1:
return obs_module_text("AdvSceneSwitcher.unit.secends");
case 60:
return obs_module_text("AdvSceneSwitcher.unit.minutes");
case 60 * 60:
return obs_module_text("AdvSceneSwitcher.unit.hours");
}
return "???";
}
QString makeExtendText(SceneSequenceSwitch *s, int curLen = 0)
{
if (!s) {
return "";
}
QString ext = "";
ext = QString::number(s->delay / s->delayMultiplier) + " ";
ext += delayMultiplierToString(s->delayMultiplier);
QString sceneName = GetWeakSourceName(s->scene).c_str();
if (s->targetType == SwitchTargetType::SceneGroup && s->group) {
sceneName = QString::fromStdString(s->group->name);
}
if (sceneName.isEmpty()) {
sceneName = obs_module_text("AdvSceneSwitcher.selectScene");
}
ext += " -> [" + sceneName + "]";
if (ext.length() + curLen > max_extend_text_size) {
return "...";
}
if (s->extendedSequence.get()) {
return ext +=
" | " + makeExtendText(s->extendedSequence.get(),
curLen + ext.length());
} else {
return ext;
}
}
void populateDelayUnits(QComboBox *list)
@@ -369,14 +607,36 @@ void populateDelayUnits(QComboBox *list)
list->addItem(obs_module_text("AdvSceneSwitcher.unit.hours"));
}
SequenceWidget::SequenceWidget(QWidget *parent, SceneSequenceSwitch *s)
: SwitchWidget(parent, s, true, true)
SequenceWidget::SequenceWidget(QWidget *parent, SceneSequenceSwitch *s,
bool extendSequence, bool editExtendMode,
bool showExtendText)
: SwitchWidget(parent, s, !extendSequence, true)
{
this->setParent(parent);
delay = new QDoubleSpinBox();
delayUnits = new QComboBox();
startScenes = new QComboBox();
interruptible = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.sceneSequenceTab.interruptible"));
extendText = new QLabel();
extend = new QPushButton();
reduce = new QPushButton();
extend->setProperty("themeID",
QVariant(QStringLiteral("addIconSmall")));
reduce->setProperty("themeID",
QVariant(QStringLiteral("removeIconSmall")));
extend->setMaximumSize(22, 22);
reduce->setMaximumSize(22, 22);
// We need to extend the generic SwitchWidget::SceneChanged()
// with our own SequenceWidget::SceneChanged()
// so the old singal / slot needs to be disconnected
scenes->disconnect();
QWidget::connect(scenes, SIGNAL(currentTextChanged(const QString &)),
this, SLOT(SceneChanged(const QString &)));
QWidget::connect(delay, SIGNAL(valueChanged(double)), this,
SLOT(DelayChanged(double)));
@@ -388,48 +648,80 @@ SequenceWidget::SequenceWidget(QWidget *parent, SceneSequenceSwitch *s)
QWidget::connect(interruptible, SIGNAL(stateChanged(int)), this,
SLOT(InterruptibleChanged(int)));
QWidget::connect(extend, SIGNAL(clicked()), this,
SLOT(ExtendClicked()));
QWidget::connect(reduce, SIGNAL(clicked()), this,
SLOT(ReduceClicked()));
delay->setMaximum(99999.000000);
AdvSceneSwitcher::populateSceneSelection(startScenes, false);
populateDelayUnits(delayUnits);
interruptible->setToolTip(obs_module_text(
"AdvSceneSwitcher.sceneSequenceTab.interruptibleHint"));
if (s) {
switch (s->delayMultiplier) {
case 1:
delayUnits->setCurrentIndex(0);
break;
case 60:
delayUnits->setCurrentIndex(1);
break;
case 3600:
delayUnits->setCurrentIndex(2);
break;
default:
delayUnits->setCurrentIndex(0);
// The extended sequence widgets never exist on their own and are always
// place inside a non-extend sequence widget
if (extendSequence) {
QHBoxLayout *mainLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{scenes}}", scenes},
{"{{delay}}", delay},
{"{{delayUnits}}", delayUnits},
{"{{transitions}}", transitions}};
placeWidgets(
obs_module_text(
"AdvSceneSwitcher.sceneSequenceTab.extendEntry"),
mainLayout, widgetPlaceholders);
setLayout(mainLayout);
} else {
QHBoxLayout *startSequence = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{startScenes}}", startScenes},
{"{{scenes}}", scenes},
{"{{delay}}", delay},
{"{{delayUnits}}", delayUnits},
{"{{transitions}}", transitions},
{"{{interruptible}}", interruptible}};
placeWidgets(obs_module_text(
"AdvSceneSwitcher.sceneSequenceTab.entry"),
startSequence, widgetPlaceholders);
// The exetend widgets placed here
extendSequenceLayout = new QVBoxLayout;
if (s) {
if (!editExtendMode) {
extendText->setText(makeExtendText(
s->extendedSequence.get()));
} else {
auto cur = s->extendedSequence.get();
while (cur != nullptr) {
extendSequenceLayout->addWidget(
new SequenceWidget(parent, cur,
true, true));
cur = cur->extendedSequence.get();
}
}
}
startScenes->setCurrentText(
GetWeakSourceName(s->startScene).c_str());
interruptible->setChecked(s->interruptible);
QHBoxLayout *extendSequenceControlsLayout = new QHBoxLayout;
if (editExtendMode) {
extendSequenceControlsLayout->addWidget(extend);
extendSequenceControlsLayout->addWidget(reduce);
}
extendSequenceControlsLayout->addStretch();
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(startSequence);
mainLayout->addLayout(extendSequenceLayout);
mainLayout->addWidget(extendText);
mainLayout->addLayout(extendSequenceControlsLayout);
setLayout(mainLayout);
}
QHBoxLayout *mainLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{startScenes}}", startScenes},
{"{{scenes}}", scenes},
{"{{delay}}", delay},
{"{{delayUnits}}", delayUnits},
{"{{transitions}}", transitions},
{"{{interruptible}}", interruptible}};
placeWidgets(obs_module_text("AdvSceneSwitcher.sceneSequenceTab.entry"),
mainLayout, widgetPlaceholders);
setLayout(mainLayout);
switchData = s;
UpdateWidgetStatus(showExtendText);
loading = false;
UpdateDelay();
}
SceneSequenceSwitch *SequenceWidget::getSwitchData()
@@ -493,6 +785,32 @@ void SequenceWidget::DelayUnitsChanged(int idx)
UpdateDelay();
}
void SequenceWidget::setExtendedSequenceStartScene()
{
switchData->extendedSequence->startScene = switchData->scene;
switchData->extendedSequence->startTargetType = SwitchTargetType::Scene;
if (switchData->targetType == SwitchTargetType::SceneGroup) {
switchData->extendedSequence->startScene = nullptr;
switchData->extendedSequence->startTargetType =
SwitchTargetType::SceneGroup;
}
}
void SequenceWidget::SceneChanged(const QString &text)
{
if (loading || !switchData) {
return;
}
SwitchWidget::SceneChanged(text);
std::lock_guard<std::mutex> lock(switcher->m);
if (switchData->extendedSequence) {
setExtendedSequenceStartScene();
}
}
void SequenceWidget::StartSceneChanged(const QString &text)
{
if (loading || !switchData) {
@@ -511,4 +829,69 @@ void SequenceWidget::InterruptibleChanged(int state)
std::lock_guard<std::mutex> lock(switcher->m);
switchData->interruptible = state;
auto cur = switchData->extendedSequence.get();
while (cur != nullptr) {
cur->interruptible = state;
cur = cur->extendedSequence.get();
}
}
void SequenceWidget::UpdateWidgetStatus(bool showExtendText)
{
if (showExtendText) {
extendText->setText(
makeExtendText(switchData->extendedSequence.get()));
}
switch (switchData->delayMultiplier) {
case 1:
delayUnits->setCurrentIndex(0);
break;
case 60:
delayUnits->setCurrentIndex(1);
break;
case 3600:
delayUnits->setCurrentIndex(2);
break;
default:
delayUnits->setCurrentIndex(0);
}
UpdateDelay();
startScenes->setCurrentText(
GetWeakSourceName(switchData->startScene).c_str());
interruptible->setChecked(switchData->interruptible);
SwitchWidget::showSwitchData();
}
void SequenceWidget::ExtendClicked()
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
auto es = switchData->extend();
SequenceWidget *ew = new SequenceWidget(this->parentWidget(), es, true);
extendSequenceLayout->addWidget(ew);
}
void SequenceWidget::ReduceClicked()
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switchData->reduce();
int count = extendSequenceLayout->count();
auto item = extendSequenceLayout->takeAt(count - 1);
if (item) {
item->widget()->setVisible(false);
delete item;
}
}

View File

@@ -241,7 +241,7 @@ void TimeSwitch::load(obs_data_t *obj)
time = QTime::fromString(obs_data_get_string(obj, "time"));
}
inline void populateTriggers(QComboBox *list)
static inline void populateTriggers(QComboBox *list)
{
list->addItem(obs_module_text("AdvSceneSwitcher.timeTab.anyDay"));
list->addItem(obs_module_text("AdvSceneSwitcher.timeTab.mondays"));

View File

@@ -1,7 +1,11 @@
#include <thread>
#include "headers/advanced-scene-switcher.hpp"
#include "headers/utility.hpp"
constexpr auto default_def_transition_dealy = 50;
bool DefaultSceneTransition::pause = false;
unsigned int DefaultSceneTransition::delay = default_def_transition_dealy;
void AdvSceneSwitcher::on_transitionsAdd_clicked()
{
@@ -148,42 +152,28 @@ void AdvSceneSwitcher::on_defaultTransitionsDown_clicked()
switcher->defaultSceneTransitions[index + 1]);
}
void SwitcherData::checkDefaultSceneTransitions(bool &match,
OBSWeakSource &transition)
void SwitcherData::checkDefaultSceneTransitions()
{
if (checkedDefTransition || DefaultSceneTransition::pause) {
if (DefaultSceneTransition::pause || stop) {
return;
}
obs_source_t *currentSource = obs_frontend_get_current_scene();
obs_weak_source_t *ws = obs_source_get_weak_source(currentSource);
for (DefaultSceneTransition &s : defaultSceneTransitions) {
if (s.scene == ws) {
if (!s.initialized()) {
continue;
}
match = true;
transition = s.transition;
obs_source_t *currentSceneSource = obs_frontend_get_current_scene();
obs_weak_source_t *currentScene =
obs_source_get_weak_source(currentSceneSource);
for (auto &t : defaultSceneTransitions) {
if (t.checkMatch(currentScene)) {
if (verbose) {
s.logMatch();
t.logMatch();
}
t.setTransition();
break;
}
}
obs_source_release(currentSource);
obs_weak_source_release(ws);
checkedDefTransition = true;
}
void SwitcherData::setCurrentDefTransition(OBSWeakSource &transition)
{
obs_source_t *transitionSource = obs_weak_source_get_source(transition);
obs_frontend_set_current_transition(transitionSource);
obs_source_release(transitionSource);
obs_weak_source_release(currentScene);
obs_source_release(currentSceneSource);
}
void AdvSceneSwitcher::on_transitionOverridecheckBox_stateChanged(int state)
@@ -200,6 +190,16 @@ void AdvSceneSwitcher::on_transitionOverridecheckBox_stateChanged(int state)
}
}
void AdvSceneSwitcher::defTransitionDelayValueChanged(int value)
{
if (loading) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
DefaultSceneTransition::delay = value;
}
obs_weak_source_t *getNextTransition(obs_weak_source_t *scene1,
obs_weak_source_t *scene2)
{
@@ -334,6 +334,10 @@ void SwitcherData::saveSceneTransitions(obs_data_t *obj)
obs_data_set_bool(obj, "tansitionOverrideOverride",
switcher->tansitionOverrideOverride);
obs_data_set_default_int(obj, "defTransitionDelay",
default_def_transition_dealy);
obs_data_set_int(obj, "defTransitionDelay",
DefaultSceneTransition::delay);
}
void SwitcherData::loadSceneTransitions(obs_data_t *obj)
@@ -386,6 +390,8 @@ void SwitcherData::loadSceneTransitions(obs_data_t *obj)
switcher->tansitionOverrideOverride =
obs_data_get_bool(obj, "tansitionOverrideOverride");
DefaultSceneTransition::delay =
obs_data_get_int(obj, "defTransitionDelay");
}
void AdvSceneSwitcher::setupTransitionsTab()
@@ -424,6 +430,24 @@ void AdvSceneSwitcher::setupTransitionsTab()
ui->transitionOverridecheckBox->setChecked(
switcher->tansitionOverrideOverride);
QSpinBox *defTransitionDelay = new QSpinBox();
defTransitionDelay->setSuffix("ms");
defTransitionDelay->setMinimum(50);
defTransitionDelay->setMaximum(10000);
defTransitionDelay->setValue(DefaultSceneTransition::delay);
defTransitionDelay->setToolTip(obs_module_text(
"AdvSceneSwitcher.transitionTab.defaultTransition.delay.help"));
QWidget::connect(defTransitionDelay, SIGNAL(valueChanged(int)), this,
SLOT(defTransitionDelayValueChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{defTransitionDelay}}", defTransitionDelay}};
placeWidgets(
obs_module_text(
"AdvSceneSwitcher.transitionTab.defaultTransition.delay"),
ui->defTransitionDelayLayout, widgetPlaceholders);
}
bool SceneTransition::initialized()
@@ -439,7 +463,7 @@ bool SceneTransition::valid()
TransitionSwitchWidget::TransitionSwitchWidget(QWidget *parent,
SceneTransition *s)
: SwitchWidget(parent, s, false)
: SwitchWidget(parent, s, false, false, false)
{
scenes2 = new QComboBox();
@@ -498,7 +522,7 @@ void TransitionSwitchWidget::Scene2Changed(const QString &text)
DefTransitionSwitchWidget::DefTransitionSwitchWidget(QWidget *parent,
DefaultSceneTransition *s)
: SwitchWidget(parent, s, false)
: SwitchWidget(parent, s, false, false, false)
{
QHBoxLayout *mainLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
@@ -533,3 +557,37 @@ void DefTransitionSwitchWidget::swapSwitchData(DefTransitionSwitchWidget *s1,
s1->setSwitchData(s2->getSwitchData());
s2->setSwitchData(t);
}
bool DefaultSceneTransition::checkMatch(OBSWeakSource currentScene)
{
return scene == currentScene;
}
void setTransitionDelayed(OBSWeakSource transition, unsigned int delay)
{
// A hardcoded delay of 50 ms before switching transition type is
// necessary due to OBS_FRONTEND_EVENT_SCENE_CHANGED seemingly firing a
// bit too early and thus leading to canceled transitions.
//
// The same is to be the case for OBS_FRONTEND_EVENT_TRANSITION_STOPPED.
//
// 50 ms was chosen as a default value as it seems to avoid the problem
// mentioned above and becuase that is the minimum value which can be
// chosen for the scene switcher's check interval.
// Thus it can be made sure that the delayed setting of the transition
// does not interfere with any new scene changes triggered by the scene
// switcher
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
obs_source_t *transitionSource = obs_weak_source_get_source(transition);
obs_frontend_set_current_transition(transitionSource);
obs_source_release(transitionSource);
}
void DefaultSceneTransition::setTransition()
{
std::thread t;
t = std::thread(setTransitionDelayed, transition, delay);
t.detach();
}

698
src/switch-video.cpp Normal file
View File

@@ -0,0 +1,698 @@
#include <QFileDialog>
#include <QBuffer>
#include <QToolTip>
#include <thread>
#include "headers/advanced-scene-switcher.hpp"
#include "headers/utility.hpp"
bool VideoSwitch::pause = false;
static QMetaObject::Connection addPulse;
static void ScreenshotTick(void *param, float);
AdvSSScreenshotObj::AdvSSScreenshotObj(obs_source_t *source)
: weakSource(OBSGetWeakRef(source))
{
obs_add_tick_callback(ScreenshotTick, this);
}
AdvSSScreenshotObj::~AdvSSScreenshotObj()
{
obs_enter_graphics();
gs_stagesurface_destroy(stagesurf);
gs_texrender_destroy(texrender);
obs_leave_graphics();
obs_remove_tick_callback(ScreenshotTick, this);
}
void AdvSSScreenshotObj::Screenshot()
{
OBSSource source = OBSGetStrongRef(weakSource);
if (source) {
cx = obs_source_get_base_width(source);
cy = obs_source_get_base_height(source);
} else {
obs_video_info ovi;
obs_get_video_info(&ovi);
cx = ovi.base_width;
cy = ovi.base_height;
}
if (!cx || !cy) {
blog(LOG_WARNING, "Cannot screenshot, invalid target size");
obs_remove_tick_callback(ScreenshotTick, this);
done = true;
return;
}
texrender = gs_texrender_create(GS_RGBA, GS_ZS_NONE);
stagesurf = gs_stagesurface_create(cx, cy, GS_RGBA);
gs_texrender_reset(texrender);
if (gs_texrender_begin(texrender, cx, cy)) {
vec4 zero;
vec4_zero(&zero);
gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0);
gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f);
gs_blend_state_push();
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
if (source) {
obs_source_inc_showing(source);
obs_source_video_render(source);
obs_source_dec_showing(source);
} else {
obs_render_main_texture();
}
gs_blend_state_pop();
gs_texrender_end(texrender);
}
}
void AdvSSScreenshotObj::Download()
{
gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender));
}
void AdvSSScreenshotObj::Copy()
{
uint8_t *videoData = nullptr;
uint32_t videoLinesize = 0;
image = QImage(cx, cy, QImage::Format::Format_RGBX8888);
if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) {
int linesize = image.bytesPerLine();
for (int y = 0; y < (int)cy; y++)
memcpy(image.scanLine(y),
videoData + (y * videoLinesize), linesize);
gs_stagesurface_unmap(stagesurf);
}
}
void AdvSSScreenshotObj::MarkDone()
{
time = std::chrono::high_resolution_clock::now();
done = true;
}
#define STAGE_SCREENSHOT 0
#define STAGE_DOWNLOAD 1
#define STAGE_COPY_AND_SAVE 2
#define STAGE_FINISH 3
static void ScreenshotTick(void *param, float)
{
AdvSSScreenshotObj *data =
reinterpret_cast<AdvSSScreenshotObj *>(param);
if (data->stage == STAGE_FINISH) {
return;
}
obs_enter_graphics();
switch (data->stage) {
case STAGE_SCREENSHOT:
data->Screenshot();
break;
case STAGE_DOWNLOAD:
data->Download();
break;
case STAGE_COPY_AND_SAVE:
data->Copy();
data->MarkDone();
obs_remove_tick_callback(ScreenshotTick, data);
break;
}
obs_leave_graphics();
data->stage++;
}
void AdvSceneSwitcher::on_videoAdd_clicked()
{
std::lock_guard<std::mutex> lock(switcher->m);
switcher->videoSwitches.emplace_back();
VideoSwitchWidget *sw =
new VideoSwitchWidget(this, &switcher->videoSwitches.back());
listAddClicked(ui->videoSwitches, sw, ui->videoAdd, &addPulse);
ui->videoHelp->setVisible(false);
}
void AdvSceneSwitcher::on_videoRemove_clicked()
{
QListWidgetItem *item = ui->videoSwitches->currentItem();
if (!item) {
return;
}
{
std::lock_guard<std::mutex> lock(switcher->m);
int idx = ui->videoSwitches->currentRow();
auto &switches = switcher->videoSwitches;
switches.erase(switches.begin() + idx);
}
delete item;
}
void AdvSceneSwitcher::on_videoUp_clicked()
{
int index = ui->videoSwitches->currentRow();
if (!listMoveUp(ui->videoSwitches)) {
return;
}
VideoSwitchWidget *s1 =
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
ui->videoSwitches->item(index));
VideoSwitchWidget *s2 =
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
ui->videoSwitches->item(index - 1));
VideoSwitchWidget::swapSwitchData(s1, s2);
std::lock_guard<std::mutex> lock(switcher->m);
std::swap(switcher->videoSwitches[index],
switcher->videoSwitches[index - 1]);
}
void AdvSceneSwitcher::on_videoDown_clicked()
{
int index = ui->videoSwitches->currentRow();
if (!listMoveDown(ui->videoSwitches)) {
return;
}
VideoSwitchWidget *s1 =
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
ui->videoSwitches->item(index));
VideoSwitchWidget *s2 =
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
ui->videoSwitches->item(index + 1));
VideoSwitchWidget::swapSwitchData(s1, s2);
std::lock_guard<std::mutex> lock(switcher->m);
std::swap(switcher->videoSwitches[index],
switcher->videoSwitches[index + 1]);
}
void AdvSceneSwitcher::on_getScreenshot_clicked()
{
QListWidgetItem *item = ui->videoSwitches->currentItem();
if (!item) {
return;
}
VideoSwitchWidget *sw =
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(item);
auto s = sw->getSwitchData();
if (!s || !s->videoSource) {
return;
}
auto source = obs_weak_source_get_source(s->videoSource);
auto screenshotData = std::make_unique<AdvSSScreenshotObj>(source);
obs_source_release(source);
QString filePath = QFileDialog::getSaveFileName(this);
if (filePath.isEmpty()) {
return;
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return;
}
// During selection of the save path enough time should usually have
// passed already
// Add this just in case ...
if (!screenshotData->done) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
if (!screenshotData->done) {
DisplayMessage("Failed to get screenshot of source!");
return;
}
screenshotData->image.save(file.fileName());
sw->SetFilePath(file.fileName());
}
void SwitcherData::checkVideoSwitch(bool &match, OBSWeakSource &scene,
OBSWeakSource &transition)
{
if (VideoSwitch::pause) {
return;
}
for (auto &s : videoSwitches) {
bool matched = s.checkMatch();
if (!match && matched) {
match = true;
scene = s.getScene();
transition = s.transition;
if (verbose) {
s.logMatch();
}
}
}
}
void SwitcherData::saveVideoSwitches(obs_data_t *obj)
{
obs_data_array_t *videoArray = obs_data_array_create();
for (VideoSwitch &s : videoSwitches) {
obs_data_t *array_obj = obs_data_create();
s.save(array_obj);
obs_data_array_push_back(videoArray, array_obj);
obs_data_release(array_obj);
}
obs_data_set_array(obj, "videoSwitches", videoArray);
obs_data_array_release(videoArray);
}
void SwitcherData::loadVideoSwitches(obs_data_t *obj)
{
switcher->videoSwitches.clear();
obs_data_array_t *videoArray = obs_data_get_array(obj, "videoSwitches");
size_t count = obs_data_array_count(videoArray);
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(videoArray, i);
switcher->videoSwitches.emplace_back();
videoSwitches.back().load(array_obj);
obs_data_release(array_obj);
}
obs_data_array_release(videoArray);
}
void AdvSceneSwitcher::setupVideoTab()
{
for (auto &s : switcher->videoSwitches) {
QListWidgetItem *item;
item = new QListWidgetItem(ui->videoSwitches);
ui->videoSwitches->addItem(item);
VideoSwitchWidget *sw = new VideoSwitchWidget(this, &s);
item->setSizeHint(sw->minimumSizeHint());
ui->videoSwitches->setItemWidget(item, sw);
}
if (switcher->videoSwitches.size() == 0) {
addPulse = PulseWidget(ui->videoAdd, QColor(Qt::green));
ui->videoHelp->setVisible(true);
} else {
ui->videoHelp->setVisible(false);
}
ui->getScreenshot->setToolTip(
obs_module_text("AdvSceneSwitcher.videoTab.getScreenshotHelp"));
}
bool VideoSwitch::initialized()
{
return SceneSwitcherEntry::initialized() && videoSource;
}
bool VideoSwitch::valid()
{
return !initialized() ||
(SceneSwitcherEntry::valid() && WeakSourceValid(videoSource));
}
void VideoSwitch::save(obs_data_t *obj)
{
SceneSwitcherEntry::save(obj);
obs_data_set_string(obj, "videoSource",
GetWeakSourceName(videoSource).c_str());
obs_data_set_int(obj, "condition", static_cast<int>(condition));
obs_data_set_double(obj, "duration", duration);
obs_data_set_string(obj, "filePath", file.c_str());
obs_data_set_bool(obj, "ignoreInactiveSource", ignoreInactiveSource);
}
void VideoSwitch::load(obs_data_t *obj)
{
SceneSwitcherEntry::load(obj);
const char *videoSourceName = obs_data_get_string(obj, "videoSource");
videoSource = GetWeakSourceByName(videoSourceName);
condition = static_cast<videoSwitchType>(
obs_data_get_int(obj, "condition"));
duration = obs_data_get_double(obj, "duration");
file = obs_data_get_string(obj, "filePath");
ignoreInactiveSource = obs_data_get_bool(obj, "ignoreInactiveSource");
if (condition != videoSwitchType::HAS_NOT_CHANGED) {
(void)loadImageFromFile();
}
}
void VideoSwitch::getScreenshot()
{
auto source = obs_weak_source_get_source(videoSource);
screenshotData = std::make_unique<AdvSSScreenshotObj>(source);
obs_source_release(source);
}
bool VideoSwitch::loadImageFromFile()
{
if (!matchImage.load(QString::fromStdString(file))) {
blog(LOG_WARNING, "Cannot load image data from file '%s'",
file.c_str());
return false;
}
matchImage =
matchImage.convertToFormat(QImage::Format::Format_RGBX8888);
return true;
}
bool VideoSwitch::checkMatch()
{
if (ignoreInactiveSource) {
obs_source_t *vs = obs_weak_source_get_source(videoSource);
bool videoActive = obs_source_active(vs);
obs_source_release(vs);
if (!videoActive) {
screenshotData.reset(nullptr);
return false;
}
}
bool match = false;
if (screenshotData) {
if (screenshotData->done) {
bool conditionMatch = false;
switch (condition) {
case videoSwitchType::MATCH:
conditionMatch = screenshotData->image ==
matchImage;
break;
case videoSwitchType::DIFFER:
conditionMatch = screenshotData->image !=
matchImage;
break;
case videoSwitchType::HAS_NOT_CHANGED:
conditionMatch = screenshotData->image ==
matchImage;
break;
default:
break;
}
if (conditionMatch) {
currentMatchDuration +=
std::chrono::duration_cast<
std::chrono::milliseconds>(
screenshotData->time -
previousTime);
} else {
currentMatchDuration = {};
}
bool durationMatch = currentMatchDuration.count() >=
duration * 1000;
if (conditionMatch && durationMatch) {
match = true;
}
if (condition == videoSwitchType::HAS_NOT_CHANGED) {
matchImage = std::move(screenshotData->image);
}
previousTime = std::move(screenshotData->time);
screenshotData.reset(nullptr);
}
}
getScreenshot();
return match;
}
void swap(VideoSwitch &first, VideoSwitch &second)
{
std::swap(first.targetType, second.targetType);
std::swap(first.group, second.group);
std::swap(first.scene, second.scene);
std::swap(first.transition, second.transition);
std::swap(first.usePreviousScene, second.usePreviousScene);
std::swap(first.videoSource, second.videoSource);
}
static inline void populateConditionSelection(QComboBox *list)
{
list->addItem(
obs_module_text("AdvSceneSwitcher.videoTab.condition.match"));
list->setItemData(
0,
obs_module_text(
"AdvSceneSwitcher.videoTab.condition.match.tooltip"),
Qt::ToolTipRole);
list->addItem(
obs_module_text("AdvSceneSwitcher.videoTab.condition.differ"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.videoTab.condition.hasNotChanged"));
}
VideoSwitchWidget::VideoSwitchWidget(QWidget *parent, VideoSwitch *s)
: SwitchWidget(parent, s, true, true)
{
videoSources = new QComboBox();
condition = new QComboBox();
duration = new QDoubleSpinBox();
filePath = new QLineEdit();
browseButton =
new QPushButton(obs_module_text("AdvSceneSwitcher.browse"));
ignoreInactiveSource = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.videoTab.ignoreInactiveSource"));
filePath->setFixedWidth(100);
browseButton->setStyleSheet("border:1px solid gray;");
duration->setMinimum(0.0);
duration->setMaximum(99.000000);
duration->setSuffix("s");
QWidget::connect(videoSources,
SIGNAL(currentTextChanged(const QString &)), this,
SLOT(SourceChanged(const QString &)));
QWidget::connect(condition, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int)));
QWidget::connect(duration, SIGNAL(valueChanged(double)), this,
SLOT(DurationChanged(double)));
QWidget::connect(filePath, SIGNAL(editingFinished()), this,
SLOT(FilePathChanged()));
QWidget::connect(browseButton, SIGNAL(clicked()), this,
SLOT(BrowseButtonClicked()));
QWidget::connect(ignoreInactiveSource, SIGNAL(stateChanged(int)), this,
SLOT(IgnoreInactiveChanged(int)));
// TODO:
// Figure out why scene do not work for "match exactly".
// Until then do not allow selecting scenes
AdvSceneSwitcher::populateVideoSelection(videoSources, false);
populateConditionSelection(condition);
if (s) {
videoSources->setCurrentText(
GetWeakSourceName(s->videoSource).c_str());
condition->setCurrentIndex(static_cast<int>(s->condition));
duration->setValue(s->duration);
filePath->setText(QString::fromStdString(s->file));
ignoreInactiveSource->setChecked(s->ignoreInactiveSource);
if (s->condition == videoSwitchType::HAS_NOT_CHANGED) {
filePath->hide();
browseButton->hide();
}
}
QHBoxLayout *switchLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{videoSources}}", videoSources},
{"{{condition}}", condition},
{"{{duration}}", duration},
{"{{filePath}}", filePath},
{"{{browseButton}}", browseButton},
{"{{ignoreInactiveSource}}", ignoreInactiveSource},
{"{{scenes}}", scenes},
{"{{transitions}}", transitions}};
placeWidgets(obs_module_text("AdvSceneSwitcher.videoTab.entry"),
switchLayout, widgetPlaceholders);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(switchLayout);
setLayout(mainLayout);
switchData = s;
UpdatePreviewTooltip();
loading = false;
}
VideoSwitch *VideoSwitchWidget::getSwitchData()
{
return switchData;
}
void VideoSwitchWidget::setSwitchData(VideoSwitch *s)
{
switchData = s;
}
void VideoSwitchWidget::swapSwitchData(VideoSwitchWidget *s1,
VideoSwitchWidget *s2)
{
SwitchWidget::swapSwitchData(s1, s2);
VideoSwitch *t = s1->getSwitchData();
s1->setSwitchData(s2->getSwitchData());
s2->setSwitchData(t);
}
void VideoSwitchWidget::UpdatePreviewTooltip()
{
if (!switchData ||
switchData->condition == videoSwitchType::HAS_NOT_CHANGED) {
return;
}
QImage preview =
switchData->matchImage.scaled({300, 300}, Qt::KeepAspectRatio);
QByteArray data;
QBuffer buffer(&data);
if (!preview.save(&buffer, "PNG")) {
return;
}
QString html =
QString("<html><img src='data:image/png;base64, %0'/></html>")
.arg(QString(data.toBase64()));
this->setToolTip(html);
}
void VideoSwitchWidget::SetFilePath(const QString &text)
{
filePath->setText(text);
FilePathChanged();
}
void VideoSwitchWidget::SourceChanged(const QString &text)
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switchData->videoSource = GetWeakSourceByQString(text);
}
void VideoSwitchWidget::ConditionChanged(int cond)
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switchData->condition = static_cast<videoSwitchType>(cond);
if (switchData->condition == videoSwitchType::HAS_NOT_CHANGED) {
filePath->hide();
browseButton->hide();
} else {
filePath->show();
browseButton->show();
}
// Reload image data to avoid incorrect matches.
//
// Condition type HAS_NOT_CHANGED will use matchImage to store previous
// frame of video source, which will differ from the image stored at
// specified file location.
if (switchData->loadImageFromFile()) {
UpdatePreviewTooltip();
}
}
void VideoSwitchWidget::DurationChanged(double dur)
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switchData->duration = dur;
}
void VideoSwitchWidget::FilePathChanged()
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switchData->file = filePath->text().toUtf8().constData();
if (switchData->loadImageFromFile()) {
UpdatePreviewTooltip();
}
}
void VideoSwitchWidget::BrowseButtonClicked()
{
if (loading || !switchData) {
return;
}
QString path = QFileDialog::getOpenFileName(
this,
tr(obs_module_text("AdvSceneSwitcher.fileTab.selectRead")),
QDir::currentPath(),
tr(obs_module_text("AdvSceneSwitcher.fileTab.anyFileType")));
if (path.isEmpty()) {
return;
}
filePath->setText(path);
FilePathChanged();
}
void VideoSwitchWidget::IgnoreInactiveChanged(int state)
{
if (loading || !switchData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
switchData->ignoreInactiveSource = state;
}

View File

@@ -176,7 +176,7 @@ void checkWindowTitleSwitchDirect(WindowSwitch &s,
OBSWeakSource &scene,
OBSWeakSource &transition)
{
bool focus = s.window == currentWindowTitle;
bool focus = (!s.focus || s.window == currentWindowTitle);
bool fullscreen = (!s.fullscreen || isFullscreen(s.window));
bool max = (!s.maximized || isMaximized(s.window));
@@ -202,7 +202,7 @@ void checkWindowTitleSwitchRegex(WindowSwitch &s,
} catch (const std::regex_error &) {
}
bool focus = window == currentWindowTitle;
bool focus = (!s.focus || window == currentWindowTitle);
bool fullscreen = (!s.fullscreen || isFullscreen(window));
bool max = (!s.maximized || isMaximized(window));

View File

@@ -43,6 +43,16 @@ void SwitcherData::Prune()
sceneSequenceSwitches.erase(
sceneSequenceSwitches.begin() + i--);
}
auto cur = &s;
while (cur != nullptr) {
if (cur->extendedSequence &&
!cur->extendedSequence->valid()) {
cur->extendedSequence.reset(nullptr);
s.activeSequence = nullptr;
}
cur = cur->extendedSequence.get();
}
}
for (size_t i = 0; i < sceneTransitions.size(); i++) {

View File

@@ -1,3 +1,4 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <util/platform.h>
#include "../headers/advanced-scene-switcher.hpp"