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