mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-12 03:28:20 +08:00
[feat] add headless compatibility capture and usbmmidd virtual display plugging
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user