From e4bb034303bdf5385f27ecb380890a1bebc08eb8 Mon Sep 17 00:00:00 2001
From: WarmUpTill
Date: Fri, 5 Mar 2021 14:08:17 +0100
Subject: [PATCH] Add video tab (#131)
This tab will allow switching scenes based on the video output of sources.
---
CMakeLists.txt | 2 +
data/locale/de-DE.ini | 14 +
data/locale/en-US.ini | 14 +
forms/advanced-scene-switcher.ui | 185 +++++++
src/advanced-scene-switcher.cpp | 42 +-
src/general.cpp | 86 ++-
src/headers/advanced-scene-switcher.hpp | 10 +
src/headers/switch-pause.hpp | 1 +
src/headers/switch-video.hpp | 98 ++++
src/headers/switcher-data-structs.hpp | 13 +-
src/switch-audio.cpp | 2 +-
src/switch-pause.cpp | 6 +
src/switch-priority.cpp | 2 +-
src/switch-video.cpp | 698 ++++++++++++++++++++++++
14 files changed, 1142 insertions(+), 31 deletions(-)
create mode 100644 src/headers/switch-video.hpp
create mode 100644 src/switch-video.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index aedf4f43..46364b6a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -73,6 +73,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
)
@@ -99,6 +100,7 @@ set(advanced-scene-switcher_SOURCES
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
diff --git a/data/locale/de-DE.ini b/data/locale/de-DE.ini
index dc9c2d34..c8bede9e 100644
--- a/data/locale/de-DE.ini
+++ b/data/locale/de-DE.ini
@@ -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"
@@ -199,6 +200,18 @@ AdvSceneSwitcher.audioTab.multiMatchfallbackCondition="Wenn mehrere Einträge zu
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="
Dieser Tab ermöglicht es basierend auf der Videoausgabe von Quellen scenen zu wechseln.
Für eine noch bessere Implementierung dieser Funktionalität siehe Pixel Match Switcher.
Klicke auf das markierte Plus Symbol, um einen neuen Eintrag hinzuzufügen..
"
+
; Scene Group Tab
AdvSceneSwitcher.sceneGroupTab.title="Szenengruppe"
AdvSceneSwitcher.sceneGroupTab.list="Szenengruppen"
@@ -258,6 +271,7 @@ 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--"
diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini
index edd9829c..6fd2fdeb 100644
--- a/data/locale/en-US.ini
+++ b/data/locale/en-US.ini
@@ -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"
@@ -199,6 +200,18 @@ 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="This tab will allow you to switch scenes based on the current video output of selected sources.
Make sure to check out Pixel Match Switcher for an even better implementation of this functionality.
Click on the highlighted plus symbol to continue.
"
+
; Scene Group Tab
AdvSceneSwitcher.sceneGroupTab.title="Scene Group"
AdvSceneSwitcher.sceneGroupTab.list="Scene Groups"
@@ -257,6 +270,7 @@ 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--"
diff --git a/forms/advanced-scene-switcher.ui b/forms/advanced-scene-switcher.ui
index ec8cc3db..fb17462a 100644
--- a/forms/advanced-scene-switcher.ui
+++ b/forms/advanced-scene-switcher.ui
@@ -3084,6 +3084,191 @@
+
+
+ AdvSceneSwitcher.videoTab.title
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+
+ -
+
+
+ AdvSceneSwitcher.videoTab.help
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ addIconSmall
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ removeIconSmall
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+ ../../../forms/images/up.svg../../../forms/images/up.svg
+
+
+ true
+
+
+ upArrowIconSmall
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+ ../../../forms/images/down.svg../../../forms/images/down.svg
+
+
+ true
+
+
+ downArrowIconSmall
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ AdvSceneSwitcher.videoTab.getScreenshot
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
AdvSceneSwitcher.sceneGroupTab.title
diff --git a/src/advanced-scene-switcher.cpp b/src/advanced-scene-switcher.cpp
index 4d8e735d..0bd006dc 100644
--- a/src/advanced-scene-switcher.cpp
+++ b/src/advanced-scene-switcher.cpp
@@ -59,6 +59,7 @@ void AdvSceneSwitcher::loadUI()
setupFileTab();
setupTimeTab();
setupAudioTab();
+ setupVideoTab();
setupSceneGroupTab();
setupTriggerTab();
@@ -119,8 +120,6 @@ void AdvSceneSwitcher::populateSceneSelection(QComboBox *sel, bool addPrevious,
std::string selectText,
bool selectable)
{
- sel->clear();
-
if (addSelect) {
if (selectText.empty()) {
addSelectionEntry(
@@ -235,6 +234,42 @@ 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 *list =
+ reinterpret_cast *>(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 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) {
@@ -563,6 +598,9 @@ bool SwitcherData::checkForMatch(OBSWeakSource &scene,
case audio_func:
checkAudioSwitch(match, scene, transition);
break;
+ case video_func:
+ checkVideoSwitch(match, scene, transition);
+ break;
}
if (switcher->stop) {
diff --git a/src/general.cpp b/src/general.cpp
index cc7b6867..ffc4201c 100644
--- a/src/general.cpp
+++ b/src/general.cpp
@@ -4,7 +4,7 @@
#include "headers/utility.hpp"
#include "headers/version.h"
-constexpr auto tab_count = 15;
+constexpr auto tab_count = 16;
QMetaObject::Connection inactivePluse;
@@ -326,9 +326,12 @@ int findTabIndex(QTabWidget *tabWidget, int pos)
tabName = "audioTab";
break;
case 13:
- tabName = "sceneGroupTab";
+ tabName = "videoTab";
break;
case 14:
+ tabName = "sceneGroupTab";
+ break;
+ case 15:
tabName = "sceneTriggerTab";
break;
}
@@ -383,22 +386,23 @@ 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);
+ loadSceneTriggers(obj);
+ loadGeneralSettings(obj);
+ loadHotkeys(obj);
}
void SwitcherData::saveSettings(obs_data_t *obj)
@@ -420,6 +424,7 @@ void SwitcherData::saveSettings(obs_data_t *obj)
saveMediaSwitches(obj);
saveTimeSwitches(obj);
saveAudioSwitches(obj);
+ saveVideoSwitches(obj);
saveSceneTriggers(obj);
saveGeneralSettings(obj);
saveHotkeys(obj);
@@ -467,15 +472,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(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]);
@@ -491,8 +496,9 @@ 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, "sceneGroupTabPos", switcher->tabOrder[14]);
+ obs_data_set_int(obj, "triggerTabPos", switcher->tabOrder[15]);
}
void SwitcherData::loadGeneralSettings(obs_data_t *obj)
@@ -555,6 +561,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);
@@ -565,6 +573,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",
@@ -584,8 +593,9 @@ 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, "sceneGroupTabPos", 14);
+ obs_data_set_default_int(obj, "triggerTabPos", 15);
switcher->tabOrder.emplace_back(
(int)(obs_data_get_int(obj, "generalTabPos")));
@@ -613,10 +623,30 @@ 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, "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(tab_count);
+ std::iota(switcher->tabOrder.begin(), switcher->tabOrder.end(), 0);
}
void SwitcherData::checkNoMatchSwitch(bool &match, OBSWeakSource &scene,
@@ -765,6 +795,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 =
diff --git a/src/headers/advanced-scene-switcher.hpp b/src/headers/advanced-scene-switcher.hpp
index 6f6e7d24..07b7d4fd 100644
--- a/src/headers/advanced-scene-switcher.hpp
+++ b/src/headers/advanced-scene-switcher.hpp
@@ -59,6 +59,7 @@ public:
void setupAudioTab();
void setupSceneGroupTab();
void setupTriggerTab();
+ void setupVideoTab();
void setTabOrder();
static bool DisplayMessage(QString msg, bool question = false);
@@ -82,6 +83,9 @@ public:
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,
@@ -201,6 +205,12 @@ 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_sceneGroupAdd_clicked();
void on_sceneGroupRemove_clicked();
void on_sceneGroupUp_clicked();
diff --git a/src/headers/switch-pause.hpp b/src/headers/switch-pause.hpp
index 3e884218..eeb15a40 100644
--- a/src/headers/switch-pause.hpp
+++ b/src/headers/switch-pause.hpp
@@ -19,6 +19,7 @@ enum class PauseTarget {
Idle,
Sequence,
Audio,
+ Video,
};
struct PauseEntry : SceneSwitcherEntry {
diff --git a/src/headers/switch-video.hpp b/src/headers/switch-video.hpp
new file mode 100644
index 00000000..f313028f
--- /dev/null
+++ b/src/headers/switch-video.hpp
@@ -0,0 +1,98 @@
+#pragma once
+#include
+
+#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 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;
+};
diff --git a/src/headers/switcher-data-structs.hpp b/src/headers/switcher-data-structs.hpp
index d02993d6..0ff019d0 100644
--- a/src/headers/switcher-data-structs.hpp
+++ b/src/headers/switcher-data-structs.hpp
@@ -21,6 +21,7 @@
#include "switch-transitions.hpp"
#include "switch-window.hpp"
#include "switch-sequence.hpp"
+#include "switch-video.hpp"
constexpr auto default_interval = 300;
constexpr auto previous_scene_name = "Previous Scene";
@@ -113,12 +114,15 @@ struct SwitcherData {
std::deque audioSwitches;
AudioSwitchFallback audioFallback;
+ std::deque videoSwitches;
+
std::deque sceneGroups;
std::vector functionNamesByPriority = std::vector{
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;
@@ -164,6 +168,9 @@ struct SwitcherData {
bool prioFuncsValid();
+ bool tabOrderValid();
+ void resetTabOrder();
+
void writeSceneInfoToFile();
void writeToStatusFile(QString status);
@@ -193,6 +200,8 @@ 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);
@@ -215,6 +224,7 @@ 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 saveGeneralSettings(obs_data_t *obj);
void saveHotkeys(obs_data_t *obj);
void saveVersion(obs_data_t *obj, std::string currentVersion);
@@ -235,6 +245,7 @@ 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 loadGeneralSettings(obs_data_t *obj);
void loadHotkeys(obs_data_t *obj);
diff --git a/src/switch-audio.cpp b/src/switch-audio.cpp
index fc3eef50..73c96235 100644
--- a/src/switch-audio.cpp
+++ b/src/switch-audio.cpp
@@ -438,7 +438,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"));
diff --git a/src/switch-pause.cpp b/src/switch-pause.cpp
index ee141a58..a3202a69 100644
--- a/src/switch-pause.cpp
+++ b/src/switch-pause.cpp
@@ -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)
diff --git a/src/switch-priority.cpp b/src/switch-priority.cpp
index de16b63e..c7c16629 100644
--- a/src/switch-priority.cpp
+++ b/src/switch-priority.cpp
@@ -66,7 +66,7 @@ bool SwitcherData::prioFuncsValid()
}
for (int p : functionNamesByPriority) {
- if (p < 0 || p > 8) {
+ if (p < 0 || p > 9) {
return false;
}
}
diff --git a/src/switch-video.cpp b/src/switch-video.cpp
new file mode 100644
index 00000000..949ab9b1
--- /dev/null
+++ b/src/switch-video.cpp
@@ -0,0 +1,698 @@
+#include
+#include
+#include
+#include
+
+#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(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 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 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 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 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(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(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(
+ 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(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(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 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("
")
+ .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 lock(switcher->m);
+ switchData->videoSource = GetWeakSourceByQString(text);
+}
+
+void VideoSwitchWidget::ConditionChanged(int cond)
+{
+ if (loading || !switchData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ switchData->condition = static_cast(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 lock(switcher->m);
+ switchData->duration = dur;
+}
+
+void VideoSwitchWidget::FilePathChanged()
+{
+ if (loading || !switchData) {
+ return;
+ }
+
+ std::lock_guard 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 lock(switcher->m);
+ switchData->ignoreInactiveSource = state;
+}