#include "settings.h" #include #include #include #include #include #include #include "switcher.h" #ifdef _WIN32 #define NOMINMAX #include #endif #ifdef __APPLE__ #include #include #endif using namespace std; //scene switching is done in here void Switcher::switcherThreadFunc() { bool sceneRoundTripActive = false; size_t roundTripPos = 0; bool match = false; string windowname = ""; string sceneName = ""; bool checkFullscreen = false; bool pauseSwitching = false; size_t sleepTime = 1000; while (isRunning) { //get Scene Name obs_source_t * transitionUsed = obs_get_output_source(0); obs_source_t * sceneUsed = obs_transition_get_active_source(transitionUsed); const char *sceneUsedName = obs_source_get_name(sceneUsed); obs_source_release(sceneUsed); obs_source_release(transitionUsed); //check if scene switching should be paused if ((sceneUsedName) && find(pauseScenes.begin(), pauseScenes.end(), string(sceneUsedName)) != pauseScenes.end()) { pauseSwitching = true; } else { pauseSwitching = false; } if (sceneRoundTripActive) { //did the user switch to another scene during scene round trip ? if ((sceneUsedName) && find(sceneRoundTrip.first.begin(), sceneRoundTrip.first.end(), string(sceneUsedName)) == sceneRoundTrip.first.end()) { sceneRoundTripActive = false; roundTripPos = 0; } } //check if a Scene Round Trip should be started else if ((sceneUsedName) && sceneRoundTrip.first.size() > 0 && strcmp(sceneUsedName, sceneRoundTrip.first.front().c_str()) == 0) { sceneRoundTripActive = true; roundTripPos = 0; } //are we in pause mode? if (!pauseSwitching) { /////////////////////////////////////////////// //finding the scene to switch to /////////////////////////////////////////////// if (!sceneRoundTripActive) { //get active window title windowname = GetActiveWindowTitle(); //should we ignore this window name? if (find(ignoreNames.begin(), ignoreNames.end(), windowname) == ignoreNames.end()) { //reset values match = false; sceneName = ""; checkFullscreen = false; //first check if there is a direct match map::iterator it = settingsMap.find(windowname); if (it != settingsMap.end()) { sceneName = it->second.sceneName; checkFullscreen = it->second.isFullscreen; match = true; } else { //maybe there is a regular expression match for (map::iterator iter = settingsMap.begin(); iter != settingsMap.end(); ++iter) { try { if (regex_match(windowname, regex(iter->first))) { sceneName = iter->second.sceneName; checkFullscreen = iter->second.isFullscreen; match = true; break; } } catch (...) {} } } /////////////////////////////////////////////// //cursor regions check /////////////////////////////////////////////// if (!match) { pair cursorPos = getCursorXY(); // compare against rectangular region format regex rules = regex("<-?[0-9]+x-?[0-9]+.-?[0-9]+x-?[0-9]+>"); int minRegionSize = 99999; for (map::iterator iter = settingsMap.begin(); iter != settingsMap.end(); ++iter) { try { if (regex_match(iter->first, rules)) { int minX, minY, maxX, maxY; int parsedValues = sscanf(iter->first.c_str(), "<%dx%d.%dx%d>", &minX, &minY, &maxX, &maxY); if (parsedValues == 4) { if (cursorPos.first >= minX && cursorPos.second >= minY && cursorPos.first <= maxX && cursorPos.second <= maxY) { // prioritize smaller regions over larger regions int regionSize = (maxX - minX) + (maxY - minY); if (regionSize < minRegionSize) { match = true; sceneName = iter->second.sceneName; checkFullscreen = iter->second.isFullscreen; minRegionSize = regionSize; // break; } } } } } catch (...) {} } } } } /////////////////////////////////////////////// //switch the scene /////////////////////////////////////////////// //do we only switch if window is also fullscreen? || are we in sceneRoundTripActive? match = (match && (!checkFullscreen || (checkFullscreen && isWindowFullscreen()))) || sceneRoundTripActive; //match or backup scene set if (settingsMap.find("Backup Scene Name") != settingsMap.end() || match) { //no match -> backup scene if (!match) { sceneName = settingsMap.find("Backup Scene Name")->second.sceneName; } if (sceneRoundTripActive) { sceneName = sceneRoundTrip.first[roundTripPos]; } //check if current scene is already the desired scene transitionUsed = obs_get_output_source(0); sceneUsed = obs_transition_get_active_source(transitionUsed); const char *sceneUsedName = obs_source_get_name(sceneUsed); obs_source_release(sceneUsed); if ((sceneUsedName) && strcmp(sceneUsedName, sceneName.c_str()) != 0) { //switch scene obs_source_t *source = obs_get_source_by_name(sceneName.c_str()); //<--- do some more testing here regarding scene duplication if (source != NULL) { //create transition to new scene (otherwise UI wont work anymore) obs_transition_start(transitionUsed, OBS_TRANSITION_MODE_AUTO, 300, source); //OBS_TRANSITION_MODE_AUTO uses the obs user settings for transitions } obs_source_release(source); } obs_source_release(transitionUsed); } /////////////////////////////////////////////// //Scene Round Trip /////////////////////////////////////////////// if (sceneRoundTripActive) { //get delay and wait try { sleepTime = stoi(sceneRoundTrip.second[roundTripPos]) * 1000; } catch (...) { //just wait for 3 seconds if value was not set properly sleepTime = 3000; } //are we done with the Scene Round trip? if (roundTripPos + 1 == sceneRoundTrip.first.size()) { sceneRoundTripActive = false; roundTripPos = 0; } else { //prepare for next sceneName,Delay pair roundTripPos++; } } } //sleep for a bit auto locked = unique_lock(mtx); terminate.wait_for(locked, chrono::milliseconds(sleepTime)); //this_thread::sleep_for(chrono::milliseconds(sleepTime)); sleepTime = 1000; } } #ifdef _WIN32 pair Switcher::getCursorXY() { pair pos(0, 0); POINT cursorPos; if (GetPhysicalCursorPos(&cursorPos)) { pos.first = cursorPos.x; pos.second = cursorPos.y; } return pos; } #endif #ifdef __APPLE__ pair Switcher::getCursorXY() { pair pos(0, 0); CGEventRef event = CGEventCreate(NULL); CGPoint cursorPos = CGEventGetLocation(event); CFRelease(event); pos.first = cursorPos.x; pos.second = cursorPos.y; return pos; } #endif #ifdef _WIN32 void Switcher::firstLoad() { settings.load(); settingsMap = settings.getMap(); sceneRoundTrip = settings.getSceneRoundTrip(); pauseScenes = settings.getPauseScenes(); ignoreNames = settings.getIgnoreNames(); if (!settings.getStartMessageDisable()) { string message = "The following settings were found for Scene Switcher:\n"; for (auto it = settingsMap.cbegin(); it != settingsMap.cend(); ++it) { message += (it->first) + " -> " + it->second.sceneName + "\n"; } message += "\n(settings file located at: " + settings.getSettingsFilePath() + ")"; MessageBoxA(0, message.c_str(), "Scene Switcher", 0); } } #endif #ifdef __APPLE__ void Switcher::firstLoad() { settings.load(); settingsMap = settings.getMap(); sceneRoundTrip = settings.getSceneRoundTrip(); pauseScenes = settings.getPauseScenes(); ignoreNames = settings.getIgnoreNames(); if (!settings.getStartMessageDisable()) { string message = "The following settings were found for Scene Switcher:\n"; for (auto it = settingsMap.cbegin(); it != settingsMap.cend(); ++it) { message += (it->first) + " -> " + it->second.sceneName + "\n"; } message += "\n(settings file located at: " + settings.getSettingsFilePath() + ")"; SInt32 nRes = 0; CFUserNotificationRef pDlg = NULL; const void* keys[] = { kCFUserNotificationAlertHeaderKey, kCFUserNotificationAlertMessageKey }; const void* vals[] = { CFSTR("Test Foundation Message Box"), CFStringCreateWithCString(kCFAllocatorDefault,message.c_str(),kCFStringEncodingMacRoman) }; CFDictionaryRef dict = CFDictionaryCreate(0, keys, vals, sizeof(keys) / sizeof(*keys), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); pDlg = CFUserNotificationCreate(kCFAllocatorDefault, 0, kCFUserNotificationPlainAlertLevel, &nRes, dict); } } #endif //load the settings needed to start the thread void Switcher::load() { settings.load(); settingsMap = settings.getMap(); sceneRoundTrip = settings.getSceneRoundTrip(); pauseScenes = settings.getPauseScenes(); ignoreNames = settings.getIgnoreNames(); } //start thread void Switcher::start() { isRunning = true; switcherThread = thread(&Switcher::switcherThreadFunc, this); } //checks if active window is in fullscreen #ifdef _WIN32 bool Switcher::isWindowFullscreen() { RECT appBounds; RECT rc; GetWindowRect(GetDesktopWindow(), &rc); HWND hwnd = GetForegroundWindow(); //Check we haven't picked up the desktop or the shell if (hwnd != GetDesktopWindow() || hwnd != GetShellWindow()) { GetWindowRect(hwnd, &appBounds); //determine if window is fullscreen if (rc.bottom == appBounds.bottom && rc.top == appBounds.top && rc.left == appBounds.left && rc.right == appBounds.right) { return true; } } return false; } #endif #ifdef __APPLE__ bool Switcher::isWindowFullscreen() { //get screen resolution string cmd = "osascript -e 'tell application \"Finder\" to get the bounds of the window of the desktop'"; char resolution[256]; FILE * f1 = popen(cmd.c_str(), "r"); fgets(resolution, 255, f1); pclose(f1); string resolutionString = string(resolution); //get window resolution cmd = "osascript " "-e 'global frontApp, frontAppName, windowTitle, boundsValue' " "-e 'set windowTitle to \"\"' " "-e 'tell application \"System Events\"' " "-e 'set frontApp to first application process whose frontmost is true' " "-e 'set frontAppName to name of frontApp' " "-e 'tell process frontAppName' " "-e 'tell (1st window whose value of attribute \"AXMain\" is true)' " "-e 'set windowTitle to value of attribute \"AXTitle\"' " "-e 'end tell' " "-e 'end tell' " "-e 'end tell' " "-e 'tell application frontAppName' " "-e 'set boundsValue to bounds of front window' " "-e 'end tell' " "-e 'return boundsValue' "; char bounds[256]; FILE * f2 = popen(cmd.c_str(), "r"); fgets(bounds, 255, f2); pclose(f2); string boundsString = string(bounds); return resolutionString.compare(boundsString) == 0; } #endif //reads the title of the currently active window #ifdef _WIN32 string Switcher::GetActiveWindowTitle() { char wnd_title[256]; //get handle of currently active window HWND hwnd = GetForegroundWindow(); GetWindowTextA(hwnd, wnd_title, sizeof(wnd_title)); return wnd_title; } #endif #ifdef __APPLE__ string Switcher::GetActiveWindowTitle() { string cmd = "osascript " "-e 'global frontApp, frontAppName, windowTitle' " "-e 'set windowTitle to \"\"' " "-e 'tell application \"System Events\"' " "-e 'set frontApp to first application process whose frontmost is true' " "-e 'set frontAppName to name of frontApp' " "-e 'tell process frontAppName' " "-e 'tell (1st window whose value of attribute \"AXMain\" is true)' " "-e 'set windowTitle to value of attribute \"AXTitle\"' " "-e 'end tell' " "-e 'end tell' " "-e 'end tell' " "-e 'return windowTitle' "; char buffer[256]; FILE * f = popen(cmd.c_str(), "r"); fgets(buffer, 255, f); pclose(f); //osascript adds carriage return that we need to remove string windowname = string(buffer); windowname.pop_back(); return windowname; } #endif //tell the thread to stop void Switcher::stop() { isRunning = false; terminate.notify_one(); if (switcherThread.joinable()) switcherThread.join(); } string Switcher::getSettingsFilePath() { return settings.getSettingsFilePath();; } void Switcher::setSettingsFilePath(string path) { settings.setSettingsFilePath(path); } Switcher::~Switcher() { stop(); } bool Switcher::getIsRunning() { return isRunning; }