diff --git a/packages/core/scripts/test-hyperframe-runtime-duration-guards.ts b/packages/core/scripts/test-hyperframe-runtime-duration-guards.ts index 9d68a5d35b..a1555aec97 100644 --- a/packages/core/scripts/test-hyperframe-runtime-duration-guards.ts +++ b/packages/core/scripts/test-hyperframe-runtime-duration-guards.ts @@ -25,7 +25,7 @@ assert( "init.ts should not hard-clamp safe duration to media floor", ); assert( - initSource.includes("resolveMediaWindowDurationSeconds"), + initSource.includes("longestMediaWindow"), "init.ts should compute media window duration across timed media", ); diff --git a/packages/core/src/runtime/compositionLength.test.ts b/packages/core/src/runtime/compositionLength.test.ts index 57ec301c3a..1a53370ff6 100644 --- a/packages/core/src/runtime/compositionLength.test.ts +++ b/packages/core/src/runtime/compositionLength.test.ts @@ -4,7 +4,9 @@ import { findRootCompositionElement, LOOP_INFLATED_TIMELINE_SECONDS, MIN_VALID_TIMELINE_DURATION_SECONDS, + longestMediaWindow, readStaticCompositionMeta, + resolveCompositionLength, resolveCompositionLengthSeconds, resolveContentDerivedDuration, resolveMediaWindowDurationSeconds, @@ -20,6 +22,20 @@ describe("resolveCompositionLengthSeconds", () => { derived: () => 0, }; + it("says which input set the length", () => { + expect(resolveCompositionLength({ ...base, declared: 9 }).from).toBe("declared"); + expect(resolveCompositionLength({ ...base, timeline: () => 10, floors: () => [4] }).from).toBe( + "timeline", + ); + expect(resolveCompositionLength({ ...base, floors: () => [null, 7] })).toEqual({ + seconds: 7, + from: "floor", + floorIndex: 1, + }); + expect(resolveCompositionLength({ ...base, fallback: 5 }).from).toBe("fallback"); + expect(resolveCompositionLength({ ...base, derived: () => 2 }).from).toBe("derived"); + }); + it("takes a declared length and reads nothing else", () => { const timeline = vi.fn(() => 10); const floors = vi.fn(() => [12]); @@ -147,6 +163,34 @@ describe("rules moved unchanged from the runtime", () => { expect(readStaticCompositionMeta(parseHTMLContent(html))?.durationSeconds).toBe(3); }); + it("names the clip missing its trim when its full media length sets the film's length", () => { + const doc = parseHTMLContent( + `
` + + `` + + `
`, + ); + const natural: Record = { cut: 3600, source: 5400 }; + const media = { + mediaStart: (el: HTMLMediaElement) => Number(el.getAttribute("data-start")), + mediaDuration: (el: HTMLMediaElement) => + el.hasAttribute("data-duration") + ? Number(el.getAttribute("data-duration")) + : natural[el.id]!, + }; + const longest = longestMediaWindow(doc, media); + expect(longest?.element.id).toBe("source"); + expect(longest?.seconds).toBe(5402); + expect( + resolveCompositionLength({ + declared: null, + timeline: () => 3, + floors: () => [longest?.seconds ?? null, 3], + fallback: 0, + derived: () => 0, + }), + ).toEqual({ seconds: 5402, from: "floor", floorIndex: 0 }); + }); + it("skips a media clip of one frame or less", () => { const doc = parseHTMLContent( `
`, diff --git a/packages/core/src/runtime/compositionLength.ts b/packages/core/src/runtime/compositionLength.ts index cd8a14c1cc..975829c3d8 100644 --- a/packages/core/src/runtime/compositionLength.ts +++ b/packages/core/src/runtime/compositionLength.ts @@ -34,15 +34,17 @@ export function readCompositionSize(root: Element): { }; } -/** The latest end among the document's timed video and audio, at their absolute starts. */ -export function resolveMediaWindowDurationSeconds( +type MediaTiming = { + mediaStart: (el: HTMLMediaElement) => number; + mediaDuration: (el: HTMLMediaElement) => number | null; +}; + +/** The timed `