Если сделать скриншот Netflix или окна воспроизведения в Spotify, на месте видео окажется чёрный прямоугольник. То же произойдёт при демонстрации экрана в Zoom, в записи через OBS и даже в Snipping Tool. Звук идёт, содержимого нет.

Это не защита кодека и не трюк с OpenGL-поверхностями. Это один флаг в одном API, который сообщает оконной системе: «это окно не должно попадать в захваченные кадры». Флаг публичный, документированный, появился в Windows 10 ещё в 2020 году и используется любым приложением, которому нужно закрыть содержимое от скриншотов: менеджерами паролей, банковскими клиентами, 2FA-токенами.

На macOS раньше был симметричный аналог, но в macOS 15 Sequoia Apple сломала его против ScreenCaptureKit, и теперь картина там сильно запутаннее. На Linux всё зависит от дисплейного сервера. В браузерах работает через цепочку платформенных API.

Опыт накопился за то время, пока мы собирали десктопное приложение для онлайн-собеседований, которому эта механика нужна технически: окно с подсказками не должно попадать в демонстрацию экрана. Про продукт — в одном абзаце в конце. Вся остальная статья про то, что под капотом.

Кто захватывает экран, когда вы нажимаете Share Screen

Когда Zoom просит доступ к демонстрации экрана, он не делает фотографий монитора. Он подписывается на поток кадров от операционной системы. Какое именно API при этом используется — важно, потому что у них разная архитектура и разное поведение с защищёнными окнами.

На Windows основных путей три.

GDI BitBlt от десктопного DC. Самый старый способ, работает с Windows 2000. Вызов BitBlt от GetDC(NULL)копирует пиксели из поверхности DWM (Desktop Window Manager) в произвольный HDC. Медленно, без аппаратного ускорения, но работает везде. Используют старые приложения и некоторые мониторинговые утилиты.

Desktop Duplication API (DXGI). Появился в Windows 8. Работает через IDXGIOutputDuplication::AcquireNextFrame — возвращает GPU-текстуру с текущим кадром десктопа, уже скомпонованным из всех окон. Быстро, аппаратно, но захватывает только целиком монитор. Использовался в классических Zoom, TeamViewer, AnyDesk и старых версиях Teams.

IDXGIOutputDuplication* duplication = nullptr;
output1->DuplicateOutput(d3dDevice, &duplication);

DXGI_OUTDUPL_FRAME_INFO frameInfo;
IDXGIResource* desktopResource = nullptr;
duplication->AcquireNextFrame(500, &frameInfo, &desktopResource);
// в desktopResource — текстура с текущим кадром рабочего стола

Windows.Graphics.Capture (WGC). Пришёл в Windows 10 1803 (2018 год). Это то, что Microsoft сейчас официально рекомендует. Умеет захватывать как монитор целиком, так и конкретное окно по HWND. Используется в современном OBS, Microsoft Teams, Chromium (и, соответственно, во всех звонках через браузер), обновлённых версиях Zoom, системном Snipping Tool.

auto item = GraphicsCaptureItem::CreateFromHwnd(targetHwnd);
auto device = CreateDirect3DDevice(dxgiDevice);
auto framePool = Direct3D11CaptureFramePool::Create(
    device, DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, size);
auto session = framePool.CreateCaptureSession(item);
session.StartCapture();
// кадры приходят через FrameArrived

Все три способа в итоге читают пиксели из DWM — единственного места в Windows, где существует «то, что сейчас на экране». Отсюда и растёт механизм защиты.

SetWindowDisplayAffinity: три режима и как они работают

У DWM есть атрибут display affinity для каждого окна. Он говорит композитору, как включать окно в потоки захвата. Задаётся через функцию SetWindowDisplayAffinity:

BOOL SetWindowDisplayAffinity(HWND hwnd, DWORD affinity);

Значений три:

Константа

Значение

Поведение

WDA_NONE

0x00

Дефолт. Окно видно во всех захватах

WDA_MONITOR

0x01

На мониторе окно отображается, в захвате — чёрный прямоугольник

WDA_EXCLUDEFROMCAPTURE

0x11

Окна в захвате нет вообще, как будто оно не существует

WDA_MONITOR работает с Windows Vista. Это тот самый режим, который используют Netflix и Spotify — отсюда чёрный квадрат на скриншотах Netflix. Наблюдатель видит, что там что-то есть, но содержимое не получает.

WDA_EXCLUDEFROMCAPTURE — более аккуратный вариант, появился в Windows 10 версии 2004 (May 2020 Update, build 19041). Разница принципиальная: при WDA_MONITOR в захваченном кадре остаётся чёрная дыра в форме окна. При WDA_EXCLUDEFROMCAPTURE окна в потоке нет совсем, и сквозь него видно то, что под ним.

