From 9b3270bc08b740e7f3777cb14253bd6cad67baa3 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Mon, 25 May 2026 00:20:42 +0700 Subject: [PATCH 1/2] fix(editor): refresh companion audio preview --- src/components/video-editor/VideoEditor.tsx | 4 +++ .../video-editor/audio/useAudioPreviewSync.ts | 29 +++++++++++++++---- .../audio/useSourceAudioFallback.ts | 6 +++- .../video-editor/audio/useVideoEditorAudio.ts | 8 +++-- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 89d5457eb..2e39b09c8 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -501,6 +501,7 @@ export default function VideoEditor() { >({}); const [defaultSourceAudioTrackSettings, setDefaultSourceAudioTrackSettings] = useState({}); + const [sourceAudioFallbackRefreshKey, setSourceAudioFallbackRefreshKey] = useState(0); const [hasClipSourceAudio, setHasClipSourceAudio] = useState(false); const [autoCaptions, setAutoCaptions] = useState([]); const [autoCaptionSettings, setAutoCaptionSettings] = useState( @@ -2318,6 +2319,7 @@ export default function VideoEditor() { ? (session.timeOffsetMs ?? prev.timeOffsetMs) : DEFAULT_WEBCAM_TIME_OFFSET_MS, })); + setSourceAudioFallbackRefreshKey((key) => key + 1); }); }, [videoSourcePath]); @@ -3173,6 +3175,7 @@ export default function VideoEditor() { duration, isPlaying, previewVolume, + sourceAudioFallbackRefreshKey, summarizeErrorMessage, onSourceFallbackLoadError: (error) => { toast.warning( @@ -3190,6 +3193,7 @@ export default function VideoEditor() { if (!video.paused && !video.ended) { playback.pause(); } else { + audio.playSourceAudioPreview(); playback.play().catch((err) => console.error("Video play failed:", err)); } } diff --git a/src/components/video-editor/audio/useAudioPreviewSync.ts b/src/components/video-editor/audio/useAudioPreviewSync.ts index 3ff9d1164..c00ff9f6d 100644 --- a/src/components/video-editor/audio/useAudioPreviewSync.ts +++ b/src/components/video-editor/audio/useAudioPreviewSync.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { buildResolvedAudioPlan } from "@/lib/exporter/audioRoutingEngine"; import { resolveMediaElementSource } from "@/lib/exporter/localMediaSource"; import { @@ -72,7 +72,7 @@ export function useAudioPreviewSync({ const sourceAudioResumePromiseRef = useRef | null>(null); const lastSourceAudioSyncTimeRef = useRef(null); - const ensureSourceAudioContext = () => { + const ensureSourceAudioContext = useCallback(() => { if (!sourceAudioContextRef.current) { const context = new AudioContext({ latencyHint: "interactive" }); const masterGain = context.createGain(); @@ -82,9 +82,9 @@ export function useAudioPreviewSync({ sourceAudioMasterGainRef.current = masterGain; } return sourceAudioContextRef.current; - }; + }, []); - const ensureSourceAudioRunning = () => { + const ensureSourceAudioRunning = useCallback(() => { const context = ensureSourceAudioContext(); if (context.state === "running") { return Promise.resolve(); @@ -98,7 +98,15 @@ export function useAudioPreviewSync({ }); } return sourceAudioResumePromiseRef.current; - }; + }, [ensureSourceAudioContext]); + + const playSourceAudioPreview = useCallback(() => { + void ensureSourceAudioRunning(); + for (const audio of sourceAudioElementsRef.current.values()) { + if (!audio.src) continue; + audio.play().catch(() => undefined); + } + }, [ensureSourceAudioRunning]); useEffect(() => { let cancelled = false; @@ -216,6 +224,10 @@ export function useAudioPreviewSync({ sourceAudioElementRevokersRef.current.set(audioPath, resolved.revoke); latestAudio.src = resolved.src; + latestAudio.load(); + if (isPlaying) { + playSourceAudioPreview(); + } } catch (error) { if (cancelled) { return; @@ -252,10 +264,12 @@ export function useAudioPreviewSync({ }; }, [ getSourceTrackPreviewGain, + isPlaying, isCurrentClipMuted, onSourceFallbackLoadError, resolvedSourceTracks, previewVolume, + playSourceAudioPreview, ]); useEffect(() => { @@ -425,6 +439,7 @@ export function useAudioPreviewSync({ previewVolume, resolvedSourceTracks, sourceAudioFallbackStartDelayMsByPath, + ensureSourceAudioRunning, ]); useEffect(() => { @@ -438,5 +453,7 @@ export function useAudioPreviewSync({ } } }); - }, [isPlaying, resolvedSourceTracks.length]); + }, [isPlaying, resolvedSourceTracks.length, ensureSourceAudioRunning]); + + return { playSourceAudioPreview }; } diff --git a/src/components/video-editor/audio/useSourceAudioFallback.ts b/src/components/video-editor/audio/useSourceAudioFallback.ts index 69be8c247..2cfbbf752 100644 --- a/src/components/video-editor/audio/useSourceAudioFallback.ts +++ b/src/components/video-editor/audio/useSourceAudioFallback.ts @@ -4,11 +4,13 @@ import { SOURCE_AUDIO_FALLBACK_TOAST_ID } from "@/components/video-editor/audio/ interface UseSourceAudioFallbackParams { currentSourcePath: string | null; + refreshKey?: number; summarizeErrorMessage: (message: string) => string; } export function useSourceAudioFallback({ currentSourcePath, + refreshKey = 0, summarizeErrorMessage, }: UseSourceAudioFallbackParams) { const [sourceAudioFallbackPaths, setSourceAudioFallbackPaths] = useState([]); @@ -17,6 +19,8 @@ export function useSourceAudioFallback({ useEffect(() => { let cancelled = false; + // Refetch when late recording sidecars are finalized after the editor opens. + void refreshKey; setSourceAudioFallbackPaths([]); setSourceAudioFallbackStartDelayMsByPath({}); @@ -62,7 +66,7 @@ export function useSourceAudioFallback({ return () => { cancelled = true; }; - }, [currentSourcePath, summarizeErrorMessage]); + }, [currentSourcePath, refreshKey, summarizeErrorMessage]); return { sourceAudioFallbackPaths, sourceAudioFallbackStartDelayMsByPath }; } diff --git a/src/components/video-editor/audio/useVideoEditorAudio.ts b/src/components/video-editor/audio/useVideoEditorAudio.ts index a82ea9412..9d61f850a 100644 --- a/src/components/video-editor/audio/useVideoEditorAudio.ts +++ b/src/components/video-editor/audio/useVideoEditorAudio.ts @@ -1,11 +1,11 @@ import React, { useMemo } from "react"; +import type { SourceAudioTrackSettings } from "@/components/video-editor/audio/audioTypes"; import { resolveSourceTrackRoutingPolicy } from "@/lib/exporter/sourceTrackRoutingPolicy"; import type { AudioRegion, ClipRegion, SpeedRegion, } from "../types"; -import type { SourceAudioTrackSettings } from "@/components/video-editor/audio/audioTypes"; import { getActiveClipIdAtSourceTime, isClipMutedById } from "./clipAudio"; import { useAudioPreviewSync } from "./useAudioPreviewSync"; import { useClipAudioSettingsController } from "./useClipAudioSettingsController"; @@ -45,6 +45,7 @@ interface UseVideoEditorAudioParams { duration: number; isPlaying: boolean; previewVolume: number; + sourceAudioFallbackRefreshKey?: number; summarizeErrorMessage: (message: string) => string; onSourceFallbackLoadError: (error: unknown) => void; } @@ -64,6 +65,7 @@ export function useVideoEditorAudio({ duration, isPlaying, previewVolume, + sourceAudioFallbackRefreshKey = 0, summarizeErrorMessage, onSourceFallbackLoadError, }: UseVideoEditorAudioParams) { @@ -75,6 +77,7 @@ export function useVideoEditorAudio({ const { sourceAudioFallbackPaths, sourceAudioFallbackStartDelayMsByPath } = useSourceAudioFallback({ currentSourcePath: fallbackLookupSourcePath, + refreshKey: sourceAudioFallbackRefreshKey, summarizeErrorMessage, }); @@ -113,7 +116,7 @@ export function useVideoEditorAudio({ setDefaultSourceAudioTrackSettings, }); - useAudioPreviewSync({ + const { playSourceAudioPreview } = useAudioPreviewSync({ audioRegions, previewVolume, isPlaying, @@ -138,6 +141,7 @@ export function useVideoEditorAudio({ sourceAudioTrackMeta, activeSourceAudioTrackSettings, selectedClipSourceAudioTrackSettings, + playSourceAudioPreview, getSourceAudioTrackSettingsForClip, onSourceAudioTracksMetaChange, onSelectedClipSourceAudioTrackVolumeChange, From fe47b07464a0ec58a8304e369d37a419cf29e1c8 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Mon, 25 May 2026 00:28:37 +0700 Subject: [PATCH 2/2] fix(editor): route companion audio resume paths --- src/components/video-editor/VideoEditor.tsx | 30 ++++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 2e39b09c8..9e80a8549 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -2299,23 +2299,25 @@ export default function VideoEditor() { } return window.electronAPI.onRecordingSessionChanged((session) => { + const sessionSourcePath = session?.videoPath ? fromFileUrl(session.videoPath) : null; + const sessionWebcamPath = session?.webcamPath ? fromFileUrl(session.webcamPath) : null; console.log("[VideoEditor] onRecordingSessionChanged received!", { hasSession: Boolean(session), hasSessionVideoPath: Boolean(session?.videoPath), hasVideoSourcePath: Boolean(videoSourcePath), - match: session?.videoPath === videoSourcePath, - hasWebcamPath: Boolean(session?.webcamPath), + match: sessionSourcePath === videoSourcePath, + hasWebcamPath: Boolean(sessionWebcamPath), }); - if (!session || session.videoPath !== videoSourcePath) { + if (!session || sessionSourcePath !== videoSourcePath) { return; } setWebcam((prev) => ({ ...prev, - enabled: Boolean(session.webcamPath), - sourcePath: session.webcamPath ?? null, - timeOffsetMs: session.webcamPath + enabled: Boolean(sessionWebcamPath), + sourcePath: sessionWebcamPath, + timeOffsetMs: sessionWebcamPath ? (session.timeOffsetMs ?? prev.timeOffsetMs) : DEFAULT_WEBCAM_TIME_OFFSET_MS, })); @@ -3185,6 +3187,15 @@ export default function VideoEditor() { }, }); + const startPlayback = useCallback(() => { + const playback = videoPlaybackRef.current; + const video = playback?.video; + if (!playback || !video) return; + + audio.playSourceAudioPreview(); + playback.play().catch((err) => console.error("Video play failed:", err)); + }, [audio.playSourceAudioPreview]); + function togglePlayPause() { const playback = videoPlaybackRef.current; const video = playback?.video; @@ -3193,8 +3204,7 @@ export default function VideoEditor() { if (!video.paused && !video.ended) { playback.pause(); } else { - audio.playSourceAudioPreview(); - playback.play().catch((err) => console.error("Video play failed:", err)); + startPlayback(); } } @@ -3905,7 +3915,7 @@ export default function VideoEditor() { const playback = videoPlaybackRef.current; if (playback?.video) { if (playback.video.paused) { - playback.play().catch(console.error); + startPlayback(); } else { playback.pause(); } @@ -3915,7 +3925,7 @@ export default function VideoEditor() { window.addEventListener("keydown", handleKeyDown, { capture: true }); return () => window.removeEventListener("keydown", handleKeyDown, { capture: true }); - }, [shortcuts, isMac, handleUndo, handleRedo]); + }, [shortcuts, isMac, handleUndo, handleRedo, startPlayback]); useEffect(() => { if (selectedZoomId && !zoomRegions.some((region) => region.id === selectedZoomId)) {