Files
obs-studio/plugins/frontend-tools/auto-scene-switcher.cpp
PatTheMav 1829492e6b cmake: Move frontend plugins into main plugins dir
Frontend plugins should not require being placed in the frontend
directory to be built successfully. Indeed they should only depend
on libobs and the obs-frontend-api and thus their source tree should
be able to exist anywhere (even standalone) and the plugin should still
compile successfully (just like any 3rd party plugin).

Thus moving those plugins into the main plugin directory ensures that
they don't require on any "special sauce" within the source tree to
compile.
2026-06-05 16:00:18 -04:00

508 lines
11 KiB
C++

#include <obs-frontend-api.h>
#include <obs-module.h>
#include <obs.hpp>
#include <util/util.hpp>
#include <QMainWindow>
#include <QMessageBox>
#include <QAction>
#include "auto-scene-switcher.hpp"
#include "tool-helpers.hpp"
#include <condition_variable>
#include <chrono>
#include <string>
#include <vector>
#include <thread>
#include <regex>
#include <mutex>
using namespace std;
#define DEFAULT_INTERVAL 300
struct SceneSwitch {
OBSWeakSource scene;
string window;
regex re;
inline SceneSwitch(OBSWeakSource scene_, const char *window_) : scene(scene_), window(window_), re(window_) {}
};
static inline bool WeakSourceValid(obs_weak_source_t *ws)
{
OBSSourceAutoRelease source = obs_weak_source_get_source(ws);
return !!source;
}
struct SwitcherData {
thread th;
condition_variable cv;
mutex m;
bool stop = false;
vector<SceneSwitch> switches;
OBSWeakSource nonMatchingScene;
int interval = DEFAULT_INTERVAL;
bool switchIfNotMatching = false;
void Thread();
void Start();
void Stop();
void Prune()
{
for (size_t i = 0; i < switches.size(); i++) {
SceneSwitch &s = switches[i];
if (!WeakSourceValid(s.scene))
switches.erase(switches.begin() + i--);
}
if (nonMatchingScene && !WeakSourceValid(nonMatchingScene)) {
switchIfNotMatching = false;
nonMatchingScene = nullptr;
}
}
inline ~SwitcherData() { Stop(); }
};
static SwitcherData *switcher = nullptr;
static inline QString MakeSwitchName(const QString &scene, const QString &window)
{
return QStringLiteral("[") + scene + QStringLiteral("]: ") + window;
}
SceneSwitcher::SceneSwitcher(QWidget *parent) : QDialog(parent), ui(new Ui_SceneSwitcher)
{
ui->setupUi(this);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
lock_guard<mutex> lock(switcher->m);
switcher->Prune();
BPtr<char *> scenes = obs_frontend_get_scene_names();
char **temp = scenes;
while (*temp) {
const char *name = *temp;
ui->scenes->addItem(name);
ui->noMatchSwitchScene->addItem(name);
temp++;
}
if (switcher->switchIfNotMatching)
ui->noMatchSwitch->setChecked(true);
else
ui->noMatchDontSwitch->setChecked(true);
ui->noMatchSwitchScene->setCurrentText(GetWeakSourceName(switcher->nonMatchingScene).c_str());
ui->checkInterval->setValue(switcher->interval);
vector<string> windows;
GetWindowList(windows);
for (string &window : windows)
ui->windows->addItem(window.c_str());
for (auto &s : switcher->switches) {
string sceneName = GetWeakSourceName(s.scene);
QString text = MakeSwitchName(sceneName.c_str(), s.window.c_str());
QListWidgetItem *item = new QListWidgetItem(text, ui->switches);
item->setData(Qt::UserRole, s.window.c_str());
}
if (switcher->th.joinable())
SetStarted();
else
SetStopped();
loading = false;
connect(this, &QDialog::finished, this, &SceneSwitcher::finished);
}
void SceneSwitcher::finished()
{
obs_frontend_save();
}
int SceneSwitcher::FindByData(const QString &window)
{
int count = ui->switches->count();
int idx = -1;
for (int i = 0; i < count; i++) {
QListWidgetItem *item = ui->switches->item(i);
QString itemWindow = item->data(Qt::UserRole).toString();
if (itemWindow == window) {
idx = i;
break;
}
}
return idx;
}
void SceneSwitcher::on_switches_currentRowChanged(int idx)
{
if (loading)
return;
if (idx == -1)
return;
QListWidgetItem *item = ui->switches->item(idx);
QString window = item->data(Qt::UserRole).toString();
lock_guard<mutex> lock(switcher->m);
for (auto &s : switcher->switches) {
if (window.compare(s.window.c_str()) == 0) {
string name = GetWeakSourceName(s.scene);
ui->scenes->setCurrentText(name.c_str());
ui->windows->setCurrentText(window);
break;
}
}
}
void SceneSwitcher::on_close_clicked()
{
done(0);
}
void SceneSwitcher::on_add_clicked()
{
QString sceneName = ui->scenes->currentText();
QString windowName = ui->windows->currentText();
if (windowName.isEmpty())
return;
OBSWeakSource source = GetWeakSourceByQString(sceneName);
QVariant v = QVariant::fromValue(windowName);
QString text = MakeSwitchName(sceneName, windowName);
int idx = FindByData(windowName);
if (idx == -1) {
try {
lock_guard<mutex> lock(switcher->m);
switcher->switches.emplace_back(source, windowName.toUtf8().constData());
QListWidgetItem *item = new QListWidgetItem(text, ui->switches);
item->setData(Qt::UserRole, v);
} catch (const regex_error &) {
QMessageBox::warning(this, obs_module_text("InvalidRegex.Title"),
obs_module_text("InvalidRegex.Text"));
}
} else {
QListWidgetItem *item = ui->switches->item(idx);
item->setText(text);
string window = windowName.toUtf8().constData();
{
lock_guard<mutex> lock(switcher->m);
for (auto &s : switcher->switches) {
if (s.window == window) {
s.scene = source;
break;
}
}
}
ui->switches->sortItems();
}
}
void SceneSwitcher::on_remove_clicked()
{
QListWidgetItem *item = ui->switches->currentItem();
if (!item)
return;
string window = item->data(Qt::UserRole).toString().toUtf8().constData();
{
lock_guard<mutex> lock(switcher->m);
auto &switches = switcher->switches;
for (auto it = switches.begin(); it != switches.end(); ++it) {
auto &s = *it;
if (s.window == window) {
switches.erase(it);
break;
}
}
}
delete item;
}
void SceneSwitcher::UpdateNonMatchingScene(const QString &name)
{
OBSSourceAutoRelease scene = obs_get_source_by_name(name.toUtf8().constData());
OBSWeakSourceAutoRelease ws = obs_source_get_weak_source(scene);
switcher->nonMatchingScene = ws.Get();
}
void SceneSwitcher::on_noMatchDontSwitch_clicked()
{
if (loading)
return;
lock_guard<mutex> lock(switcher->m);
switcher->switchIfNotMatching = false;
}
void SceneSwitcher::on_noMatchSwitch_clicked()
{
if (loading)
return;
lock_guard<mutex> lock(switcher->m);
switcher->switchIfNotMatching = true;
UpdateNonMatchingScene(ui->noMatchSwitchScene->currentText());
}
void SceneSwitcher::on_noMatchSwitchScene_currentTextChanged(const QString &text)
{
if (loading)
return;
lock_guard<mutex> lock(switcher->m);
UpdateNonMatchingScene(text);
}
void SceneSwitcher::on_checkInterval_valueChanged(int value)
{
if (loading)
return;
lock_guard<mutex> lock(switcher->m);
switcher->interval = value;
}
void SceneSwitcher::SetStarted()
{
ui->toggleStartButton->setText(obs_module_text("Stop"));
ui->pluginRunningText->setText(obs_module_text("Active"));
}
void SceneSwitcher::SetStopped()
{
ui->toggleStartButton->setText(obs_module_text("Start"));
ui->pluginRunningText->setText(obs_module_text("Inactive"));
}
void SceneSwitcher::on_toggleStartButton_clicked()
{
if (switcher->th.joinable()) {
switcher->Stop();
SetStopped();
} else {
switcher->Start();
SetStarted();
}
}
static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *)
{
if (saving) {
lock_guard<mutex> lock(switcher->m);
OBSDataAutoRelease obj = obs_data_create();
OBSDataArrayAutoRelease array = obs_data_array_create();
switcher->Prune();
for (SceneSwitch &s : switcher->switches) {
OBSDataAutoRelease array_obj = obs_data_create();
OBSSourceAutoRelease source = obs_weak_source_get_source(s.scene);
if (source) {
const char *n = obs_source_get_name(source);
obs_data_set_string(array_obj, "scene", n);
obs_data_set_string(array_obj, "window_title", s.window.c_str());
obs_data_array_push_back(array, array_obj);
}
}
string nonMatchingSceneName = GetWeakSourceName(switcher->nonMatchingScene);
obs_data_set_int(obj, "interval", switcher->interval);
obs_data_set_string(obj, "non_matching_scene", nonMatchingSceneName.c_str());
obs_data_set_bool(obj, "switch_if_not_matching", switcher->switchIfNotMatching);
obs_data_set_bool(obj, "active", switcher->th.joinable());
obs_data_set_array(obj, "switches", array);
obs_data_set_obj(save_data, "auto-scene-switcher", obj);
} else {
switcher->m.lock();
OBSDataAutoRelease obj = obs_data_get_obj(save_data, "auto-scene-switcher");
OBSDataArrayAutoRelease array = obs_data_get_array(obj, "switches");
size_t count = obs_data_array_count(array);
if (!obj)
obj = obs_data_create();
obs_data_set_default_int(obj, "interval", DEFAULT_INTERVAL);
switcher->interval = obs_data_get_int(obj, "interval");
switcher->switchIfNotMatching = obs_data_get_bool(obj, "switch_if_not_matching");
string nonMatchingScene = obs_data_get_string(obj, "non_matching_scene");
bool active = obs_data_get_bool(obj, "active");
switcher->nonMatchingScene = GetWeakSourceByName(nonMatchingScene.c_str());
switcher->switches.clear();
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease array_obj = obs_data_array_item(array, i);
const char *scene = obs_data_get_string(array_obj, "scene");
const char *window = obs_data_get_string(array_obj, "window_title");
switcher->switches.emplace_back(GetWeakSourceByName(scene), window);
}
switcher->m.unlock();
if (active)
switcher->Start();
else
switcher->Stop();
}
}
void SwitcherData::Thread()
{
chrono::duration<long long, milli> duration = chrono::milliseconds(interval);
string lastTitle;
string title;
for (;;) {
unique_lock<mutex> lock(m);
OBSWeakSource scene;
bool match = false;
cv.wait_for(lock, duration);
if (switcher->stop) {
switcher->stop = false;
break;
}
duration = chrono::milliseconds(interval);
GetCurrentWindowTitle(title);
if (lastTitle != title) {
switcher->Prune();
for (SceneSwitch &s : switches) {
if (s.window == title) {
match = true;
scene = s.scene;
break;
}
}
/* try regex */
if (!match) {
for (SceneSwitch &s : switches) {
try {
bool matches = regex_match(title, s.re);
if (matches) {
match = true;
scene = s.scene;
break;
}
} catch (const regex_error &) {
}
}
}
if (!match && switchIfNotMatching && nonMatchingScene) {
match = true;
scene = nonMatchingScene;
}
if (match) {
OBSSourceAutoRelease source = obs_weak_source_get_source(scene);
OBSSourceAutoRelease currentSource = obs_frontend_get_current_scene();
if (source && source != currentSource)
obs_frontend_set_current_scene(source);
}
}
lastTitle = title;
}
}
void SwitcherData::Start()
{
if (!switcher->th.joinable())
switcher->th = thread([]() { switcher->Thread(); });
}
void SwitcherData::Stop()
{
if (th.joinable()) {
{
lock_guard<mutex> lock(m);
stop = true;
}
cv.notify_one();
th.join();
}
}
extern "C" void FreeSceneSwitcher()
{
CleanupSceneSwitcher();
delete switcher;
switcher = nullptr;
}
static void OBSEvent(enum obs_frontend_event event, void *)
{
if (event == OBS_FRONTEND_EVENT_EXIT)
FreeSceneSwitcher();
}
extern "C" void InitSceneSwitcher()
{
#if !defined(__APPLE__) && !defined(_WIN32)
if (QApplication::platformName().contains("wayland"))
return;
#endif
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(obs_module_text("SceneSwitcher"));
switcher = new SwitcherData;
auto cb = []() {
obs_frontend_push_ui_translation(obs_module_get_string);
QMainWindow *window = (QMainWindow *)obs_frontend_get_main_window();
SceneSwitcher ss(window);
ss.exec();
obs_frontend_pop_ui_translation();
};
obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
obs_frontend_add_event_callback(OBSEvent, nullptr);
QObject::connect(action, &QAction::triggered, action, cb);
}