The runtime re-derived the composition's total duration on every animation
frame. transportTick called getSafeTimelineDurationSeconds unconditionally,
and deriving it scans every media element in the document and walks each
one's composition ancestry to resolve an absolute start and duration.
That value is a function of the composition, not of the playhead, so a
paused editor with nothing happening recomputed the same answer ~60 times a
second. On a 91-media-element composition the media scan ran 1.05 times per
frame while the editor sat idle and untouched.
Derive it once and reuse it until an input could have changed. The inputs,
and the signal that catches each:
- timing attributes edited (live editing, variables re-applied, the
runtime's own autostamping) -> MutationObserver with an attribute filter
- timed elements added or removed, including nested compositions that
mount asynchronously after init -> the same observer, childList+subtree
- media metadata arriving, or el.load() resetting duration to NaN, neither
of which mutates the DOM -> capture-phase media event listeners
- a timeline registered or lengthened in window.__timelines, a plain
object nothing can observe -> a registry signature compared on read
Observer records are delivered in a microtask, so the queue is drained on
read as well; otherwise an edit read back in the same synchronous block, the
ordinary live-editing case, would be served the pre-edit value.
The render path does not read the cache at all. Capture depends on the exact
duration and a frame rendered against a wrong one cannot be recovered, so it
pays the full derivation on every frame exactly as before.
Measured on the 91-media-element composition, idle and untouched: media
scans per frame 1.05 -> 0.05. Across 100 single-frame seeks: 2.28 -> 0.11
per seek, so seeking does not invalidate the cache either. The residual is
a separate per-20-tick caller in timeline.ts, untouched here.
@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