diff --git a/src/components/video-editor/audio/useAudioPreviewSync.ts b/src/components/video-editor/audio/useAudioPreviewSync.ts index c00ff9f6d..1ede8b3b1 100644 --- a/src/components/video-editor/audio/useAudioPreviewSync.ts +++ b/src/components/video-editor/audio/useAudioPreviewSync.ts @@ -6,6 +6,7 @@ import { enablePitchPreservingPlayback, estimateCompanionAudioStartDelaySeconds, getMediaSyncPlaybackRate, + resolvePreviewMediaDuration, } from "@/lib/mediaTiming"; import type { AudioRegion, SpeedRegion } from "../types"; @@ -381,7 +382,7 @@ export function useAudioPreviewSync({ audio.volume = Math.max(0, Math.min(1, getSourceTrackPreviewGain(sourceAudioPath) * (isCurrentClipMuted ? 0 : previewVolume))); enablePitchPreservingPlayback(audio); - const audioDuration = Number.isFinite(audio.duration) ? audio.duration : null; + const audioDuration = resolvePreviewMediaDuration(audio.duration, duration); const isMicCompanionTrack = /\.mic\./i.test(sourceAudioPath); const rawStartDelaySeconds = estimateCompanionAudioStartDelaySeconds( duration, diff --git a/src/lib/mediaTiming.test.ts b/src/lib/mediaTiming.test.ts index 4d6716ed2..167896e4b 100644 --- a/src/lib/mediaTiming.test.ts +++ b/src/lib/mediaTiming.test.ts @@ -7,6 +7,7 @@ import { getEffectiveRecordingDurationMs, getEffectiveVideoStreamDurationSeconds, getMediaSyncPlaybackRate, + resolvePreviewMediaDuration, } from "./mediaTiming"; describe("clampMediaTimeToDuration", () => { @@ -21,6 +22,30 @@ describe("clampMediaTimeToDuration", () => { }); }); +describe("resolvePreviewMediaDuration", () => { + it("keeps reported durations that approximately match the timeline", () => { + expect(resolvePreviewMediaDuration(120, 120)).toBe(120); + expect(resolvePreviewMediaDuration(119.25, 120)).toBe(119.25); + }); + + it("treats non-finite or missing reported durations as unknown", () => { + expect(resolvePreviewMediaDuration(Number.POSITIVE_INFINITY, 120)).toBeNull(); + expect(resolvePreviewMediaDuration(Number.NaN, 120)).toBeNull(); + expect(resolvePreviewMediaDuration(null, 120)).toBeNull(); + }); + + it("treats implausibly short reported durations as unknown", () => { + expect(resolvePreviewMediaDuration(20, 120)).toBeNull(); + expect(resolvePreviewMediaDuration(0, 120)).toBeNull(); + }); + + it("falls back to the reported duration when the timeline duration is unknown", () => { + expect(resolvePreviewMediaDuration(20, Number.NaN)).toBe(20); + expect(resolvePreviewMediaDuration(20, null)).toBe(20); + expect(resolvePreviewMediaDuration(Number.NaN, Number.NaN)).toBeNull(); + }); +}); + describe("estimateCompanionAudioStartDelaySeconds", () => { it("keeps small inferred offsets when the companion audio is only slightly shorter", () => { expect(estimateCompanionAudioStartDelaySeconds(10, 9.6)).toBeCloseTo(0.4); diff --git a/src/lib/mediaTiming.ts b/src/lib/mediaTiming.ts index 38467098d..aeb96ef9b 100644 --- a/src/lib/mediaTiming.ts +++ b/src/lib/mediaTiming.ts @@ -7,6 +7,47 @@ export function clampMediaTimeToDuration(targetTime: number, duration?: number | return Math.max(0, Math.min(safeTargetTime, Math.max(0, duration))); } +const MIN_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS = 0.5; +const MAX_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS = 1; +const PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_RATIO = 0.01; + +export function resolvePreviewMediaDuration( + reportedDuration?: number | null, + timelineDuration?: number | null, +): number | null { + if (!Number.isFinite(reportedDuration)) { + return null; + } + + const safeReportedDuration = Math.max(0, reportedDuration ?? 0); + if ( + !Number.isFinite(timelineDuration) || + timelineDuration === null || + timelineDuration === undefined + ) { + return safeReportedDuration; + } + + const safeTimelineDuration = Math.max(0, timelineDuration); + if (safeTimelineDuration === 0) { + return safeReportedDuration; + } + + const shortfallToleranceSeconds = Math.min( + MAX_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS, + Math.max( + MIN_PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_SECONDS, + safeTimelineDuration * PREVIEW_MEDIA_DURATION_SHORTFALL_TOLERANCE_RATIO, + ), + ); + + if (safeTimelineDuration - safeReportedDuration > shortfallToleranceSeconds) { + return null; + } + + return safeReportedDuration; +} + const MIN_COMPANION_AUDIO_DELAY_SECONDS = 0.025; const MAX_INFERRED_COMPANION_AUDIO_DELAY_SECONDS = 0.5;