diff --git a/packages/producer/src/services/htmlCompiler.test.ts b/packages/producer/src/services/htmlCompiler.test.ts
index edfb63a8b0..95b49a8d56 100644
--- a/packages/producer/src/services/htmlCompiler.test.ts
+++ b/packages/producer/src/services/htmlCompiler.test.ts
@@ -1224,17 +1224,19 @@ describe("template-wrapped sub-composition media offsets", () => {
const compiled = await compileForRender(projectDir, indexPath, projectDir);
+ // The 4s clip closes with its 2s host (data-start 2 + data-duration 2),
+ // not at its own authored end.
expect(compiled.videos).toHaveLength(1);
expect(compiled.videos[0]).toMatchObject({
id: "scene-video",
start: 2,
- end: 6,
+ end: 4,
});
expect(compiled.audios).toHaveLength(1);
expect(compiled.audios[0]).toMatchObject({
id: "scene-video-audio",
start: 2,
- end: 6,
+ end: 4,
});
});
diff --git a/packages/producer/src/services/renderMediaCollector.test.ts b/packages/producer/src/services/renderMediaCollector.test.ts
index 67e52049fe..f06a269c59 100644
--- a/packages/producer/src/services/renderMediaCollector.test.ts
+++ b/packages/producer/src/services/renderMediaCollector.test.ts
@@ -17,6 +17,25 @@ describe("collectRenderMedia host windows", () => {
expect(videos.find((v) => v.id === "blue")).toMatchObject({ start: 2, end: 4 });
});
+ it("closes a host authored with data-duration but no data-end", () => {
+ // A slot shortened to 2s over a 4s scene file: the runtime hides the
+ // scene's descendants past 2s, so the planner must stop its media there.
+ const html =
+ `
` +
+ ``;
+
+ const { videos, audios } = collectRenderMedia(html);
+ expect(audios.find((a) => a.id === "hook-sound")).toMatchObject({ start: 0, end: 2 });
+ expect(videos.find((v) => v.id === "late")).toBeUndefined();
+ // A host whose start is an id-ref is bounded at resolved start + duration.
+ expect(audios.find((a) => a.id === "body-sound")).toMatchObject({ start: 2, end: 4 });
+ });
+
it("preserves an explicitly marked legacy-global media window", () => {
const html =
`` +
@@ -27,7 +46,8 @@ describe("collectRenderMedia host windows", () => {
const { videos, audios } = collectRenderMedia(html);
expect(videos.find((video) => video.id === "local")).toMatchObject({ start: 4, end: 6 });
expect(videos.find((video) => video.id === "global")).toMatchObject({ start: 2, end: 4 });
- expect(audios.find((audio) => audio.id === "local-audio")).toMatchObject({ start: 4, end: 0 });
- expect(audios.find((audio) => audio.id === "global-audio")).toMatchObject({ start: 2, end: 0 });
+ // Open-ended audio tracks close with the host (data-start 2 + data-duration 6).
+ expect(audios.find((audio) => audio.id === "local-audio")).toMatchObject({ start: 4, end: 8 });
+ expect(audios.find((audio) => audio.id === "global-audio")).toMatchObject({ start: 2, end: 8 });
});
});
diff --git a/packages/producer/src/services/renderMediaCollector.ts b/packages/producer/src/services/renderMediaCollector.ts
index db890615ce..ec7c2c2c20 100644
--- a/packages/producer/src/services/renderMediaCollector.ts
+++ b/packages/producer/src/services/renderMediaCollector.ts
@@ -41,6 +41,19 @@ import {
*/
const COMPOSITION_HOST_ATTR = "data-composition-file";
+/**
+ * Where a composition host closes, in its parent's time. `data-end` wins; a
+ * host authored with only `data-duration` closes at start + duration — the
+ * same window the runtime uses to hide the host's descendants, so nested
+ * media stops with the scene instead of running to the scene file's end.
+ */
+function resolveHostEnd(host: Element, hostStart: number): number | null {
+ const end = parseNumeric(host.getAttribute("data-end"));
+ if (end != null) return end;
+ const duration = parseNumeric(host.getAttribute("data-duration"));
+ return duration == null ? null : hostStart + duration;
+}
+
interface HostWindow {
/** Seconds to add to a descendant's authored, scene-relative start. */
offset: number;
@@ -84,7 +97,7 @@ function resolveHostWindow(
// parentElement walks leaf → root; the offsets accumulate root → leaf.
for (const host of hosts.reverse()) {
const hostStart = resolveReferencedStart(document, host, startCache, visiting);
- const hostEnd = parseNumeric(host.getAttribute("data-end"));
+ const hostEnd = resolveHostEnd(host, hostStart);
if (hostEnd != null) limit = Math.min(limit, offset + hostEnd);
offset += hostStart;
}