在macOS Catalina上检测屏幕录制设置

时间:2019-06-14 11:25:50

标签: macos screen-recording macos-catalina

检测用户是否已启用此API的可靠方法是什么?

CGWindowListCreateImage返回有效的对象,即使禁用了屏幕录制API。可能有多种组合(kCGWindowListOptionIncludingWindowkCGWindowListOptionOnScreenBelowWindow),只有一些组合会返回NULL。

- (CGImageRef)createScreenshotImage
{
    NSWindow *window = [[self view] window];
    NSRect rect = [window frame];

    rect.origin.y = NSHeight([[window screen] frame]) - NSMaxY([window frame]);
    CGImageRef screenshot = CGWindowListCreateImage(
                                                    rect,
                                                    kCGWindowListOptionIncludingWindow,
                                                    //kCGWindowListOptionOnScreenBelowWindow,
                                                    0,//(CGWindowID)[window windowNumber],
                                                    kCGWindowImageBoundsIgnoreFraming);//kCGWindowImageDefault
    return screenshot;
}

唯一可靠的方法是通过CGDisplayStreamCreate,因为苹果每年都会更改隐私设置,因此这是有风险的。

   - (BOOL)canRecordScreen
    {
        if (@available(macOS 10.15, *)) {
            CGDisplayStreamRef stream = CGDisplayStreamCreate(CGMainDisplayID(), 1, 1, kCVPixelFormatType_32BGRA, nil, ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
                ;
            });
            BOOL canRecord = stream != NULL;
            if (stream) { 
              CFRelease(stream); 
            }
            return canRecord;
        } else {
            return YES;
        }
    }

8 个答案:

答案 0 :(得分:12)

这里介绍的所有解决方案都以一种或另一种方式存在缺陷。问题的根源在于,您了解窗口的权限(通过窗口列表中的名称)与您了解窗口的进程所有者(例如WindowServer和Dock)之间没有关联。您查看屏幕上像素的权限是两组稀疏信息的组合。

以下是启发式方法,涵盖了macOS 10.15.1以来的所有情况:

BOOL canRecordScreen = YES;
if (@available(macOS 10.15, *)) {
    canRecordScreen = NO;
    NSRunningApplication *runningApplication = NSRunningApplication.currentApplication;
    NSNumber *ourProcessIdentifier = [NSNumber numberWithInteger:runningApplication.processIdentifier];

    CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
    NSUInteger numberOfWindows = CFArrayGetCount(windowList);
    for (int index = 0; index < numberOfWindows; index++) {
        // get information for each window
        NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, index);
        NSString *windowName = windowInfo[(id)kCGWindowName];
        NSNumber *processIdentifier = windowInfo[(id)kCGWindowOwnerPID];

        // don't check windows owned by this process
        if (! [processIdentifier isEqual:ourProcessIdentifier]) {
            // get process information for each window
            pid_t pid = processIdentifier.intValue;
            NSRunningApplication *windowRunningApplication = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
            if (! windowRunningApplication) {
                // ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar"
            }
            else {
                NSString *windowExecutableName = windowRunningApplication.executableURL.lastPathComponent;
                if (windowName) {
                    if ([windowExecutableName isEqual:@"Dock"]) {
                        // ignore the Dock, which provides the desktop picture
                    }
                    else {
                        canRecordScreen = YES;
                        break;
                    }
                }
            }
        }
    }
    CFRelease(windowList);
}

如果未设置canRecordScreen,则需要建立某种对话框,警告用户他们只能看到菜单栏,桌面图片和应用程序自己的窗口。这是我们的应用how we presented it中的xScope

是的,我仍然为little regard to usability引入这些保护而感到痛苦。

答案 1 :(得分:9)

Apple 提供直接的低级 API 来检查访问和授予访问权限。无需使用棘手的解决方法。

/* Checks whether the current process already has screen capture access */
@available(macOS 10.15, *)
public func CGPreflightScreenCaptureAccess() -> Bool

