From 8ffebefac2372b2ac598e8da83acf3864223bb64 Mon Sep 17 00:00:00 2001 From: Long Date: Fri, 3 Apr 2026 09:55:41 +0700 Subject: [PATCH] fix(launch): keep webcam preview stream attached across remounts Store the live webcam preview stream in a stable ref and reattach it through callback refs so late-mounted preview videos receive srcObject immediately while preserving drag-end click-through restoration. Made-with: Cursor --- src/components/launch/LaunchWindow.tsx | 83 +++++++++++++++++--------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f2af0fcb..d32c5508 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -57,7 +57,7 @@ interface DesktopSource { const LOCALE_LABELS: Record = { en: "EN", es: "ES", - "zh-CN": "中文", + "zh-CN": "??", }; const COUNTDOWN_OPTIONS = [0, 3, 5, 10]; @@ -207,6 +207,7 @@ export function LaunchWindow() { const moreButtonRef = useRef(null); const webcamPreviewRef = useRef(null); const recordingWebcamPreviewRef = useRef(null); + const previewStreamRef = useRef(null); const webcamPreviewDragStartRef = useRef<{ pointerId: number; startX: number; @@ -290,29 +291,58 @@ export function LaunchWindow() { return; } + const wasDragging = isDraggingRef.current; webcamPreviewDragStartRef.current = null; isDraggingRef.current = false; if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } + if (wasDragging) { + window.electronAPI?.hudOverlaySetIgnoreMouse?.(true); + } }; + const attachPreviewStreamToNode = useCallback((videoElement: HTMLVideoElement | null) => { + const previewStream = previewStreamRef.current; + if (!videoElement || !previewStream || videoElement.srcObject === previewStream) { + return; + } + + videoElement.srcObject = previewStream; + const playPromise = videoElement.play(); + if (playPromise) { + playPromise.catch(() => { + // Ignore autoplay interruptions while the preview element mounts. + }); + } + }, []); + + const setWebcamPreviewNode = useCallback( + (node: HTMLVideoElement | null) => { + webcamPreviewRef.current = node; + attachPreviewStreamToNode(node); + }, + [attachPreviewStreamToNode], + ); + + const setRecordingWebcamPreviewNode = useCallback( + (node: HTMLVideoElement | null) => { + recordingWebcamPreviewRef.current = node; + attachPreviewStreamToNode(node); + }, + [attachPreviewStreamToNode], + ); + useEffect(() => { let mounted = true; - let previewStream: MediaStream | null = null; - const initialWebcam = webcamPreviewRef.current; - const initialRecordingWebcam = recordingWebcamPreviewRef.current; const startPreview = async () => { - if ( - !shouldStreamWebcamPreview || - (!initialWebcam && !initialRecordingWebcam) - ) { + if (!shouldStreamWebcamPreview) { return; } try { - previewStream = await navigator.mediaDevices.getUserMedia({ + const previewStream = await navigator.mediaDevices.getUserMedia({ video: webcamDeviceId ? { deviceId: { exact: webcamDeviceId }, @@ -333,17 +363,9 @@ export function LaunchWindow() { return; } - [initialWebcam, initialRecordingWebcam] - .filter((node): node is HTMLVideoElement => Boolean(node)) - .forEach((videoElement) => { - videoElement.srcObject = previewStream; - const playPromise = videoElement.play(); - if (playPromise) { - playPromise.catch(() => { - // Ignore autoplay interruptions while the preview element mounts. - }); - } - }); + previewStreamRef.current = previewStream; + attachPreviewStreamToNode(webcamPreviewRef.current); + attachPreviewStreamToNode(recordingWebcamPreviewRef.current); } catch (error) { console.warn("Failed to start live webcam preview:", error); } @@ -353,15 +375,16 @@ export function LaunchWindow() { return () => { mounted = false; - [initialWebcam, initialRecordingWebcam] + [webcamPreviewRef.current, recordingWebcamPreviewRef.current] .filter((node): node is HTMLVideoElement => Boolean(node)) .forEach((videoElement) => { videoElement.pause(); videoElement.srcObject = null; }); - previewStream?.getTracks().forEach((track) => track.stop()); + previewStreamRef.current?.getTracks().forEach((track) => track.stop()); + previewStreamRef.current = null; }; - }, [shouldStreamWebcamPreview, webcamDeviceId]); + }, [attachPreviewStreamToNode, shouldStreamWebcamPreview, webcamDeviceId]); useEffect(() => { let timer: NodeJS.Timeout | null = null; @@ -614,10 +637,10 @@ export function LaunchWindow() { const type = s.sourceType ?? (isWindow ? "window" : "screen"); let displayName = s.name; let appName = s.appName; - if (isWindow && !appName && s.name.includes(" — ")) { - const parts = s.name.split(" — "); + if (isWindow && !appName && s.name.includes(" ? ")) { + const parts = s.name.split(" ? "); appName = parts[0]?.trim(); - displayName = parts.slice(1).join(" — ").trim() || s.name; + displayName = parts.slice(1).join(" ? ").trim() || s.name; } else if (isWindow && s.windowTitle) { displayName = s.windowTitle; } @@ -1117,7 +1140,7 @@ export function LaunchWindow() {