Fix Windows native capture state and monitor adapter
This commit is contained in:
+83
-24
@@ -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);
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user