* 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
@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
Related packages
@hyperframes/engine— rendering engine that drives the browser@hyperframes/producer— full render pipeline (capture + encode)hyperframes— CLI