Merge pull request #586 from webadderallorg/fix/editor-refresh-companion-audio-preview

fix(editor): refresh companion audio preview
This commit is contained in:
Phạm Thị Minh Hồng
2026-05-25 00:33:11 +07:00
committed by GitHub
4 changed files with 57 additions and 18 deletions
+23 -9
View File
@@ -501,6 +501,7 @@ export default function VideoEditor() {
>({});
const [defaultSourceAudioTrackSettings, setDefaultSourceAudioTrackSettings] =
useState<SourceAudioTrackSettings>({});
const [sourceAudioFallbackRefreshKey, setSourceAudioFallbackRefreshKey] = useState(0);
const [hasClipSourceAudio, setHasClipSourceAudio] = useState(false);
const [autoCaptions, setAutoCaptions] = useState<CaptionCue[]>([]);
const [autoCaptionSettings, setAutoCaptionSettings] = useState<AutoCaptionSettings>(
@@ -2298,26 +2299,29 @@ 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,
}));
setSourceAudioFallbackRefreshKey((key) => key + 1);
});
}, [videoSourcePath]);
@@ -3173,6 +3177,7 @@ export default function VideoEditor() {
duration,
isPlaying,
previewVolume,
sourceAudioFallbackRefreshKey,
summarizeErrorMessage,
onSourceFallbackLoadError: (error) => {
toast.warning(
@@ -3182,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;
@@ -3190,7 +3204,7 @@ export default function VideoEditor() {
if (!video.paused && !video.ended) {
playback.pause();
} else {
playback.play().catch((err) => console.error("Video play failed:", err));
startPlayback();
}
}
@@ -3901,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();
}
@@ -3911,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)) {
@@ -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<Promise<void> | null>(null);
const lastSourceAudioSyncTimeRef = useRef<number | null>(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 };
}
@@ -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<string[]>([]);
@@ -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 };
}
@@ -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,