Если сделать скриншот 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);
Значений три:
|
Константа |
Значение |
Поведение |
|---|---|---|
|
|
0x00 |
Дефолт. Окно видно во всех захватах |
|
|
0x01 |
На мониторе окно отображается, в захвате — чёрный прямоугольник |
|
|
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 |
Видит окно с |
|---|---|---|
|
Zoom (актуальный) |
ScreenCaptureKit |
Да, видит |
|
Microsoft Teams (новый) |
ScreenCaptureKit |
Да, видит |
|
QuickTime Screen Recording |
ScreenCaptureKit |
Да, видит |
|
System Screenshot (Cmd+Shift+3/4/5) |
ScreenCaptureKit |
Да, видит |
|
Google Chrome ( |
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:
-
Приложение вызывает
org.freedesktop.portal.ScreenCast. -
Портал показывает пользователю системный диалог выбора источника (монитор, окно, приложение).
-
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 не делает того, что обещает.
ссылка на оригинал статьи https://habr.com/ru/articles/1025310/