From 045850417f13df76ab9c8b6fbeea178b68acab6b Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:08:43 +1100 Subject: [PATCH] feat(debug): add native capture diagnostics --- electron/electron-env.d.ts | 25 +++++ electron/ipc/handlers.ts | 166 ++++++++++++++++++++++++++++++++- electron/preload.ts | 3 + src/hooks/useScreenRecorder.ts | 21 +++++ 4 files changed, 211 insertions(+), 4 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 1f570cd8..c6bbddcf 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -22,6 +22,27 @@ declare namespace NodeJS { } // Used in Renderer process, expose in `preload.ts` +interface NativeCaptureDiagnostics { + backend: "windows-wgc" | "mac-screencapturekit" | "browser-store" | "ffmpeg"; + phase: "availability" | "start" | "stop" | "mux"; + timestamp: string; + sourceId?: string | null; + sourceType?: "screen" | "window" | "unknown"; + displayId?: number | null; + displayBounds?: { x: number; y: number; width: number; height: number } | null; + windowHandle?: number | null; + helperPath?: string | null; + outputPath?: string | null; + systemAudioPath?: string | null; + microphonePath?: string | null; + osRelease?: string; + supported?: boolean; + helperExists?: boolean; + fileSizeBytes?: number | null; + processOutput?: string; + error?: string; +} + interface Window { electronAPI: { hudOverlayHide: () => void; @@ -55,6 +76,10 @@ interface Window { message?: string; error?: string; }>; + getLastNativeCaptureDiagnostics: () => Promise<{ + success: boolean; + diagnostics?: NativeCaptureDiagnostics | null; + }>; pauseNativeScreenRecording: () => Promise<{ success: boolean; message?: string; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 64c7cda4..c26ba63b 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -84,6 +84,27 @@ type WindowBounds = { height: number } +type NativeCaptureDiagnostics = { + backend: 'windows-wgc' | 'mac-screencapturekit' | 'browser-store' | 'ffmpeg' + phase: 'availability' | 'start' | 'stop' | 'mux' + timestamp: string + sourceId?: string | null + sourceType?: SelectedSource['sourceType'] | 'unknown' + displayId?: number | null + displayBounds?: WindowBounds | null + windowHandle?: number | null + helperPath?: string | null + outputPath?: string | null + systemAudioPath?: string | null + microphonePath?: string | null + osRelease?: string + supported?: boolean + helperExists?: boolean + fileSizeBytes?: number | null + processOutput?: string + error?: string +} + type RecordingSessionData = { videoPath: string webcamPath?: string | null @@ -129,6 +150,7 @@ let windowsCapturePaused = false let windowsSystemAudioPath: string | null = null let windowsMicAudioPath: string | null = null let windowsPendingVideoPath: string | null = null +let lastNativeCaptureDiagnostics: NativeCaptureDiagnostics | null = null let ffmpegScreenRecordingActive = false let ffmpegCaptureProcess: ChildProcessWithoutNullStreams | null = null let ffmpegCaptureOutputBuffer = '' @@ -238,6 +260,30 @@ async function getRecordingsDir() { return targetDir } +function recordNativeCaptureDiagnostics( + diagnostics: Omit, +) { + lastNativeCaptureDiagnostics = { + timestamp: new Date().toISOString(), + ...diagnostics, + } + + return lastNativeCaptureDiagnostics +} + +async function getFileSizeIfPresent(filePath: string | null | undefined) { + if (!filePath) { + return null + } + + try { + const stat = await fs.stat(filePath) + return stat.size + } catch { + return null + } +} + async function getProjectsDir() { const projectsDir = path.join(await getRecordingsDir(), PROJECTS_DIRECTORY_NAME) await fs.mkdir(projectsDir, { recursive: true }) @@ -1698,15 +1744,38 @@ function getCursorMonitorExePath() { async function isNativeWindowsCaptureAvailable(): Promise { if (process.platform !== 'win32') return false + const helperPath = getWindowsCaptureExePath() + const os = await import('node:os') + const [major, , build] = os.release().split('.').map(Number) + const supported = major >= 10 && build >= 19041 + let helperExists = false + try { - await fs.access(getWindowsCaptureExePath(), fsConstants.X_OK) + await fs.access(helperPath, fsConstants.X_OK) + helperExists = true } catch { + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'availability', + helperPath, + helperExists, + osRelease: os.release(), + supported, + error: 'Native Windows capture helper is missing or not executable.', + }) return false } - const os = await import('node:os') - const [major, , build] = os.release().split('.').map(Number) - return major >= 10 && build >= 19041 + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'availability', + helperPath, + helperExists, + osRelease: os.release(), + supported, + }) + + return supported } function waitForWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) { @@ -3024,6 +3093,7 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} const recordingsDir = await getRecordingsDir() const timestamp = Date.now() const outputPath = path.join(recordingsDir, `recording-${timestamp}.mp4`) + const displayBounds = source?.id?.startsWith('window:') ? null : getDisplayBoundsForSource(source) const config: Record = { outputPath, @@ -3070,6 +3140,20 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} } } + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'start', + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? 'unknown', + displayId: typeof config.displayId === 'number' ? config.displayId : null, + displayBounds, + windowHandle: typeof config.windowHandle === 'number' ? config.windowHandle : null, + helperPath: exePath, + outputPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + }) + windowsCaptureOutputBuffer = '' windowsCaptureTargetPath = outputPath windowsCaptureStopRequested = false @@ -3090,8 +3174,34 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} await waitForWindowsCaptureStart(windowsCaptureProcess) windowsNativeCaptureActive = true nativeScreenRecordingActive = true + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'start', + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? 'unknown', + displayId: typeof config.displayId === 'number' ? config.displayId : null, + displayBounds, + windowHandle: typeof config.windowHandle === 'number' ? config.windowHandle : null, + helperPath: exePath, + outputPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + }) return { success: true } } catch (error) { + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'start', + sourceId: source?.id ?? null, + sourceType: source?.sourceType ?? 'unknown', + helperPath: windowsCaptureTargetPath ? getWindowsCaptureExePath() : null, + outputPath: windowsCaptureTargetPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + error: String(error), + }) console.error('Failed to start native Windows capture:', error) try { windowsCaptureProcess?.kill() } catch { /* ignore */ } windowsNativeCaptureActive = false @@ -3255,6 +3365,15 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} } windowsPendingVideoPath = finalVideoPath + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'stop', + outputPath: finalVideoPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: await getFileSizeIfPresent(finalVideoPath), + }) return { success: true, path: finalVideoPath } } catch (error) { console.error('Failed to stop native Windows capture:', error) @@ -3273,12 +3392,32 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} try { await fs.access(fallbackPath) windowsPendingVideoPath = fallbackPath + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'stop', + outputPath: fallbackPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + fileSizeBytes: await getFileSizeIfPresent(fallbackPath), + error: String(error), + }) return { success: true, path: fallbackPath } } catch { // File doesn't exist } } + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'stop', + outputPath: fallbackPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + processOutput: windowsCaptureOutputBuffer.trim() || undefined, + error: String(error), + }) + return { success: false, message: 'Failed to stop native Windows capture', @@ -3452,6 +3591,10 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} return { available: await isNativeWindowsCaptureAvailable() } }) + ipcMain.handle('get-last-native-capture-diagnostics', async () => { + return { success: true, diagnostics: lastNativeCaptureDiagnostics } + }) + ipcMain.handle('mux-native-windows-recording', async () => { const videoPath = windowsPendingVideoPath windowsPendingVideoPath = null @@ -3467,9 +3610,24 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} windowsMicAudioPath = null } + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'mux', + outputPath: videoPath, + fileSizeBytes: await getFileSizeIfPresent(videoPath), + }) return await finalizeStoredVideo(videoPath) } catch (error) { console.error('Failed to mux native Windows recording:', error) + recordNativeCaptureDiagnostics({ + backend: 'windows-wgc', + phase: 'mux', + outputPath: videoPath, + systemAudioPath: windowsSystemAudioPath, + microphonePath: windowsMicAudioPath, + fileSizeBytes: await getFileSizeIfPresent(videoPath), + error: String(error), + }) windowsSystemAudioPath = null windowsMicAudioPath = null try { diff --git a/electron/preload.ts b/electron/preload.ts index 638511d0..e25dde55 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -68,6 +68,9 @@ contextBridge.exposeInMainWorld("electronAPI", { stopNativeScreenRecording: () => { return ipcRenderer.invoke("stop-native-screen-recording"); }, + getLastNativeCaptureDiagnostics: () => { + return ipcRenderer.invoke("get-last-native-capture-diagnostics"); + }, pauseNativeScreenRecording: () => { return ipcRenderer.invoke("pause-native-screen-recording"); }, diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index af3b0b93..8c5ef70e 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -94,6 +94,21 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const accumulatedPausedDurationMs = useRef(0); const pauseStartedAtMs = useRef(null); + const logNativeCaptureDiagnostics = useCallback(async (context: string) => { + if (typeof window.electronAPI?.getLastNativeCaptureDiagnostics !== "function") { + return; + } + + try { + const result = await window.electronAPI.getLastNativeCaptureDiagnostics(); + if (result.success && result.diagnostics) { + console.warn(`[NativeCaptureDiagnostics:${context}]`, result.diagnostics); + } + } catch (error) { + console.warn("Failed to load native capture diagnostics:", error); + } + }, []); + const resetRecordingClock = useCallback((startedAt: number) => { startTime.current = startedAt; accumulatedPausedDurationMs.current = 0; @@ -404,6 +419,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (!result.success || !result.path) { console.error("Failed to stop native screen recording:", result.error ?? result.message); + void logNativeCaptureDiagnostics("stop-native-screen-recording"); return; } @@ -411,6 +427,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (isNativeWindows) { const muxResult = await window.electronAPI.muxNativeWindowsRecording(); + if (!muxResult?.success) { + void logNativeCaptureDiagnostics("mux-native-windows-recording"); + } finalPath = muxResult?.path ?? result.path; } @@ -546,6 +565,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const nativeWindowsResult = await window.electronAPI.isNativeWindowsCaptureAvailable(); useNativeWindowsCapture = nativeWindowsResult.available; if (!useNativeWindowsCapture && !hasShownNativeWindowsFallbackToast.current) { + void logNativeCaptureDiagnostics("is-native-windows-capture-available"); hasShownNativeWindowsFallbackToast.current = true; toast.info( "Native Windows capture is unavailable. Falling back to browser capture.", @@ -586,6 +606,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (!nativeResult.success) { if (useNativeWindowsCapture) { console.warn("Native Windows capture failed, falling back to browser capture:", nativeResult.error ?? nativeResult.message); + void logNativeCaptureDiagnostics("start-native-screen-recording"); if (!hasShownNativeWindowsFallbackToast.current) { hasShownNativeWindowsFallbackToast.current = true; toast.warning(