Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/producer/src/services/htmlCompiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});

Expand Down
24 changes: 22 additions & 2 deletions packages/producer/src/services/renderMediaCollector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
`<div data-composition-file="hook.html" data-composition-id="hook" data-start="0" data-duration="2">` +
`<audio ${MEDIA_RENDER_ID_ATTR}="hook-sound" id="hook-sound" src="hook.m4a" data-start="0" data-duration="4" data-end="4"></audio>` +
`<video ${MEDIA_RENDER_ID_ATTR}="late" id="late" src="late.mp4" data-start="2.5" data-duration="1" data-end="3.5"></video>` +
`</div>` +
`<div data-composition-file="body.html" data-composition-id="body" data-start="hook" data-duration="2">` +
`<audio ${MEDIA_RENDER_ID_ATTR}="body-sound" id="body-sound" src="body.m4a" data-start="0" data-duration="4" data-end="4"></audio>` +
`</div>`;

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 =
`<div data-composition-file="scene.html" data-composition-id="scene" data-start="2" data-duration="6">` +
Expand All @@ -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 });
});
});
15 changes: 14 additions & 1 deletion packages/producer/src/services/renderMediaCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading