From 7823507a1879f6b4f963c985d3401f445475d598 Mon Sep 17 00:00:00 2001 From: huanld Date: Thu, 28 May 2026 13:22:24 +0700 Subject: [PATCH] Fix Windows native capture state and monitor adapter --- electron/ipc/handlers.ts | 107 ++++++++++++++---- .../native/wgc-capture/src/wgc_session.cpp | 85 +++++++++++++- electron/native/wgc-capture/src/wgc_session.h | 4 +- package-lock.json | 4 +- package.json | 2 +- 5 files changed, 169 insertions(+), 33 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 4fad32a..1eb88f8 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -426,6 +426,7 @@ let nativeWindowsCursorRecordingStartMs = 0; let nativeWindowsPauseStartedAtMs: number | null = null; let nativeWindowsPauseRanges: Array<{ startMs: number; endMs: number }> = []; let nativeWindowsIsPaused = false; +let nativeWindowsCaptureStopping = false; const NATIVE_WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 15_000; let nativeMacCaptureProcess: ChildProcessWithoutNullStreams | null = null; let nativeMacCaptureOutput = ""; @@ -1337,6 +1338,81 @@ function completeNativeWindowsCursorPauseRange(endMs = Date.now()) { nativeWindowsPauseStartedAtMs = null; } +function resetNativeWindowsCaptureState() { + nativeWindowsCaptureProcess = null; + nativeWindowsCaptureTargetPath = null; + nativeWindowsCaptureWebcamTargetPath = null; + nativeWindowsCaptureRecordingId = null; + nativeWindowsCursorOffsetMs = 0; + nativeWindowsCursorCaptureMode = "editable-overlay"; + nativeWindowsCursorRecordingStartMs = 0; + nativeWindowsPauseStartedAtMs = null; + nativeWindowsPauseRanges = []; + nativeWindowsIsPaused = false; + nativeWindowsCaptureStopping = false; + clearGuideHotkeyRecording(); +} + +function hasActiveNativeWindowsCaptureProcess() { + const proc = nativeWindowsCaptureProcess; + if (!proc) { + return false; + } + if (proc.exitCode === null && !proc.killed) { + return true; + } + + console.warn("[native-wgc] clearing stale Windows capture process state", { + exitCode: proc.exitCode, + killed: proc.killed, + }); + resetNativeWindowsCaptureState(); + return false; +} + +function attachNativeWindowsCaptureLifecycle( + proc: ChildProcessWithoutNullStreams, + sourceName: string, + onRecordingStateChange?: (recording: boolean, sourceName: string) => void, +) { + const cleanupAfterUnexpectedExit = async () => { + try { + await stopCursorRecording(); + } catch (error) { + console.warn("[native-wgc] failed to stop cursor recording after helper exit", error); + } + pendingCursorRecordingData = null; + resetNativeWindowsCaptureState(); + onRecordingStateChange?.(false, sourceName); + }; + + function onClose(code: number | null, signal: NodeJS.Signals | null) { + proc.off("error", onError); + if (nativeWindowsCaptureProcess !== proc || nativeWindowsCaptureStopping) { + return; + } + + console.warn("[native-wgc] Windows capture helper exited before stop was requested", { + code, + signal, + output: nativeWindowsCaptureOutput.trim(), + }); + void cleanupAfterUnexpectedExit(); + } + function onError(error: Error) { + proc.off("close", onClose); + if (nativeWindowsCaptureProcess !== proc || nativeWindowsCaptureStopping) { + return; + } + + console.warn("[native-wgc] Windows capture helper errored before stop was requested", error); + void cleanupAfterUnexpectedExit(); + } + + proc.once("close", onClose); + proc.once("error", onError); +} + function waitForNativeWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { @@ -2001,7 +2077,7 @@ export function registerIpcHandlers( error: "Windows Graphics Capture requires Windows 10 build 19041 or newer.", }; } - if (nativeWindowsCaptureProcess) { + if (hasActiveNativeWindowsCaptureProcess()) { return { success: false, error: "Native Windows capture is already running." }; } @@ -2150,6 +2226,7 @@ export function registerIpcHandlers( }); const source = selectedSource || { name: "Screen" }; + attachNativeWindowsCaptureLifecycle(proc, source.name, onRecordingStateChange); startGuideHotkeyRecording(recordingId, bounds); if (onRecordingStateChange) { onRecordingStateChange(true, source.name); @@ -2164,17 +2241,7 @@ export function registerIpcHandlers( } catch (error) { console.error("Failed to start native Windows recording:", error); nativeWindowsCaptureProcess?.kill(); - nativeWindowsCaptureProcess = null; - nativeWindowsCaptureTargetPath = null; - nativeWindowsCaptureWebcamTargetPath = null; - nativeWindowsCaptureRecordingId = null; - nativeWindowsCursorOffsetMs = 0; - nativeWindowsCursorCaptureMode = "editable-overlay"; - nativeWindowsCursorRecordingStartMs = 0; - nativeWindowsPauseStartedAtMs = null; - nativeWindowsPauseRanges = []; - nativeWindowsIsPaused = false; - clearGuideHotkeyRecording(); + resetNativeWindowsCaptureState(); await stopCursorRecording(); return { success: false, error: String(error) }; } @@ -2433,11 +2500,13 @@ export function registerIpcHandlers( const recordingId = nativeWindowsCaptureRecordingId ?? Date.now(); const cursorCaptureMode = nativeWindowsCursorCaptureMode; - if (!proc) { + if (!proc || proc.exitCode !== null || proc.killed) { + resetNativeWindowsCaptureState(); return { success: false, error: "Native Windows capture is not running." }; } try { + nativeWindowsCaptureStopping = true; completeNativeWindowsCursorPauseRange(); const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc); proc.stdin.write("stop\n"); @@ -2499,17 +2568,7 @@ export function registerIpcHandlers( await stopCursorRecording(); return { success: false, error: String(error) }; } finally { - nativeWindowsCaptureProcess = null; - nativeWindowsCaptureTargetPath = null; - nativeWindowsCaptureWebcamTargetPath = null; - nativeWindowsCaptureRecordingId = null; - nativeWindowsCursorOffsetMs = 0; - nativeWindowsCursorCaptureMode = "editable-overlay"; - nativeWindowsCursorRecordingStartMs = 0; - nativeWindowsPauseStartedAtMs = null; - nativeWindowsPauseRanges = []; - nativeWindowsIsPaused = false; - clearGuideHotkeyRecording(); + resetNativeWindowsCaptureState(); const source = selectedSource || { name: "Screen" }; if (onRecordingStateChange) { onRecordingStateChange(false, source.name); diff --git a/electron/native/wgc-capture/src/wgc_session.cpp b/electron/native/wgc-capture/src/wgc_session.cpp index e20096c..8309797 100644 --- a/electron/native/wgc-capture/src/wgc_session.cpp +++ b/electron/native/wgc-capture/src/wgc_session.cpp @@ -28,6 +28,60 @@ bool succeeded(HRESULT hr, const char* label) { return false; } +Microsoft::WRL::ComPtr findAdapterForMonitor(HMONITOR monitor) { + if (!monitor) { + return nullptr; + } + + Microsoft::WRL::ComPtr factory; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); + if (FAILED(hr) || !factory) { + std::cerr << "WARNING: CreateDXGIFactory1 failed while resolving monitor adapter (hr=0x" + << std::hex << hr << std::dec << ")" << std::endl; + return nullptr; + } + + for (UINT adapterIndex = 0;; ++adapterIndex) { + Microsoft::WRL::ComPtr adapter; + hr = factory->EnumAdapters1(adapterIndex, adapter.GetAddressOf()); + if (hr == DXGI_ERROR_NOT_FOUND) { + break; + } + if (FAILED(hr) || !adapter) { + continue; + } + + DXGI_ADAPTER_DESC1 adapterDesc{}; + if (SUCCEEDED(adapter->GetDesc1(&adapterDesc)) && + (adapterDesc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0) { + continue; + } + + for (UINT outputIndex = 0;; ++outputIndex) { + Microsoft::WRL::ComPtr output; + hr = adapter->EnumOutputs(outputIndex, output.GetAddressOf()); + if (hr == DXGI_ERROR_NOT_FOUND) { + break; + } + if (FAILED(hr) || !output) { + continue; + } + + DXGI_OUTPUT_DESC outputDesc{}; + if (SUCCEEDED(output->GetDesc(&outputDesc)) && outputDesc.Monitor == monitor) { + std::cout << "{\"event\":\"display-adapter-resolved\",\"schemaVersion\":2," + << "\"adapterIndex\":" << adapterIndex + << ",\"outputIndex\":" << outputIndex << "}" << std::endl; + return adapter; + } + } + } + + std::cerr << "WARNING: Could not resolve DXGI adapter for selected monitor; using default adapter" + << std::endl; + return nullptr; +} + int64_t timeSpanToHns(wf::TimeSpan const& value) { return value.count(); } @@ -38,7 +92,7 @@ WgcSession::~WgcSession() { stop(); } -bool WgcSession::createD3DDevice() { +bool WgcSession::createD3DDevice(IDXGIAdapter* adapter) { UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; #if defined(_DEBUG) flags |= D3D11_CREATE_DEVICE_DEBUG; @@ -53,8 +107,8 @@ bool WgcSession::createD3DDevice() { D3D_FEATURE_LEVEL featureLevel{}; HRESULT hr = D3D11CreateDevice( - nullptr, - D3D_DRIVER_TYPE_HARDWARE, + adapter, + adapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, featureLevels, @@ -67,6 +121,23 @@ bool WgcSession::createD3DDevice() { #if defined(_DEBUG) if (FAILED(hr)) { flags &= ~D3D11_CREATE_DEVICE_DEBUG; + hr = D3D11CreateDevice( + adapter, + adapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE, + nullptr, + flags, + featureLevels, + ARRAYSIZE(featureLevels), + D3D11_SDK_VERSION, + &d3dDevice_, + &featureLevel, + &d3dContext_); + } +#endif + + if (FAILED(hr) && adapter) { + std::cerr << "WARNING: D3D11CreateDevice failed for selected monitor adapter (hr=0x" + << std::hex << hr << std::dec << "); retrying default adapter" << std::endl; hr = D3D11CreateDevice( nullptr, D3D_DRIVER_TYPE_HARDWARE, @@ -79,7 +150,6 @@ bool WgcSession::createD3DDevice() { &featureLevel, &d3dContext_); } -#endif if (!succeeded(hr, "D3D11CreateDevice")) { return false; @@ -100,6 +170,11 @@ bool WgcSession::createD3DDevice() { return true; } +bool WgcSession::createD3DDeviceForMonitor(HMONITOR monitor) { + auto adapter = findAdapterForMonitor(monitor); + return createD3DDevice(adapter.Get()); +} + bool WgcSession::createCaptureItem(HMONITOR monitor) { auto factory = winrt::get_activation_factory(); auto interop = factory.as(); @@ -188,7 +263,7 @@ bool WgcSession::applySessionOptions(bool captureCursor) { bool WgcSession::initialize(HMONITOR monitor, int fps, bool captureCursor) { fps_ = fps > 0 ? fps : 60; - if (!createD3DDevice()) { + if (!createD3DDeviceForMonitor(monitor)) { return false; } if (!createCaptureItem(monitor)) { diff --git a/electron/native/wgc-capture/src/wgc_session.h b/electron/native/wgc-capture/src/wgc_session.h index 43de21a..f186bfa 100644 --- a/electron/native/wgc-capture/src/wgc_session.h +++ b/electron/native/wgc-capture/src/wgc_session.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -34,7 +35,8 @@ public: ID3D11DeviceContext* context() const; private: - bool createD3DDevice(); + bool createD3DDevice(IDXGIAdapter* adapter = nullptr); + bool createD3DDeviceForMonitor(HMONITOR monitor); bool createCaptureItem(HMONITOR monitor); bool createCaptureItem(HWND window); bool applySessionOptions(bool captureCursor); diff --git a/package-lock.json b/package-lock.json index 6879eee..421d538 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.4.4", + "version": "1.4.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.4.4", + "version": "1.4.6", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", diff --git a/package.json b/package.json index c22bd2c..b656c32 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openscreen", "private": true, - "version": "1.4.4", + "version": "1.4.6", "type": "module", "packageManager": "npm@10.9.4", "engines": {