From a13a0774b4764e32eaa88160b972f2e1b56ef818 Mon Sep 17 00:00:00 2001
From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com>
Date: Mon, 24 Aug 2026 21:04:57 +0200
Subject: [PATCH] Add "Now Playing" condition type
---
data/locale/en-US.ini | 18 +-
plugins/CMakeLists.txt | 1 +
plugins/now-playing/CMakeLists.txt | 17 +
.../macro-condition-now-playing.cpp | 338 ++++++++++++++++++
.../macro-condition-now-playing.hpp | 86 +++++
5 files changed, 459 insertions(+), 1 deletion(-)
create mode 100644 plugins/now-playing/CMakeLists.txt
create mode 100644 plugins/now-playing/macro-condition-now-playing.cpp
create mode 100644 plugins/now-playing/macro-condition-now-playing.hpp
diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini
index e5b5154c..7a51fbcc 100644
--- a/data/locale/en-US.ini
+++ b/data/locale/en-US.ini
@@ -904,9 +904,19 @@ AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening"
AdvSceneSwitcher.condition.streamDeck.pluginDownload="
The Stream Deck plugin can be found here on GitHub.
"
AdvSceneSwitcher.condition.gameCapture="Game capture"
AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
-
AdvSceneSwitcher.condition.screenshot="Screenshot"
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
+AdvSceneSwitcher.condition.nowPlaying="Now Playing"
+AdvSceneSwitcher.condition.nowPlaying.checkType.playbackState="Playback state"
+AdvSceneSwitcher.condition.nowPlaying.checkType.title="Title"
+AdvSceneSwitcher.condition.nowPlaying.checkType.artist="Artist"
+AdvSceneSwitcher.condition.nowPlaying.checkType.album="Album"
+AdvSceneSwitcher.condition.nowPlaying.checkType.appName="App name"
+AdvSceneSwitcher.condition.nowPlaying.playbackState.playing="Playing"
+AdvSceneSwitcher.condition.nowPlaying.playbackState.paused="Paused"
+AdvSceneSwitcher.condition.nowPlaying.playbackState.stopped="Stopped"
+AdvSceneSwitcher.condition.nowPlaying.playbackState.opening="Opening"
+AdvSceneSwitcher.condition.nowPlaying.playbackState.changing="Changing"
AdvSceneSwitcher.condition.mqtt="MQTT"
AdvSceneSwitcher.condition.mqtt.layout.match="Message was received from{{connection}} which matches{{regex}}:"
AdvSceneSwitcher.condition.mqtt.layout.listen="Set message selection to incoming message:{{listenButton}}"
@@ -2599,6 +2609,12 @@ AdvSceneSwitcher.tempVar.gameCapture.class.description="Window class of the appl
AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
+AdvSceneSwitcher.tempVar.nowPlaying.title="Title"
+AdvSceneSwitcher.tempVar.nowPlaying.artist="Artist"
+AdvSceneSwitcher.tempVar.nowPlaying.album="Album"
+AdvSceneSwitcher.tempVar.nowPlaying.appName="App name"
+AdvSceneSwitcher.tempVar.nowPlaying.playbackStatus="Playback status"
+
AdvSceneSwitcher.tempVar.http.status="Status code"
AdvSceneSwitcher.tempVar.http.error="Error"
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index 863171c9..e781971f 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -42,6 +42,7 @@ add_plugin(twitch)
add_plugin(usb)
add_plugin(video)
add_plugin(speech)
+add_plugin(now-playing)
# ---------------------------------------------------------------------------- #
diff --git a/plugins/now-playing/CMakeLists.txt b/plugins/now-playing/CMakeLists.txt
new file mode 100644
index 00000000..9b117a38
--- /dev/null
+++ b/plugins/now-playing/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.14)
+project(advanced-scene-switcher-now-playing)
+
+if(NOT WIN32)
+ message(WARNING "Now playing condition is only supported on Windows.")
+ return()
+endif()
+
+add_library(${PROJECT_NAME} MODULE)
+target_sources(${PROJECT_NAME} PRIVATE macro-condition-now-playing.cpp
+ macro-condition-now-playing.hpp)
+
+target_link_libraries(${PROJECT_NAME} PRIVATE windowsapp)
+
+setup_advss_plugin(${PROJECT_NAME})
+set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
+install_advss_plugin(${PROJECT_NAME})
diff --git a/plugins/now-playing/macro-condition-now-playing.cpp b/plugins/now-playing/macro-condition-now-playing.cpp
new file mode 100644
index 00000000..82c43b04
--- /dev/null
+++ b/plugins/now-playing/macro-condition-now-playing.cpp
@@ -0,0 +1,338 @@
+#include "macro-condition-now-playing.hpp"
+#include "layout-helpers.hpp"
+#include "sync-helpers.hpp"
+
+#include
+
+#include
+#include
+#include
+
+namespace advss {
+
+using namespace winrt::Windows::Media::Control;
+
+const std::string MacroConditionNowPlaying::id = "now_playing";
+
+bool MacroConditionNowPlaying::_registered = MacroConditionFactory::Register(
+ MacroConditionNowPlaying::id,
+ {MacroConditionNowPlaying::Create, MacroConditionNowPlayingEdit::Create,
+ "AdvSceneSwitcher.condition.nowPlaying"});
+
+// ---------------------------------------------------------------------------
+
+struct SessionInfo {
+ bool hasSession = false;
+ std::string title;
+ std::string artist;
+ std::string album;
+ std::string appName;
+ GlobalSystemMediaTransportControlsSessionPlaybackStatus playbackStatus =
+ GlobalSystemMediaTransportControlsSessionPlaybackStatus::Closed;
+};
+
+static bool initWinRT()
+{
+ HRESULT hr = RoInitialize(RO_INIT_MULTITHREADED);
+ // S_OK: initialized, S_FALSE: already initialized with same type,
+ // RPC_E_CHANGED_MODE: thread is STA (OBS UI thread) - WinRT still usable
+ return SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE;
+}
+
+static SessionInfo getCurrentSessionInfo()
+{
+ thread_local bool initialized = initWinRT();
+ (void)initialized;
+
+ SessionInfo info;
+
+ try {
+ auto manager =
+ GlobalSystemMediaTransportControlsSessionManager::
+ RequestAsync()
+ .get();
+ auto session = manager.GetCurrentSession();
+ if (!session) {
+ return info;
+ }
+
+ info.hasSession = true;
+ info.appName = winrt::to_string(session.SourceAppUserModelId());
+
+ auto playbackInfo = session.GetPlaybackInfo();
+ info.playbackStatus = playbackInfo.PlaybackStatus();
+
+ auto props = session.TryGetMediaPropertiesAsync().get();
+ if (props) {
+ info.title = winrt::to_string(props.Title());
+ info.artist = winrt::to_string(props.Artist());
+ info.album = winrt::to_string(props.AlbumTitle());
+ }
+ } catch (const winrt::hresult_error &e) {
+ blog(LOG_WARNING, "now playing: WinRT error 0x%08X",
+ static_cast(e.code()));
+ }
+
+ return info;
+}
+
+static std::string PlaybackStatusToString(
+ GlobalSystemMediaTransportControlsSessionPlaybackStatus status)
+{
+ switch (status) {
+ case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing:
+ return "Playing";
+ case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused:
+ return "Paused";
+ case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Stopped:
+ return "Stopped";
+ case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Opened:
+ return "Opening";
+ case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Changing:
+ return "Changing";
+ default:
+ return "Closed";
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+bool MacroConditionNowPlaying::CheckCondition()
+{
+ const auto info = getCurrentSessionInfo();
+
+ SetTempVarValue("title", info.title);
+ SetTempVarValue("artist", info.artist);
+ SetTempVarValue("album", info.album);
+ SetTempVarValue("appName", info.appName);
+ SetTempVarValue("playbackStatus",
+ PlaybackStatusToString(info.playbackStatus));
+
+ if (!info.hasSession) {
+ return false;
+ }
+
+ auto textMatches = [this](const std::string &field) -> bool {
+ const QString pattern = QString::fromStdString(_matchText);
+ const QString value = QString::fromStdString(field);
+ if (_regex.Enabled()) {
+ return _regex.Matches(value, pattern);
+ }
+ return value == pattern;
+ };
+
+ switch (_checkType) {
+ case CheckType::PLAYBACK_STATE: {
+ using S =
+ GlobalSystemMediaTransportControlsSessionPlaybackStatus;
+ switch (_playbackState) {
+ case PlaybackState::PLAYING:
+ return info.playbackStatus == S::Playing;
+ case PlaybackState::PAUSED:
+ return info.playbackStatus == S::Paused;
+ case PlaybackState::STOPPED:
+ return info.playbackStatus == S::Stopped;
+ case PlaybackState::OPENING:
+ return info.playbackStatus == S::Opened;
+ case PlaybackState::CHANGING:
+ return info.playbackStatus == S::Changing;
+ }
+ break;
+ }
+ case CheckType::TITLE:
+ return textMatches(info.title);
+ case CheckType::ARTIST:
+ return textMatches(info.artist);
+ case CheckType::ALBUM:
+ return textMatches(info.album);
+ case CheckType::APP_NAME:
+ return textMatches(info.appName);
+ }
+
+ return false;
+}
+
+bool MacroConditionNowPlaying::Save(obs_data_t *obj) const
+{
+ MacroCondition::Save(obj);
+ obs_data_set_int(obj, "checkType", static_cast(_checkType));
+ obs_data_set_int(obj, "playbackState",
+ static_cast(_playbackState));
+ _matchText.Save(obj, "matchText");
+ _regex.Save(obj);
+ return true;
+}
+
+bool MacroConditionNowPlaying::Load(obs_data_t *obj)
+{
+ MacroCondition::Load(obj);
+ _checkType = static_cast(obs_data_get_int(obj, "checkType"));
+ _playbackState = static_cast(
+ obs_data_get_int(obj, "playbackState"));
+ _matchText.Load(obj, "matchText");
+ _regex.Load(obj);
+ return true;
+}
+
+void MacroConditionNowPlaying::SetupTempVars()
+{
+ AddTempvar(
+ "title",
+ obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.title"));
+ AddTempvar(
+ "artist",
+ obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.artist"));
+ AddTempvar(
+ "album",
+ obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.album"));
+ AddTempvar(
+ "appName",
+ obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.appName"));
+ AddTempvar(
+ "playbackStatus",
+ obs_module_text(
+ "AdvSceneSwitcher.tempVar.nowPlaying.playbackStatus"));
+}
+
+// ---------------------------------------------------------------------------
+
+static void populateCheckTypeSelection(QComboBox *list)
+{
+ static const std::map checkTypes = {
+ {MacroConditionNowPlaying::CheckType::PLAYBACK_STATE,
+ "AdvSceneSwitcher.condition.nowPlaying.checkType.playbackState"},
+ {MacroConditionNowPlaying::CheckType::TITLE,
+ "AdvSceneSwitcher.condition.nowPlaying.checkType.title"},
+ {MacroConditionNowPlaying::CheckType::ARTIST,
+ "AdvSceneSwitcher.condition.nowPlaying.checkType.artist"},
+ {MacroConditionNowPlaying::CheckType::ALBUM,
+ "AdvSceneSwitcher.condition.nowPlaying.checkType.album"},
+ {MacroConditionNowPlaying::CheckType::APP_NAME,
+ "AdvSceneSwitcher.condition.nowPlaying.checkType.appName"},
+ };
+
+ for (const auto &[type, name] : checkTypes) {
+ list->addItem(obs_module_text(name.c_str()),
+ static_cast(type));
+ }
+}
+
+static void populatePlaybackStateSelection(QComboBox *list)
+{
+ static const std::map playbackStates = {
+ {MacroConditionNowPlaying::PlaybackState::PLAYING,
+ "AdvSceneSwitcher.condition.nowPlaying.playbackState.playing"},
+ {MacroConditionNowPlaying::PlaybackState::PAUSED,
+ "AdvSceneSwitcher.condition.nowPlaying.playbackState.paused"},
+ {MacroConditionNowPlaying::PlaybackState::STOPPED,
+ "AdvSceneSwitcher.condition.nowPlaying.playbackState.stopped"},
+ {MacroConditionNowPlaying::PlaybackState::OPENING,
+ "AdvSceneSwitcher.condition.nowPlaying.playbackState.opening"},
+ {MacroConditionNowPlaying::PlaybackState::CHANGING,
+ "AdvSceneSwitcher.condition.nowPlaying.playbackState.changing"},
+ };
+
+ for (const auto &[state, name] : playbackStates) {
+ list->addItem(obs_module_text(name.c_str()),
+ static_cast(state));
+ }
+}
+
+MacroConditionNowPlayingEdit::MacroConditionNowPlayingEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent),
+ _checkType(new QComboBox(this)),
+ _playbackState(new QComboBox(this)),
+ _matchText(new VariableLineEdit(this)),
+ _regex(new RegexConfigWidget(this))
+{
+ populateCheckTypeSelection(_checkType);
+ populatePlaybackStateSelection(_playbackState);
+
+ connect(_checkType, QOverload::of(&QComboBox::currentIndexChanged),
+ this, &MacroConditionNowPlayingEdit::CheckTypeChanged);
+ connect(_playbackState,
+ QOverload::of(&QComboBox::currentIndexChanged), this,
+ &MacroConditionNowPlayingEdit::PlaybackStateChanged);
+ connect(_matchText, &VariableLineEdit::textEdited, this,
+ &MacroConditionNowPlayingEdit::MatchTextChanged);
+ connect(_regex, &RegexConfigWidget::RegexConfigChanged, this,
+ &MacroConditionNowPlayingEdit::RegexChanged);
+
+ _layout = new QHBoxLayout();
+ _layout->addWidget(_checkType);
+ _layout->addWidget(_playbackState);
+ _layout->addWidget(_matchText);
+ _layout->addWidget(_regex);
+ setLayout(_layout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+void MacroConditionNowPlayingEdit::UpdateEntryData()
+{
+ if (!_entryData) {
+ return;
+ }
+
+ _checkType->setCurrentIndex(
+ _checkType->findData(static_cast(_entryData->_checkType)));
+ _playbackState->setCurrentIndex(_playbackState->findData(
+ static_cast(_entryData->_playbackState)));
+ _matchText->setText(QString::fromStdString(_entryData->_matchText));
+ _regex->SetRegexConfig(_entryData->_regex);
+
+ SetWidgetVisibility();
+}
+
+void MacroConditionNowPlayingEdit::CheckTypeChanged(int)
+{
+ GUARD_LOADING_AND_LOCK();
+ _entryData->_checkType =
+ static_cast(
+ _checkType->currentData().toInt());
+ SetWidgetVisibility();
+}
+
+void MacroConditionNowPlayingEdit::PlaybackStateChanged(int)
+{
+ GUARD_LOADING_AND_LOCK();
+ _entryData->_playbackState =
+ static_cast(
+ _playbackState->currentData().toInt());
+}
+
+void MacroConditionNowPlayingEdit::MatchTextChanged(const QString &text)
+{
+ GUARD_LOADING_AND_LOCK();
+ _entryData->_matchText = text.toStdString();
+}
+
+void MacroConditionNowPlayingEdit::RegexChanged(const RegexConfig ®ex)
+{
+ GUARD_LOADING_AND_LOCK();
+ _entryData->_regex = regex;
+}
+
+void MacroConditionNowPlayingEdit::SetWidgetVisibility()
+{
+ const bool isStateCheck =
+ _entryData->_checkType ==
+ MacroConditionNowPlaying::CheckType::PLAYBACK_STATE;
+ _playbackState->setVisible(isStateCheck);
+ _matchText->setVisible(!isStateCheck);
+ _regex->setVisible(!isStateCheck);
+
+ if (isStateCheck) {
+ AddStretchIfNecessary(_layout);
+ } else {
+ RemoveStretchIfPresent(_layout);
+ }
+
+ adjustSize();
+ updateGeometry();
+}
+
+} // namespace advss
diff --git a/plugins/now-playing/macro-condition-now-playing.hpp b/plugins/now-playing/macro-condition-now-playing.hpp
new file mode 100644
index 00000000..d50c7de8
--- /dev/null
+++ b/plugins/now-playing/macro-condition-now-playing.hpp
@@ -0,0 +1,86 @@
+#pragma once
+#include "macro-condition-edit.hpp"
+#include "regex-config.hpp"
+#include "variable-line-edit.hpp"
+
+#include
+
+namespace advss {
+
+class MacroConditionNowPlaying : public MacroCondition {
+public:
+ enum class CheckType {
+ PLAYBACK_STATE,
+ TITLE,
+ ARTIST,
+ ALBUM,
+ APP_NAME,
+ };
+
+ enum class PlaybackState {
+ PLAYING,
+ PAUSED,
+ STOPPED,
+ OPENING,
+ CHANGING,
+ };
+
+ MacroConditionNowPlaying(Macro *m) : MacroCondition(m) {}
+ bool CheckCondition();
+ bool Save(obs_data_t *obj) const;
+ bool Load(obs_data_t *obj);
+ std::string GetId() const { return id; }
+ static std::shared_ptr Create(Macro *m)
+ {
+ return std::make_shared(m);
+ }
+
+ CheckType _checkType = CheckType::PLAYBACK_STATE;
+ PlaybackState _playbackState = PlaybackState::PLAYING;
+ StringVariable _matchText;
+ RegexConfig _regex;
+
+private:
+ void SetupTempVars();
+
+ static bool _registered;
+ static const std::string id;
+};
+
+class MacroConditionNowPlayingEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionNowPlayingEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionNowPlayingEdit(
+ parent,
+ std::dynamic_pointer_cast(
+ cond));
+ }
+
+private slots:
+ void CheckTypeChanged(int);
+ void PlaybackStateChanged(int);
+ void MatchTextChanged(const QString &);
+ void RegexChanged(const RegexConfig &);
+
+private:
+ void SetWidgetVisibility();
+
+ QHBoxLayout *_layout;
+ QComboBox *_checkType;
+ QComboBox *_playbackState;
+ VariableLineEdit *_matchText;
+ RegexConfigWidget *_regex;
+
+ std::shared_ptr _entryData;
+ bool _loading = true;
+};
+
+} // namespace advss