diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index c0cf0cdb..a3f663f2 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -209,7 +209,6 @@ interface Window { hudOverlayHide: () => void; hudOverlayClose: () => void; hudOverlayRendererReady: () => void; - hudOverlaySetWebcamPreviewVisible: (visible: boolean) => void; getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>; getHudOverlayMousePassthroughSupported: () => Promise<{ success: boolean; @@ -677,7 +676,6 @@ interface Window { options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, ) => Promise<{ success: boolean; webcamPath: string | null }>; setCurrentRecordingSession: ( @@ -686,7 +684,6 @@ interface Window { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, options?: { preserveProjectPath?: boolean }, ) => Promise<{ success: boolean }>; @@ -697,7 +694,6 @@ interface Window { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }; }>; getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>; @@ -843,11 +839,7 @@ interface Window { /** Returns the app version from package.json */ getAppVersion: () => Promise; /** Hide the OS cursor before browser capture starts. */ - hideOsCursor: () => Promise<{ - success: boolean; - unsupported?: boolean; - platform?: string; - }>; + hideOsCursor: () => Promise<{ success: boolean }>; /** Recording preferences (mic, system audio) */ getRecordingPreferences: () => Promise<{ success: boolean; diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts index e9daa53a..dac1058c 100644 --- a/electron/hudOverlayBounds.test.ts +++ b/electron/hudOverlayBounds.test.ts @@ -3,8 +3,6 @@ import { describe, expect, it } from "vitest"; import { getHudOverlayWindowBounds, resizeHudOverlayFallbackBounds, - shouldExpandHudOverlayFallback, - shouldResizeHudOverlayFallback, } from "./hudOverlayBounds"; describe("getHudOverlayWindowBounds", () => { @@ -145,53 +143,3 @@ describe("resizeHudOverlayFallbackBounds", () => { }); }); }); - -describe("shouldResizeHudOverlayFallback", () => { - it("allows non-passthrough HUD windows to expand for menus when idle", () => { - expect(shouldResizeHudOverlayFallback(false, false)).toBe(true); - }); - - it("does not resize full passthrough HUD windows", () => { - expect(shouldResizeHudOverlayFallback(true, false)).toBe(false); - }); - - it("keeps the recording HUD compact in non-passthrough mode", () => { - expect(shouldResizeHudOverlayFallback(false, true)).toBe(false); - }); - - it("keeps the fallback stable while source selection is active", () => { - expect(shouldResizeHudOverlayFallback(false, false, true)).toBe(false); - }); -}); - -describe("shouldExpandHudOverlayFallback", () => { - it("expands while recording only when the floating webcam preview is visible", () => { - expect( - shouldExpandHudOverlayFallback({ - fallbackExpanded: false, - recordingActive: true, - webcamPreviewVisible: true, - }), - ).toBe(true); - }); - - it("keeps the compact recording fallback when there is no webcam preview", () => { - expect( - shouldExpandHudOverlayFallback({ - fallbackExpanded: false, - recordingActive: true, - webcamPreviewVisible: false, - }), - ).toBe(false); - }); - - it("preserves manual fallback expansion outside recording", () => { - expect( - shouldExpandHudOverlayFallback({ - fallbackExpanded: true, - recordingActive: false, - webcamPreviewVisible: false, - }), - ).toBe(true); - }); -}); diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts index 2a8bff82..22facf93 100644 --- a/electron/hudOverlayBounds.ts +++ b/electron/hudOverlayBounds.ts @@ -37,27 +37,6 @@ export function getHudOverlayWindowBounds( height, }; } - -export function shouldResizeHudOverlayFallback( - mousePassthroughSupported: boolean, - recordingActive: boolean, - interactionLocked = false, -): boolean { - return !mousePassthroughSupported && !recordingActive && !interactionLocked; -} - -export function shouldExpandHudOverlayFallback({ - fallbackExpanded, - recordingActive, - webcamPreviewVisible, -}: { - fallbackExpanded: boolean; - recordingActive: boolean; - webcamPreviewVisible: boolean; -}): boolean { - return fallbackExpanded || (recordingActive && webcamPreviewVisible); -} - export function resizeHudOverlayFallbackBounds( workArea: HudOverlayWorkArea, currentBounds: HudOverlayWorkArea, diff --git a/electron/ipc/register/project.ts b/electron/ipc/register/project.ts index 7af983b0..a65fd907 100644 --- a/electron/ipc/register/project.ts +++ b/electron/ipc/register/project.ts @@ -533,7 +533,7 @@ export function registerProjectHandlers() { return { success: false, error: String(error), message: 'Failed to open projects folder.' } } }) - ipcMain.handle('set-current-video-path', async (_, path: string, options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean; nativeCaptureUnavailable?: boolean }) => { + ipcMain.handle('set-current-video-path', async (_, path: string, options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean }) => { setCurrentVideoPath(normalizeVideoSourcePath(path) ?? path) approveUserPath(currentVideoPath) const resolvedSession = await resolveRecordingSession(currentVideoPath) @@ -548,10 +548,6 @@ export function registerProjectHandlers() { hideOverlayCursorByDefault: normalizeBoolean(options?.hideOverlayCursorByDefault) || normalizeBoolean(resolvedSession.hideOverlayCursorByDefault), - nativeCaptureUnavailable: - normalizeBoolean( - options?.nativeCaptureUnavailable ?? resolvedSession.nativeCaptureUnavailable, - ), } setCurrentRecordingSession(nextSession) @@ -577,7 +573,7 @@ export function registerProjectHandlers() { return { success: true, webcamPath: nextSession.webcamPath ?? null } }) - ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; nativeCaptureUnavailable?: boolean }, options?: { preserveProjectPath?: boolean }) => { + ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean }, options?: { preserveProjectPath?: boolean }) => { const normalizedVideoPath = normalizeVideoSourcePath(session.videoPath) ?? session.videoPath setCurrentVideoPath(normalizedVideoPath) setCurrentRecordingSession({ @@ -585,7 +581,6 @@ export function registerProjectHandlers() { webcamPath: normalizeVideoSourcePath(session.webcamPath ?? null), timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs), hideOverlayCursorByDefault: normalizeBoolean(session.hideOverlayCursorByDefault), - nativeCaptureUnavailable: normalizeBoolean(session.nativeCaptureUnavailable), }); await rememberApprovedLocalReadPath(currentRecordingSession!.videoPath) await rememberApprovedLocalReadPath(currentRecordingSession!.webcamPath) diff --git a/electron/ipc/register/settings.ts b/electron/ipc/register/settings.ts index 5b4498a8..e84f6317 100644 --- a/electron/ipc/register/settings.ts +++ b/electron/ipc/register/settings.ts @@ -114,7 +114,7 @@ export function registerSettingsHandlers() { // --------------------------------------------------------------------------- ipcMain.handle("hide-cursor", () => { if (process.platform !== "win32") { - return { success: false, unsupported: true, platform: process.platform }; + return { success: true }; } return { success: hideCursor() }; diff --git a/electron/ipc/register/sourceMapping.test.ts b/electron/ipc/register/sourceMapping.test.ts index a4c67c16..d0b68e75 100644 --- a/electron/ipc/register/sourceMapping.test.ts +++ b/electron/ipc/register/sourceMapping.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest"; import { getScreenSourceIdForDisplay, LINUX_PORTAL_SCREEN_SOURCE_ID, - shouldUseSyntheticLinuxPortalSource, } from "./sourceMapping"; describe("getScreenSourceIdForDisplay", () => { @@ -48,66 +47,4 @@ describe("getScreenSourceIdForDisplay", () => { }), ).toBe("screen:fallback:42"); }); -}); - -describe("shouldUseSyntheticLinuxPortalSource", () => { - it("keeps Wayland portal capture on the synthetic source path", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID, - }), - ).toBe(true); - }); - - it("lets X11 use Electron desktopCapturer sources instead of a synthetic id", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "x11", DISPLAY: ":0" }, - platform: "linux", - sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID, - }), - ).toBe(false); - }); - - it("recovers stale fallback ids through the synthetic path on Wayland", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: "screen:fallback:0", - }), - ).toBe(true); - }); - - it("defaults unknown Linux sessions with WAYLAND_DISPLAY to the synthetic path", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: null, - }), - ).toBe(true); - }); - - it("does not synthesize for concrete source ids", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: "screen:42:0", - }), - ).toBe(false); - }); - - it("does not synthesize outside Linux", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, - platform: "win32", - sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID, - }), - ).toBe(false); - }); -}); +}); \ No newline at end of file diff --git a/electron/ipc/register/sourceMapping.ts b/electron/ipc/register/sourceMapping.ts index b6a4366d..a61b4cf7 100644 --- a/electron/ipc/register/sourceMapping.ts +++ b/electron/ipc/register/sourceMapping.ts @@ -32,28 +32,4 @@ export function getScreenSourceIdForDisplay({ } return `screen:fallback:${displayId}`; -} - -export function shouldUseSyntheticLinuxPortalSource({ - env, - platform, - sourceId, -}: { - env: NodeJS.ProcessEnv; - platform: NodeJS.Platform | string; - sourceId?: string | null; -}) { - if (platform !== "linux") { - return false; - } - - if ( - sourceId && - sourceId !== LINUX_PORTAL_SCREEN_SOURCE_ID && - !sourceId.startsWith("screen:fallback:") - ) { - return false; - } - - return isLikelyLinuxWaylandSession(env); -} +} \ No newline at end of file diff --git a/electron/ipc/register/sources.ts b/electron/ipc/register/sources.ts index b59d9ead..33c9ee74 100644 --- a/electron/ipc/register/sources.ts +++ b/electron/ipc/register/sources.ts @@ -1,20 +1,20 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { app, BrowserWindow, desktopCapturer, ipcMain } from "electron"; -import { reassertHudOverlayMousePassthrough } from "../../windows"; import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants"; +import { selectedSource, setSelectedSource } from "../state"; +import type { SelectedSource } from "../types"; +import { getScreen, parseWindowId } from "../utils"; +import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg"; +import { getScreenSourceIdForDisplay } from "./sourceMapping"; import { getNativeMacWindowSources, - resolveLinuxWindowBounds, resolveMacWindowBounds, resolveWindowsWindowBounds, + resolveLinuxWindowBounds, stopWindowBoundsCapture, } from "../cursor/bounds"; -import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg"; -import { selectedSource, setSelectedSource } from "../state"; -import type { SelectedSource } from "../types"; -import { getScreen, parseWindowId } from "../utils"; -import { getScreenSourceIdForDisplay } from "./sourceMapping"; +import { reassertHudOverlayMousePassthrough } from "../../windows"; const execFileAsync = promisify(execFile); const SOURCE_LIST_CACHE_TTL_MS = 1200; diff --git a/electron/ipc/types.ts b/electron/ipc/types.ts index 9680da73..58f5425b 100644 --- a/electron/ipc/types.ts +++ b/electron/ipc/types.ts @@ -48,7 +48,6 @@ export type RecordingSessionData = { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }; export type PauseSegment = { diff --git a/electron/main.ts b/electron/main.ts index 68829e43..ed05ffeb 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -25,7 +25,6 @@ import { killWindowsCaptureProcess, registerIpcHandlers, } from "./ipc/handlers"; -import { shouldUseSyntheticLinuxPortalSource } from "./ipc/register/sourceMapping"; import { ensureMediaServer } from "./mediaServer"; import { ensurePackagedRendererServer } from "./rendererServer"; import type { UpdateToastPayload } from "./updater"; @@ -232,7 +231,7 @@ function showHudOverlayFromTray() { if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { hud.showInactive(); hud.moveTop(); - reassertHudOverlayMouseState({ interactiveGraceMs: 1200 }); + reassertHudOverlayMouseState(); return true; } @@ -1015,18 +1014,12 @@ app.whenReady().then(async () => { // is set we skip getSources entirely and hand back a synthetic // source id; Chromium then opens the portal once to actually // resolve the capture. - // Default to the sentinel on Linux/Wayland when no source has been + // Default to the sentinel on Linux when no source has been // pre-selected (e.g. fresh session where the renderer skipped the // source picker entirely). This avoids calling getSources() which // would itself trigger an extra portal dialog. - // X11 does not need this synthetic path; use Electron's documented - // desktopCapturer source flow there so getDisplayMedia receives a - // real source id instead of a Wayland-only portal sentinel. - const isLinuxPortalSentinel = shouldUseSyntheticLinuxPortalSource({ - env: process.env, - platform: process.platform, - sourceId, - }); + const isLinuxPortalSentinel = + process.platform === "linux" && (sourceId === "screen:linux-portal" || !sourceId); if (isLinuxPortalSentinel) { callback({ video: { id: "screen:0:0", name: "Entire screen" } }); return; diff --git a/electron/preload.ts b/electron/preload.ts index 384682a1..04695d6b 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -182,9 +182,6 @@ contextBridge.exposeInMainWorld("electronAPI", { hudOverlayRendererReady: () => { ipcRenderer.send("hud-overlay-renderer-ready"); }, - hudOverlaySetWebcamPreviewVisible: (visible: boolean) => { - ipcRenderer.send("hud-overlay-set-webcam-preview-visible", visible); - }, getHudOverlayCaptureProtection: () => { return ipcRenderer.invoke("get-hud-overlay-capture-protection"); }, @@ -693,7 +690,6 @@ contextBridge.exposeInMainWorld("electronAPI", { options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, ) => { return ipcRenderer.invoke("set-current-video-path", path, options); @@ -704,7 +700,6 @@ contextBridge.exposeInMainWorld("electronAPI", { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, options?: { preserveProjectPath?: boolean }, ) => { diff --git a/electron/windows.ts b/electron/windows.ts index 6173fc03..478584b5 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -5,12 +5,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; import { USER_DATA_PATH } from "./appPaths"; -import { - getHudOverlayWindowBounds, - resizeHudOverlayFallbackBounds, - shouldExpandHudOverlayFallback, - shouldResizeHudOverlayFallback, -} from "./hudOverlayBounds"; +import { getHudOverlayWindowBounds, resizeHudOverlayFallbackBounds } from "./hudOverlayBounds"; import { getPackagedRendererBaseUrl } from "./rendererServer"; const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url)); @@ -35,7 +30,6 @@ let hudOverlayIgnoringMouse = true; let hudOverlaySourceSelectionActive = false; let hudOverlayMouseReassertTimer: NodeJS.Timeout | null = null; let hudOverlayRecordingActive = false; -let hudOverlayWebcamPreviewVisible = false; let countdownWindow: BrowserWindow | null = null; let updateToastWindow: BrowserWindow | null = null; @@ -196,15 +190,10 @@ function getHudOverlayDisplay() { function getHudOverlayBounds() { const { workArea } = getHudOverlayDisplay(); - const fallbackExpanded = shouldExpandHudOverlayFallback({ - fallbackExpanded: hudOverlayFallbackExpanded, - recordingActive: hudOverlayRecordingActive, - webcamPreviewVisible: hudOverlayWebcamPreviewVisible, - }); return getHudOverlayWindowBounds( workArea, isHudOverlayMousePassthroughSupported() && !hudOverlayRecordingActive, - fallbackExpanded, + hudOverlayFallbackExpanded, ); } @@ -311,21 +300,8 @@ function setHudOverlayMousePassthrough(ignore: boolean) { return; } - if (hudOverlaySourceSelectionActive) { - hudOverlayFallbackExpanded = false; - hudOverlayWindow.setIgnoreMouseEvents(false); - return; - } - - const mousePassthroughSupported = isHudOverlayMousePassthroughSupported(); - if (!mousePassthroughSupported) { - if ( - shouldResizeHudOverlayFallback( - mousePassthroughSupported, - hudOverlayRecordingActive, - hudOverlaySourceSelectionActive, - ) - ) { + if (!isHudOverlayMousePassthroughSupported()) { + if (process.platform !== "linux") { setHudOverlayFallbackExpanded(!ignore); } hudOverlayWindow.setIgnoreMouseEvents(false); @@ -434,18 +410,6 @@ ipcMain.handle("get-hud-overlay-mouse-passthrough-supported", () => { }; }); -ipcMain.on("hud-overlay-set-webcam-preview-visible", (_event, visible: boolean) => { - const nextVisible = Boolean(visible); - if (hudOverlayWebcamPreviewVisible === nextVisible) { - return; - } - - hudOverlayWebcamPreviewVisible = nextVisible; - if (hudOverlayRecordingActive) { - applyHudOverlayBounds(); - } -}); - ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => { loadHudOverlayCaptureProtectionSetting(); hudOverlayHiddenFromCapture = Boolean(enabled); @@ -468,7 +432,6 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) export function createHudOverlayWindow(): BrowserWindow { loadHudOverlayCaptureProtectionSetting(); hudOverlayFallbackExpanded = false; - hudOverlayWebcamPreviewVisible = false; const initialBounds = getHudOverlayBounds(); let hasShownHudWindow = false; @@ -642,11 +605,7 @@ export function getHudOverlayWindow(): BrowserWindow | null { * hover detection on the HUD is immediately restored without requiring the * user to move their mouse over the bar. */ -export function reassertHudOverlayMousePassthrough({ - interactiveGraceMs = 50, -}: { - interactiveGraceMs?: number; -} = {}): void { +export function reassertHudOverlayMousePassthrough(): void { if (process.platform !== "win32" || !isHudOverlayMousePassthroughSupported()) { return; } @@ -672,7 +631,7 @@ export function reassertHudOverlayMousePassthrough({ if (!hud.isDestroyed()) { setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); } - }, interactiveGraceMs); + }, 50); } export function setHudOverlayRecordingActive(recording: boolean): void { diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 09cca63e..d7dc51bc 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -152,16 +152,6 @@ function LaunchWindowContent() { hudOverlayMousePassthroughSupported, }); - useEffect(() => { - window.electronAPI?.hudOverlaySetWebcamPreviewVisible?.(showRecordingWebcamPreview); - }, [showRecordingWebcamPreview]); - - useEffect(() => { - return () => { - window.electronAPI?.hudOverlaySetWebcamPreviewVisible?.(false); - }; - }, []); - const { recordingHudOffset, isHudDragging, diff --git a/src/components/launch/hooks/useLaunchHudInteractionState.ts b/src/components/launch/hooks/useLaunchHudInteractionState.ts index d7c086b1..42f60b72 100644 --- a/src/components/launch/hooks/useLaunchHudInteractionState.ts +++ b/src/components/launch/hooks/useLaunchHudInteractionState.ts @@ -13,26 +13,10 @@ export function useLaunchHudInteractionState({ }) { const isMouseOverHudRef = useRef(false); const timeoutRef = useRef(null); - const lastInteractiveReassertAtRef = useRef(0); - - const setHudMouseInteractive = useCallback((force = false) => { - const now = performance.now(); - if ( - !force && - isMouseOverHudRef.current && - now - lastInteractiveReassertAtRef.current < 250 - ) { - return; - } - - isMouseOverHudRef.current = true; - lastInteractiveReassertAtRef.current = now; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); - }, []); useEffect(() => { if (openId !== null) { + if (timeoutRef.current) clearTimeout(timeoutRef.current); window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); } else { // Proactively check if we should ignore mouse when popover closes @@ -45,7 +29,7 @@ export function useLaunchHudInteractionState({ }, [openId]); useEffect(() => { - const handleMouseTracking = (e: globalThis.MouseEvent) => { + const handleMouseOver = (e: globalThis.MouseEvent) => { const target = e.target as HTMLElement | null; if (!target) return; const isInteractive = !!target.closest( @@ -53,12 +37,15 @@ export function useLaunchHudInteractionState({ ); if (isInteractive) { - setHudMouseInteractive(); - } else { + isMouseOverHudRef.current = true; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + } else if (openId === null) { isMouseOverHudRef.current = false; if (timeoutRef.current) clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { if ( + openId === null && !isHudDraggingRef.current && !isWebcamPreviewDraggingRef.current && !webcamPreviewDragStartRef.current && @@ -70,26 +57,20 @@ export function useLaunchHudInteractionState({ } }; - window.addEventListener("mouseover", handleMouseTracking); - window.addEventListener("mousemove", handleMouseTracking); - return () => { - window.removeEventListener("mouseover", handleMouseTracking); - window.removeEventListener("mousemove", handleMouseTracking); - }; - }, [ - isHudDraggingRef, - isWebcamPreviewDraggingRef, - setHudMouseInteractive, - webcamPreviewDragStartRef, - ]); + window.addEventListener("mouseover", handleMouseOver); + return () => window.removeEventListener("mouseover", handleMouseOver); + }, [openId, isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef]); const beginInteractiveHudAction = useCallback(() => { - setHudMouseInteractive(true); - }, [setHudMouseInteractive]); + isMouseOverHudRef.current = true; + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + }, []); const handleHudMouseEnter = useCallback(() => { - setHudMouseInteractive(true); - }, [setHudMouseInteractive]); + isMouseOverHudRef.current = true; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + }, []); const handleHudMouseLeave = useCallback( (event: MouseEvent) => { @@ -104,6 +85,7 @@ export function useLaunchHudInteractionState({ timeoutRef.current = setTimeout(() => { if ( + openId === null && !isHudDraggingRef.current && !isWebcamPreviewDraggingRef.current && !webcamPreviewDragStartRef.current && @@ -113,7 +95,7 @@ export function useLaunchHudInteractionState({ } }, 300); }, - [isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef], + [openId, isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef], ); return { diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 1a4c1955..1fa52661 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -3587,7 +3587,6 @@ export function SettingsPanel({ @@ -3596,20 +3595,11 @@ export function SettingsPanel({ - {nativeCaptureUnavailableSession ? ( -
- {tSettings( - "effects.cursorOverlayUnavailable", - "Cursor overlay is unavailable for this recording because the captured video already contains the system cursor.", - )} -
- ) : null}
{ - if (nextShowCursor && sessionNativeCaptureUnavailable) { - setNativeCaptureUnavailableModalOpen(true); - return; - } - - setSessionShowCursorOverride(null); - setShowCursor(nextShowCursor); - }, - [sessionNativeCaptureUnavailable], - ); + const handleShowCursorChange = useCallback((nextShowCursor: boolean) => { + setSessionShowCursorOverride(null); + setShowCursor(nextShowCursor); + }, []); const remountPreview = useCallback(() => { setIsPreviewReady(false); diff --git a/src/hooks/useScreenRecorder.test.ts b/src/hooks/useScreenRecorder.test.ts index ed16b6a5..85811467 100644 --- a/src/hooks/useScreenRecorder.test.ts +++ b/src/hooks/useScreenRecorder.test.ts @@ -3,12 +3,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { createBrowserRecordingOptions, createProcessedMicrophoneConstraints, - getScreenCaptureCursorSetting, normalizeBrowserMicrophoneProfile, resolveBrowserCaptureCursorPolicy, - resolveLinuxPortalCursorPresentation, - shouldLockHudDuringDisplaySelection, - shouldUseLinuxPortalCapture, shouldUseNativeWindowsCaptureForSource, } from "./useScreenRecorder"; @@ -149,7 +145,6 @@ describe("resolveBrowserCaptureCursorPolicy", () => { streamCursor: "never", hideOsCursorBeforeRecording: true, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: false, }); }); @@ -160,128 +155,8 @@ describe("resolveBrowserCaptureCursorPolicy", () => { streamCursor: "always", hideOsCursorBeforeRecording: false, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, }); }); - - it("does not fake OS cursor hiding on Linux portal capture", () => { - expect(resolveBrowserCaptureCursorPolicy({ platform: "linux" })).toEqual({ - streamCursor: "never", - hideOsCursorBeforeRecording: false, - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }); - }); -}); - -describe("resolveLinuxPortalCursorPresentation", () => { - it("enables the Recordly overlay only when the portal confirms cursor-hidden capture", () => { - expect( - resolveLinuxPortalCursorPresentation({ - requestedCursor: "never", - actualCursor: "never", - }), - ).toEqual({ - hideEditorOverlayCursorByDefault: false, - nativeCaptureUnavailable: false, - }); - }); - - it("keeps the overlay disabled when the portal embeds or omits cursor settings", () => { - expect( - resolveLinuxPortalCursorPresentation({ - requestedCursor: "never", - actualCursor: "always", - }), - ).toEqual({ - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }); - expect( - resolveLinuxPortalCursorPresentation({ - requestedCursor: "never", - actualCursor: null, - }), - ).toEqual({ - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }); - }); -}); - -describe("shouldUseLinuxPortalCapture", () => { - it("uses the portal when the selected source is the Linux sentinel", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:linux-portal", - selectedSourceId: "screen:linux-portal", - }), - ).toBe(true); - }); - - it("uses the portal when a stale screen fallback resolves to the Linux sentinel", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:linux-portal", - selectedSourceId: "screen:fallback:42", - }), - ).toBe(true); - }); - - it("keeps live Electron screen sources on browser getUserMedia", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:42:0", - selectedSourceId: "screen:42:0", - }), - ).toBe(false); - }); - - it("prefers a live Electron source over stale portal selection state", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:42:0", - selectedSourceId: "screen:linux-portal", - }), - ).toBe(false); - }); -}); - -describe("shouldLockHudDuringDisplaySelection", () => { - it("locks HUD fallback resizing while Linux portal selection is active", () => { - expect( - shouldLockHudDuringDisplaySelection({ - platform: "linux", - useLinuxPortal: true, - }), - ).toBe(true); - }); - - it("keeps non-portal capture flows interactive", () => { - expect( - shouldLockHudDuringDisplaySelection({ - platform: "linux", - useLinuxPortal: false, - }), - ).toBe(false); - expect( - shouldLockHudDuringDisplaySelection({ - platform: "win32", - useLinuxPortal: true, - }), - ).toBe(false); - }); -}); - -describe("getScreenCaptureCursorSetting", () => { - it("normalizes only supported screen-capture cursor settings", () => { - expect(getScreenCaptureCursorSetting({ cursor: "motion" } as MediaTrackSettings)).toBe( - "motion", - ); - expect( - getScreenCaptureCursorSetting({ cursor: "hidden" } as MediaTrackSettings), - ).toBeNull(); - }); }); describe("shouldUseNativeWindowsCaptureForSource", () => { diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index bb1f4e3d..a382ea3d 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -46,12 +46,10 @@ export type BrowserMicrophoneProfile = | "no-noise-suppression" | "raw"; type BrowserCaptureCursorMode = "always" | "never"; -type BrowserCaptureCursorSetting = BrowserCaptureCursorMode | "motion"; export type BrowserCaptureCursorPolicy = { streamCursor: BrowserCaptureCursorMode; hideOsCursorBeforeRecording: boolean; hideEditorOverlayCursorByDefault: boolean; - nativeCaptureUnavailable: boolean; }; const DEFAULT_BROWSER_MICROPHONE_PROFILE: BrowserMicrophoneProfile = "processed"; const BROWSER_MICROPHONE_PROFILES = new Set([ @@ -128,33 +126,6 @@ type DesktopCaptureMediaDevices = { getDisplayMedia: (constraints: unknown) => Promise; }; -export function shouldUseLinuxPortalCapture({ - browserCaptureSourceId, - selectedSourceId, -}: { - browserCaptureSourceId?: string; - selectedSourceId?: string; -}) { - if (browserCaptureSourceId && browserCaptureSourceId !== LINUX_PORTAL_SOURCE.id) { - return false; - } - - return ( - selectedSourceId === LINUX_PORTAL_SOURCE.id || - browserCaptureSourceId === LINUX_PORTAL_SOURCE.id - ); -} - -export function shouldLockHudDuringDisplaySelection({ - platform, - useLinuxPortal, -}: { - platform?: string; - useLinuxPortal: boolean; -}) { - return platform === "linux" && useLinuxPortal; -} - type UseScreenRecorderReturn = { recording: boolean; paused: boolean; @@ -219,10 +190,8 @@ export function normalizeBrowserMicrophoneProfile(value?: string | null): Browse export function resolveBrowserCaptureCursorPolicy({ nativeWindowsCaptureStartFailed = false, - platform, }: { nativeWindowsCaptureStartFailed?: boolean; - platform?: string; } = {}): BrowserCaptureCursorPolicy { if (nativeWindowsCaptureStartFailed) { // If WGC already failed, avoid the telemetry overlay path that can lag on @@ -231,19 +200,6 @@ export function resolveBrowserCaptureCursorPolicy({ streamCursor: "always", hideOsCursorBeforeRecording: false, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }; - } - - if (platform === "linux") { - // Linux screen capture runs through xdg-desktop-portal/PipeWire. Ask the - // portal to omit the cursor, but do not pretend we can globally hide the - // OS cursor from Electron when the portal/compositor ignores that request. - return { - streamCursor: "never", - hideOsCursorBeforeRecording: false, - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, }; } @@ -251,37 +207,6 @@ export function resolveBrowserCaptureCursorPolicy({ streamCursor: "never", hideOsCursorBeforeRecording: true, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: false, - }; -} - -export function getScreenCaptureCursorSetting( - settings: MediaTrackSettings | null | undefined, -): BrowserCaptureCursorSetting | null { - const cursor = (settings as { cursor?: unknown } | null | undefined)?.cursor; - return cursor === "always" || cursor === "never" || cursor === "motion" ? cursor : null; -} - -export function resolveLinuxPortalCursorPresentation({ - actualCursor, - requestedCursor, -}: { - actualCursor: BrowserCaptureCursorSetting | null; - requestedCursor: BrowserCaptureCursorMode; -}): Pick< - BrowserCaptureCursorPolicy, - "hideEditorOverlayCursorByDefault" | "nativeCaptureUnavailable" -> { - if (requestedCursor === "never" && actualCursor === "never") { - return { - hideEditorOverlayCursorByDefault: false, - nativeCaptureUnavailable: false, - }; - } - - return { - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, }; } @@ -447,7 +372,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); const requestedBrowserMicrophoneProfile = useRef(null); const hideEditorOverlayCursorByDefault = useRef(false); - const nativeCaptureUnavailableForCursorOverlay = useRef(false); const notifyRecordingFinalizationFailure = useCallback(async (message: string) => { setFinalizing(false); @@ -711,7 +635,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { // on Wayland that triggers an additional xdg-desktop-portal dialog. // The sentinel is handled later by routing through getDisplayMedia, // which lets the portal pick the source in a single dialog. - if (source.id === LINUX_PORTAL_SOURCE.id) { + if (source.id === "screen:linux-portal") { return source; } @@ -756,7 +680,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const start = performance.now(); console.log("[PERF:RENDERER] Finalize Session & Switch to Editor: STARTED"); const shouldHideOverlayCursor = hideEditorOverlayCursorByDefault.current; - const nativeCaptureUnavailable = nativeCaptureUnavailableForCursorOverlay.current; try { if (webcamPath) { await window.electronAPI.setCurrentRecordingSession({ @@ -764,12 +687,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { webcamPath, timeOffsetMs: webcamTimeOffsetMs.current, hideOverlayCursorByDefault: shouldHideOverlayCursor, - nativeCaptureUnavailable, }); } else { await window.electronAPI.setCurrentVideoPath(videoPath, { hideOverlayCursorByDefault: shouldHideOverlayCursor, - nativeCaptureUnavailable, }); } } catch (error) { @@ -778,7 +699,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { try { await window.electronAPI.setCurrentVideoPath(videoPath, { hideOverlayCursorByDefault: shouldHideOverlayCursor, - nativeCaptureUnavailable, }); } catch (fallbackError) { console.error("Failed to persist fallback video path:", fallbackError); @@ -1246,8 +1166,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { webcamPath, timeOffsetMs: webcamTimeOffsetMs.current, hideOverlayCursorByDefault: hideEditorOverlayCursorByDefault.current, - nativeCaptureUnavailable: - nativeCaptureUnavailableForCursorOverlay.current, }); console.log( @@ -1456,7 +1374,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { try { const platform = await window.electronAPI.getPlatform(); hideEditorOverlayCursorByDefault.current = false; - nativeCaptureUnavailableForCursorOverlay.current = false; const existingSource = await window.electronAPI.getSelectedSource(); const selectedSource = existingSource ?? (platform === "linux" ? LINUX_PORTAL_SOURCE : null); @@ -1467,7 +1384,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { // Persist the synthetic Linux portal sentinel to main so that the // setDisplayMediaRequestHandler can short-circuit getSources() and // avoid triggering an extra portal dialog. - if (!existingSource && selectedSource.id === LINUX_PORTAL_SOURCE.id) { + if (!existingSource && selectedSource.id === "screen:linux-portal") { try { await window.electronAPI.selectSource(selectedSource); } catch (err) { @@ -1663,12 +1580,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const browserCursorPolicy = resolveBrowserCaptureCursorPolicy({ nativeWindowsCaptureStartFailed, - platform, }); hideEditorOverlayCursorByDefault.current = browserCursorPolicy.hideEditorOverlayCursorByDefault; - nativeCaptureUnavailableForCursorOverlay.current = - browserCursorPolicy.nativeCaptureUnavailable; const wantsAudioCapture = microphoneEnabled || systemAudioEnabled; const browserCaptureSource = await resolveBrowserCaptureSource(selectedSource); @@ -1699,13 +1613,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { let videoTrack: MediaStreamTrack | undefined; let systemAudioIncluded = false; const mediaDevices = navigator.mediaDevices as DesktopCaptureMediaDevices; - const useLinuxPortal = shouldUseLinuxPortalCapture({ - browserCaptureSourceId: browserCaptureSource.id, - selectedSourceId: selectedSource.id, - }); - if (shouldLockHudDuringDisplaySelection({ platform, useLinuxPortal })) { - setHudSourceSelectionActive(true); - } + const useLinuxPortal = selectedSource.id === "screen:linux-portal"; const browserScreenVideoConstraints = { mandatory: { chromeMediaSource: CHROME_MEDIA_SOURCE, @@ -1858,27 +1766,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { throw new Error("Media stream is not available."); } - if (useLinuxPortal) { - const actualCursor = getScreenCaptureCursorSetting(videoTrack.getSettings()); - const cursorPresentation = resolveLinuxPortalCursorPresentation({ - actualCursor, - requestedCursor: browserCursorPolicy.streamCursor, - }); - hideEditorOverlayCursorByDefault.current = - cursorPresentation.hideEditorOverlayCursorByDefault; - nativeCaptureUnavailableForCursorOverlay.current = - cursorPresentation.nativeCaptureUnavailable; - if (cursorPresentation.nativeCaptureUnavailable) { - console.warn( - "Linux portal did not confirm cursor-hidden capture; disabling Recordly cursor overlay for this recording.", - { - actualCursor, - requestedCursor: browserCursorPolicy.streamCursor, - }, - ); - } - } - try { await videoTrack.applyConstraints({ frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE }, @@ -1984,8 +1871,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { timeOffsetMs: webcamTimeOffsetMs.current, hideOverlayCursorByDefault: hideEditorOverlayCursorByDefault.current, - nativeCaptureUnavailable: - nativeCaptureUnavailableForCursorOverlay.current, }); } } finally {