diff --git a/src/hooks/useScreenRecorder.test.ts b/src/hooks/useScreenRecorder.test.ts index 7cf480cfa..2c422c4fd 100644 --- a/src/hooks/useScreenRecorder.test.ts +++ b/src/hooks/useScreenRecorder.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { createProcessedMicrophoneConstraints, normalizeBrowserMicrophoneProfile, + resolveBrowserCaptureCursorPolicy, } from "./useScreenRecorder"; type RecordingState = "inactive" | "recording" | "paused"; @@ -108,6 +109,26 @@ describe("createProcessedMicrophoneConstraints", () => { }); }); +describe("resolveBrowserCaptureCursorPolicy", () => { + it("preserves the existing hidden-cursor browser policy by default", () => { + expect(resolveBrowserCaptureCursorPolicy()).toEqual({ + streamCursor: "never", + hideOsCursorBeforeRecording: true, + hideEditorOverlayCursorByDefault: true, + }); + }); + + it("uses the browser captured cursor after native Windows capture fails to start", () => { + expect( + resolveBrowserCaptureCursorPolicy({ nativeWindowsCaptureStartFailed: true }), + ).toEqual({ + streamCursor: "always", + hideOsCursorBeforeRecording: false, + hideEditorOverlayCursorByDefault: true, + }); + }); +}); + function stopRecording( recorder: ReturnType, isNativeRecording: boolean, diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 6f021761a..cb34670ca 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -46,6 +46,12 @@ export type BrowserMicrophoneProfile = | "no-echo" | "no-noise-suppression" | "raw"; +type BrowserCaptureCursorMode = "always" | "never"; +export type BrowserCaptureCursorPolicy = { + streamCursor: BrowserCaptureCursorMode; + hideOsCursorBeforeRecording: boolean; + hideEditorOverlayCursorByDefault: boolean; +}; const DEFAULT_BROWSER_MICROPHONE_PROFILE: BrowserMicrophoneProfile = "no-agc"; const BROWSER_MICROPHONE_PROFILES = new Set([ "processed", @@ -183,6 +189,28 @@ export function normalizeBrowserMicrophoneProfile(value?: string | null): Browse : DEFAULT_BROWSER_MICROPHONE_PROFILE; } +export function resolveBrowserCaptureCursorPolicy({ + nativeWindowsCaptureStartFailed = false, +}: { + nativeWindowsCaptureStartFailed?: boolean; +} = {}): BrowserCaptureCursorPolicy { + if (nativeWindowsCaptureStartFailed) { + // If WGC already failed, avoid the telemetry overlay path that can lag on + // constrained Windows systems; keep the browser-captured cursor instead. + return { + streamCursor: "always", + hideOsCursorBeforeRecording: false, + hideEditorOverlayCursorByDefault: true, + }; + } + + return { + streamCursor: "never", + hideOsCursorBeforeRecording: true, + hideEditorOverlayCursorByDefault: true, + }; +} + export function createProcessedMicrophoneConstraints( microphoneDeviceId?: string, profile: BrowserMicrophoneProfile = DEFAULT_BROWSER_MICROPHONE_PROFILE, @@ -1331,6 +1359,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { typeof window.electronAPI.startNativeScreenRecording === "function"; let useNativeWindowsCapture = false; + let nativeWindowsCaptureStartFailed = false; if ( platform === "win32" && (selectedSource.id?.startsWith("screen:") || @@ -1385,7 +1414,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); if (!nativeResult.success) { if (useNativeWindowsCapture) { - hideEditorOverlayCursorByDefault.current = true; + nativeWindowsCaptureStartFailed = true; console.warn( "Native Windows capture failed, falling back to browser capture:", nativeResult.error ?? nativeResult.message, @@ -1495,7 +1524,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } - hideEditorOverlayCursorByDefault.current = true; + const browserCursorPolicy = resolveBrowserCaptureCursorPolicy({ + nativeWindowsCaptureStartFailed, + }); + hideEditorOverlayCursorByDefault.current = + browserCursorPolicy.hideEditorOverlayCursorByDefault; const wantsAudioCapture = microphoneEnabled || systemAudioEnabled; const browserCaptureSource = await resolveBrowserCaptureSource(selectedSource); @@ -1509,10 +1542,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); } - try { - await window.electronAPI.hideOsCursor?.(); - } catch { - console.warn("Could not hide OS cursor before recording."); + if (browserCursorPolicy.hideOsCursorBeforeRecording) { + try { + const hideCursorResult = await window.electronAPI.hideOsCursor?.(); + if (hideCursorResult && !hideCursorResult.success) { + console.warn("Could not hide OS cursor before recording.", hideCursorResult); + } + } catch { + console.warn("Could not hide OS cursor before recording."); + } } let videoTrack: MediaStreamTrack | undefined; @@ -1527,9 +1565,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { maxHeight: TARGET_HEIGHT, maxFrameRate: TARGET_FRAME_RATE, minFrameRate: MIN_FRAME_RATE, - googCaptureCursor: false, + googCaptureCursor: browserCursorPolicy.streamCursor === "always", }, - cursor: "never" as const, + cursor: browserCursorPolicy.streamCursor, }; if (wantsAudioCapture) { @@ -1542,7 +1580,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH }, height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT }, frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE }, - cursor: "never", + cursor: browserCursorPolicy.streamCursor, }, selfBrowserSurface: "exclude", surfaceSwitching: "exclude", @@ -1653,7 +1691,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH }, height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT }, frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE }, - cursor: "never", + cursor: browserCursorPolicy.streamCursor, }, selfBrowserSurface: "exclude", surfaceSwitching: "exclude",