#include "macro-action-media.hpp" #include "layout-helpers.hpp" #include "selection-helpers.hpp" #include "macro-helpers.hpp" namespace advss { const std::string MacroActionMedia::id = "media"; bool MacroActionMedia::_registered = MacroActionFactory::Register( MacroActionMedia::id, {MacroActionMedia::Create, MacroActionMediaEdit::Create, "AdvSceneSwitcher.action.media"}); static const std::map actionTypes = { {MacroActionMedia::Action::PLAY, "AdvSceneSwitcher.action.media.type.play"}, {MacroActionMedia::Action::PAUSE, "AdvSceneSwitcher.action.media.type.pause"}, {MacroActionMedia::Action::STOP, "AdvSceneSwitcher.action.media.type.stop"}, {MacroActionMedia::Action::RESTART, "AdvSceneSwitcher.action.media.type.restart"}, {MacroActionMedia::Action::NEXT, "AdvSceneSwitcher.action.media.type.next"}, {MacroActionMedia::Action::PREVIOUS, "AdvSceneSwitcher.action.media.type.previous"}, {MacroActionMedia::Action::SEEK_DURATION, "AdvSceneSwitcher.action.media.type.seek.duration"}, {MacroActionMedia::Action::SEEK_PERCENTAGE, "AdvSceneSwitcher.action.media.type.seek.percentage"}, {MacroActionMedia::Action::WAIT_FOR_PLAYBACK_STOP, "AdvSceneSwitcher.action.media.type.waitForPlaybackStop"}, }; static const std::map selectionTypes = { {MacroActionMedia::SelectionType::SOURCE, "AdvSceneSwitcher.action.media.selectionType.source"}, {MacroActionMedia::SelectionType::SCENE_ITEM, "AdvSceneSwitcher.action.media.selectionType.sceneItem"}, }; std::shared_ptr MacroActionMedia::Create(Macro *m) { return std::make_shared(m); } std::shared_ptr MacroActionMedia::Copy() const { return std::make_shared(*this); } std::string MacroActionMedia::GetShortDesc() const { if (_selection == SelectionType::SOURCE) { return _mediaSource.ToString(); } return _scene.ToString() + " - " + _sceneItem.ToString(); } void MacroActionMedia::SeekToPercentage(obs_source_t *source) const { auto totalTimeMs = obs_source_media_get_duration(source); auto percentageTimeMs = round(totalTimeMs * _seekPercentage / 100); obs_source_media_set_time(source, percentageTimeMs); } static void waitHelper(std::unique_lock *lock, Macro *macro, obs_source_t *source) { using namespace std::chrono_literals; // Some media source types support playlists. (E.g. VLC Source) // Whenever a playlist item ends the media state briefly changes to // state OBS_MEDIA_STATE_ENDED. // To make sure that playback really has ended we check that the state // is not OBS_MEDIA_STATE_PLAYING at least twice in a row. static const int playingStateBreakThreshold = 2; int playingStateCount = 0; while (true) { if (MacroWaitShouldAbort() || MacroIsStopped(macro)) { break; } if (obs_source_media_get_state(source) != OBS_MEDIA_STATE_PLAYING) { playingStateCount++; } if (playingStateCount >= playingStateBreakThreshold) { break; } GetMacroWaitCV().wait_for(*lock, 10ms); } } void MacroActionMedia::PerformActionHelper(obs_source_t *source) const { obs_media_state state = obs_source_media_get_state(source); switch (_action) { case Action::PLAY: if (state == OBS_MEDIA_STATE_STOPPED || state == OBS_MEDIA_STATE_ENDED) { obs_source_media_restart(source); } else { obs_source_media_play_pause(source, false); } break; case Action::PAUSE: obs_source_media_play_pause(source, true); break; case Action::STOP: obs_source_media_stop(source); break; case Action::RESTART: obs_source_media_restart(source); break; case Action::NEXT: obs_source_media_next(source); break; case Action::PREVIOUS: obs_source_media_previous(source); break; case Action::SEEK_DURATION: obs_source_media_set_time(source, _seekDuration.Milliseconds()); break; case Action::SEEK_PERCENTAGE: SeekToPercentage(source); break; case Action::WAIT_FOR_PLAYBACK_STOP: { std::unique_lock lock(*GetMutex()); waitHelper(&lock, GetMacro(), source); break; } default: break; } } bool MacroActionMedia::PerformAction() { if (_selection == SelectionType::SOURCE) { OBSSourceAutoRelease source = obs_weak_source_get_source(_mediaSource.GetSource()); PerformActionHelper(source); } else { const auto items = _sceneItem.GetSceneItems(_scene); for (const auto &item : items) { PerformActionHelper(obs_sceneitem_get_source(item)); } } return true; } void MacroActionMedia::LogAction() const { auto it = actionTypes.find(_action); if (it != actionTypes.end()) { ablog(LOG_INFO, "performed action \"%s\" for source \"%s\"", it->second.c_str(), _selection == SelectionType::SOURCE ? _mediaSource.ToString(true).c_str() : _sceneItem.ToString().c_str()); } else { blog(LOG_WARNING, "ignored unknown media action %d", static_cast(_action)); } } bool MacroActionMedia::Save(obs_data_t *obj) const { MacroAction::Save(obj); obs_data_set_int(obj, "action", static_cast(_action)); obs_data_set_int(obj, "selectionType", static_cast(_selection)); _seekDuration.Save(obj); _seekPercentage.Save(obj, "seekPercentage"); _mediaSource.Save(obj, "mediaSource"); _scene.Save(obj); _sceneItem.Save(obj); return true; } bool MacroActionMedia::Load(obs_data_t *obj) { MacroAction::Load(obj); _action = static_cast(obs_data_get_int(obj, "action")); _selection = static_cast( obs_data_get_int(obj, "selectionType")); _seekDuration.Load(obj); _seekPercentage.Load(obj, "seekPercentage"); _mediaSource.Load(obj, "mediaSource"); _scene.Load(obj); _sceneItem.Load(obj); return true; } void MacroActionMedia::ResolveVariablesToFixedValues() { _seekDuration.ResolveVariables(); _seekPercentage.ResolveVariables(); _mediaSource.ResolveVariables(); _sceneItem.ResolveVariables(); _scene.ResolveVariables(); } static inline void populateActionSelection(QComboBox *list) { for (const auto &[_, name] : actionTypes) { list->addItem(obs_module_text(name.c_str())); } } static inline void populateSelectionTypeSelection(QComboBox *list) { for (const auto &[_, name] : selectionTypes) { list->addItem(obs_module_text(name.c_str())); } } static QStringList getMediaSourcesList() { auto sources = GetMediaSourceNames(); sources.sort(); return sources; } MacroActionMediaEdit::MacroActionMediaEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), _actions(new QComboBox()), _selectionTypes(new QComboBox()), _seekDuration(new DurationSelection()), _seekPercentage(new SliderSpinBox( 0, 100, obs_module_text( "AdvSceneSwitcher.action.media.seek.percentage.label"))), _sources(new SourceSelectionWidget(this, getMediaSourcesList, true)), _sceneItems(new SceneItemSelectionWidget( parent, { SceneItemSelection::Type::SOURCE_NAME, SceneItemSelection::Type::VARIABLE_NAME, SceneItemSelection::Type::SOURCE_NAME_PATTERN, SceneItemSelection::Type::SOURCE_GROUP, SceneItemSelection::Type::SOURCE_TYPE, SceneItemSelection::Type::INDEX, SceneItemSelection::Type::INDEX_RANGE, SceneItemSelection::Type::ALL, }, // All instances of a media source will be in the same state. // So, for example, restarting one instance of a source will // automatically restart all other instances of that source, // too. // Thus, we hide the name clash resolution options SceneItemSelectionWidget::NameClashMode::HIDE)), _scenes(new SceneSelectionWidget(this, true, false, true, true, true)) { populateActionSelection(_actions); populateSelectionTypeSelection(_selectionTypes); QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this, SLOT(ActionChanged(int))); QWidget::connect(_selectionTypes, SIGNAL(currentIndexChanged(int)), this, SLOT(SelectionTypeChanged(int))); QWidget::connect(_seekDuration, SIGNAL(DurationChanged(const Duration &)), this, SLOT(SeekDurationChanged(const Duration &))); QWidget::connect( _seekPercentage, SIGNAL(DoubleValueChanged(const NumberVariable &)), this, SLOT(SeekPercentageChanged(const NumberVariable &))); QWidget::connect(_sources, SIGNAL(SourceChanged(const SourceSelection &)), this, SLOT(SourceChanged(const SourceSelection &))); QWidget::connect(_sceneItems, SIGNAL(SceneItemChanged(const SceneItemSelection &)), this, SLOT(SourceChanged(const SceneItemSelection &))); QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)), _sceneItems, SLOT(SceneChanged(const SceneSelection &))); QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)), this, SLOT(SceneChanged(const SceneSelection &))); auto layout = new QHBoxLayout; PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.media.entry"), layout, {{"{{actions}}", _actions}, {"{{selectionTypes}}", _selectionTypes}, {"{{seekDuration}}", _seekDuration}, {"{{seekPercentage}}", _seekPercentage}, {"{{mediaSources}}", _sources}, {"{{scenes}}", _scenes}, {"{{sceneItems}}", _sceneItems}}); setLayout(layout); _entryData = entryData; UpdateEntryData(); _loading = false; } void MacroActionMediaEdit::ActionChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_action = static_cast(value); SetWidgetVisibility(); } void MacroActionMediaEdit::SelectionTypeChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_selection = static_cast(value); SetWidgetVisibility(); } void MacroActionMediaEdit::SeekDurationChanged(const Duration &seekDuration) { GUARD_LOADING_AND_LOCK(); _entryData->_seekDuration = seekDuration; } void MacroActionMediaEdit::SeekPercentageChanged( const NumberVariable &seekPercentage) { GUARD_LOADING_AND_LOCK(); _entryData->_seekPercentage = seekPercentage; } void MacroActionMediaEdit::SourceChanged(const SourceSelection &source) { GUARD_LOADING_AND_LOCK(); _entryData->_mediaSource = source; emit HeaderInfoChanged( QString::fromStdString(_entryData->GetShortDesc())); } void MacroActionMediaEdit::SourceChanged(const SceneItemSelection &item) { GUARD_LOADING_AND_LOCK(); _entryData->_sceneItem = item; emit HeaderInfoChanged( QString::fromStdString(_entryData->GetShortDesc())); adjustSize(); updateGeometry(); } void MacroActionMediaEdit::SceneChanged(const SceneSelection &scene) { GUARD_LOADING_AND_LOCK(); _entryData->_scene = scene; emit HeaderInfoChanged( QString::fromStdString(_entryData->GetShortDesc())); } void MacroActionMediaEdit::SetWidgetVisibility() { if (!_entryData) { return; } _sources->setVisible(_entryData->_selection == MacroActionMedia::SelectionType::SOURCE); _scenes->setVisible(_entryData->_selection == MacroActionMedia::SelectionType::SCENE_ITEM); _sceneItems->setVisible(_entryData->_selection == MacroActionMedia::SelectionType::SCENE_ITEM); _seekDuration->setVisible(_entryData->_action == MacroActionMedia::Action::SEEK_DURATION); _seekPercentage->setVisible(_entryData->_action == MacroActionMedia::Action::SEEK_PERCENTAGE); adjustSize(); updateGeometry(); } void MacroActionMediaEdit::UpdateEntryData() { if (!_entryData) { return; } _actions->setCurrentIndex(static_cast(_entryData->_action)); _selectionTypes->setCurrentIndex( static_cast(_entryData->_selection)); _seekDuration->SetDuration(_entryData->_seekDuration); _seekPercentage->SetDoubleValue(_entryData->_seekPercentage); _sources->SetSource(_entryData->_mediaSource); _scenes->SetScene(_entryData->_scene); _sceneItems->SetSceneItem((_entryData->_sceneItem)); SetWidgetVisibility(); } } // namespace advss