#include "advanced-scene-switcher.hpp" #include "backup.hpp" #include "curl-helper.hpp" #include "log-helper.hpp" #include "macro-helpers.hpp" #include "obs-module-helper.hpp" #include "path-helpers.hpp" #include "platform-funcs.hpp" #include "scene-switch-helpers.hpp" #include "source-helpers.hpp" #include "status-control.hpp" #include "switcher-data.hpp" #include "ui-helpers.hpp" #include "tab-helpers.hpp" #include "utility.hpp" #include "version.h" #include #include #include #include #include #include #include namespace advss { AdvSceneSwitcher *AdvSceneSwitcher::window = nullptr; /****************************************************************************** * Create the Advanced Scene Switcher settings window ******************************************************************************/ AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent) : QDialog(parent), ui(new Ui_AdvSceneSwitcher) { switcher->settingsWindowOpened = true; ui->setupUi(this); std::lock_guard lock(switcher->m); switcher->Prune(); LoadUI(); } AdvSceneSwitcher::~AdvSceneSwitcher() { if (switcher) { switcher->settingsWindowOpened = false; SaveLastOpenedTab(ui->tabWidget); } } static bool translationAvailable() { return !!strcmp(obs_module_text("AdvSceneSwitcher.pluginName"), "AdvSceneSwitcher.pluginName"); } static void DisplayMissingDependencyWarning() { if (!switcher->warnPluginLoadFailure || switcher->loadFailureLibs.isEmpty()) { return; } QString failedLibsString = "
    "; for (const auto &lib : switcher->loadFailureLibs) { failedLibsString += "
  • " + lib + "
  • "; } failedLibsString += "
