mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 11:16:27 +00:00
* fix(player): inject runtime immediately for nested compositions Compositions that use `data-composition-src` on child elements require the HyperFrames runtime to load those scenes — there is no way for the iframe to render without it. The existing probe loop delayed runtime injection behind a 5-tick attempts gate so the adapter path could try to resolve a timeline first. For nested compositions that race lost: a composition like the `product-promo` registry example registers an inline pre-runtime GSAP timeline at `window.__timelines["main"]` (covering only a partial duration, e.g. 14s of a 20s master) while the iframe document loads. The probe's adapter check finds that timeline and locks the player into a "ready" state against it — which short-circuits the attempts gate and the runtime never gets injected. The iframe ends up blank because the runtime is what would have loaded the child scenes via `data-composition-src`. This change splits the injection decision into a pure helper, `shouldInjectRuntime(state)`, and treats nested compositions as "inject immediately, skip the gate." Self-contained GSAP-only compositions retain the 5-tick grace period so the adapter path keeps first shot for them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(core): propagate play/pause to all sibling timelines Pausing or playing the master timeline only called `.pause()` / `.play()` on `state.capturedTimeline` — the single adapter-selected timeline. In a nested composition (a master with `data-composition-src` children), each scene's own timeline is registered as a sibling in `window.__timelines`, so they would keep advancing after the user clicked pause. The player UI froze at the paused time while the visual content continued to animate, eventually finishing all scene-level animations and landing on an empty end-state. Wire `window.__timelines` into the runtime player via a new `getTimelineRegistry` dep, iterate the registry on play/pause, and forward `timeScale` to siblings when play() starts so a changed playback-rate applies uniformly. Covered by 7 new unit tests in player.test.ts, including the identity- equality check (don't double-invoke the master), playbackRate propagation, a broken-sibling swallow, and a back-compat case with no registry supplied. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
39 lines
1.7 KiB
TypeScript
39 lines
1.7 KiB
TypeScript
/**
|
|
* Decide whether the player should inject the HyperFrames runtime on the
|
|
* current probe tick.
|
|
*
|
|
* The player polls the loaded iframe every 200ms to discover either:
|
|
* - a runtime bridge already installed (`window.__hf` / `window.__player`), or
|
|
* - GSAP timelines registered at `window.__timelines`.
|
|
*
|
|
* Two classes of composition require different injection timing:
|
|
*
|
|
* Nested — the composition uses `data-composition-src` on child elements to
|
|
* lazy-load sub-scenes. The runtime is what loads those children, so the
|
|
* composition cannot possibly render on its own. We inject immediately; if
|
|
* we waited, an inline pre-runtime `gsap.timeline` (common for authoring a
|
|
* preview before the runtime rebuilds the master timeline) would register
|
|
* at `__timelines["main"]` with a partial duration, and the adapter path
|
|
* would then lock the player into `ready` against that incomplete timeline.
|
|
*
|
|
* Self-contained — the composition has no nested scenes and ships all of
|
|
* its animation inline (timelines registered under `__timelines`). These
|
|
* don't strictly need the runtime; the adapter can drive them directly.
|
|
* We give the adapter path first shot (a 5-tick grace period) and only
|
|
* inject the runtime as a fallback if no adapter emerges.
|
|
*/
|
|
export interface ProbeState {
|
|
hasRuntime: boolean;
|
|
hasTimelines: boolean;
|
|
hasNestedCompositions: boolean;
|
|
runtimeInjected: boolean;
|
|
attempts: number;
|
|
}
|
|
|
|
export function shouldInjectRuntime(state: ProbeState): boolean {
|
|
if (state.hasRuntime || state.runtimeInjected) return false;
|
|
if (state.hasNestedCompositions) return true;
|
|
if (state.hasTimelines && state.attempts >= 5) return true;
|
|
return false;
|
|
}
|