Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -3173,6 +3177,7 @@ export default function VideoEditor() {
duration,
isPlaying,
previewVolume,
sourceAudioFallbackRefreshKey,
summarizeErrorMessage,
onSourceFallbackLoadError: (error) => {
toast.warning(
Expand All @@ -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;
Expand All @@ -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();
}
}

Expand Down Expand Up @@ -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();
}
Expand All @@ -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)) {
Expand Down
29 changes: 23 additions & 6 deletions src/components/video-editor/audio/useAudioPreviewSync.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -252,10 +264,12 @@ export function useAudioPreviewSync({
};
}, [
getSourceTrackPreviewGain,
isPlaying,
isCurrentClipMuted,
onSourceFallbackLoadError,
resolvedSourceTracks,
previewVolume,
playSourceAudioPreview,
]);

useEffect(() => {
Expand Down Expand Up @@ -425,6 +439,7 @@ export function useAudioPreviewSync({
previewVolume,
resolvedSourceTracks,
sourceAudioFallbackStartDelayMsByPath,
ensureSourceAudioRunning,
]);

useEffect(() => {
Expand All @@ -438,5 +453,7 @@ export function useAudioPreviewSync({
}
}
});
}, [isPlaying, resolvedSourceTracks.length]);
}, [isPlaying, resolvedSourceTracks.length, ensureSourceAudioRunning]);

return { playSourceAudioPreview };
}
6 changes: 5 additions & 1 deletion src/components/video-editor/audio/useSourceAudioFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]>([]);
Expand All @@ -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({});

Expand Down Expand Up @@ -62,7 +66,7 @@ export function useSourceAudioFallback({
return () => {
cancelled = true;
};
}, [currentSourcePath, summarizeErrorMessage]);
}, [currentSourcePath, refreshKey, summarizeErrorMessage]);

return { sourceAudioFallbackPaths, sourceAudioFallbackStartDelayMsByPath };
}
8 changes: 6 additions & 2 deletions src/components/video-editor/audio/useVideoEditorAudio.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -45,6 +45,7 @@ interface UseVideoEditorAudioParams {
duration: number;
isPlaying: boolean;
previewVolume: number;
sourceAudioFallbackRefreshKey?: number;
summarizeErrorMessage: (message: string) => string;
onSourceFallbackLoadError: (error: unknown) => void;
}
Expand All @@ -64,6 +65,7 @@ export function useVideoEditorAudio({
duration,
isPlaying,
previewVolume,
sourceAudioFallbackRefreshKey = 0,
summarizeErrorMessage,
onSourceFallbackLoadError,
}: UseVideoEditorAudioParams) {
Expand All @@ -75,6 +77,7 @@ export function useVideoEditorAudio({
const { sourceAudioFallbackPaths, sourceAudioFallbackStartDelayMsByPath } =
useSourceAudioFallback({
currentSourcePath: fallbackLookupSourcePath,
refreshKey: sourceAudioFallbackRefreshKey,
summarizeErrorMessage,
});

Expand Down Expand Up @@ -113,7 +116,7 @@ export function useVideoEditorAudio({
setDefaultSourceAudioTrackSettings,
});

useAudioPreviewSync({
const { playSourceAudioPreview } = useAudioPreviewSync({
audioRegions,
previewVolume,
isPlaying,
Expand All @@ -138,6 +141,7 @@ export function useVideoEditorAudio({
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
playSourceAudioPreview,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
Expand Down