"; QString warning(obs_module_text( "AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailureMessage")); DisplayMessage(warning.arg(failedLibsString)); } static void DisplayMissingDataDirWarning() { if (translationAvailable()) { return; } QString msg = "Failed to find plug-in's 'data' directory.\n" "Please check installation instructions!\n\n" "Data most likely expected at:\n\n"; #ifdef _WIN32 msg += QString::fromStdString( (std::filesystem::current_path().string())); msg += "/"; #endif msg += obs_get_module_data_path(obs_current_module()); DisplayMessage(msg); } void AdvSceneSwitcher::LoadUI() { DisplayMissingDataDirWarning(); DisplayMissingDependencyWarning(); SetupGeneralTab(); SetupTitleTab(); SetupExecutableTab(); SetupRegionTab(); SetupPauseTab(); SetupSequenceTab(); SetupTransitionsTab(); SetupIdleTab(); SetupRandomTab(); SetupMediaTab(); SetupFileTab(); SetupTimeTab(); SetupAudioTab(); SetupVideoTab(); SetupNetworkTab(); SetupSceneGroupTab(); SetupTriggerTab(); SetupMacroTab(); SetupOtherTabs(ui->tabWidget); SetDeprecationWarnings(); SetTabOrder(ui->tabWidget); SetCurrentTab(ui->tabWidget); RestoreWindowGeo(); CheckFirstTimeSetup(); loading = false; } bool AdvSceneSwitcher::eventFilter(QObject *obj, QEvent *event) { auto eventType = event->type(); if (obj == ui->macroElseActions && eventType == QEvent::Resize) { QResizeEvent *resizeEvent = static_cast(event); if (resizeEvent->size().height() == 0) { SetElseActionsStateToHidden(); return QDialog::eventFilter(obj, event); } SetElseActionsStateToVisible(); } else if (eventType == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); auto pressedKey = keyEvent->key(); if (obj == ui->macros && ui->macros->isVisible()) { if (pressedKey == Qt::Key_F2) { RenameSelectedMacro(); } else if (pressedKey == Qt::Key_Delete) { RemoveSelectedMacros(); } } } return QDialog::eventFilter(obj, event); } /****************************************************************************** * Saving and loading ******************************************************************************/ static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *) { if (!switcher) { return; } if (saving) { std::lock_guard lock(switcher->m); switcher->Prune(); OBSDataAutoRelease data = obs_data_create(); switcher->SaveSettings(data); obs_data_set_obj(save_data, "advanced-scene-switcher", data); } else { // Stop the scene switcher at least once to // avoid scene duplication issues with scene collection changes switcher->Stop(); OBSDataAutoRelease data = obs_data_get_obj(save_data, "advanced-scene-switcher"); if (!data) { data = obs_data_create(); } switcher->m.lock(); if (switcher->VersionChanged(data, g_GIT_SHA1)) { auto json = obs_data_get_json(data); static QString jsonQString = json ? json : ""; std::thread t([]() { obs_queue_task( OBS_TASK_UI, [](void *) { AskForBackup(jsonQString); }, nullptr, false); }); t.detach(); } switcher->LoadSettings(data); switcher->m.unlock(); if (!switcher->stop) { 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(); switcher->firstIntervalAfterStop = true; while (true) { std::unique_lock lock(m); mainLoopLock = &lock; 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; 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", (long int)duration.count()); SetWaitScene(); cv.wait_for(lock, duration); startTime = std::chrono::high_resolution_clock::now(); sleep = 0; linger = 0; Prune(); if (stop) { break; } if (checkPause()) { continue; } SetPreconditions(); 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", (long int)duration.count()); SetWaitScene(); 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; } } ResetForNextInterval(); if (match) { if (macroMatch) { RunMacros(); } else { SwitchScene({scene, transition, 0}); } } writeSceneInfoToFile(); switcher->firstInterval = false; switcher->firstIntervalAfterStop = false; } mainLoopLock = nullptr; blog(LOG_INFO, "stopped"); } void SwitcherData::SetPreconditions() { // Window title lastTitle = currentTitle; std::string title; GetCurrentWindowTitle(title); for (auto &window : ignoreWindowsSwitches) { bool equals = (title == window); bool matches = false; if (!equals) { try { std::regex expr(window); matches = std::regex_match(title, expr); } catch (const std::regex_error &) { } } if (equals || matches) { title = lastTitle; break; } } currentTitle = title; // Process name GetForegroundProcessName(currentForegroundProcess); // Macro InvalidateMacroTempVarValues(); } void SwitcherData::ResetForNextInterval() { // Plugin reset functions for (const auto &func : resetIntervalSteps) { func(); } } 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; } static void ResetMacros() { for (auto &m : GetMacros()) { ResetMacroRunCount(m.get()); ResetMacroConditionTimers(m.get()); } } void AutoStartActionQueues(); void SwitcherData::Start() { if (!(th && th->isRunning())) { ResetForNextInterval(); ResetMacros(); AutoStartActionQueues(); 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()); } if (showSystemTrayNotifications) { DisplayTrayMessage( obs_module_text("AdvSceneSwitcher.pluginName"), obs_module_text("AdvSceneSwitcher.running")); } } void CloseAllInputDialogs(); void StopAndClearAllActionQueues(); void SwitcherData::Stop() { if (th && th->isRunning()) { stop = true; cv.notify_all(); SetMacroAbortWait(true); GetMacroWaitCV().notify_all(); GetMacroTransitionCV().notify_all(); StopAllMacros(); StopAndClearAllActionQueues(); CloseAllInputDialogs(); th->wait(); delete th; th = nullptr; writeToStatusFile("Advanced Scene Switcher stopped"); } server.stop(); client.disconnect(); if (showSystemTrayNotifications) { DisplayTrayMessage( obs_module_text("AdvSceneSwitcher.pluginName"), obs_module_text("AdvSceneSwitcher.stopped")); } } void SwitcherData::SetWaitScene() { waitScene = obs_frontend_get_current_scene(); obs_source_release(waitScene); } bool SwitcherData::SceneChangedDuringWait() { OBSSourceAutoRelease currentSource = obs_frontend_get_current_scene(); if (!currentSource) { return true; } return (waitScene && currentSource != waitScene); } // Relies on the fact that switcher->currentScene will only be updated on event // OBS_FRONTEND_EVENT_SCENE_CHANGED but obs_frontend_get_current_scene() will // already return the scene to be transitioned to. bool SwitcherData::AnySceneTransitionStarted() { OBSSourceAutoRelease currentSceneSrouce = obs_frontend_get_current_scene(); OBSWeakSourceAutoRelease currentScene = obs_source_get_weak_source(currentSceneSrouce); bool ret = switcher->currentScene != currentScene; return ret; } /****************************************************************************** * OBS module setup ******************************************************************************/ extern "C" EXPORT void FreeSceneSwitcher() { PlatformCleanup(); RunPluginCleanupSteps(); delete switcher; switcher = nullptr; } static void handleSceneChange() { // Stop waiting if scene was changed if (switcher->SceneChangedDuringWait()) { switcher->cv.notify_one(); } // Set current and previous scene OBSSourceAutoRelease source = obs_frontend_get_current_scene(); OBSWeakSourceAutoRelease ws = obs_source_get_weak_source(source); if (ws && ws != switcher->currentScene) { switcher->previousScene = switcher->currentScene; switcher->currentScene = ws; vblog(LOG_INFO, "current scene: %s", GetWeakSourceName(switcher->currentScene).c_str()); vblog(LOG_INFO, "previous scene: %s", GetWeakSourceName(switcher->previousScene).c_str()); } switcher->checkTriggers(); switcher->checkDefaultSceneTransitions(); if (switcher->networkConfig.ShouldSendFrontendSceneChange()) { switcher->server.sendMessage({ws.Get(), nullptr, 0}); } } static void setLiveTime() { switcher->liveTime = QDateTime::currentDateTime(); } static void resetLiveTime() { switcher->liveTime = QDateTime(); } static void checkAutoStartRecording() { if (switcher->autoStartEvent == SwitcherData::AutoStart::RECORDING || switcher->autoStartEvent == SwitcherData::AutoStart::RECORINDG_OR_STREAMING) switcher->Start(); } static void checkAutoStartStreaming() { if (switcher->autoStartEvent == SwitcherData::AutoStart::STREAMING || switcher->autoStartEvent == SwitcherData::AutoStart::RECORINDG_OR_STREAMING) switcher->Start(); } static void handlePeviewSceneChange() { if (switcher->networkConfig.ShouldSendPrviewSceneChange()) { OBSSourceAutoRelease source = obs_frontend_get_current_preview_scene(); OBSWeakSourceAutoRelease weak = obs_source_get_weak_source(source); switcher->server.sendMessage({weak.Get(), nullptr, 0}, true); } } static void handleTransitionEnd() { GetMacroTransitionCV().notify_all(); } static void handleShutdown() { if (!switcher) { return; } switcher->obsIsShuttingDown = true; if (ShutdownCheckIsNecessary()) { switcher->Stop(); CheckMacros(); RunMacros(); // Unfortunately this will not work as OBS will now allow saving // the scene collection data at this point, So any OBS specific // changes done during shutdown will be lost // // TODO: Look for a way to possibly resolve this obs_frontend_save(); } BackupSettingsOfCurrentVersion(); } static void handleSceneCollectionChanging() { if (switcher->settingsWindowOpened) { AdvSceneSwitcher::window->close(); } if (!switcher->stop) { switcher->sceneColletionStop = true; switcher->Stop(); } } static void handleSceneCollectionCleanup() { // Reset the plugin to default settings to avoid settings being carried // over to newly created scene collections. // Skip this step for the initial OBS startup. // OBSBasic::LoadData() will call ClearSceneData() which will trigger // this function to be called. // This would result in the plugin being started with default settings // before the "real" settings are loaded. static bool isOBSStartup = true; if (isOBSStartup) { isOBSStartup = false; return; } SaveSceneSwitcher(nullptr, false, nullptr); } // 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) { if (!switcher) { return; } switch (event) { case OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN: // Note: We are intentionally not listening for // OBS_FRONTEND_EVENT_EXIT here as at that point all scene // collection data will already have been cleared and thus all // macros will have already been cleared handleShutdown(); break; case OBS_FRONTEND_EVENT_SCENE_CHANGED: handleSceneChange(); break; case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: handlePeviewSceneChange(); break; case OBS_FRONTEND_EVENT_RECORDING_STARTED: setLiveTime(); checkAutoStartRecording(); break; case OBS_FRONTEND_EVENT_STREAMING_STARTED: setLiveTime(); checkAutoStartStreaming(); break; case OBS_FRONTEND_EVENT_RECORDING_STOPPED: case OBS_FRONTEND_EVENT_STREAMING_STOPPED: resetLiveTime(); break; case OBS_FRONTEND_EVENT_TRANSITION_STOPPED: handleTransitionEnd(); break; #if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 2, 0) case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING: handleSceneCollectionChanging(); break; case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: handleSceneCollectionCleanup(); break; #endif default: break; } } static void LoadPlugins() { QFileInfo libPath( QString(obs_get_module_binary_path(obs_current_module()))); QString pluginDir(libPath.absolutePath() + "/adv-ss-plugins"); #ifdef _WIN32 QString libPattern = "*.dll"; SetDllDirectory(pluginDir.toStdWString().c_str()); #else QString libPattern = "*.so"; #endif QDirIterator it(pluginDir, QStringList() << libPattern, QDir::Files); while (it.hasNext()) { auto file = it.next(); blog(LOG_INFO, "attempting to load \"%s\"", file.toStdString().c_str()); auto lib = new QLibrary(file, nullptr); if (lib->load()) { blog(LOG_INFO, "successfully loaded \"%s\"", file.toStdString().c_str()); } else { blog(LOG_WARNING, "failed to load \"%s\": %s", file.toStdString().c_str(), lib->errorString().toStdString().c_str()); switcher->loadFailureLibs << file; } } } void OpenSettingsWindow() { if (switcher->settingsWindowOpened) { AdvSceneSwitcher::window->show(); AdvSceneSwitcher::window->raise(); AdvSceneSwitcher::window->activateWindow(); } else { AdvSceneSwitcher::window = new AdvSceneSwitcher(static_cast( obs_frontend_get_main_window())); AdvSceneSwitcher::window->setAttribute(Qt::WA_DeleteOnClose); AdvSceneSwitcher::window->show(); } } QWidget *GetSettingsWindow() { return SettingsWindowIsOpened() ? AdvSceneSwitcher::window : nullptr; } void SetupActionQueues(); extern "C" EXPORT void InitSceneSwitcher(obs_module_t *module, translateFunc translate) { blog(LOG_INFO, "version: %s", g_GIT_TAG); blog(LOG_INFO, "version: %s", g_GIT_SHA1); switcher = new SwitcherData(module, translate); PlatformInit(); LoadPlugins(); SetupDock(); SetupActionQueues(); RunPluginInitSteps(); obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr); obs_frontend_add_event_callback(OBSEvent, switcher); QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction( obs_module_text("AdvSceneSwitcher.pluginName")); action->connect(action, &QAction::triggered, OpenSettingsWindow); } } // namespace advss