Минимальный работающий пример на C++:

#include <windows.h>
#include <cstdio>

int main() {
    HWND hwnd = GetConsoleWindow();
    if (!SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE)) {
        printf("SetWindowDisplayAffinity failed: %lu\n", GetLastError());
        return 1;
    }
    printf("Окно теперь невидимо для захвата. Попробуй сделать скриншот.\n");
    Sleep(60000);
    return 0;
}

Скомпилируйте, запустите, откройте Snipping Tool — консоли в снимке не будет.

Из C# / WPF то же через P/Invoke:

[DllImport("user32.dll")]
static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);

const uint WDA_EXCLUDEFROMCAPTURE = 0x11;
var hwnd = new WindowInteropHelper(this).Handle;
SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);

В Electron для этого есть высокоуровневая обёртка:

mainWindow.setContentProtection(true);

Под капотом она вызывает SetWindowDisplayAffinity с WDA_EXCLUDEFROMCAPTURE на Windows и меняет sharingType на macOS. В Tauri аналогично через with_content_protection(true).

Механика под капотом такая. DWM держит отдельные композиционные пайплайны для каждого клиента-захватчика. Когда окно помечено WDA_EXCLUDEFROMCAPTURE, его слой попадает в пайплайн для физического монитора и не попадает в пайплайны для capture-клиентов. Разделение происходит на уровне композиции, а не фильтра поверх готового кадра, поэтому обойти его на уровне чтения пикселей невозможно.

Что реально видят приложения с включённым флагом

Проверили на Windows 11 23H2 все пути захвата с окном, у которого выставлен WDA_EXCLUDEFROMCAPTURE:

  • GDI BitBlt от десктопного DC — окно отсутствует.

  • Desktop Duplication (DXGI) — окно отсутствует. В Windows 11 24H2 есть особенность с тем, что AcquireNextFrame теперь пробуждается и на обновлениях скрытого окна, но контент всё равно не отдаётся.

  • Windows.Graphics.Capture (WGC) — окно отсутствует.

  • PrintWindow — флаг игнорируется, окно попадает в bitmap. PrintWindow идёт мимо DWM и просит окно отрисоваться напрямую в указанный DC. Защита не работает. Zoom и другие системы демонстрации PrintWindow не используют, но при аудите стоит помнить.

Итог для типичных сценариев: Zoom Screen Share, Google Meet (через Chrome), Microsoft Teams, Discord, Slack Huddle, OBS, Snipping Tool, Lightshot, Greenshot — во всём этом окно с WDA_EXCLUDEFROMCAPTURE отсутствует. Не чёрный прямоугольник, а отсутствие объекта.

Одно ограничение: работает на Windows 10 20H1 и новее. На Windows 10 1809 LTSC и ранее флаг не поддерживается, SetWindowDisplayAffinity с WDA_EXCLUDEFROMCAPTURE вернёт FALSE и GetLastError() == ERROR_INVALID_PARAMETER. Откатываемся на WDA_MONITOR, и тогда в чужой демонстрации будет чёрный прямоугольник вместо невидимости.

Отдельная тема — атрибут WCA_EXCLUDED_FROM_DDA через недокументированный SetWindowCompositionAttribute. Работает с Windows 10 1709, исключает окно только из Desktop Duplication, оставляя видимым в остальных API. На практике не нужен, потому что WDA_EXCLUDEFROMCAPTURE решает ту же задачу полнее, но в legacy-кодовых базах иногда встречается.

macOS до 15: sharingType как стандартное решение

На macOS исторически использовалось свойство sharingType у NSWindow:

window.sharingType = .none

Значения:

  • .readWrite — окно доступно для чтения и изменения из других процессов (редко используется)

  • .readOnly — дефолт, окно захватываемо

  • .none — окно исключено из захвата

Под капотом работало через WindowServer. На macOS нет DWM, композицию делает сам WindowServer через Core Graphics. Флаг NSWindowSharingNone говорил WindowServer: «не отдавай содержимое этого окна в API захвата».

До macOS 14 Sonoma включительно это работало против всего:

  • CGWindowListCreateImage — legacy API захвата, с macOS 10.5. Используется в старых скриншотных утилитах и некоторых не обновлённых приложениях.

  • ScreenCaptureKit — новый framework, введён в macOS 12.3 в 2022 году. На него мигрируют все современные приложения, потому что Apple помечает остальные API как deprecated.