使用上述函数检查屏幕截图访问。

如果未授予访问权限,请使用以下功能提示访问

/* Requests event listening access if absent, potentially prompting */
@available(macOS 10.15, *)
public func CGRequestScreenCaptureAccess() -> Bool

Screenshot taken from documentation

答案 2 :(得分:3)

@ marek-h发布了一个很好的示例,该示例可以检测屏幕录制设置而不显示隐私警报。 顺便说一句,@ jordan-h提到当应用程序通过beginSheetModalForWindow发出警报时,此解决方案不起作用。

我发现SystemUIServer进程始终在创建一些带有名称的窗口:AppleVolumeExtra,AppleClockExtra,AppleBluetoothExtra ...

在“隐私”偏好设置中启用屏幕录制之前,我们无法获得这些窗口的名称。而当我们至少可以得到这些名称之一时,则意味着用户已启用屏幕录制。

因此,我们可以检查窗口的名称(由SystemUIServer进程创建)以检测屏幕录制首选项,并且在macOS Catalina上可以正常工作。

#include <AppKit/AppKit.h>
#include <libproc.h>

bool isScreenRecordingEnabled()
{
    if (@available(macos 10.15, *)) {
        bool bRet = false;
        CFArrayRef list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
        if (list) {
            int n = (int)(CFArrayGetCount(list));
            for (int i = 0; i < n; i++) {
                NSDictionary* info = (NSDictionary*)(CFArrayGetValueAtIndex(list, (CFIndex)i));
                NSString* name = info[(id)kCGWindowName];
                NSNumber* pid = info[(id)kCGWindowOwnerPID];
                if (pid != nil && name != nil) {
                    int nPid = [pid intValue];
                    char path[PROC_PIDPATHINFO_MAXSIZE+1];
                    int lenPath = proc_pidpath(nPid, path, PROC_PIDPATHINFO_MAXSIZE);
                    if (lenPath > 0) {
                        path[lenPath] = 0;
                        if (strcmp(path, "/System/Library/CoreServices/SystemUIServer.app/Contents/MacOS/SystemUIServer") == 0) {
                            bRet = true;
                            break;
                        }
                    }
                }
            }
            CFRelease(list);
        }
        return bRet;
    } else {
        return true;
    }
}

答案 3 :(得分:2)

我不知道专门用于获取屏幕录像许可状态的API。除了创建CGDisplayStream并检查是否为nil外,Advances in macOS Security WWDC演示还提到,除非获得许可,否则不会返回CGWindowListCopyWindowInfo() API的某些元数据。因此,尽管存在依赖该功能的实现细节的相同问题,但类似的事情似乎确实可行:

private func canRecordScreen() -> Bool {
    guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
    return windows.allSatisfy({ window in
        let windowName = window[kCGWindowName as String] as? String
        return windowName != nil
    })
}

答案 4 :(得分:2)

截至19年11月chockenberry的答案正确。

@onelittlefish指出,如果用户未在“隐私”窗格中启用屏幕录制访问权限,则将省略kCGWindowName。此方法也不会触发隐私警报。

- (BOOL)canRecordScreen
{
    if (@available(macOS 10.15, *)) {
        CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
        NSUInteger numberOfWindows = CFArrayGetCount(windowList);
        NSUInteger numberOfWindowsWithName = 0;
        for (int idx = 0; idx < numberOfWindows; idx++) {
            NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, idx);
            NSString *windowName = windowInfo[(id)kCGWindowName];
            if (windowName) {
                numberOfWindowsWithName++;
            } else {
                //no kCGWindowName detected -> not enabled
                break; //breaking early, numberOfWindowsWithName not increased
            }

        }
        CFRelease(windowList);
        return numberOfWindows == numberOfWindowsWithName;
    }
    return YES;
}

答案 5 :(得分:1)

最有利的答案并不完全正确,他遗漏了一些观点,例如分享国家。

我们可以在WWDC(https://developer.apple.com/videos/play/wwdc2019/701/?time=1007)中找到答案

以下是WWDC的一些摘录: 除非用户已预先批准该应用程序进行屏幕记录,否则窗口名称和共享状态将不可用。这是因为某些应用程序在窗口名称中放置了敏感数据,例如帐户名或更可能的网页URL。

- (BOOL)ScreeningRecordPermissionCheck {
    if (@available(macOS 10.15, *)) {
        CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
        NSUInteger numberOfWindows = CFArrayGetCount(windowList);
        NSUInteger numberOfWindowsWithInfoGet = 0;
        for (int idx = 0; idx < numberOfWindows; idx++) {

            NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, idx);
            NSString *windowName = windowInfo[(id)kCGWindowName];
            NSNumber* sharingType = windowInfo[(id)kCGWindowSharingState];

            if (windowName || kCGWindowSharingNone != sharingType.intValue) {
                numberOfWindowsWithInfoGet++;
            } else {
                NSNumber* pid = windowInfo[(id)kCGWindowOwnerPID];
                NSString* appName = windowInfo[(id)kCGWindowOwnerName];
                NSLog(@"windowInfo get Fail pid:%lu appName:%@", pid.integerValue, appName);
            }
        }
        CFRelease(windowList);
        if (numberOfWindows == numberOfWindowsWithInfoGet) {
            return YES;
        } else {
            return NO;
        }
    }
    return YES;
}

答案 6 :(得分:1)

从MacOS 10.15.7开始,启发式获取可见窗口的窗口名称的方法,因此知道我们拥有屏幕捕获权限,这种方法并不总是有效。有时我们只是找不到可以查询的有效窗口,并错误地推断出我们没有权限。

但是,我发现了另一种直接查询(使用sqlite)Apple TCC数据库的方法-保留权限的模型。屏幕记录权限可在“系统级别” TCC数据库(位于/Library/Application Support/com.apple.TCC/TCC.db中)中找到。如果您使用sqlite打开数据库,并查询:SELECT allowed FROM access WHERE client="com.myCompany.myApp" AND service="kTCCServiceScreenCapture",您将得到答案。

与其他答案相比有两个缺点:

  • 要打开此TCC.db数据库,您的应用必须具有“ 完全磁盘访问权限”权限。它不需要以“ root”特权运行,如果您没有“ Full disk access”权限,root特权也无济于事。
  • 运行大约需要15毫秒,这比查询窗口列表要慢。

正面-这是对实际事物的直接查询,并且不依赖于查询时存在的任何窗口或进程。

这里有一些草稿代码可以做到这一点:

NSString *client = @"com.myCompany.myApp";
sqlite3 *tccDb = NULL;
sqlite3_stmt *statement = NULL;

NSString *pathToSystemTCCDB = @"/Library/Application Support/com.apple.TCC/TCC.db";
const char *pathToDBFile = [pathToSystemTCCDB fileSystemRepresentation];
if (sqlite3_open(pathToDBFile, &tccDb) != SQLITE_OK)
   return nil;
    
const char *query = [[NSString stringWithFormat: @"SELECT allowed FROM access WHERE client=\"%@\" AND service=\"kTCCServiceScreenCapture\"",client] UTF8String];
if (sqlite3_prepare_v2(tccDb, query , -1, &statement, nil) != SQLITE_OK)
   return nil;
    
BOOL allowed = NO;
while (sqlite3_step(statement) == SQLITE_ROW)
    allowed |= (sqlite3_column_int(statement, 0) == 1);

if (statement)
    sqlite3_finalize(statement);

if (tccDb)
    sqlite3_close(tccDb);

return @(allowed);

}

答案 7 :(得分:0)

以上答案无法正常工作。下面是正确的答案。

private var canRecordScreen : Bool {
    guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
    return windows.allSatisfy({ window in
        let windowName = window[kCGWindowName as String] as? String
        let isSharingEnabled = window[kCGWindowSharingState as String] as? Int
        return windowName != nil || isSharingEnabled == 1
    })
  }