Определение настроек записи экрана в macOS Catalina

Каков надежный способ определить, активировал ли пользователь этот API?

CGWindowListCreateImage возвращает допустимый объект, даже если API записи экрана отключен. Возможны несколько комбинаций (kCGWindowListOptionIncludingWindow, kCGWindowListOptionOnScreenBelowWindow), и только некоторые из них вернут 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;
        }
    }

person Marek H    schedule 14.06.2019    source источник
comment
Как пользователь отключает API записи экрана?   -  person TheNextman    schedule 14.06.2019
comment
Может непонятно. В Каталине появился новый переключатель конфиденциальности. Использование API вызовет окно конфиденциальности, в котором у пользователя есть 2 варианта: 1) запретить, 2) открыть настройки системы и включить вручную. Нет кнопки разрешения.   -  person Marek H    schedule 16.06.2019
comment
Спасибо, извините, я не могу помочь с вашим вопросом, но это полезно знать :)   -  person TheNextman    schedule 17.06.2019
comment
@MarekH Можем ли мы обойти / подавить это окно конфиденциальности.   -  person Sangram Shivankar    schedule 08.01.2020
comment
Есть ли обновления API в macOS 11?   -  person Jimmy    schedule 17.12.2020
comment
@ Джимми Да. Использование CGRequestScreenCaptureAccess ()   -  person Marek H    schedule 03.01.2021


Ответы (8)


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

Снимок экрана взят из документации

person Somesh Karthik    schedule 23.12.2020
comment
Добавлено в macOS11 beta 3. Просто нужно проверить, правильно ли он работает во всех версиях 10.15 codeworkshop.net/objc-diff/sdkdiffs/macos/11.0b3/ - person Marek H; 03.01.2021
comment
Интересно, что сообщество так долго не знало об этом API. - person Jimmy; 08.01.2021
comment
Кажется, это хорошо работает на Big Sur, но вылетает в Catalina (по крайней мере, 10.15.7). - person Jordan H; 17.02.2021
comment
Он вылетает только в том случае, если вы не вызываете его в основной очереди. У меня это хорошо работает: dispatch_sync(dispatch_get_main_queue(), ^{ CGRequestScreenCaptureAccess(); }); - person Jezevec; 04.05.2021

Все представленные здесь решения так или иначе имеют изъяны. Корень проблемы в том, что нет корреляции между вашим разрешением знать об окне (через имя в списке окон), вашим разрешением знать о процессе-владельце окна (например, 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 не установлен, вам нужно будет создать своего рода диалоговое окно, предупреждающее пользователя о том, что он сможет видеть только строку меню, изображение рабочего стола и собственные окна приложения. Вот как мы представили это в нашем приложении xScope.

И да, мне все еще горько, что эти средства защиты были введены с незначительным вниманием к удобству использования.

person chockenberry    schedule 21.11.2019
comment
comment
Знаем ли мы, работает ли это, если на устройстве не установлен английский язык? Похоже, хорошее решение, но слово Dock заставляет меня волноваться ... хотя, думаю, имена исполняемых файлов должны быть такими же ... - person Max Chuquimia; 23.11.2019
comment
@ max-chuquimia Да, он будет работать, когда на устройстве не установлен английский язык. Крейг использует lastPathComponent, а не localizedName, поэтому имя является истинным исполняемым файлом, а не его локализованным именем. Я тестировал это явно при написании аналогичного кода для своего приложения (папка по умолчанию X). - person Jon Gotow; 23.11.2019
comment
Будет ли это работать, если нет открытых окон? Это, очевидно, крайний случай, и есть много других системных окон, но просто любопытно, является ли это пуленепробиваемым решением, когда приложение устанавливается на чистую macOS и выполняет эту проверку. Может ли быть ситуация, когда просто нет окон, чтобы это точно определить? - person Ian Bytchek; 04.12.2019
comment
Большое спасибо за этот код, я сомневаюсь, что сам понял бы это! Добавив эту проверку к некоторому программному обеспечению, которое я поддерживаю, я начал получать в нем несколько отчетов о сбоях. Похоже, что CGWindowListCopyWindowInfo() иногда возвращает NULL. ???? Я пока не знаю точных обстоятельств, это должна быть довольно необычная ситуация, но похоже, что нам нужно с ней справиться. - person pmdj; 21.02.2020
comment
Это не работает, если на экране нет открытых окон. - person Roland Rabien; 03.04.2020
comment
@MaxChuquimia вам нужно будет пройти еще один шаг - идентифицировать Dock не по имени, а по его идентификатору пакета, который не зависит от языка (com.apple.dock), затем спросить пакет Dock о его локализованном имени, а затем найти его в Имя Окна. - person Motti Shneor; 01.11.2020

@ 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;
    }
}
person Fred Zhang    schedule 10.11.2019
comment
Это также работает, когда приложение представляет предупреждение через beginSheetModalForWindow. - person Fred Zhang; 10.11.2019
comment
Пожалуйста, добавьте пояснение к своему ответу, чтобы он был более понятным. - person mukund patel; 10.11.2019
comment
Привет, Фред, не могли бы вы объяснить более подробно, почему это должно работать. Было бы лучше, если бы люди видели пример. Ваше решение не считает количество окон на экране. Его также нельзя использовать для копирования, потому что у него есть неизвестные константы, такие как sl_true. - person Marek H; 10.11.2019
comment
Привет, Марек, я обновил пост дополнительным описанием и примером кода. - person Fred Zhang; 12.11.2019
comment
kCGWindowListOptionOnScreenOnly не работал в полноэкранном режиме, поэтому я изменил его на kCGWindowListOptionAll, и тогда все в порядке. - person Fred Zhang; 20.11.2019
comment
Вопрос: кто вам обещал, что у SystemUIServer ВСЕГДА будут эти окна? или какие окна? Я могу представить себе множество сценариев, в которых ОС не имеет экранных окон. Этот прием хорош только в том случае, если вы можете найти окно, существование которого обещано некоторым процессом. Почему бы не использовать для этой задачи наше собственное окно? таким образом мы можем гарантировать его существование, видимость, доступность и т. д. - person Motti Shneor; 03.11.2020