В Electron поверх этого та же строчка setContentProtection(true) — она под капотом выставляла sharingType = .none.

macOS 15 Sequoia: sharingType больше не работает с ScreenCaptureKit

Осенью 2024 вышла macOS 15. В ней Apple изменила поведение WindowServer: композиция теперь сначала собирает все видимые окна в единый framebuffer, а ScreenCaptureKit захватывает уже этот framebuffer. Флаг sharingType = .none перестал исключать окно из потока SCStream.

Подтверждение — в ветке Apple Developer Forums, где разработчик спрашивает напрямую: работает ли kCGWindowSharingStateSharingNone против ScreenCaptureKit на 15.4+. Ответ официального представителя Apple: «At this time there are no public APIs for preventing screen capture».

Важный нюанс — это сломало не всё. sharingType = .none продолжает работать против legacy-API (CGWindowListCreateImage и то, что через него построено). А вот против ScreenCaptureKit — нет.

У этого есть практическое следствие, потому что разные приложения мигрируют на ScreenCaptureKit в разном темпе:

Приложение

Что использует на macOS 15

Видит окно с sharingType = .none

Zoom (актуальный)

ScreenCaptureKit

Да, видит

Microsoft Teams (новый)

ScreenCaptureKit

Да, видит

QuickTime Screen Recording

ScreenCaptureKit

Да, видит

System Screenshot (Cmd+Shift+3/4/5)

ScreenCaptureKit

Да, видит

Google Chrome (getDisplayMedia)

Legacy CoreGraphics

Нет, не видит

Google Meet в Chrome

Через Chrome → CoreGraphics

Нет, не видит

OBS (старые версии)

CoreGraphics

Нет

OBS (новые версии с SCK)

ScreenCaptureKit

Да

То есть на macOS 15 окно с sharingType = .none всё ещё скрыто от звонка в Google Meet через Chrome, но уже видно в демонстрации через десктопный Zoom. Это не ошибка реализации, это архитектурное решение Apple, и публичного обходного пути нет.

Сломалось это и в Electron, и в Tauri — оба зафиксировали у себя как апстрим-блокер, который решить без приватных API нельзя. Частные обходы существуют, но любое приложение, которое ими пользуется, не пройдёт App Store review и рискует сломаться при очередном обновлении macOS.

Для всех, кто строит продукты с требованием «не попадать в демонстрацию экрана на macOS», эта регрессия принципиальная. Либо ограничиваемся macOS 14 и ранее, либо принимаем, что на macOS 15+ защита от ScreenCaptureKit невозможна и придётся переключаться на поведенческие способы (автоматически сворачивать окно при начале демонстрации, детектить SCStream по системным сигналам).

Linux: X11 не спрячешь, Wayland по-другому устроен

На Linux ответ зависит от дисплейного сервера.

X11. Любой клиент с доступом к дисплею может вызвать XGetImage или XCompositeNameWindowPixmap и получить пиксели любого окна, включая чужие. X-сервер не различает «свои» и «чужие» окна для клиента. Это архитектура протокола, а не баг. Прятать окно от захвата в X11 в общем случае нельзя — composit-расширения для конкретных window manager (Picom, KWin, Mutter) могут что-то позволять, но кросс-WM решения нет.

Wayland. Другая история. В Wayland клиент в принципе не может читать чужие surface. Захват экрана возможен только через xdg-desktop-portal + PipeWire:

  1. Приложение вызывает org.freedesktop.portal.ScreenCast.

  2. Портал показывает пользователю системный диалог выбора источника (монитор, окно, приложение).

  3. Composit передаёт PipeWire-поток только для выбранного источника.

Возможность пометить окно как «не отдавать в захват» в Wayland-протоколе не стандартизирована. На уровне композитора теоретически можно реализовать через приватные расширения, но публичного API нет. На практике: если пользователь через портал выбрал захватить монитор целиком, любое окно в этот поток попадёт.

Обходное рассуждение для Wayland: проектировать UX так, чтобы пользователь всегда выбирал в портале конкретное окно (например, окно браузера или конкретное приложение звонка), а не весь экран. Тогда исключение не нужно, потому что окно ассистента не выбрано.

Браузеры и getDisplayMedia

