diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 9cc3d413..189f54bf 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -186,6 +186,7 @@ import { extendAutoFullTrackClip, type FigureData, getClipSourceEndMs, + getTimelineDurationMs, type Padding, mapSourceTimeToTimelineTime as resolveSourceTimeToTimelineTime, mapTimelineTimeToSourceTime as resolveTimelineTimeToSourceTime, @@ -3418,6 +3419,10 @@ export default function VideoEditor() { () => mapSourceTimeToTimelineTime(currentTime * 1000) / 1000, [currentTime, mapSourceTimeToTimelineTime], ); + const timelineDuration = useMemo( + () => getTimelineDurationMs(clipRegions, duration * 1000) / 1000, + [clipRegions, duration], + ); // Merge clip speeds into speed regions so playback + export respect per-clip speed const effectiveSpeedRegions = useMemo(() => { @@ -6549,14 +6554,17 @@ export default function VideoEditor() { handleSeek( next ? next.time / 1000 - : Math.min(duration, timelinePlayheadTime + 5), + : Math.min( + timelineDuration, + timelinePlayheadTime + 5, + ), ); }} > - {formatTime(duration)} + {formatTime(timelineDuration)} @@ -6642,7 +6650,7 @@ export default function VideoEditor() { { it("extends the default full-track clip when metadata duration grows", () => { @@ -157,3 +158,23 @@ describe("clip timeline mapping", () => { expect(deriveNextId("clip", clipsFromTrims.map((clip) => clip.id))).toBe(4); }); }); + +describe("getTimelineDurationMs", () => { + it("extends the timeline when a slow clip becomes longer than the source duration", () => { + expect( + getTimelineDurationMs( + [{ id: "clip-1", startMs: 0, endMs: 20_000, speed: 0.5 }], + 10_000, + ), + ).toBe(20_000); + }); + + it("keeps the source duration when speed edits make clips shorter", () => { + expect( + getTimelineDurationMs( + [{ id: "clip-1", startMs: 0, endMs: 5_000, speed: 2 }], + 10_000, + ), + ).toBe(10_000); + }); +}); diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index 18ef9a1c..92f84fd6 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -177,6 +177,18 @@ export function getClipSourceEndMs(clip: ClipRegion): number { return Math.round(clip.startMs + displayDurationMs * speed); } +export function getTimelineDurationMs(clips: ClipRegion[], sourceDurationMs: number): number { + const baseDurationMs = Math.max(0, Math.round(sourceDurationMs)); + if (clips.length === 0) { + return baseDurationMs; + } + + return clips.reduce( + (durationMs, clip) => Math.max(durationMs, Math.max(0, Math.round(clip.endMs))), + baseDurationMs, + ); +} + export function sortClipRegions(clips: ClipRegion[]): ClipRegion[] { return [...clips].sort((left, right) => left.startMs - right.startMs); }