auto-scene-switcher.cpp 12 KB

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