auto-scene-switcher.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. #include <obs-frontend-api.h>
  2. #include <obs-module.h>
  3. #include <obs.hpp>
  4. #include <util/util.hpp>
  5. #include <QMainWindow>
  6. #include <QMessageBox>
  7. #include <QAction>
  8. #include "auto-scene-switcher.hpp"
  9. #include <condition_variable>
  10. #include <chrono>
  11. #include <string>
  12. #include <vector>
  13. #include <thread>
  14. #include <regex>
  15. #include <mutex>
  16. using namespace std;
  17. #define DEFAULT_INTERVAL 300
  18. struct SceneSwitch {
  19. OBSWeakSource scene;
  20. string window;
  21. regex re;
  22. inline SceneSwitch(OBSWeakSource scene_, const char *window_)
  23. : scene(scene_), window(window_), re(window_)
  24. {
  25. }
  26. };
  27. static inline bool WeakSourceValid(obs_weak_source_t *ws)
  28. {
  29. obs_source_t *source = obs_weak_source_get_source(ws);
  30. if (source)
  31. obs_source_release(source);
  32. return !!source;
  33. }
  34. struct SwitcherData {
  35. thread th;
  36. condition_variable cv;
  37. mutex m;
  38. bool stop = false;
  39. vector<SceneSwitch> switches;
  40. OBSWeakSource nonMatchingScene;
  41. int interval = DEFAULT_INTERVAL;
  42. bool switchIfNotMatching = false;
  43. bool startAtLaunch = false;
  44. void Thread();
  45. void Start();
  46. void Stop();
  47. void Prune()
  48. {
  49. for (size_t i = 0; i < switches.size(); i++) {
  50. SceneSwitch &s = switches[i];
  51. if (!WeakSourceValid(s.scene))
  52. switches.erase(switches.begin() + i--);
  53. }
  54. if (nonMatchingScene && !WeakSourceValid(nonMatchingScene)) {
  55. switchIfNotMatching = false;
  56. nonMatchingScene = nullptr;
  57. }
  58. }
  59. inline ~SwitcherData()
  60. {
  61. Stop();
  62. }
  63. };
  64. static SwitcherData *switcher = nullptr;
  65. static inline QString MakeSwitchName(const QString &scene,
  66. const QString &window)
  67. {
  68. return QStringLiteral("[") + scene + QStringLiteral("]: ") + window;
  69. }
  70. static inline string GetWeakSourceName(obs_weak_source_t *weak_source)
  71. {
  72. string name;
  73. obs_source_t *source = obs_weak_source_get_source(weak_source);
  74. if (source) {
  75. name = obs_source_get_name(source);
  76. obs_source_release(source);
  77. }
  78. return name;
  79. }
  80. static inline OBSWeakSource GetWeakSourceByName(const char *name)
  81. {
  82. OBSWeakSource weak;
  83. obs_source_t *source = obs_get_source_by_name(name);
  84. if (source) {
  85. weak = obs_source_get_weak_source(source);
  86. obs_weak_source_release(weak);
  87. obs_source_release(source);
  88. }
  89. return weak;
  90. }
  91. static inline OBSWeakSource GetWeakSourceByQString(const QString &name)
  92. {
  93. return GetWeakSourceByName(name.toUtf8().constData());
  94. }
  95. SceneSwitcher::SceneSwitcher(QWidget *parent)
  96. : QDialog(parent),
  97. ui(new Ui_SceneSwitcher)
  98. {
  99. ui->setupUi(this);
  100. lock_guard<mutex> lock(switcher->m);
  101. switcher->Prune();
  102. BPtr<char*> scenes = obs_frontend_get_scene_names();
  103. char **temp = scenes;
  104. while (*temp) {
  105. const char *name = *temp;
  106. ui->scenes->addItem(name);
  107. ui->noMatchSwitchScene->addItem(name);
  108. temp++;
  109. }
  110. if (switcher->switchIfNotMatching)
  111. ui->noMatchSwitch->setChecked(true);
  112. else
  113. ui->noMatchDontSwitch->setChecked(true);
  114. ui->noMatchSwitchScene->setCurrentText(
  115. GetWeakSourceName(switcher->nonMatchingScene).c_str());
  116. ui->checkInterval->setValue(switcher->interval);
  117. vector<string> windows;
  118. GetWindowList(windows);
  119. for (string &window : windows)
  120. ui->windows->addItem(window.c_str());
  121. for (auto &s : switcher->switches) {
  122. string sceneName = GetWeakSourceName(s.scene);
  123. QString text = MakeSwitchName(sceneName.c_str(),
  124. s.window.c_str());
  125. QListWidgetItem *item = new QListWidgetItem(text,
  126. ui->switches);
  127. item->setData(Qt::UserRole, s.window.c_str());
  128. }
  129. if (switcher->th.joinable())
  130. SetStarted();
  131. else
  132. SetStopped();
  133. loading = false;
  134. }
  135. void SceneSwitcher::closeEvent(QCloseEvent*)
  136. {
  137. obs_frontend_save();
  138. }
  139. int SceneSwitcher::FindByData(const QString &window)
  140. {
  141. int count = ui->switches->count();
  142. int idx = -1;
  143. for (int i = 0; i < count; i++) {
  144. QListWidgetItem *item = ui->switches->item(i);
  145. QString itemWindow =
  146. item->data(Qt::UserRole).toString();
  147. if (itemWindow == window) {
  148. idx = i;
  149. break;
  150. }
  151. }
  152. return idx;
  153. }
  154. void SceneSwitcher::on_switches_currentRowChanged(int idx)
  155. {
  156. if (loading)
  157. return;
  158. if (idx == -1)
  159. return;
  160. QListWidgetItem *item = ui->switches->item(idx);
  161. QString window = item->data(Qt::UserRole).toString();
  162. lock_guard<mutex> lock(switcher->m);
  163. for (auto &s : switcher->switches) {
  164. if (window.compare(s.window.c_str()) == 0) {
  165. string name = GetWeakSourceName(s.scene);
  166. ui->scenes->setCurrentText(name.c_str());
  167. ui->windows->setCurrentText(window);
  168. break;
  169. }
  170. }
  171. }
  172. void SceneSwitcher::on_close_clicked()
  173. {
  174. done(0);
  175. }
  176. void SceneSwitcher::on_add_clicked()
  177. {
  178. QString sceneName = ui->scenes->currentText();
  179. QString windowName = ui->windows->currentText();
  180. if (windowName.isEmpty())
  181. return;
  182. OBSWeakSource source = GetWeakSourceByQString(sceneName);
  183. QVariant v = QVariant::fromValue(windowName);
  184. QString text = MakeSwitchName(sceneName, windowName);
  185. int idx = FindByData(windowName);
  186. if (idx == -1) {
  187. try {
  188. lock_guard<mutex> lock(switcher->m);
  189. switcher->switches.emplace_back(source,
  190. windowName.toUtf8().constData());
  191. QListWidgetItem *item = new QListWidgetItem(text,
  192. ui->switches);
  193. item->setData(Qt::UserRole, v);
  194. } catch (const regex_error &) {
  195. QMessageBox::warning(this,
  196. obs_module_text("InvalidRegex.Title"),
  197. obs_module_text("InvalidRegex.Text"));
  198. }
  199. } else {
  200. QListWidgetItem *item = ui->switches->item(idx);
  201. item->setText(text);
  202. string window = windowName.toUtf8().constData();
  203. {
  204. lock_guard<mutex> lock(switcher->m);
  205. for (auto &s : switcher->switches) {
  206. if (s.window == window) {
  207. s.scene = source;
  208. break;
  209. }
  210. }
  211. }
  212. ui->switches->sortItems();
  213. }
  214. }
  215. void SceneSwitcher::on_remove_clicked()
  216. {
  217. QListWidgetItem *item = ui->switches->currentItem();
  218. if (!item)
  219. return;
  220. string window =
  221. item->data(Qt::UserRole).toString().toUtf8().constData();
  222. {
  223. lock_guard<mutex> lock(switcher->m);
  224. auto &switches = switcher->switches;
  225. for (auto it = switches.begin(); it != switches.end(); ++it) {
  226. auto &s = *it;
  227. if (s.window == window) {
  228. switches.erase(it);
  229. break;
  230. }
  231. }
  232. }
  233. delete item;
  234. }
  235. void SceneSwitcher::on_startAtLaunch_toggled(bool value)
  236. {
  237. if (loading)
  238. return;
  239. lock_guard<mutex> lock(switcher->m);
  240. switcher->startAtLaunch = value;
  241. }
  242. void SceneSwitcher::UpdateNonMatchingScene(const QString &name)
  243. {
  244. obs_source_t *scene = obs_get_source_by_name(
  245. name.toUtf8().constData());
  246. obs_weak_source_t *ws = obs_source_get_weak_source(scene);
  247. switcher->nonMatchingScene = ws;
  248. obs_weak_source_release(ws);
  249. obs_source_release(scene);
  250. }
  251. void SceneSwitcher::on_noMatchDontSwitch_clicked()
  252. {
  253. if (loading)
  254. return;
  255. lock_guard<mutex> lock(switcher->m);
  256. switcher->switchIfNotMatching = false;
  257. }
  258. void SceneSwitcher::on_noMatchSwitch_clicked()
  259. {
  260. if (loading)
  261. return;
  262. lock_guard<mutex> lock(switcher->m);
  263. switcher->switchIfNotMatching = true;
  264. UpdateNonMatchingScene(ui->noMatchSwitchScene->currentText());
  265. }
  266. void SceneSwitcher::on_noMatchSwitchScene_currentTextChanged(
  267. const QString &text)
  268. {
  269. if (loading)
  270. return;
  271. lock_guard<mutex> lock(switcher->m);
  272. UpdateNonMatchingScene(text);
  273. }
  274. void SceneSwitcher::on_checkInterval_valueChanged(int value)
  275. {
  276. if (loading)
  277. return;
  278. lock_guard<mutex> lock(switcher->m);
  279. switcher->interval = value;
  280. }
  281. void SceneSwitcher::SetStarted()
  282. {
  283. ui->toggleStartButton->setText(obs_module_text("Stop"));
  284. ui->pluginRunningText->setText(obs_module_text("Active"));
  285. }
  286. void SceneSwitcher::SetStopped()
  287. {
  288. ui->toggleStartButton->setText(obs_module_text("Start"));
  289. ui->pluginRunningText->setText(obs_module_text("Inactive"));
  290. }
  291. void SceneSwitcher::on_toggleStartButton_clicked()
  292. {
  293. if (switcher->th.joinable()) {
  294. switcher->Stop();
  295. SetStopped();
  296. } else {
  297. switcher->Start();
  298. SetStarted();
  299. }
  300. }
  301. static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *)
  302. {
  303. if (saving) {
  304. lock_guard<mutex> lock(switcher->m);
  305. obs_data_t *obj = obs_data_create();
  306. obs_data_array_t *array = obs_data_array_create();
  307. switcher->Prune();
  308. for (SceneSwitch &s : switcher->switches) {
  309. obs_data_t *array_obj = obs_data_create();
  310. obs_source_t *source = obs_weak_source_get_source(
  311. s.scene);
  312. if (source) {
  313. const char *n = obs_source_get_name(source);
  314. obs_data_set_string(array_obj, "scene", n);
  315. obs_data_set_string(array_obj, "window_title",
  316. s.window.c_str());
  317. obs_data_array_push_back(array, array_obj);
  318. obs_source_release(source);
  319. }
  320. obs_data_release(array_obj);
  321. }
  322. string nonMatchingSceneName =
  323. GetWeakSourceName(switcher->nonMatchingScene);
  324. obs_data_set_int(obj, "interval", switcher->interval);
  325. obs_data_set_string(obj, "non_matching_scene",
  326. nonMatchingSceneName.c_str());
  327. obs_data_set_bool(obj, "switch_if_not_matching",
  328. switcher->switchIfNotMatching);
  329. obs_data_set_bool(obj, "active", switcher->th.joinable());
  330. obs_data_set_array(obj, "switches", array);
  331. obs_data_set_obj(save_data, "auto-scene-switcher", obj);
  332. obs_data_array_release(array);
  333. obs_data_release(obj);
  334. } else {
  335. switcher->m.lock();
  336. obs_data_t *obj = obs_data_get_obj(save_data,
  337. "auto-scene-switcher");
  338. obs_data_array_t *array = obs_data_get_array(obj, "switches");
  339. size_t count = obs_data_array_count(array);
  340. if (!obj)
  341. obj = obs_data_create();
  342. obs_data_set_default_int(obj, "interval", DEFAULT_INTERVAL);
  343. switcher->interval = obs_data_get_int(obj, "interval");
  344. switcher->switchIfNotMatching =
  345. obs_data_get_bool(obj, "switch_if_not_matching");
  346. string nonMatchingScene =
  347. obs_data_get_string(obj, "non_matching_scene");
  348. bool active = obs_data_get_bool(obj, "active");
  349. switcher->nonMatchingScene =
  350. GetWeakSourceByName(nonMatchingScene.c_str());
  351. switcher->switches.clear();
  352. for (size_t i = 0; i < count; i++) {
  353. obs_data_t *array_obj = obs_data_array_item(array, i);
  354. const char *scene =
  355. obs_data_get_string(array_obj, "scene");
  356. const char *window =
  357. obs_data_get_string(array_obj, "window_title");
  358. switcher->switches.emplace_back(
  359. GetWeakSourceByName(scene),
  360. window);
  361. obs_data_release(array_obj);
  362. }
  363. obs_data_array_release(array);
  364. obs_data_release(obj);
  365. switcher->m.unlock();
  366. if (active)
  367. switcher->Start();
  368. else
  369. switcher->Stop();
  370. }
  371. }
  372. void SwitcherData::Thread()
  373. {
  374. chrono::duration<long long, milli> duration =
  375. chrono::milliseconds(interval);
  376. string lastTitle;
  377. string title;
  378. for (;;) {
  379. unique_lock<mutex> lock(m);
  380. OBSWeakSource scene;
  381. bool match = false;
  382. cv.wait_for(lock, duration);
  383. if (switcher->stop) {
  384. switcher->stop = false;
  385. break;
  386. }
  387. duration = chrono::milliseconds(interval);
  388. GetCurrentWindowTitle(title);
  389. if (lastTitle != title) {
  390. switcher->Prune();
  391. for (SceneSwitch &s : switches) {
  392. if (s.window == title) {
  393. match = true;
  394. scene = s.scene;
  395. break;
  396. }
  397. }
  398. /* try regex */
  399. if (!match) {
  400. for (SceneSwitch &s : switches) {
  401. try {
  402. bool matches = regex_match(
  403. title, s.re);
  404. if (matches) {
  405. match = true;
  406. scene = s.scene;
  407. break;
  408. }
  409. } catch (const regex_error &) {}
  410. }
  411. }
  412. if (!match && switchIfNotMatching &&
  413. nonMatchingScene) {
  414. match = true;
  415. scene = nonMatchingScene;
  416. }
  417. if (match) {
  418. obs_source_t *source =
  419. obs_weak_source_get_source(scene);
  420. obs_source_t *currentSource =
  421. obs_frontend_get_current_scene();
  422. if (source && source != currentSource)
  423. obs_frontend_set_current_scene(source);
  424. obs_source_release(currentSource);
  425. obs_source_release(source);
  426. }
  427. }
  428. lastTitle = title;
  429. }
  430. }
  431. void SwitcherData::Start()
  432. {
  433. if (!switcher->th.joinable())
  434. switcher->th = thread([] () {switcher->Thread();});
  435. }
  436. void SwitcherData::Stop()
  437. {
  438. if (th.joinable()) {
  439. {
  440. lock_guard<mutex> lock(m);
  441. stop = true;
  442. }
  443. cv.notify_one();
  444. th.join();
  445. }
  446. }
  447. extern "C" void FreeSceneSwitcher()
  448. {
  449. delete switcher;
  450. switcher = nullptr;
  451. }
  452. static void OBSEvent(enum obs_frontend_event event, void *)
  453. {
  454. if (event == OBS_FRONTEND_EVENT_EXIT)
  455. FreeSceneSwitcher();
  456. }
  457. extern "C" void InitSceneSwitcher()
  458. {
  459. QAction *action = (QAction*)obs_frontend_add_tools_menu_qaction(
  460. obs_module_text("SceneSwitcher"));
  461. switcher = new SwitcherData;
  462. auto cb = [] ()
  463. {
  464. obs_frontend_push_ui_translation(obs_module_get_string);
  465. QMainWindow *window =
  466. (QMainWindow*)obs_frontend_get_main_window();
  467. SceneSwitcher ss(window);
  468. ss.exec();
  469. obs_frontend_pop_ui_translation();
  470. };
  471. obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
  472. obs_frontend_add_event_callback(OBSEvent, nullptr);
  473. action->connect(action, &QAction::triggered, cb);
  474. }