Fix Windows native capture state and monitor adapter

This commit is contained in:
huanld
2026-05-28 13:22:24 +07:00
parent 0b78ff6f7d
commit 7823507a18
5 changed files with 169 additions and 33 deletions
+83 -24
View File
@@ -426,6 +426,7 @@ let nativeWindowsCursorRecordingStartMs = 0;
let nativeWindowsPauseStartedAtMs: number | null = null; let nativeWindowsPauseStartedAtMs: number | null = null;
let nativeWindowsPauseRanges: Array<{ startMs: number; endMs: number }> = []; let nativeWindowsPauseRanges: Array<{ startMs: number; endMs: number }> = [];
let nativeWindowsIsPaused = false; let nativeWindowsIsPaused = false;
let nativeWindowsCaptureStopping = false;
const NATIVE_WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 15_000; const NATIVE_WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 15_000;
let nativeMacCaptureProcess: ChildProcessWithoutNullStreams | null = null; let nativeMacCaptureProcess: ChildProcessWithoutNullStreams | null = null;
let nativeMacCaptureOutput = ""; let nativeMacCaptureOutput = "";
@@ -1337,6 +1338,81 @@ function completeNativeWindowsCursorPauseRange(endMs = Date.now()) {
nativeWindowsPauseStartedAtMs = null; 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) { function waitForNativeWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -2001,7 +2077,7 @@ export function registerIpcHandlers(
error: "Windows Graphics Capture requires Windows 10 build 19041 or newer.", 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." }; return { success: false, error: "Native Windows capture is already running." };
} }
@@ -2150,6 +2226,7 @@ export function registerIpcHandlers(
}); });
const source = selectedSource || { name: "Screen" }; const source = selectedSource || { name: "Screen" };
attachNativeWindowsCaptureLifecycle(proc, source.name, onRecordingStateChange);
startGuideHotkeyRecording(recordingId, bounds); startGuideHotkeyRecording(recordingId, bounds);
if (onRecordingStateChange) { if (onRecordingStateChange) {
onRecordingStateChange(true, source.name); onRecordingStateChange(true, source.name);
@@ -2164,17 +2241,7 @@ export function registerIpcHandlers(
} catch (error) { } catch (error) {
console.error("Failed to start native Windows recording:", error); console.error("Failed to start native Windows recording:", error);
nativeWindowsCaptureProcess?.kill(); nativeWindowsCaptureProcess?.kill();
nativeWindowsCaptureProcess = null; resetNativeWindowsCaptureState();
nativeWindowsCaptureTargetPath = null;
nativeWindowsCaptureWebcamTargetPath = null;
nativeWindowsCaptureRecordingId = null;
nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = "editable-overlay";
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
clearGuideHotkeyRecording();
await stopCursorRecording(); await stopCursorRecording();
return { success: false, error: String(error) }; return { success: false, error: String(error) };
} }
@@ -2433,11 +2500,13 @@ export function registerIpcHandlers(
const recordingId = nativeWindowsCaptureRecordingId ?? Date.now(); const recordingId = nativeWindowsCaptureRecordingId ?? Date.now();
const cursorCaptureMode = nativeWindowsCursorCaptureMode; const cursorCaptureMode = nativeWindowsCursorCaptureMode;
if (!proc) { if (!proc || proc.exitCode !== null || proc.killed) {
resetNativeWindowsCaptureState();
return { success: false, error: "Native Windows capture is not running." }; return { success: false, error: "Native Windows capture is not running." };
} }
try { try {
nativeWindowsCaptureStopping = true;
completeNativeWindowsCursorPauseRange(); completeNativeWindowsCursorPauseRange();
const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc); const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc);
proc.stdin.write("stop\n"); proc.stdin.write("stop\n");
@@ -2499,17 +2568,7 @@ export function registerIpcHandlers(
await stopCursorRecording(); await stopCursorRecording();
return { success: false, error: String(error) }; return { success: false, error: String(error) };
} finally { } finally {
nativeWindowsCaptureProcess = null; resetNativeWindowsCaptureState();
nativeWindowsCaptureTargetPath = null;
nativeWindowsCaptureWebcamTargetPath = null;
nativeWindowsCaptureRecordingId = null;
nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = "editable-overlay";
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
clearGuideHotkeyRecording();
const source = selectedSource || { name: "Screen" }; const source = selectedSource || { name: "Screen" };
if (onRecordingStateChange) { if (onRecordingStateChange) {
onRecordingStateChange(false, source.name); onRecordingStateChange(false, source.name);
@@ -28,6 +28,60 @@ bool succeeded(HRESULT hr, const char* label) {
return false; return false;
} }
Microsoft::WRL::ComPtr<IDXGIAdapter1> findAdapterForMonitor(HMONITOR monitor) {
if (!monitor) {
return nullptr;
}
Microsoft::WRL::ComPtr<IDXGIFactory1> 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<IDXGIAdapter1> 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<IDXGIOutput> 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) { int64_t timeSpanToHns(wf::TimeSpan const& value) {
return value.count(); return value.count();
} }
@@ -38,7 +92,7 @@ WgcSession::~WgcSession() {
stop(); stop();
} }
bool WgcSession::createD3DDevice() { bool WgcSession::createD3DDevice(IDXGIAdapter* adapter) {
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(_DEBUG) #if defined(_DEBUG)
flags |= D3D11_CREATE_DEVICE_DEBUG; flags |= D3D11_CREATE_DEVICE_DEBUG;
@@ -53,8 +107,8 @@ bool WgcSession::createD3DDevice() {
D3D_FEATURE_LEVEL featureLevel{}; D3D_FEATURE_LEVEL featureLevel{};
HRESULT hr = D3D11CreateDevice( HRESULT hr = D3D11CreateDevice(
nullptr, adapter,
D3D_DRIVER_TYPE_HARDWARE, adapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE,
nullptr, nullptr,
flags, flags,
featureLevels, featureLevels,
@@ -67,6 +121,23 @@ bool WgcSession::createD3DDevice() {
#if defined(_DEBUG) #if defined(_DEBUG)
if (FAILED(hr)) { if (FAILED(hr)) {
flags &= ~D3D11_CREATE_DEVICE_DEBUG; 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( hr = D3D11CreateDevice(
nullptr, nullptr,
D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_HARDWARE,
@@ -79,7 +150,6 @@ bool WgcSession::createD3DDevice() {
&featureLevel, &featureLevel,
&d3dContext_); &d3dContext_);
} }
#endif
if (!succeeded(hr, "D3D11CreateDevice")) { if (!succeeded(hr, "D3D11CreateDevice")) {
return false; return false;
@@ -100,6 +170,11 @@ bool WgcSession::createD3DDevice() {
return true; return true;
} }
bool WgcSession::createD3DDeviceForMonitor(HMONITOR monitor) {
auto adapter = findAdapterForMonitor(monitor);
return createD3DDevice(adapter.Get());
}
bool WgcSession::createCaptureItem(HMONITOR monitor) { bool WgcSession::createCaptureItem(HMONITOR monitor) {
auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>(); auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
auto interop = factory.as<IGraphicsCaptureItemInterop>(); auto interop = factory.as<IGraphicsCaptureItemInterop>();
@@ -188,7 +263,7 @@ bool WgcSession::applySessionOptions(bool captureCursor) {
bool WgcSession::initialize(HMONITOR monitor, int fps, bool captureCursor) { bool WgcSession::initialize(HMONITOR monitor, int fps, bool captureCursor) {
fps_ = fps > 0 ? fps : 60; fps_ = fps > 0 ? fps : 60;
if (!createD3DDevice()) { if (!createD3DDeviceForMonitor(monitor)) {
return false; return false;
} }
if (!createCaptureItem(monitor)) { if (!createCaptureItem(monitor)) {
@@ -2,6 +2,7 @@
#include <Windows.h> #include <Windows.h>
#include <d3d11.h> #include <d3d11.h>
#include <dxgi.h>
#include <windows.graphics.capture.h> #include <windows.graphics.capture.h>
#include <windows.graphics.directx.direct3d11.interop.h> #include <windows.graphics.directx.direct3d11.interop.h>
#include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.h>
@@ -34,7 +35,8 @@ public:
ID3D11DeviceContext* context() const; ID3D11DeviceContext* context() const;
private: private:
bool createD3DDevice(); bool createD3DDevice(IDXGIAdapter* adapter = nullptr);
bool createD3DDeviceForMonitor(HMONITOR monitor);
bool createCaptureItem(HMONITOR monitor); bool createCaptureItem(HMONITOR monitor);
bool createCaptureItem(HWND window); bool createCaptureItem(HWND window);
bool applySessionOptions(bool captureCursor); bool applySessionOptions(bool captureCursor);
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "openscreen", "name": "openscreen",
"version": "1.4.4", "version": "1.4.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openscreen", "name": "openscreen",
"version": "1.4.4", "version": "1.4.6",
"dependencies": { "dependencies": {
"@fix-webm-duration/fix": "^1.0.1", "@fix-webm-duration/fix": "^1.0.1",
"@pixi/filter-drop-shadow": "^5.2.0", "@pixi/filter-drop-shadow": "^5.2.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "openscreen", "name": "openscreen",
"private": true, "private": true,
"version": "1.4.4", "version": "1.4.6",
"type": "module", "type": "module",
"packageManager": "npm@10.9.4", "packageManager": "npm@10.9.4",
"engines": { "engines": {