Skip to content

fix(producer): bound nested media by a host's data-duration when it has no data-end - #3975

Open
miga-heygen wants to merge 1 commit into
mainfrom
fix/host-data-duration-bounds-nested-media
Open

miga-heygen wants to merge 1 commit into
mainfrom
fix/host-data-duration-bounds-nested-media

Conversation

@miga-heygen

Copy link
Copy Markdown
Contributor

What

The render-media planner now closes a composition host (data-composition-file element) at the same window the runtime uses to hide its descendants. resolveHostEnd in packages/producer/src/services/renderMediaCollector.ts delegates to the runtime's own resolveAuthoredTimingWindow from @hyperframes/core: a positive data-duration closes the host at start + duration; data-end applies only when data-duration is absent or non-positive, and only when it lies past the host start.

Why

The compiler only stamps data-end on media tags, so a host authored with just data-duration was unbounded for the media planner. Audio and video nested in a shortened scene kept playing over the next scene (#3964).

The first cut of this fix read data-end first and data-duration second, which is the reverse of the runtime's precedence (resolveDurationForElement in packages/core/src/runtime/startResolver.ts). It also treated data-duration="0" as a real duration, which collapsed the host window to its start and dropped every nested clip. Sharing the runtime's resolver instead of re-implementing the rule removes both divergences and the local parseNumeric copy (parseFloat vs the runtime's Number parsing).

How

  • resolveHostEnd returns resolveAuthoredTimingWindow({ start, duration, end })?.end ?? null.
  • resolveHostWindow doc block rewritten to describe the shared rule.
  • Tests in renderMediaCollector.test.ts:
    • host with data-duration and no data-end bounds nested media (including an id-ref data-start);
    • conflicting data-start=2 data-duration=2 data-end=6 bounds nested media at 4 (duration wins);
    • data-duration="0" and "-1" are treated as absent: unbounded when there is no data-end, otherwise fall back to data-end; nested clips are not dropped;
    • data-end at or before the host start is ignored rather than collapsing the window.
  • htmlCompiler.test.ts: the template-wrapped sub-composition test now expects the 4s clip to close with its 2s host (end: 4, was 6).

Verification

  • bun test packages/producer/src/services/renderMediaCollector.test.ts — 7 pass.
  • bun test packages/producer/src/services/htmlCompiler.test.ts — 135 pass.
  • bun run --cwd packages/producer typecheck — clean; oxlint and oxfmt --check clean on the touched files.
  • Mutation checks: reverting to end-first precedence fails the conflicting-attribute test; removing the non-positive guard fails both it.each cases; reading data-end raw fails the end-before-start test.

Closes #3964

Supersedes #3965 — carries valeriangalliat's fix verbatim with signed commits so it can merge under the repo's signed-commit ruleset; adds duration-first precedence to match the runtime and a non-positive-duration guard, with tests.

— Miga

…as no data-end

The runtime hides a slot's descendants at data-start + data-duration, and
the compiler only stamps data-end on media tags, so a composition host
authored with data-duration was unbounded for the media planner: audio
and video nested in a shortened scene kept playing over the next scene.
The template-wrapped compile test asserted that unbounded end (a 4s clip
in a 2s slot ending at 6); it now expects the slot end.

The host end now comes from the runtime's own resolveAuthoredTimingWindow
rather than a local re-implementation, so precedence matches the runtime
exactly: a positive data-duration wins over a conflicting data-end, and a
zero or negative data-duration is treated as absent (falling back to
data-end, or unbounded) instead of collapsing the host window to its start
and dropping every nested clip. Tests cover the conflicting-attribute case
and the non-positive-duration fallback.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at a1f32d44 — planner's host-window resolver now delegates to the runtime's own; divergence removed by construction

Clean fix. 3 files, +96/-14, one commit co-authored by valeriangalliat + miguel-heygen (bot-carrier repackage of #3965 with signed commits to satisfy the repo's signed-commit ruleset).

Head verified: a1f32d4414d2e340b5683359d87aa3f99db12b26.

The change is the right shape

resolveHostEnd in packages/producer/src/services/renderMediaCollector.ts:44-56 now returns resolveAuthoredTimingWindow({start, duration, end})?.end ?? null, imported from @hyperframes/core. That is the same resolver the runtime uses for resolveDurationForElement — so the planner's host-close window and the runtime's descendant-hide window cannot drift by construction. The previous local parseNumeric (Number.parseFloat) is deleted alongside the divergent read-order (data-end first) and the data-duration="0" mishandling (which collapsed the window to hostStart and dropped every nested clip).

The docstring update on resolveHostWindow (:72-81) correctly cites the shared resolver as the new source-of-truth for the host end, and the doc block on resolveHostEnd names all three fold rules: positive-duration wins over conflicting end, non-positive-duration falls back to end, and end-before-start is ignored.

Test coverage matches the fix rules exactly

Four new tests in renderMediaCollector.test.ts:20-90 pin every rule the fix relies on:

  • closes a host authored with data-duration but no data-end — the primary scenario (#3964): audio in a 2s-slot-over-4s-scene closes at 2s not 4s; a video whose own window is entirely after 2s is dropped from the plan; an id-ref data-start="hook" chains correctly.
  • prefers data-duration over a conflicting data-end, like the runtime — precedence test: data-start=2 data-duration=2 data-end=6 → close at 4 (duration wins).
  • it.each(["0", "-1"]) — non-positive-duration fallback: unbounded when no data-end, else fall back to data-end; nested clips are NOT dropped.
  • ignores a data-end at or before the host start — end-before-start is treated as absent; window stays unbounded rather than collapsing.

The amended existing test in the same file (preserves an explicitly marked legacy-global media window) correctly reflects the new behavior: open-ended audio tracks close with the host (start 2 + duration 6end 8) rather than staying unbounded. The amended htmlCompiler.test.ts:1229-1238 template-wrapped sub-composition test correctly expects the 4s clip to close at end: 4 with its 2s host (was end: 6, which was the unbounded-scene-file bug).

Mutation checks (per PR body, verified by test shape)

  • Reverting to end-first precedence → the prefers data-duration over conflicting data-end test fails (asserts end: 4, mutation would give 6).
  • Removing the non-positive-duration guard → both it.each cases fail (open-clip would collapse to hostStart instead of staying unbounded at end: 4; capped-clip would collapse instead of falling back to end: 3).
  • Reading data-end raw without the start-past-end guard → the ignores a data-end at or before the host start test fails (window would collapse from unbounded to 1, dropping the clip that ends at 5).

Each mutation → distinct red test. Coverage is honest.

CI

At head: 43/43 checks green, zero pending, zero failing. reviewDecision: REVIEW_REQUIRED reflects that this stamp is the one that clears the gate.

Substance is Val's fix (valeriangalliat) tightened for runtime-precedence parity and non-positive-duration handling, co-authored with Miguel. Miga carries it as a signed-commit repackage of #3965. Ship it.

— Review by tai (pr-review)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

render: a composition slot's data-duration does not bound the media nested inside it (audio plays past the slot)

3 participants