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

Mar*_*k H 13 macos cocoa 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;
}
Run Code Online (Sandbox Code Playgroud)

唯一可靠的方法是CGDisplayStreamCreate冒险,因为Apple每年都会更改隐私设置。

   - (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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

cho*_*rry 10

这里介绍的所有解决方案都以一种或另一种方式存在缺陷。问题的根源在于,您了解窗口的权限(通过窗口列表中的名称)与您了解窗口的进程所有者(例如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);
}
Run Code Online (Sandbox Code Playgroud)

If canRecordScreen is not set, you'll need to put up some kind of dialog that warns the user that they'll only be able to see the menubar, desktop picture, and the app's own windows. Here's how we presented it in our app xScope.

And yes, I'm still bitter that these protections were introduced with little regard to usability.

  • 效果很好!Swift 5 版本:https://gist.github.com/soffes/da6ea98be4f56bc7b8e75079a5224b37 (6认同)
  • 我们知道当设备未设置为英语时这是否有效吗?它看起来是一个很好的解决方案,但是那里有“Dock”这个词让我担心......尽管我猜可执行文件名称应该是相同的...... (3认同)
  • @ max-chuquimia是的,当设备未设置为英语时,它将起作用。Craig使用的是lastPathComponent,而不是localizedName,因此名称是真实的可执行文件,而不是其本地化名称。在为我的应用(默认文件夹X)编写类似代码时,我对此进行了明确的测试。 (2认同)
  • 如果没有打开的窗户,这会起作用吗?这显然是一个边缘情况,还有很多其他系统窗口,但只是好奇,当应用程序安装在干净的 macOS 上并运行此检查时,这是否是一个万无一失的解决方案。是否存在根本没有窗口可以准确确定这一点的情况? (2认同)
  • 不幸的是,尽管有 API 文档,CGRequestScreenCaptureAccess 和 CGPreflightScreenCaptureAccess 并未在 Catalina 中实现,如果在 10.15 中使用将会崩溃。 (2认同)

小智 10

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

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

使用上述功能检查屏幕截图访问。

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

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

从文档中截取的屏幕截图

  • 有趣的是,社区很长一段时间都不知道这个 API。 (2认同)
  • 这似乎在 Big Sur 上运行良好,但在 Catalina 上崩溃(至少 10.15.7)。 (2认同)
  • 仅当您无法在主队列上调用它时,它才会崩溃。这对我来说效果很好:`dispatch_sync(dispatch_get_main_queue(), ^{ CGRequestScreenCaptureAccess(); });` (2认同)