[feat] add headless compatibility capture and usbmmidd virtual display plugging

This commit is contained in:
dijunkun committed 2026-09-21 01:10:30 +08:00
1 parent fb556a763d
commit 6387f2e555
11 files changed
+1510 -59

No files matched your search

@@ -0,0 +1,85 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_DISPLAY_PRESENCE_H_
#define CROSSDESK_DISPLAY_PRESENCE_H_
#include <Windows.h>
#include <algorithm>
#include <cstddef>
#include <vector>
namespace crossdesk {
// Without any attached monitor, Windows keeps the desktop on a phantom display
// target with an OS-chosen resolution. DXGI has no output to duplicate there,
// but GDI still reads the composed desktop and input injection keeps working.
enum class DisplayPresence { present, headless, unknown };
struct DisplayPresenceProbe {
bool query_succeeded = false; // QueryDisplayConfig returned a topology
size_t active_paths = 0; // active display paths, phantom included
size_t available_targets = 0; // active paths whose monitor is present
size_t desktop_devices = 0; // adapters attached to the desktop
size_t attached_monitors = 0; // monitor devices reported active
};
// Pure policy, kept separate from Win32 so it can be tested with fixtures.
inline DisplayPresence ClassifyDisplayPresence(
const DisplayPresenceProbe& probe) {
if (probe.query_succeeded) {
if (probe.available_targets > 0) return DisplayPresence::present;
if (probe.active_paths > 0 || probe.desktop_devices > 0)
return DisplayPresence::headless;
return DisplayPresence::unknown;
}
if (probe.attached_monitors > 0) return DisplayPresence::present;
if (probe.desktop_devices > 0) return DisplayPresence::headless;
return DisplayPresence::unknown;
}
inline DisplayPresenceProbe ProbeDisplayPresence() {
DisplayPresenceProbe probe;
// The buffer size can change between the two calls during a topology
// change; a few retries cover that window.
for (int attempt = 0; attempt < 4 && !probe.query_succeeded; ++attempt) {
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_count,
&mode_count) != ERROR_SUCCESS) break;
std::vector<DISPLAYCONFIG_PATH_INFO> paths((std::max)(path_count, 1u));
std::vector<DISPLAYCONFIG_MODE_INFO> modes((std::max)(mode_count, 1u));
const LONG status = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_count,
paths.data(), &mode_count,
modes.data(), nullptr);
if (status == ERROR_INSUFFICIENT_BUFFER) continue;
if (status != ERROR_SUCCESS) break;
probe.query_succeeded = true;
for (UINT32 index = 0; index < path_count; ++index) {
if (!(paths[index].flags & DISPLAYCONFIG_PATH_ACTIVE)) continue;
++probe.active_paths;
// A removed monitor can leave its path active while the target is gone.
if (paths[index].targetInfo.targetAvailable) ++probe.available_targets;
}
}
for (DWORD index = 0;; ++index) {
DISPLAY_DEVICEW adapter{sizeof(adapter)};
if (!EnumDisplayDevicesW(nullptr, index, &adapter, 0)) break;
if (!(adapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)) continue;
++probe.desktop_devices;
for (DWORD child = 0;; ++child) {
DISPLAY_DEVICEW monitor{sizeof(monitor)};
if (!EnumDisplayDevicesW(adapter.DeviceName, child, &monitor, 0)) break;
if (monitor.StateFlags & DISPLAY_DEVICE_ACTIVE) ++probe.attached_monitors;
}
}
return probe;
}
} // namespace crossdesk
#endif // CROSSDESK_DISPLAY_PRESENCE_H_
@@ -122,15 +122,25 @@ int ScreenCapturerGdi::ResetToInitialMonitor() {
void ScreenCapturerGdi::CaptureLoop() {
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
HDC screen_dc = GetDC(nullptr);
bool dc_error_logged = false;
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!screen_dc) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// The screen DC can be unavailable during a desktop switch; retry so
// capture resumes once the desktop is reachable again.
if (!dc_error_logged) {
LOG_WARN("GDI: GetDC(nullptr) failed, error={}; retrying",
GetLastError());
dc_error_logged = true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
screen_dc = GetDC(nullptr);
continue;
}
dc_error_logged = false;
int idx = monitor_index_.load();
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
@@ -157,11 +167,22 @@ void ScreenCapturerGdi::CaptureLoop() {
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
// GDI is the only backend on a headless desktop, where odd DIB sizes and
// exhausted GDI handles are plausible; never dereference a failed alloc.
void* bits = nullptr;
HDC mem_dc = CreateCompatibleDC(screen_dc);
HBITMAP dib =
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
HGDIOBJ old = SelectObject(mem_dc, dib);
HBITMAP dib = mem_dc ? CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS,
&bits, nullptr, 0)
: nullptr;
HGDIOBJ old = (dib && bits) ? SelectObject(mem_dc, dib) : nullptr;
if (!old) {
LOG_WARN("GDI: frame buffer allocation failed ({}x{}), error={}", width,
height, GetLastError());
if (dib) DeleteObject(dib);
if (mem_dc) DeleteDC(mem_dc);
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
SRCCOPY | CAPTUREBLT);
@@ -202,7 +223,7 @@ void ScreenCapturerGdi::CaptureLoop() {
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
}
ReleaseDC(nullptr, screen_dc);
if (screen_dc) ReleaseDC(nullptr, screen_dc);
}
} // namespace crossdesk
@@ -18,6 +18,7 @@
#include <display_stream_id.h>
#include "captured_nv12_frame.h"
#include "captured_cursor_state.h"
#include "display_presence.h"
#include "dxgi_cursor_state.h"
#include "interactive_desktop.h"
#include "interactive_state.h"
@@ -30,6 +31,7 @@
#include "secure_desktop_status_poller.h"
#include "service_host.h"
#include "session_helper_shared.h"
#include "../virtual_display/usbmmidd_virtual_display.h"
#include "wgc_plugin_api.h"
namespace crossdesk {
@@ -180,6 +182,63 @@ const char* CaptureBackendName(const ScreenCapturer* capturer) {
return "unavailable";
}
// Debug aid: CROSSDESK_FORCE_HEADLESS=1 makes a host that has monitors behave
// as if it had none until a usbmmidd monitor is plugged, so the virtual
// display path can be exercised without unplugging cables. Once the virtual
// monitor is attached the real probe takes over again.
bool ForcedHeadlessForTesting() {
static const bool forced = [] {
wchar_t value[8] = {0};
const DWORD length =
GetEnvironmentVariableW(L"CROSSDESK_FORCE_HEADLESS", value, 8);
return length > 0 && length < 8 && value[0] == L'1';
}();
return forced && CountUsbmmiddMonitors() == 0;
}
// Headless compatibility mode: no monitor is attached, so the desktop lives on
// a phantom target at an OS-chosen resolution. DXGI cannot duplicate it and
// WGC has no real monitor item; GDI reads the composed desktop. This is a
// best-effort baseline that needs no driver. Only an explicit Auto selection
// takes this shortcut; a pinned method keeps its own failure semantics.
bool DetectHeadlessDesktop(ScreenCaptureMethod method) {
if (method != ScreenCaptureMethod::Auto) return false;
if (ForcedHeadlessForTesting()) {
static std::atomic<bool> logged{false};
if (!logged.exchange(true)) {
LOG_WARN("Windows capturer: CROSSDESK_FORCE_HEADLESS=1 set; treating the "
"desktop as headless until a usbmmidd monitor is plugged");
}
return true;
}
const auto probe = ProbeDisplayPresence();
const auto presence = ClassifyDisplayPresence(probe);
// Sessions probe repeatedly; report transitions rather than every probe.
static std::atomic<int> last_logged{-1};
if (last_logged.exchange(static_cast<int>(presence)) !=
static_cast<int>(presence)) {
if (presence == DisplayPresence::headless) {
LOG_WARN(
"Windows capturer: no attached display (active_paths={}, "
"available_targets={}, desktop_devices={}, monitors={}); entering "
"headless compatibility mode. Resolution is fixed by Windows and "
"capture uses GDI. Attach a monitor or an HDMI dummy plug to "
"restore GPU capture.",
probe.active_paths, probe.available_targets, probe.desktop_devices,
probe.attached_monitors);
} else if (presence == DisplayPresence::unknown) {
LOG_WARN("Windows capturer: display presence unknown (query_ok={}, "
"desktop_devices={}); using the normal backend order",
probe.query_succeeded, probe.desktop_devices);
} else {
LOG_INFO("Windows capturer: attached display detected "
"(available_targets={}, monitors={})",
probe.available_targets, probe.attached_monitors);
}
}
return presence == DisplayPresence::headless;
}
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
bool show_cursor,
const std::string& stage,
@@ -502,25 +561,22 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
};
impl_.reset();
const bool headless = DetectHeadlessDesktop(capture_method_);
headless_compat_.store(headless, std::memory_order_relaxed);
int ret = -1;
auto try_init = [&](std::unique_ptr<ScreenCapturer> candidate) {
for (const auto& create : PreferredBackends(headless)) {
std::unique_ptr<ScreenCapturer> candidate = create();
ret = candidate ? candidate->Init(fps_, cb_) : -1;
if (ret != 0) {
LOG_WARN("Windows capturer: {} init failed (ret={})",
CaptureBackendName(candidate.get()), ret);
return false;
if (ret == 0) {
impl_ = std::move(candidate);
break;
}
impl_ = std::move(candidate);
return true;
};
// Short-circuiting also keeps the unused WGC plugin unloaded.
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
try_init(std::make_unique<ScreenCapturerDxgi>())) ||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
try_init(WgcPluginCapturer::Create())) ||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
try_init(std::make_unique<ScreenCapturerGdi>()))) {
LOG_INFO("Windows capturer: using {}", CaptureBackendName(impl_.get()));
LOG_WARN("Windows capturer: {} init failed (ret={})",
CaptureBackendName(candidate.get()), ret);
}
if (impl_) {
LOG_INFO("Windows capturer: using {}{}", CaptureBackendName(impl_.get()),
headless ? " (headless compatibility mode)" : "");
BuildCanonicalFromImpl();
monitor_index_.store(0, std::memory_order_relaxed);
return 0;
@@ -533,6 +589,9 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
int ScreenCapturerWin::Destroy() {
Stop();
// Stop() releases the virtual display on a worker; finish it here so the
// provisioner is idle before its owner goes away.
JoinVirtualDisplayThread();
paused_.store(false, std::memory_order_relaxed);
if (impl_) {
impl_->Destroy();
@@ -651,6 +710,51 @@ bool ScreenCapturerWin::TryStartBackend(
return true;
}
std::vector<ScreenCapturerWin::BackendFactory>
ScreenCapturerWin::PreferredBackends(bool headless) const {
// A headless desktop goes straight to GDI: DXGI has no output to duplicate
// and WGC has no monitor item.
std::vector<BackendFactory> factories;
if (!headless && IsBackendEnabled(ScreenCaptureMethod::Dxgi)) {
factories.push_back([] { return std::make_unique<ScreenCapturerDxgi>(); });
}
if (!headless && IsBackendEnabled(ScreenCaptureMethod::Wgc)) {
factories.push_back([] { return WgcPluginCapturer::Create(); });
}
if (IsBackendEnabled(ScreenCaptureMethod::Gdi)) {
factories.push_back([] { return std::make_unique<ScreenCapturerGdi>(); });
}
return factories;
}
bool ScreenCapturerWin::StartPreferredBackend(bool headless,
int monitor_index) {
for (const auto& create : PreferredBackends(headless)) {
if (TryStartBackend(create(), monitor_index)) return true;
}
return false;
}
void ScreenCapturerWin::ResetPostSecureDesktopState() {
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
SharedSecureDesktopCursorState().SetActive(false);
}
int ScreenCapturerWin::MarkStarted() {
running_.store(true, std::memory_order_relaxed);
NotifyPrivacyCapture(true);
ResetPostSecureDesktopState();
if (!secure_capture_thread_.joinable()) {
secure_capture_thread_ =
std::thread([this]() { SecureDesktopCaptureLoop(); });
}
return 0;
}
int ScreenCapturerWin::Start(bool show_cursor) {
if (!impl_) return -1;
if (running_.load(std::memory_order_relaxed)) {
@@ -670,8 +774,40 @@ int ScreenCapturerWin::Start(bool show_cursor) {
// open; logical stream IDs must remain stable.
const int requested_monitor =
monitor_index_.load(std::memory_order_relaxed);
// A monitor may have been attached or removed since the last session. Pick
// the backend for the desktop as it is now, not as it was at Init.
const bool headless = DetectHeadlessDesktop(capture_method_);
// The virtual display is provisioned off this thread (Start() runs on the
// UI thread and a driver install can take minutes). Begin in compatibility
// mode; MaybeAdoptVirtualDisplay() switches to a GPU backend once ready.
JoinVirtualDisplayThread();
virtual_display_ready_.store(false, std::memory_order_relaxed);
const bool was_headless =
headless_compat_.exchange(headless, std::memory_order_relaxed);
const bool backend_is_gdi = dynamic_cast<ScreenCapturerGdi*>(impl_.get()) != nullptr;
int ret = 0;
if (headless != was_headless || (headless && !backend_is_gdi)) {
LOG_INFO("Windows capturer: desktop presence changed (headless={}), "
"reselecting the capture backend", headless);
impl_->Destroy();
// Keep an instance around on failure so the next Start can retry.
std::unique_ptr<ScreenCapturer> previous;
{
std::lock_guard<std::mutex> lock(impl_mutex_);
previous = std::move(impl_);
}
if (!StartPreferredBackend(headless, requested_monitor)) {
std::lock_guard<std::mutex> lock(impl_mutex_);
impl_ = std::move(previous);
LOG_ERROR("Windows capturer: no capture backend could start");
return -1;
}
ret = MarkStarted();
if (ret == 0 && headless) BeginVirtualDisplayProvisioning();
return ret;
}
impl_->Destroy();
int ret = impl_->Init(fps_, cb_);
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
RebuildAliasesFromImpl();
// Canonical wire/input slots may differ from this backend's refreshed
@@ -706,42 +842,100 @@ int ScreenCapturerWin::Start(bool show_cursor) {
}
}
running_.store(true, std::memory_order_relaxed);
NotifyPrivacyCapture(true);
SharedSecureDesktopCursorState().SetActive(false);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
if (!secure_capture_thread_.joinable()) {
secure_capture_thread_ =
std::thread([this]() { SecureDesktopCaptureLoop(); });
}
return 0;
ret = MarkStarted();
if (ret == 0 && headless) BeginVirtualDisplayProvisioning();
return ret;
}
int ScreenCapturerWin::Stop() {
NotifyPrivacyCapture(false);
running_.store(false, std::memory_order_relaxed);
last_capture_progress_tick_.store(0, std::memory_order_relaxed);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
int ret = 0;
// Drain management operations before stopping/replacing their backend.
virtual_display_.RequestCancel();
StopSecureCaptureThread();
JoinVirtualDisplayThread();
ResetPostSecureDesktopState();
if (impl_) {
ret = impl_->Stop();
}
SharedCapturedCursorState().Reset();
SharedSecureDesktopCursorState().SetActive(false);
StopSecureDesktopSharedCapture(secure_shared_session_id_);
// After the backend released its duplication of the virtual output. The
// unplug itself can take seconds; do not hold the UI thread for it.
BeginVirtualDisplayRelease();
return ret;
}
void ScreenCapturerWin::JoinVirtualDisplayThread() {
if (virtual_display_thread_.joinable()) virtual_display_thread_.join();
}
void ScreenCapturerWin::BeginVirtualDisplayProvisioning() {
JoinVirtualDisplayThread();
virtual_display_thread_ = std::thread([this] {
// A usbmmidd virtual monitor turns the phantom desktop into a real
// display target, so the GPU backends and a chosen resolution apply.
std::string error;
if (virtual_display_.Acquire(kDefaultVirtualDisplayMode, &error)) {
virtual_display_ready_.store(true, std::memory_order_release);
} else if (error != "cancelled") {
LOG_WARN("Windows capturer: no usbmmidd virtual display ({}); staying "
"in headless compatibility mode",
error);
}
});
}
void ScreenCapturerWin::BeginVirtualDisplayRelease() {
JoinVirtualDisplayThread();
virtual_display_ready_.store(false, std::memory_order_relaxed);
if (!virtual_display_.NeedsRelease()) return;
virtual_display_thread_ = std::thread([this] { virtual_display_.Release(); });
}
void ScreenCapturerWin::MaybeAdoptVirtualDisplay() {
if (!virtual_display_ready_.exchange(false, std::memory_order_acq_rel))
return;
if (!running_.load(std::memory_order_relaxed)) return;
if (DetectHeadlessDesktop(capture_method_)) {
LOG_WARN("Windows capturer: virtual display plugged but the desktop "
"still reports no display; staying in compatibility mode");
return;
}
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
LOG_INFO("Windows capturer: virtual display ready, switching from "
"compatibility mode to a GPU backend");
NotifyPrivacyCapture(false);
// Public entry points dereference impl_ under impl_mutex_; take the old
// backend out under the lock, then tear it down without holding it.
std::unique_ptr<ScreenCapturer> previous;
{
std::lock_guard<std::mutex> lock(impl_mutex_);
previous = std::move(impl_);
}
if (previous) previous->Destroy();
if (StartPreferredBackend(false, current_monitor)) {
headless_compat_.store(false, std::memory_order_relaxed);
// The owner refreshes its display list and input mapping every tick from
// GetDisplayInfoList(), so the new geometry is picked up without a hint.
} else {
LOG_ERROR("Windows capturer: no GPU backend started on the virtual "
"display; restoring compatibility capture");
{
std::lock_guard<std::mutex> lock(impl_mutex_);
impl_ = std::move(previous);
}
// RestoreMonitor takes impl_mutex_ itself (via SwitchTo).
if (impl_ && impl_->Init(fps_, cb_) == 0) {
RestoreMonitor(current_monitor);
impl_->Start(show_cursor_.load(std::memory_order_relaxed));
}
}
NotifyPrivacyCapture(true);
}
int ScreenCapturerWin::Pause(int monitor_index) {
paused_.store(true, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(impl_mutex_);
@@ -949,21 +1143,15 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
return true;
}
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
TryStartBackend(std::make_unique<ScreenCapturerDxgi>(), current_monitor)) ||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
TryStartBackend(WgcPluginCapturer::Create(), current_monitor)) ||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
TryStartBackend(std::make_unique<ScreenCapturerGdi>(), current_monitor))) {
if (StartPreferredBackend(headless_compat_.load(std::memory_order_relaxed),
current_monitor)) {
return true;
}
if (impl_) {
LOG_WARN(
"Windows capturer: all backend restart attempts after secure desktop "
"failed (last_ret={})",
ret);
}
LOG_WARN(
"Windows capturer: all backend restart attempts after secure desktop "
"failed (last_ret={})",
ret);
return false;
}
@@ -1405,6 +1593,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
MaybeAdoptVirtualDisplay();
CheckCaptureProgress(now);
if (post_secure_restart_pending) {
if (now >= post_secure_restart_deadline_tick) {
@@ -12,6 +12,7 @@
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
@@ -22,6 +23,8 @@
#include "screen_capturer.h"
#include "screen_capture_method.h"
#include "privacy_controller.h"
// Relative: platform include directories stay private to the capture target.
#include "../virtual_display/virtual_display_provisioner.h"
namespace crossdesk {
@@ -50,6 +53,8 @@ class ScreenCapturerWin : public ScreenCapturer {
std::vector<DisplayInfo> GetDisplayInfoList() override;
int GetCurrentMonitorIndex() const override;
// True while this session captures a usbmmidd monitor it plugged itself.
bool IsVirtualDisplayActive() const { return virtual_display_.active(); }
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
// Set before Init(), while capture is stopped.
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
@@ -58,6 +63,7 @@ class ScreenCapturerWin : public ScreenCapturer {
static bool CurrentFrameIsFromSecureDesktop();
private:
using BackendFactory = std::function<std::unique_ptr<ScreenCapturer>()>;
std::unique_ptr<ScreenCapturer> impl_;
// Serializes backend replacement (secure-desktop recovery runs on the
// management thread) against the public entry points that dereference it.
@@ -87,6 +93,16 @@ class ScreenCapturerWin : public ScreenCapturer {
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};
bool applied_show_cursor_ = true;
std::atomic<bool> headless_compat_{false};
// Session-scoped: plugged on a headless desktop after Start() returns and
// released after Stop(), so an attached physical monitor is never shadowed
// between sessions. Both run on virtual_display_thread_ because the helper
// may need up to two minutes to install the driver; Start() itself begins
// in GDI compatibility mode and the management thread adopts the virtual
// display (switching to a GPU backend) once virtual_display_ready_ is set.
VirtualDisplayProvisioner virtual_display_;
std::thread virtual_display_thread_;
std::atomic<bool> virtual_display_ready_{false};
std::atomic<int> monitor_index_{0};
std::atomic<bool> secure_desktop_capture_active_{false};
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
@@ -112,8 +128,16 @@ class ScreenCapturerWin : public ScreenCapturer {
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl(bool preserve_backend_slots = false);
void RestoreMonitor(int monitor_index);
// Backends in preference order for the current desktop; each factory is
// invoked lazily so an unused WGC plugin is never loaded.
std::vector<BackendFactory> PreferredBackends(bool headless) const;
bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate,
int monitor_index);
bool StartPreferredBackend(bool headless, int monitor_index);
void ResetPostSecureDesktopState();
// Common tail of every successful Start(): publish running state and
// launch the management thread.
int MarkStarted();
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
const char* stream_id,
const MiniRtcNativeVideoFrame* native_frame = nullptr,
@@ -121,6 +145,13 @@ class ScreenCapturerWin : public ScreenCapturer {
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void CheckCaptureProgress(ULONGLONG now);
// Virtual display provisioning runs off the caller's thread.
void JoinVirtualDisplayThread();
void BeginVirtualDisplayProvisioning();
void BeginVirtualDisplayRelease();
// Management thread: once the worker plugged the virtual display, replace
// the GDI compatibility backend with a GPU backend on that display.
void MaybeAdoptVirtualDisplay();
void ApplyCursorCaptureSetting();
void SecureDesktopCaptureLoop();
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,