Если сделать скриншот 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 не делает того, что обещает.