#include #include #include #include #include #include #include #include #include #include "headers/advanced-scene-switcher.hpp" #include "headers/curl-helper.hpp" #include "headers/utility.hpp" #include "headers/version.h" SwitcherData *switcher = nullptr; /****************************************************************************** * Create the Advanced Scene Switcher settings window ******************************************************************************/ AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent) : QDialog(parent), ui(new Ui_AdvSceneSwitcher) { ui->setupUi(this); std::lock_guard lock(switcher->m); switcher->Prune(); loadUI(); } bool translationAvailable() { return !!strcmp(obs_module_text("AdvSceneSwitcher.pluginName"), "AdvSceneSwitcher.pluginName"); } void AdvSceneSwitcher::loadUI() { if (!translationAvailable()) { (void)DisplayMessage( "Failed to find plug-in's 'data' directory.\n" "Please check installation instructions!"); } #if __APPLE__ setMinimumHeight(700); #endif setupGeneralTab(); setupTitleTab(); setupExecutableTab(); setupRegionTab(); setupPauseTab(); setupSequenceTab(); setupTransitionsTab(); setupIdleTab(); setupRandomTab(); setupMediaTab(); setupFileTab(); setupTimeTab(); setupAudioTab(); setupVideoTab(); setupNetworkTab(); setupSceneGroupTab(); setupTriggerTab(); setupMacroTab(); setTabOrder(); restoreWindowGeo(); loading = false; } /****************************************************************************** * UI helpers ******************************************************************************/ void AdvSceneSwitcher::addSelectionEntry(QComboBox *sel, const char *description, bool selectable, const char *tooltip) { sel->addItem(description); if (strcmp(tooltip, "") != 0) { sel->setItemData(0, tooltip, Qt::ToolTipRole); } QStandardItemModel *model = qobject_cast(sel->model()); QModelIndex firstIndex = model->index(0, sel->modelColumn(), sel->rootModelIndex()); QStandardItem *firstItem = model->itemFromIndex(firstIndex); if (!selectable) { firstItem->setSelectable(false); firstItem->setEnabled(false); } } void AdvSceneSwitcher::populateSceneSelection(QComboBox *sel, bool addPrevious, bool addSceneGroup, bool addSelect, std::string selectText, bool selectable) { if (addSelect) { if (selectText.empty()) { addSelectionEntry( sel, obs_module_text("AdvSceneSwitcher.selectScene"), selectable, obs_module_text( "AdvSceneSwitcher.invaildEntriesWillNotBeSaved")); } else { addSelectionEntry(sel, selectText.c_str(), selectable); } } BPtr scenes = obs_frontend_get_scene_names(); char **temp = scenes; while (*temp) { const char *name = *temp; sel->addItem(name); temp++; } if (addPrevious) { sel->addItem(obs_module_text( "AdvSceneSwitcher.selectPreviousScene")); } if (addSceneGroup) { for (auto &sg : switcher->sceneGroups) { sel->addItem(QString::fromStdString(sg.name)); } } } void AdvSceneSwitcher::populateTransitionSelection(QComboBox *sel, bool addCurrent, bool addSelect, bool selectable) { if (addSelect) { addSelectionEntry( sel, obs_module_text("AdvSceneSwitcher.selectTransition"), selectable); } if (addCurrent) { sel->addItem( obs_module_text("AdvSceneSwitcher.currentTransition")); } obs_frontend_source_list *transitions = new obs_frontend_source_list(); obs_frontend_get_transitions(transitions); for (size_t i = 0; i < transitions->sources.num; i++) { const char *name = obs_source_get_name(transitions->sources.array[i]); sel->addItem(name); } obs_frontend_source_list_free(transitions); } void AdvSceneSwitcher::populateWindowSelection(QComboBox *sel, bool addSelect) { if (addSelect) { addSelectionEntry( sel, obs_module_text("AdvSceneSwitcher.selectWindow")); } std::vector windows; GetWindowList(windows); sort(windows.begin(), windows.end()); for (std::string &window : windows) { sel->addItem(window.c_str()); } #ifdef WIN32 sel->setItemData(0, obs_module_text("AdvSceneSwitcher.selectWindowTip"), Qt::ToolTipRole); #endif } void AdvSceneSwitcher::populateAudioSelection(QComboBox *sel, bool addSelect) { if (addSelect) { addSelectionEntry( sel, obs_module_text("AdvSceneSwitcher.selectAudioSource"), 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); if ((flags & OBS_SOURCE_AUDIO) != 0) { list->push_back(obs_source_get_name(source)); } return true; }; std::vector audioSources; obs_enum_sources(sourceEnum, &audioSources); sort(audioSources.begin(), audioSources.end()); for (std::string &source : audioSources) { sel->addItem(source.c_str()); } } 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) { addSelectionEntry( sel, obs_module_text("AdvSceneSwitcher.selectMediaSource"), false, obs_module_text( "AdvSceneSwitcher.invaildEntriesWillNotBeSaved")); } auto sourceEnum = [](void *data, obs_source_t *source) -> bool /* -- */ { std::vector *list = reinterpret_cast *>(data); std::string sourceId = obs_source_get_id(source); if (sourceId.compare("ffmpeg_source") == 0 || sourceId.compare("vlc_source") == 0) { list->push_back(obs_source_get_name(source)); } return true; }; std::vector mediaSources; obs_enum_sources(sourceEnum, &mediaSources); sort(mediaSources.begin(), mediaSources.end()); for (std::string &source : mediaSources) { sel->addItem(source.c_str()); } } void AdvSceneSwitcher::populateProcessSelection(QComboBox *sel, bool addSelect) { if (addSelect) { addSelectionEntry( sel, obs_module_text("AdvSceneSwitcher.selectProcess")); } QStringList processes; GetProcessList(processes); processes.sort(); for (QString &process : processes) sel->addItem(process); } void AdvSceneSwitcher::listAddClicked(QListWidget *list, SwitchWidget *newWidget, QPushButton *addButton, QMetaObject::Connection *addHighlight) { if (!list || !newWidget) { blog(LOG_WARNING, "listAddClicked called without valid list or widget"); return; } if (addButton && addHighlight) { addButton->disconnect(*addHighlight); } QListWidgetItem *item; item = new QListWidgetItem(list); list->addItem(item); item->setSizeHint(newWidget->minimumSizeHint()); list->setItemWidget(item, newWidget); list->scrollToItem(item); } bool AdvSceneSwitcher::listMoveUp(QListWidget *list) { int index = list->currentRow(); if (index == -1 || index == 0) { return false; } QWidget *row = list->itemWidget(list->currentItem()); QListWidgetItem *itemN = list->currentItem()->clone(); list->insertItem(index - 1, itemN); list->setItemWidget(itemN, row); list->takeItem(index + 1); list->setCurrentRow(index - 1); return true; } bool AdvSceneSwitcher::listMoveDown(QListWidget *list) { int index = list->currentRow(); if (index == -1 || index == list->count() - 1) { return false; } QWidget *row = list->itemWidget(list->currentItem()); QListWidgetItem *itemN = list->currentItem()->clone(); list->insertItem(index + 2, itemN); list->setItemWidget(itemN, row); list->takeItem(index); list->setCurrentRow(index + 1); return true; } QMetaObject::Connection AdvSceneSwitcher::PulseWidget(QWidget *widget, QColor endColor, QColor startColor, QString specifier) { if (switcher->disableHints) { return QMetaObject::Connection(); } widget->setStyleSheet(specifier + "{ \ border-style: outset; \ border-width: 2px; \ border-radius: 10px; \ border-color: rgb(0,0,0,0) \ }"); QGraphicsColorizeEffect *eEffect = new QGraphicsColorizeEffect(widget); widget->setGraphicsEffect(eEffect); QPropertyAnimation *paAnimation = new QPropertyAnimation(eEffect, "color"); paAnimation->setStartValue(startColor); paAnimation->setEndValue(endColor); paAnimation->setDuration(1000); // Play backwards to return to original state on timer end paAnimation->setDirection(QAbstractAnimation::Backward); auto con = QWidget::connect( paAnimation, &QPropertyAnimation::finished, [paAnimation]() { QTimer::singleShot(1000, [paAnimation] { paAnimation->start(); }); }); paAnimation->start(); return con; } /****************************************************************************** * Saving and loading ******************************************************************************/ static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *) { if (saving) { std::lock_guard lock(switcher->m); switcher->Prune(); obs_data_t *obj = obs_data_create(); switcher->saveSettings(obj); obs_data_set_obj(save_data, "advanced-scene-switcher", obj); obs_data_release(obj); } else { switcher->m.lock(); obs_data_t *obj = obs_data_get_obj(save_data, "advanced-scene-switcher"); if (!obj) { obj = obs_data_create(); } if (switcher->versionChanged(obj, g_GIT_SHA1)) { AdvSceneSwitcher::AskBackup(obj); } switcher->loadSettings(obj); obs_data_release(obj); switcher->m.unlock(); // Stop the scene switcher at least once to // avoid scene duplication issues with scene collection changes bool start = !switcher->stop; switcher->Stop(); if (start) { switcher->Start(); } } } /****************************************************************************** * Main switcher thread ******************************************************************************/ void SwitcherData::Thread() { blog(LOG_INFO, "started"); int sleep = 0; int linger = 0; std::chrono::milliseconds duration; auto startTime = std::chrono::high_resolution_clock::now(); auto endTime = std::chrono::high_resolution_clock::now(); while (true) { std::unique_lock lock(m); bool match = false; OBSWeakSource scene; OBSWeakSource transition; // The previous scene might have changed during the linger duration, // if a longer transition is used than the configured check interval bool setPrevSceneAfterLinger = false; bool macroMatch = false; macroSceneSwitched = false; endTime = std::chrono::high_resolution_clock::now(); auto runTime = std::chrono::duration_cast( endTime - startTime); if (sleep) { duration = std::chrono::milliseconds(sleep); } else { duration = std::chrono::milliseconds(interval) + std::chrono::milliseconds(linger) - runTime; if (duration.count() < 1) { blog(LOG_INFO, "detected busy loop - refusing to sleep less than 1ms"); duration = std::chrono::milliseconds(50); } } vblog(LOG_INFO, "try to sleep for %ld", duration.count()); cv.wait_for(lock, duration); startTime = std::chrono::high_resolution_clock::now(); sleep = 0; linger = 0; Prune(); if (stop) { break; } if (checkPause()) { continue; } match = checkForMatch(scene, transition, linger, setPrevSceneAfterLinger, macroMatch); if (stop) { break; } checkNoMatchSwitch(match, scene, transition, sleep); checkSwitchCooldown(match); if (linger) { duration = std::chrono::milliseconds(linger); vblog(LOG_INFO, "sleep for %ld before switching scene", duration.count()); cv.wait_for(lock, duration); if (stop) { break; } if (sceneChangedDuringWait()) { vblog(LOG_INFO, "scene was changed manually - ignoring match"); match = false; linger = 0; } else if (setPrevSceneAfterLinger) { scene = previousScene; } } // After this point we will call frontend functions // obs_frontend_set_current_scene() and // obs_frontend_set_current_transition() // // During this time SaveSceneSwitcher() could be called // leading to a deadlock, so we have to unlock() lock.unlock(); if (match) { if (macroMatch) { runMacros(); } else { switchScene({scene, transition, 0}); } } writeSceneInfoToFile(); } blog(LOG_INFO, "stopped"); } bool SwitcherData::checkForMatch(OBSWeakSource &scene, OBSWeakSource &transition, int &linger, bool &setPrevSceneAfterLinger, bool ¯oMatch) { bool match = false; if (uninterruptibleSceneSequenceActive) { match = checkSceneSequence(scene, transition, linger, setPrevSceneAfterLinger); if (match) { return match; } } for (int switchFuncName : functionNamesByPriority) { switch (switchFuncName) { case read_file_func: match = checkSwitchInfoFromFile(scene, transition) || checkFileContent(scene, transition); break; case idle_func: match = checkIdleSwitch(scene, transition); break; case exe_func: match = checkExeSwitch(scene, transition); break; case screen_region_func: match = checkScreenRegionSwitch(scene, transition); break; case window_title_func: match = checkWindowTitleSwitch(scene, transition); break; case round_trip_func: match = checkSceneSequence(scene, transition, linger, setPrevSceneAfterLinger); break; case media_func: match = checkMediaSwitch(scene, transition); break; case time_func: match = checkTimeSwitch(scene, transition); break; case audio_func: match = checkAudioSwitch(scene, transition); break; case video_func: match = checkVideoSwitch(scene, transition); break; case macro_func: if (checkMacros()) { match = true; macroMatch = true; } break; } if (stop) { return false; } if (match) { break; } } return match; } void switchScene(sceneSwitchInfo sceneSwitch) { if (!sceneSwitch.scene && switcher->verbose) { blog(LOG_INFO, "nothing to switch to"); return; } obs_source_t *source = obs_weak_source_get_source(sceneSwitch.scene); obs_source_t *currentSource = obs_frontend_get_current_scene(); if (source && source != currentSource) { transitionData currentTransitionData; setNextTransition(sceneSwitch, currentSource, currentTransitionData); obs_frontend_set_current_scene(source); if (switcher->transitionOverrideOverride) { restoreTransitionOverride(source, currentTransitionData); } if (switcher->verbose) { blog(LOG_INFO, "switched scene"); } if (switcher->networkConfig.ServerEnabled) { switcher->server.sendMessage(sceneSwitch); } } obs_source_release(currentSource); obs_source_release(source); } void SwitcherData::Start() { if (!(th && th->isRunning())) { stop = false; th = new SwitcherThread(); th->start((QThread::Priority)threadPriority); // Will be overwritten quickly but might be useful writeToStatusFile("Advanced Scene Switcher running"); } if (networkConfig.ServerEnabled) { server.start(networkConfig.ServerPort, networkConfig.LockToIPv4); } if (networkConfig.ClientEnabled) { client.connect(networkConfig.GetClientUri()); } } void SwitcherData::Stop() { if (th && th->isRunning()) { stop = true; transitionCv.notify_one(); cv.notify_one(); th->wait(); delete th; th = nullptr; writeToStatusFile("Advanced Scene Switcher stopped"); } server.stop(); client.disconnect(); } bool SwitcherData::sceneChangedDuringWait() { obs_source_t *currentSource = obs_frontend_get_current_scene(); if (!currentSource) { return true; } obs_source_release(currentSource); return (waitScene && currentSource != waitScene); } /****************************************************************************** * OBS module setup ******************************************************************************/ extern "C" void FreeSceneSwitcher() { if (loaded_curl_lib) { if (switcher->curl && f_curl_cleanup) { f_curl_cleanup(switcher->curl); } delete loaded_curl_lib; loaded_curl_lib = nullptr; } delete switcher; switcher = nullptr; } void handleSceneChange(SwitcherData *s) { // Stop waiting if scene was changed if (s->sceneChangedDuringWait()) { s->cv.notify_one(); } // Set previous scene obs_source_t *source = obs_frontend_get_current_scene(); obs_weak_source_t *ws = obs_source_get_weak_source(source); obs_source_release(source); obs_weak_source_release(ws); if (source && s->previousSceneHelper != ws) { s->previousScene = s->previousSceneHelper; s->previousSceneHelper = ws; } s->checkTriggers(); s->checkDefaultSceneTransitions(); if (switcher->networkConfig.ServerEnabled && switcher->networkConfig.SendAll) { switcher->server.sendMessage({ws, nullptr, 0}); } } void setLiveTime(SwitcherData *s) { s->liveTime = QDateTime::currentDateTime(); } void resetLiveTime(SwitcherData *s) { s->liveTime = QDateTime(); } void checkAutoStartRecording(SwitcherData *s) { if (s->autoStartEvent == AutoStartEvent::RECORDING || s->autoStartEvent == AutoStartEvent::RECORINDG_OR_STREAMING) s->Start(); } void checkAutoStartStreaming(SwitcherData *s) { if (s->autoStartEvent == AutoStartEvent::STREAMING || s->autoStartEvent == AutoStartEvent::RECORINDG_OR_STREAMING) s->Start(); } // Note to future self: // be careful using switcher->m here as there is potential for deadlocks when using // frontend functions such as obs_frontend_set_current_scene() static void OBSEvent(enum obs_frontend_event event, void *switcher) { switch (event) { case OBS_FRONTEND_EVENT_EXIT: FreeSceneSwitcher(); break; case OBS_FRONTEND_EVENT_SCENE_CHANGED: handleSceneChange((SwitcherData *)switcher); break; case OBS_FRONTEND_EVENT_RECORDING_STARTED: setLiveTime((SwitcherData *)switcher); checkAutoStartRecording((SwitcherData *)switcher); break; case OBS_FRONTEND_EVENT_STREAMING_STARTED: setLiveTime((SwitcherData *)switcher); checkAutoStartStreaming((SwitcherData *)switcher); break; case OBS_FRONTEND_EVENT_RECORDING_STOPPED: case OBS_FRONTEND_EVENT_STREAMING_STOPPED: resetLiveTime((SwitcherData *)switcher); break; default: break; } } extern "C" void InitSceneSwitcher() { blog(LOG_INFO, "version: %s", g_GIT_SHA1); QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction( obs_module_text("AdvSceneSwitcher.pluginName")); switcher = new SwitcherData; if (loadCurl() && f_curl_init) { switcher->curl = f_curl_init(); } auto cb = []() { QMainWindow *window = (QMainWindow *)obs_frontend_get_main_window(); AdvSceneSwitcher ss(window); ss.exec(); }; obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr); obs_frontend_add_event_callback(OBSEvent, switcher); action->connect(action, &QAction::triggered, cb); }