e15fdf69c0
Adds functions to check and/or request specific macOS permissions (audio device access, video device access, accessibility access, and screen capture access). By default only audio capture, video capture, and accessibility are requested on launch - the first two have straight-forward "Yes/No" prompts, the latter requires people to enable OBS in the settings application (but is required for hotkey functionality, independent of scene setups).
428 lines
11 KiB
Plaintext
428 lines
11 KiB
Plaintext
/******************************************************************************
|
|
Copyright (C) 2013 by Hugh Bailey <obs.jim@gmail.com>
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
******************************************************************************/
|
|
|
|
#include <sstream>
|
|
#include <dlfcn.h>
|
|
#include <util/base.h>
|
|
#include <util/threading.h>
|
|
#include <obs-config.h>
|
|
#include "platform.hpp"
|
|
#include "obs-app.hpp"
|
|
|
|
#include <unistd.h>
|
|
#include <sys/sysctl.h>
|
|
|
|
#import <AppKit/AppKit.h>
|
|
#import <CoreFoundation/CoreFoundation.h>
|
|
#import <AVFoundation/AVFoundation.h>
|
|
#import <ApplicationServices/ApplicationServices.h>
|
|
|
|
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<string> 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<string> 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<void *>(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<void *>(
|
|
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: {
|
|
if (@available(macOS 10.14, *)) {
|
|
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;
|
|
} else {
|
|
permissionResponse = kPermissionAuthorized;
|
|
}
|
|
|
|
blog(LOG_INFO, "[macOS] Permission for audio device access %s.",
|
|
permissionResponse == kPermissionAuthorized ? "granted"
|
|
: "denied");
|
|
|
|
break;
|
|
}
|
|
case kVideoDeviceAccess: {
|
|
if (@available(macOS 10.14, *)) {
|
|
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;
|
|
} else {
|
|
permissionResponse = kPermissionAuthorized;
|
|
}
|
|
|
|
blog(LOG_INFO, "[macOS] Permission for video device access %s.",
|
|
permissionResponse == kPermissionAuthorized ? "granted"
|
|
: "denied");
|
|
|
|
break;
|
|
}
|
|
case kScreenCapture: {
|
|
if (@available(macOS 10.15, *)) {
|
|
permissionResponse = (CGPreflightScreenCaptureAccess()
|
|
? kPermissionAuthorized
|
|
: kPermissionDenied);
|
|
|
|
if (permissionResponse != kPermissionAuthorized &&
|
|
prompt_for_permission) {
|
|
permissionResponse =
|
|
(CGRequestScreenCaptureAccess()
|
|
? kPermissionAuthorized
|
|
: kPermissionDenied);
|
|
}
|
|
|
|
} else {
|
|
permissionResponse = kPermissionAuthorized;
|
|
}
|
|
|
|
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 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 <CrAppProtocol>
|
|
@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];
|
|
}
|