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 `