diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 95a85f2c..fd9c83eb 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -128,6 +128,7 @@ AdvSceneSwitcher.condition.media="Media" AdvSceneSwitcher.condition.media.anyOnScene="Any media source on" AdvSceneSwitcher.condition.media.allOnScene="All media sources on" AdvSceneSwitcher.condition.media.matchOnChange="Only match on change (Note: This option will be removed in a future version - please use time constraints instead)" +AdvSceneSwitcher.condition.media.inconsistencyInfo="Unfortunately not all media source types behave the same (e.g. Media Source vs. VLC Video Source \"Stopped\" state).\nSo please experiment what works for your setup!" AdvSceneSwitcher.condition.media.entry="{{mediaSources}}{{scenes}} state is {{states}} and {{timeRestrictions}} {{time}}" AdvSceneSwitcher.condition.video="Video" AdvSceneSwitcher.condition.video.condition.match="exactly matches" @@ -527,7 +528,7 @@ AdvSceneSwitcher.mediaTab.states.Paused="Paused" AdvSceneSwitcher.mediaTab.states.stopped="Stopped" AdvSceneSwitcher.mediaTab.states.ended="Ended" AdvSceneSwitcher.mediaTab.states.error="Error" -AdvSceneSwitcher.mediaTab.states.playedToEnd="Played to end" +AdvSceneSwitcher.mediaTab.states.playlistEnd="Ended(Playlist)" AdvSceneSwitcher.mediaTab.states.any="Any" AdvSceneSwitcher.mediaTab.timeRestriction.none="None" AdvSceneSwitcher.mediaTab.timeRestriction.shorter="Time shorter" diff --git a/src/headers/macro-condition-media.hpp b/src/headers/macro-condition-media.hpp index b587e9bd..cb60c660 100644 --- a/src/headers/macro-condition-media.hpp +++ b/src/headers/macro-condition-media.hpp @@ -31,7 +31,7 @@ enum class MediaState { // Just a marker LAST_OBS_MEDIA_STATE, // states added for use in the plugin - PLAYED_TO_END = custom_media_states_offset, + PLAYLIST_ENDED = custom_media_states_offset, ANY, }; @@ -58,6 +58,7 @@ public: void ResetSignalHandler(); static void MediaStopped(void *data, calldata_t *); static void MediaEnded(void *data, calldata_t *); + static void MediaNext(void *data, calldata_t *); MediaSourceType _sourceType = MediaSourceType::SOURCE; SceneSelection _scene; @@ -70,17 +71,19 @@ public: bool _onlyMatchonChagne = false; private: + bool CheckTime(); + bool CheckState(); + bool CheckPlaylistEnd(const obs_media_state); bool CheckMediaMatch(); bool _stopped = false; bool _ended = false; + bool _next = false; // TODO: Remove _alreadyMatched as it does not make much sense when // time restrictions for macro conditions are available. - // Trigger scene change only once even if media state might trigger repeatedly bool _alreadyMatched = false; // Workaround to enable use of "ended" to specify end of VLC playlist bool _previousStateEnded = false; - bool _playedToEnd = false; static bool _registered; static const std::string id; diff --git a/src/macro-condition-media.cpp b/src/macro-condition-media.cpp index 21acc35e..449b052d 100644 --- a/src/macro-condition-media.cpp +++ b/src/macro-condition-media.cpp @@ -40,8 +40,8 @@ static std::map mediaStates = { "AdvSceneSwitcher.mediaTab.states.ended"}, {MediaState::OBS_MEDIA_STATE_ERROR, "AdvSceneSwitcher.mediaTab.states.error"}, - {MediaState::PLAYED_TO_END, - "AdvSceneSwitcher.mediaTab.states.playedToEnd"}, + {MediaState::PLAYLIST_ENDED, + "AdvSceneSwitcher.mediaTab.states.playlistEnd"}, {MediaState::ANY, "AdvSceneSwitcher.mediaTab.states.any"}, }; @@ -51,111 +51,113 @@ MacroConditionMedia::~MacroConditionMedia() signal_handler_t *sh = obs_source_get_signal_handler(mediasource); signal_handler_disconnect(sh, "media_stopped", MediaStopped, this); signal_handler_disconnect(sh, "media_ended", MediaEnded, this); + signal_handler_disconnect(sh, "media_next", MediaNext, this); obs_source_release(mediasource); } -bool matchTime(const int64_t currentTime, const int64_t duration, - const MediaTimeRestriction restriction, const int64_t time) +bool MacroConditionMedia::CheckTime() { - bool matchedTimeNone = - (restriction == MediaTimeRestriction::TIME_RESTRICTION_NONE); - bool matchedTimeLonger = - (restriction == - MediaTimeRestriction::TIME_RESTRICTION_LONGER) && - (currentTime > time); - bool matchedTimeShorter = - (restriction == - MediaTimeRestriction::TIME_RESTRICTION_SHORTER) && - (currentTime < time); - bool matchedTimeRemainLonger = - (restriction == - MediaTimeRestriction::TIME_RESTRICTION_REMAINING_LONGER) && - (duration > currentTime && duration - currentTime > time); - bool matchedTimeRemainShorter = - (restriction == - MediaTimeRestriction::TIME_RESTRICTION_REMAINING_SHORTER) && - (duration > currentTime && duration - currentTime < time); + obs_source_t *s = obs_weak_source_get_source(_source); + auto duration = obs_source_media_get_duration(s); + auto currentTime = obs_source_media_get_time(s); + obs_source_release(s); - return matchedTimeNone || matchedTimeLonger || matchedTimeShorter || - matchedTimeRemainLonger || matchedTimeRemainShorter; + bool match = false; + + switch (_restriction) { + case MediaTimeRestriction::TIME_RESTRICTION_NONE: + match = true; + break; + case MediaTimeRestriction::TIME_RESTRICTION_SHORTER: + match = currentTime < _time.seconds * 1000; + break; + case MediaTimeRestriction::TIME_RESTRICTION_LONGER: + match = currentTime > _time.seconds * 1000; + break; + case MediaTimeRestriction::TIME_RESTRICTION_REMAINING_SHORTER: + match = duration > currentTime && + duration - currentTime < _time.seconds * 1000; + break; + case MediaTimeRestriction::TIME_RESTRICTION_REMAINING_LONGER: + match = duration > currentTime && + duration - currentTime > _time.seconds * 1000; + break; + default: + break; + } + + return match; +} + +bool MacroConditionMedia::CheckState() +{ + obs_source_t *s = obs_weak_source_get_source(_source); + obs_media_state currentState = obs_source_media_get_state(s); + obs_source_release(s); + + bool match = false; + // To be able to compare to obs_media_state more easily + int expectedState = static_cast(_state); + + switch (_state) { + case MediaState::OBS_MEDIA_STATE_STOPPED: + match = _stopped || currentState == OBS_MEDIA_STATE_STOPPED; + break; + case MediaState::OBS_MEDIA_STATE_ENDED: + match = _ended || currentState == OBS_MEDIA_STATE_ENDED; + break; + case MediaState::PLAYLIST_ENDED: + match = CheckPlaylistEnd(currentState); + break; + case MediaState::ANY: + match = true; + break; + case MediaState::OBS_MEDIA_STATE_NONE: + case MediaState::OBS_MEDIA_STATE_PLAYING: + case MediaState::OBS_MEDIA_STATE_OPENING: + case MediaState::OBS_MEDIA_STATE_BUFFERING: + case MediaState::OBS_MEDIA_STATE_PAUSED: + case MediaState::OBS_MEDIA_STATE_ERROR: + match = currentState == expectedState; + break; + default: + break; + } + + return match; +} + +bool MacroConditionMedia::CheckPlaylistEnd(const obs_media_state currentState) +{ + bool consecutiveEndedStates = false; + if (_next || currentState != OBS_MEDIA_STATE_ENDED) { + _previousStateEnded = false; + } + if (currentState == OBS_MEDIA_STATE_ENDED && _previousStateEnded) { + consecutiveEndedStates = true; + } + _previousStateEnded = _ended || currentState == OBS_MEDIA_STATE_ENDED; + return consecutiveEndedStates; } bool MacroConditionMedia::CheckMediaMatch() { - if (!_source) { return false; } bool match = false; - - obs_source_t *s = obs_weak_source_get_source(_source); - auto duration = obs_source_media_get_duration(s); - auto time = obs_source_media_get_time(s); - obs_media_state currentState = obs_source_media_get_state(s); - obs_source_release(s); - - // To be able to compare to obs_media_state more easily - int expectedState = static_cast(_state); - - bool matchedStopped = expectedState == OBS_MEDIA_STATE_STOPPED && - _stopped; - - // The signal for the state ended is intentionally not used here - // so matchedEnded can be used to specify the end of a VLC playlist - // by two consequtive matches of OBS_MEDIA_STATE_ENDED - // - // This was done to reduce the likelyhood of interpreting a single - // OBS_MEDIA_STATE_ENDED caught by obs_source_media_get_state() - // as the end of the playlist of the VLC source, while actually being - // in the middle of switching to the next item of the playlist - // - // If there is a separate obs_media_sate in the future for the - // "end of playlist reached" signal the line below can be used - // and an additional check for this new singal can be introduced - // - // bool matchedEnded = _state == OBS_MEDIA_STATE_ENDED && _ended; - - bool ended = false; - - if (currentState == OBS_MEDIA_STATE_ENDED) { - ended = _previousStateEnded; - _previousStateEnded = true; - } else { - _previousStateEnded = false; - } - - bool matchedEnded = ended && (expectedState == OBS_MEDIA_STATE_ENDED); - - // match if playedToEnd was true in last interval - // and playback is currently ended - bool matchedPlayedToEnd = _state == MediaState::PLAYED_TO_END && - _playedToEnd && ended; - - // interval * 2 to make sure not to miss any state changes - // which happened during check of the conditions - _playedToEnd = _playedToEnd || - (duration - time <= switcher->interval * 2); - - // reset for next check - if (ended) { - _playedToEnd = false; - } - _stopped = false; - _ended = false; - - bool matchedState = - (currentState == expectedState || _state == MediaState::ANY) || - matchedStopped || matchedEnded || matchedPlayedToEnd; - - bool matchedTime = - matchTime(time, duration, _restriction, _time.seconds * 1000); - bool matched = matchedState && matchedTime; + bool matched = CheckState() && CheckTime(); if (matched && !(_onlyMatchonChagne && _alreadyMatched)) { match = true; } _alreadyMatched = matched; + // reset for next check + _stopped = false; + _ended = false; + _next = false; + return match; } @@ -195,6 +197,7 @@ bool MacroConditionMedia::Save(obs_data_t *obj) obs_data_set_int(obj, "restriction", static_cast(_restriction)); _time.Save(obj); obs_data_set_bool(obj, "matchOnChagne", _onlyMatchonChagne); + obs_data_set_int(obj, "version", 0); return true; } @@ -250,11 +253,7 @@ bool MacroConditionMedia::Load(obs_data_t *obj) _restriction = static_cast( obs_data_get_int(obj, "restriction")); _time.Load(obj); - if (!obs_data_has_user_value(obj, "matchOnChagne")) { - _onlyMatchonChagne = true; - } else { - _onlyMatchonChagne = obs_data_get_bool(obj, "matchOnChagne"); - } + _onlyMatchonChagne = obs_data_get_bool(obj, "matchOnChagne"); if (_sourceType == MediaSourceType::SOURCE) { obs_source_t *mediasource = obs_weak_source_get_source(_source); @@ -262,11 +261,17 @@ bool MacroConditionMedia::Load(obs_data_t *obj) obs_source_get_signal_handler(mediasource); signal_handler_connect(sh, "media_stopped", MediaStopped, this); signal_handler_connect(sh, "media_ended", MediaEnded, this); + signal_handler_connect(sh, "media_next", MediaNext, this); obs_source_release(mediasource); } forMediaSourceOnSceneAddMediaCondition(_scene.GetScene(), this, _sources); + if (!obs_data_has_user_value(obj, "version")) { + if (_state == MediaState::OBS_MEDIA_STATE_ENDED) { + _state = MediaState::PLAYLIST_ENDED; + } + } return true; } @@ -304,6 +309,7 @@ void MacroConditionMedia::ClearSignalHandler() signal_handler_t *sh = obs_source_get_signal_handler(mediasource); signal_handler_disconnect(sh, "media_stopped", MediaStopped, this); signal_handler_disconnect(sh, "media_ended", MediaEnded, this); + signal_handler_disconnect(sh, "media_next", MediaNext, this); obs_source_release(mediasource); } @@ -313,8 +319,10 @@ void MacroConditionMedia::ResetSignalHandler() signal_handler_t *sh = obs_source_get_signal_handler(mediasource); signal_handler_disconnect(sh, "media_stopped", MediaStopped, this); signal_handler_disconnect(sh, "media_ended", MediaEnded, this); + signal_handler_disconnect(sh, "media_next", MediaNext, this); signal_handler_connect(sh, "media_stopped", MediaStopped, this); signal_handler_connect(sh, "media_ended", MediaEnded, this); + signal_handler_connect(sh, "media_next", MediaNext, this); obs_source_release(mediasource); } @@ -330,6 +338,12 @@ void MacroConditionMedia::MediaEnded(void *data, calldata_t *) media->_ended = true; } +void MacroConditionMedia::MediaNext(void *data, calldata_t *) +{ + MacroConditionMedia *media = static_cast(data); + media->_next = true; +} + static void populateMediaTimeRestrictions(QComboBox *list) { for (auto entry : mediaTimeRestrictions) { @@ -356,15 +370,18 @@ static void addAnyAndAllStates(QComboBox *list) MacroConditionMediaEdit::MacroConditionMediaEdit( QWidget *parent, std::shared_ptr entryData) - : QWidget(parent) + : QWidget(parent), + _mediaSources(new QComboBox()), + _scenes(new SceneSelectionWidget(window())), + _states(new QComboBox()), + _timeRestrictions(new QComboBox()), + _time(new DurationSelection()), + _onChange(new QCheckBox(obs_module_text( + "AdvSceneSwitcher.condition.media.matchOnChange"))) + { - _mediaSources = new QComboBox(); - _scenes = new SceneSelectionWidget(window()); - _states = new QComboBox(); - _timeRestrictions = new QComboBox(); - _time = new DurationSelection(); - _onChange = new QCheckBox(obs_module_text( - "AdvSceneSwitcher.condition.media.matchOnChange")); + _states->setToolTip(obs_module_text( + "AdvSceneSwitcher.condition.media.inconsistencyInfo")); QWidget::connect(_mediaSources, SIGNAL(currentTextChanged(const QString &)), this, @@ -548,6 +565,9 @@ void MacroConditionMediaEdit::OnChangeChanged(int value) void MacroConditionMediaEdit::SetWidgetVisibility() { _scenes->setVisible(_entryData->_sourceType != MediaSourceType::SOURCE); + if (!_onChange->isChecked()) { + _onChange->hide(); + } } int getIdxFromMediaState(MediaState state)