【问题标题】:Detecting screen recording settings on macOS Catalina在 macOS Catalina 上检测屏幕录制设置
【发布时间】:2019-10-29 00:50:35
【问题描述】:

检测用户是否启用此 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,这是有风险的,因为 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;
        }
    }

【问题讨论】:

  • 用户如何禁用录屏API?
  • 可能还不清楚。 Catalina 中有新的隐私开关。 API 的使用将触发一个隐私窗口,其中用户有 2 个选项:1)拒绝,2)打开系统设置并手动启用。没有允许按钮。
  • 谢谢,抱歉,我无法帮助您解决问题,但很高兴知道 :)
  • @MarekH 我们可以绕过/禁止这个隐私窗口吗?
  • macOS 11 中的 API 是否有任何更新?

标签: macos cocoa screen-recording macos-catalina


【解决方案1】:

这里介绍的所有解决方案都存在某种缺陷。问题的根源在于您了解窗口的权限(通过窗口列表中的名称)与您了解窗口的进程所有者(例如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 引入这些保护措施感到不满。

【讨论】:

  • 我们知道设备未设置为英语时这是否有效吗?它看起来是一个很好的解决方案,但是那里有“Dock”这个词让我很担心......虽然我猜可执行文件的名称应该是一样的......
  • @max-chuquimia 是的,它会在设备未设置为英语时工作。 Craig 使用 lastPathComponent,而不是本地化名称,因此名称是真正的可执行文件,而不是它的本地化名称。我在为我的应用程序(默认文件夹 X)编写类似代码时明确测试了这一点。
  • 如果没有打开的窗口,这会起作用吗?这显然是一个边缘案例,还有很多其他系统窗口挂着,但只是好奇,当应用程序安装在干净的 macOS 上并运行此检查时,这是否是一个防弹解决方案。是否存在根本没有窗口可以准确确定这一点的情况?
  • 非常感谢这段代码,我怀疑我自己是否能解决这个问题!现在将这个检查添加到我维护的一些软件中,我开始在其中收到一些崩溃报告。看起来CGWindowListCopyWindowInfo() 有时会返回NULL。 ? 我还不清楚具体情况,这应该是一个相当不寻常的情况,但看起来我们需要处理它。
【解决方案2】:

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

【讨论】:

  • 在macOS11 beta 3中添加。只需要验证它是否在10.15的所有版本上都能正常工作codeworkshop.net/objc-diff/sdkdiffs/macos/11.0b3/…
  • 有趣的是社区这么久都不知道这个 API。
  • 这似乎在 Big Sur 上运行良好,但在 Catalina 中崩溃(至少 10.15.7)。
  • 只有在主队列中调用失败时才会崩溃。这对我很有效:dispatch_sync(dispatch_get_main_queue(), ^{ CGRequestScreenCaptureAccess(); });
  • @Jezevec 很天真,10.15 和 10.16/11.0 只收到 300 个崩溃报告和 0 个。此 API 在 10.15 上不存在。将填充雷达
【解决方案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;
    }
}

【讨论】:

  • 当应用程序通过 beginSheetModalForWindow 显示警报时,这也有效。
  • 请在您的答案中添加解释以使其更清楚。
  • 嗨 Fred,您能否详细解释一下为什么这应该有效。如果人们看到一个例子会更好。您的解决方案不计算屏幕上的窗口数。它也不能用作复制粘贴原因有一些未知常量,如 sl_true
  • 嗨 Marek,我更新了帖子,添加了额外的描述和示例代码。
  • kCGWindowListOptionOnScreenOnly 不适合全屏模式,所以我把它改成kCGWindowListOptionAll 就可以了。
【解决方案4】:

我不知道专门用于获取屏幕录制权限状态的 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
    })
}

【讨论】:

  • Swift 代码让我微笑。对我的口味来说有点神秘,但又如此简洁!但是有一个问题:有些窗口可能没有名字! - 但是您需要“allSatisfy”,即使单个窗口没有名称,也会返回 false。这不是一个错误吗?也许只获得一个名字就足够了,知道你有截屏权限?
【解决方案5】:

截至11月19日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;
}

【讨论】:

  • 在我的应用通过beginSheetModalForWindow 显示警报之前,我发现这很有效。它从检测 31 个带有名称的窗口中的 31 个到 31 个中的 17 个。你知道这是为什么吗?
  • 公开版本中也会出现这种情况。如果任何应用程序出现警报,则在启用屏幕录制时返回 NO。 :(
  • @JordanH 您可以回退到问题中提到的 CGDisplayStreamCreate,它是解决方案之一。但是,它会触发警报
【解决方案6】:

最有利的答案并不完全正确,他遗漏了一些感觉,比如分享状态。

我们可以在 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;
}

【讨论】:

  • 那些没有名字且不是“共享”的窗口呢?
【解决方案7】:

从 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”权限运行,如果您没有“全盘访问”权限,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);

}

【讨论】:

    【解决方案8】:

    为我工作。 代码来自:https://gist.github.com/code4you2021/270859c71f90720d880ccb2474f4e7df

    import Cocoa
    
    struct ScreenRecordPermission {
        static var hasPermission: Bool {
            permissionCheck()
        }
    
        static func permissionCheck() -> Bool {
            if #available(macOS 10.15, *) {
                let runningApplication = NSRunningApplication.current
                let processIdentifier = runningApplication.processIdentifier
                guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID)
                    as? [[String: AnyObject]],
                    let _ = windows.first(where: { window -> Bool in
                        guard let windowProcessIdentifier = (window[kCGWindowOwnerPID as String] as? Int).flatMap(pid_t.init),
                              windowProcessIdentifier != processIdentifier,
                              let windowRunningApplication = NSRunningApplication(processIdentifier: windowProcessIdentifier),
                              windowRunningApplication.executableURL?.lastPathComponent != "Dock",
                              let _ = window[String(kCGWindowName)] as? String
                        else {
                            return false
                        }
    
                        return true
                    })
                else {
                    return false
                }
            }
            return true
        }
    
        static func requestPermission() {
            if #available(macOS 10.15, *) {
                CGWindowListCreateImage(CGRect(x: 0, y: 0, width: 1, height: 1), .optionOnScreenOnly, kCGNullWindowID, [])
            }
        }
    }
    
    # how to use
    # print("hasPermission: ", ScreenRecordPermission.hasPermission)
    

    【讨论】:

      【解决方案9】:

      上面的答案不能正常工作。以下是正确答案。

      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
          })
        }
      

      【讨论】:

      • 您能解释一下原因吗?
      • 如何授予录屏API权限。在“系统偏好设置”中,我看不到“+”来添加应用程序,以授予“屏幕录制”
      • 这对我有用。由于不安全的指针,上面的答案很难传递给 Swift。这行得通。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-13
      • 2023-03-14
      • 2020-02-17
      • 1970-01-01
      相关资源
      最近更新 更多