Skip to content

fix(engine,producer): parse sub-composition media inside <template> wrappers#475

Closed
MuTsunTsai wants to merge 2 commits intoheygen-com:mainfrom
MuTsunTsai:fix/sub-composition-media-offset
Closed

fix(engine,producer): parse sub-composition media inside <template> wrappers#475
MuTsunTsai wants to merge 2 commits intoheygen-com:mainfrom
MuTsunTsai:fix/sub-composition-media-offset

Conversation

@MuTsunTsai
Copy link
Copy Markdown
Contributor

Summary

Sub-compositions wrapped in <template> had their <video> / <audio> / <img> elements silently skipped during render, so the parent host's data-start offset never got applied — scene-local media played at raw scene-local time, overlapping the intro instead of following it.

Two linked fixes:

  1. parseVideoElements, parseAudioElements, parseImageElements now unwrap one level of <template> before querying the DOM.
  2. recompileWithResolutions preserves the first-pass media arrays when the inlined HTML yields no sub-composition data, instead of clobbering the already-offset values with scene-local ones.

Why

parseSubCompositions reads the raw sub-composition HTML, probes its media, and adds the parent's data-start to each clip's start/end. But those parse*Elements helpers use linkedom's querySelectorAll, which follows the browser contract of not descending into template content (it lives in a DocumentFragment). The GSAP composition guide recommends wrapping sub-composition bodies in <template>, so every composition following that convention silently parsed as zero media.

recompileWithResolutions runs a second pass after durations are resolved from the browser. By that point the HTML has already been inlined, so [data-composition-src] hosts are gone and parseSubCompositions legitimately returns empty. The old code unconditionally redid the dedupe, letting scene-local media parsed from the inlined HTML overwrite the correctly-offset media from the first pass.

What changed

  • packages/engine/src/utils/htmlTemplate.ts — new unwrapTemplate() helper.
  • packages/engine/src/services/audioMixer.tsparseAudioElements calls unwrapTemplate before parseHTML.
  • packages/engine/src/services/videoFrameExtractor.tsparseVideoElements and parseImageElements do the same.
  • packages/producer/src/services/htmlCompiler.tsrecompileWithResolutions gates the dedupe overwrite behind a hasSubMedia check.

Test plan

  • bun run --filter @hyperframes/engine test
  • bun run --filter '*' typecheck
  • bunx oxlint + bunx oxfmt --check on the edited files
  • End-to-end: render a composition with a <template>-wrapped sub-composition at data-start="4" — confirm the scene media play starting at t=4s, not t=0.

…rappers

Sub-compositions wrapped in <template> had their media silently skipped
because linkedom's querySelectorAll (like the browser) doesn't descend
into template content. Unwrap one level of <template> in parseAudio /
Video / ImageElements via a shared helper, and guard
recompileWithResolutions from clobbering first-pass offsets when the
inlined HTML yields no sub-composition media.
The browser reconciliation step copies `data-end` from the browser DOM onto
already-compiled media records when they disagree by more than a tick. For
sub-composition clips that is wrong: the compiled end was produced by
`parseSubCompositions` after adding the host's `data-start` offset, while
the browser reads the inlined DOM where the clip's `data-end` is still
scene-local. The reconciler sees the gap and replaces the offset-adjusted
end with the scene-local value, clipping the tail of every sub-composition
video/audio at render time.

Only fill `end` (and, for video, mediaStart / hasAudio) when the static
pipeline had no value — keep the compiled `end` authoritative otherwise.
Preview was already correct because it never runs this reconciliation.
@MuTsunTsai
Copy link
Copy Markdown
Contributor Author

Follow-up: also stop executeRenderJob's browser-metadata reconciliation from overwriting end / mediaStart on media that already has a compiled value. Without this, sub-composition clips render with their pre-offset scene-local end and the tail gets clipped even though parseSubCompositions computed the correct end.

@MuTsunTsai
Copy link
Copy Markdown
Contributor Author

Close as #476 fixed this.

@MuTsunTsai MuTsunTsai deleted the fix/sub-composition-media-offset branch April 24, 2026 22:47
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.

1 participant