Google Meet и все остальные веб-звонки работают через navigator.mediaDevices.getDisplayMedia(). Это WebRTC-метод, а реализация зависит от браузера и ОС:

  • Chromium на Windows — новые версии используют Windows.Graphics.Capture. WDA_EXCLUDEFROMCAPTURE работает.

  • Chromium на macOS — по состоянию на начало 2026 всё ещё использует legacy CoreGraphics. sharingType = .none продолжает работать, причём даже на macOS 15. Миграция Chrome на ScreenCaptureKit обсуждается в трекере Chromium, но пока не сделана.

  • Firefox на Windows — Windows.Graphics.Capture + фолбэк на DXGI. Работает.

  • Firefox на macOS — смешанная реализация, в целом legacy-путь, sharingType работает.

  • Safari — только ScreenCaptureKit, и значит на macOS 15+ увидит защищённое окно.

Вывод практический: на macOS 15 звонки через Chrome и Firefox (то есть Google Meet) пока что всё ещё не видят защищённое окно, а десктопный Zoom и Safari — видят. Это хрупкое равновесие, потому что Chrome рано или поздно перейдёт на ScreenCaptureKit.

Что обходит защиту в любом случае

Чтобы не выдавать флаги за абсолютную защиту, вот список способов, которые их обходят.

Аппаратный захват. HDMI-capture-карта (Elgato, AVerMedia) получает сигнал с монитора уже после того, как видеокарта отправила его в порт. DWM и WindowServer в этот момент к сигналу отношения не имеют. Защита не работает.

Камера телефона. Очевидно, но об этом забывают. Никакие API-флаги не помогут от физической съёмки.

Драйверы уровня ядра. Подписанные kernel-mode драйверы могут читать framebuffer GPU напрямую. Так делают некоторые античиты и корпоративные мониторинги. На пользовательских машинах редкость, в корпоративной среде с MDM — бывает.

Accessibility API. На Windows есть UI Automation, на macOS — Accessibility. Это не захват пикселей, а структурированный доступ к интерфейсу. Флаги affinity и sharingType на UIA не влияют. Если приложение предоставляет внятных UIA-провайдеров, оно всё равно доступно через этот канал. Как правило, хочется сделать окно «немым» для accessibility тоже.

GPU-отладчики. RenderDoc, NVIDIA NSight, PIX могут перехватить кадр до композиции. На практике это ручной инструмент разработки, автоматически не применяется.

Для обычных сценариев — демонстрация экрана в звонке, запись в OBS, скриншот в системной утилите — флагов достаточно. Для защиты от целенаправленной съёмки — нет.

Где это применялось и что из этого вышло

Мы делаем JobPath — десктопное приложение-ассистент для онлайн-собеседований, в числе прочего с функцией, где окно с подсказками не должно попадать в демонстрацию экрана интервьюеру. Разбирались в том же порядке, в котором написана статья: сначала Windows как основная платформа, потом macOS, потом подумали про Linux и отложили.

Стек в итоге такой. На Windows — SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) с фолбэком на WDA_MONITOR для машин старше 20H1. На macOS до 14 включительно — NSWindow.sharingType = .none. На macOS 15+ пришлось смириться, что против актуального Zoom защиты нет, и добавить поведенческий фолбэк: детектим системный индикатор захвата в меню-баре и автоматически закрываем окно ассистента при его появлении. Некрасиво, но это то, что сейчас возможно в рамках public API.

Что мы не сразу поняли: WDA_EXCLUDEFROMCAPTURE нужно выставлять до того, как окно станет видимым. Если выставить на уже показанном окне, DWM какое-то время продолжит отдавать его содержимое в уже открытые capture sessions, пока они не переконфигурируются на следующий кадр. Проще всего ставить флаг сразу после CreateWindowEx, до ShowWindow. То же на macOS: sharingType = .none до makeKeyAndOrderFront.

Ещё одна мелочь: на Windows при RDP-сессии (если пользователь работает не на физической машине, а через Remote Desktop) флаг ведёт себя непредсказуемо, потому что RDP формирует свой собственный capture-путь через отдельный компонент. Для JobPath это не критично, потому что на собеседованиях через RDP не ходят, но для enterprise-сценариев про это стоит знать.

Итого

Исключение окна из захвата экрана — это не обход системы и не серый приём. Это публичный API, сделанный для DRM и активно используемый менеджерами паролей, банковскими клиентами и видеостримингом. На Windows решается одной строкой и стабильно работает. На macOS до 14 то же самое, на macOS 15 Apple тихо сломала это для ScreenCaptureKit, и никакого публичного решения пока нет. В браузерах пока работает на macOS (Chrome использует legacy CoreGraphics), но это временно.

Если строите приложение с требованием «не светиться в демонстрации экрана» — закладывайте сразу, что на macOS 15+ понадобится поведенческий фолбэк. setContentProtection(true) в Electron/Tauri на этой версии macOS не делает того, что обещает.

Комментарии (0)