/****************************************************************************** Copyright (C) 2013 by Hugh Bailey This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ #include #include #include #include #include #include "platform.hpp" #include "obs-app.hpp" #include #import #import #import #import using namespace std; bool isInBundle() { NSRunningApplication *app = [NSRunningApplication currentApplication]; return [app bundleIdentifier] != nil; } bool GetDataFilePath(const char *data, string &output) { NSRunningApplication *app = [NSRunningApplication currentApplication]; NSURL *bundleURL = [app bundleURL]; NSString *path = [NSString stringWithFormat:@"Contents/Resources/%@", [NSString stringWithUTF8String:data]]; NSURL *dataURL = [bundleURL URLByAppendingPathComponent:path]; output = [[dataURL path] UTF8String]; return !access(output.c_str(), R_OK); } void CheckIfAlreadyRunning(bool &already_running) { try { NSBundle *bundle = [NSBundle mainBundle]; if (!bundle) throw "Could not find main bundle"; NSString *bundleID = [bundle bundleIdentifier]; if (!bundleID) throw "Could not find bundle identifier"; int app_count = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID] .count; already_running = app_count > 1; } catch (const char *error) { blog(LOG_ERROR, "CheckIfAlreadyRunning: %s", error); } } string GetDefaultVideoSavePath() { NSFileManager *fm = [NSFileManager defaultManager]; NSURL *url = [fm URLForDirectory:NSMoviesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:true error:nil]; if (!url) return getenv("HOME"); return url.path.fileSystemRepresentation; } vector GetPreferredLocales() { NSArray *preferred = [NSLocale preferredLanguages]; auto locales = GetLocaleNames(); auto lang_to_locale = [&locales](string lang) -> string { string lang_match = ""; for (const auto &locale : locales) { if (locale.first == lang.substr(0, locale.first.size())) return locale.first; if (!lang_match.size() && locale.first.substr(0, 2) == lang.substr(0, 2)) lang_match = locale.first; } return lang_match; }; vector result; result.reserve(preferred.count); for (NSString *lang in preferred) { string locale = lang_to_locale(lang.UTF8String); if (!locale.size()) continue; if (find(begin(result), end(result), locale) != end(result)) continue; result.emplace_back(locale); } return result; } bool IsAlwaysOnTop(QWidget *window) { return (window->windowFlags() & Qt::WindowStaysOnTopHint) != 0; } void disableColorSpaceConversion(QWidget *window) { NSView *view = (__bridge NSView *)reinterpret_cast(window->winId()); view.window.colorSpace = NSColorSpace.sRGBColorSpace; } void SetAlwaysOnTop(QWidget *window, bool enable) { Qt::WindowFlags flags = window->windowFlags(); if (enable) { /* Force the level of the window high so it sits on top of * full-screen applications like Keynote */ NSView *nsv = (__bridge NSView *)reinterpret_cast( window->winId()); NSWindow *nsw = nsv.window; [nsw setLevel:1024]; flags |= Qt::WindowStaysOnTopHint; } else { flags &= ~Qt::WindowStaysOnTopHint; } window->setWindowFlags(flags); window->show(); } bool SetDisplayAffinitySupported(void) { // Not implemented yet return false; } typedef void (*set_int_t)(int); void EnableOSXVSync(bool enable) { static bool initialized = false; static bool valid = false; static set_int_t set_debug_options = nullptr; static set_int_t deferred_updates = nullptr; if (!initialized) { void *quartzCore = dlopen("/System/Library/Frameworks/" "QuartzCore.framework/QuartzCore", RTLD_LAZY); if (quartzCore) { set_debug_options = (set_int_t)dlsym( quartzCore, "CGSSetDebugOptions"); deferred_updates = (set_int_t)dlsym( quartzCore, "CGSDeferredUpdates"); valid = set_debug_options && deferred_updates; } initialized = true; } if (valid) { set_debug_options(enable ? 0 : 0x08000000); deferred_updates(enable ? 1 : 0); } } void EnableOSXDockIcon(bool enable) { if (enable) [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; else [NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited]; } @interface DockView : NSView { @private QIcon icon; } @end @implementation DockView - (id)initWithIcon:(QIcon)icon { self = [super init]; self->icon = icon; return self; } - (void)drawRect:(NSRect)dirtyRect { CGSize size = dirtyRect.size; /* Draw regular app icon */ NSImage *appIcon = [[NSWorkspace sharedWorkspace] iconForFile:[[NSBundle mainBundle] bundlePath]]; [appIcon drawInRect:CGRectMake(0, 0, size.width, size.height)]; /* Draw small icon on top */ float iconSize = 0.45; CGImageRef image = icon.pixmap(size.width, size.height).toImage().toCGImage(); CGContextRef context = [[NSGraphicsContext currentContext] CGContext]; CGContextDrawImage(context, CGRectMake(size.width * (1 - iconSize), 0, size.width * iconSize, size.height * iconSize), image); CGImageRelease(image); } @end MacPermissionStatus CheckPermissionWithPrompt(MacPermissionType type, bool prompt_for_permission) { __block MacPermissionStatus permissionResponse = kPermissionNotDetermined; switch (type) { case kAudioDeviceAccess: { AVAuthorizationStatus audioStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; if (audioStatus == AVAuthorizationStatusNotDetermined && prompt_for_permission) { os_event_t *block_finished; os_event_init(&block_finished, OS_EVENT_TYPE_MANUAL); [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^( BOOL granted __attribute((unused))) { os_event_signal(block_finished); }]; os_event_wait(block_finished); os_event_destroy(block_finished); audioStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; } permissionResponse = (MacPermissionStatus)audioStatus; blog(LOG_INFO, "[macOS] Permission for audio device access %s.", permissionResponse == kPermissionAuthorized ? "granted" : "denied"); break; } case kVideoDeviceAccess: { AVAuthorizationStatus videoStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (videoStatus == AVAuthorizationStatusNotDetermined && prompt_for_permission) { os_event_t *block_finished; os_event_init(&block_finished, OS_EVENT_TYPE_MANUAL); [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted __attribute((unused))) { os_event_signal(block_finished); }]; os_event_wait(block_finished); os_event_destroy(block_finished); videoStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; } permissionResponse = (MacPermissionStatus)videoStatus; blog(LOG_INFO, "[macOS] Permission for video device access %s.", permissionResponse == kPermissionAuthorized ? "granted" : "denied"); break; } case kScreenCapture: { #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 if (@available(macOS 11.0, *)) { permissionResponse = (CGPreflightScreenCaptureAccess() ? kPermissionAuthorized : kPermissionDenied); if (permissionResponse != kPermissionAuthorized && prompt_for_permission) { permissionResponse = (CGRequestScreenCaptureAccess() ? kPermissionAuthorized : kPermissionDenied); } } else { #else { #endif CGDisplayStreamRef stream = CGDisplayStreamCreate( CGMainDisplayID(), 1, 1, kCVPixelFormatType_32BGRA, nil, nil); if (stream) { permissionResponse = kPermissionAuthorized; CFRelease(stream); if (prompt_for_permission) { } } else { permissionResponse = kPermissionDenied; } } blog(LOG_INFO, "[macOS] Permission for screen capture %s.", permissionResponse == kPermissionAuthorized ? "granted" : "denied"); break; } case kAccessibility: { permissionResponse = (AXIsProcessTrusted() ? kPermissionAuthorized : kPermissionDenied); if (permissionResponse != kPermissionAuthorized && prompt_for_permission) { NSDictionary *options = @{ (__bridge id)kAXTrustedCheckOptionPrompt: @YES }; permissionResponse = (AXIsProcessTrustedWithOptions( (CFDictionaryRef)options) ? kPermissionAuthorized : kPermissionDenied); } blog(LOG_INFO, "[macOS] Permission for accessibility %s.", permissionResponse == kPermissionAuthorized ? "granted" : "denied"); break; } } return permissionResponse; } void OpenMacOSPrivacyPreferences(const char *tab) { NSURL *url = [NSURL URLWithString: [NSString stringWithFormat: @"x-apple.systempreferences:com.apple.preference.security?Privacy_%s", tab]]; [[NSWorkspace sharedWorkspace] openURL:url]; } void TaskbarOverlayInit() {} void TaskbarOverlaySetStatus(TaskbarOverlayStatus status) { QIcon icon; if (status == TaskbarOverlayStatusActive) icon = QIcon::fromTheme("obs-active", QIcon(":/res/images/active_mac.png")); else if (status == TaskbarOverlayStatusPaused) icon = QIcon::fromTheme("obs-paused", QIcon(":/res/images/paused_mac.png")); NSDockTile *tile = [NSApp dockTile]; [tile setContentView:[[DockView alloc] initWithIcon:icon]]; [tile display]; } /* * This custom NSApplication subclass makes the app compatible with CEF. Qt * also has an NSApplication subclass, but it doesn't conflict thanks to Qt * using arcane magic to hook into the NSApplication superclass itself if the * program has its own NSApplication subclass. */ @protocol CrAppProtocol - (BOOL)isHandlingSendEvent; @end @interface OBSApplication : NSApplication @property (nonatomic, getter=isHandlingSendEvent) BOOL handlingSendEvent; @end @implementation OBSApplication - (void)sendEvent:(NSEvent *)event { _handlingSendEvent = YES; [super sendEvent:event]; _handlingSendEvent = NO; } @end void InstallNSThreadLocks() { [[NSThread new] start]; if ([NSThread isMultiThreaded] != 1) { abort(); } } void InstallNSApplicationSubclass() { [OBSApplication sharedApplication]; }