Files
hyperframes/packages/core
Miguel Ángel 2de3ca5f86 fix(player): the interactive playback clock absorbs a main-thread stall instead of skipping past it (#4053)
* fix(player): the interactive playback clock absorbs a main-thread stall instead of skipping past it

TransportClock's monotonic time is a bare wall-clock read with no
protection against a long gap between reads. On a composition whose
first real paint blocks the main thread for several seconds, nothing
reads the clock during that time; the next read reported the whole
gap as elapsed playback, so the composition jumped forward instead of
continuing from where it froze.

A gap over 500ms is now treated as a stall and corrected to a 33ms
advance instead of reported whole, matching GSAP's own ticker default.
The correction lives inside now() itself so every reader benefits,
since this clock is read every real frame by more than one driver.
Render/export capture is unaffected: it always pauses and seeks to an
exact time rather than playing, so the correction never engages there.

* chore(player): drop internal tracker references from stall-policy comments

* fix(player): stop the stall correction from corrupting the clock while audio is authoritative

An independent review found that the previous commit's correction ran
unconditionally on every read, including while an attached audio
source was providing the time. Audio keeps playing through a
JS-thread stall, so a gap between reads there is not a stall — but the
correction still shifted the clock's internal reference point, and
that corruption only surfaced later, on an unrelated read, once the
clock fell back to monotonic timing (audio pausing or buffering).

The correction now runs only in the monotonic branch, after audio has
had its chance to answer. Added a regression test that fails against
the previous placement and passes against this one.

* fix(player): keep the stall correction's read timestamp fresh through an audio-authoritative stretch

A second independent review found the previous commit's fix was
incomplete: it stopped a stall correction from firing while audio was
answering a read, but the internal timestamp it compares against only
moved on monotonic reads. Any monotonic read from before audio took
over stayed the comparison point for as long as audio kept answering,
so the whole audio-authoritative stretch read as one giant stall the
moment audio dropped out again — reproducing the exact corruption the
previous commit set out to fix, through a different door. The same
gap also meant a real, non-stalled audio outage got smoothed away to
almost nothing instead of advancing normally.

The read timestamp now moves on every read, audio-answered or not;
only the correction itself stays scoped to the monotonic branch. Two
regression tests reproduce both failure modes and pass here; both were
confirmed to fail against the prior placement.

* fix(player): stop comparing a monotonic read against a timestamp audio made meaningless

A third review found the previous commit's fix was still incomplete:
keeping the read timestamp fresh during audio-authoritative reads
stops it going stale, but a real gap with no reads at all — the main
thread genuinely frozen while an attached audio element kept playing
on its own, unaffected by JS scheduling — still read as a stall the
moment audio next failed to answer, discarding real elapsed time that
audio had been correctly tracking the whole way.

An audio-answered read now clears the timestamp instead of stamping
it, the same as a seek. The next monotonic read then starts from
`_playStartMs`, which nothing but a genuine discontinuity (play, seek,
rate change, detach) ever touches — an anchor that already reconstructs
real elapsed time correctly on its own, without needing the correction
to reason about a comparison point it has no way to make meaningful
once audio was involved. A stall between two purely monotonic reads,
with no audio in between, is unaffected and still corrected exactly as
before.

Two regression tests cover the transition: a real unread gap while
audio was tracking correctly must not be discarded, and a sustained
audio outage must still advance at real speed rather than being
smoothed to nothing. Both were confirmed to fail against the prior
commit's placement.

* chore(core): trim clock comments
2026-09-18 01:50:05 -04:00
..
2026-09-17 01:48:54 -04:00

@hyperframes/core

Types, parsers, generators, compiler, linter, runtime, and frame adapters for the Hyperframes video framework.

Install

npm install @hyperframes/core

Most users don't need to install core directly — the CLI, producer, and studio packages depend on it internally.

What's inside

Module Description
Types TimelineElement, CompositionSpec, Asset, canvas dimensions, defaults
Parsers parseHtml — extract timeline elements from HTML; parseGsapScript — parse GSAP animations
Generators generateHyperframesHtml — produce valid Hyperframes HTML from a composition spec
Compiler compileTimingAttrs — resolve data-start / data-duration into absolute times
Linter lintHyperframeHtml — validate Hyperframes HTML (missing attributes, overlapping tracks, etc.)
Runtime IIFE script injected into the browser — manages seek, media playback, and the window.__hf protocol
Frame Adapters Pluggable animation drivers (GSAP, Lottie, CSS, or custom)

Generated composition trust

Composition generators require trusted authors for code-bearing inputs. styles and generateHyperframesStyles preserve authored CSS, which can load external resources. animations may contain __raw: values that are emitted as JavaScript; includeScripts: true includes executable timeline code. serializeGsapAnimations also accepts raw preamble, postamble, and a code-bearing timelineVar. Never fill these inputs with untrusted data. Attribute encoding and closing-tag containment are not a sandbox; render untrusted compositions in an appropriately isolated execution environment and never serve them on a privileged origin.

Text content retains the supported inline-formatting sanitizer contract. The clip parser intentionally flattens inner formatting to text, so parse/generate is not a lossless replacement for editing the source HTML.

Frame Adapters

A frame adapter tells the engine how to seek your animation to a specific frame:

import { createGSAPFrameAdapter } from "@hyperframes/core";

const adapter = createGSAPFrameAdapter({
  getTimeline: () => gsap.timeline(),
  compositionId: "my-video",
});

Implement FrameAdapter for custom animation runtimes:

import type { FrameAdapter } from "@hyperframes/core";

const myAdapter: FrameAdapter = {
  id: "my-adapter",
  getDurationFrames: () => 300,
  seekFrame: (frame) => {
    /* seek your animation */
  },
};

Parsing and generating HTML

import { parseHtml, generateHyperframesHtml } from "@hyperframes/core";

const { elements, metadata } = parseHtml(htmlString);
const html = generateHyperframesHtml(spec);

Linting

import { lintHyperframeHtml } from "@hyperframes/core/lint";

const result = lintHyperframeHtml(htmlString);
// result.findings: { severity, message, elementId }[]

Documentation

Full documentation: hyperframes.heygen.com/packages/core