Мне неизвестен API, специально предназначенный для получения статуса разрешения на запись экрана. Помимо создания CGDisplayStream и проверки на ноль, успехи в безопасности macOS 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
    })
}
person onelittlefish    schedule 05.08.2019
comment
Swift-код вызывает у меня улыбку. немного загадочно на мой вкус, но так лаконично! Один вопрос: у некоторых окон может не быть имени! - тем не менее, вам требуется allSatisfy, и он вернет false, даже если у одного окна нет имени. Разве это не ошибка? Может быть, достаточно получить только одно имя, чтобы знать, что у вас есть права на создание снимков экрана? - person Motti Shneor; 03.11.2020

По состоянию на 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;
}
person Marek H    schedule 06.08.2019
comment
Я считаю, что это хорошо работает, пока мое приложение не выдаст предупреждение через beginSheetModalForWindow. Он идет от обнаружения 31 окна из 31 с именами до 17 из 31. Вы знаете, почему это так? - person Jordan H; 26.08.2019
comment
Это происходит и в публичном выпуске. Если какое-либо приложение представило предупреждение, оно вернет НЕТ, если включена запись экрана. :( - person Jordan H; 08.10.2019
comment
@JordanH Вы можете вернуться к CGDisplayStreamCreate, о котором идет речь, это одно из решений. Однако это вызывает предупреждение - person Marek H; 08.10.2019

Самый благоприятный ответ - не совсем правильный, он упустил некоторые смыслы, такие как разделение состояния.

мы можем найти ответ в 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;
}
person lunch-box-run    schedule 06.01.2020
comment
А как насчет окон, у которых нет имени и которые не используются совместно? - person Motti Shneor; 03.11.2020

Начиная с 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);

}

person Motti Shneor    schedule 03.11.2020

Приведенный выше ответ не работает нормально. Ниже правильный ответ.

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
    })
  }
person user12415592    schedule 22.11.2019
comment
Не могли бы вы объяснить почему? - person Ian Bytchek; 04.12.2019
comment
Как можно предоставить разрешение API для записи экрана. В системных настройках я не вижу +, чтобы добавить приложение, чтобы разрешить запись экрана - person mica; 07.12.2019
comment
Это сработало для меня. Ответ выше было очень сложно передать в Swift из-за небезопасных указателей. Это сработало. - person Jorge Silva; 05.08.2020