Source: Common Mistakes, Troubleshooting — HeyGen HyperFrames docs

The debugging reference for HeyGen Hyperframes — the two docs pages an agent reaches for when a composition renders wrong. Common Mistakes covers the 8 authoring pitfalls the linter mostly cannot catch (each as symptom → cause → fix); Troubleshooting covers the environment/tooling/render problems it can. For the framework overview, primitives, and skill install, start at the hub; this page is the self-correction playbook.

Key Takeaways

  • Two authoring mistakes cause most broken compositions: animating <video> dimensions directly, and controlling media playback from scripts. If the video looks wrong, check those two first.
  • HyperFrames owns media playback. Never call video.play() / video.pause() / set audio.currentTime — drive timing through data-start, data-media-start, data-volume and animate only visual props with GSAP.
  • Composition length = GSAP timeline length, not video length. A short timeline truncates a long video. Extend with tl.set({}, {}, SECONDS); verify with npx hyperframes compositions.
  • The timeline key must equal data-composition-id exactly or no animation runs — the single most common “static composition” cause.
  • Two perf killers are visual, not structural: oversized source images (Chrome decodes to w×h×4 RGBA regardless of file size) and stacked backdrop-filter: blur() layers. Both stutter preview before they slow render.
  • npx hyperframes lint is the cheap pre-render gate for structural errors (missing data-composition-id, missing class="clip", overlapping clips, unmuted video, deprecated attrs); npx hyperframes doctor is the environment gate (Node, FFmpeg, Docker).
  • Preview ≠ render. Preview can stutter while the rendered mp4 is perfect (render captures frame-by-frame); use --docker for deterministic output across machines.

Common Mistakes

Authoring pitfalls that break compositions. The page is explicit that these are mostly issues the linter cannot catch — except #4, which it flags. Each entry: what breaks → why → the fix.

1. Animating video element dimensions

  • Breaks: video frames stop updating, or browser performance collapses.
  • Why: GSAP animating width, height, top, or left directly on a <video> element can make the browser stop rendering frames.
  • Fix: wrap the <video> in a <div> and animate the wrapper, never the video element itself.

2. Controlling media playback in scripts

  • Breaks: audio/video plays out of sync, or plays when it shouldn’t.
  • Why: calling video.play(), video.pause(), or setting audio.currentTime fights the framework — HyperFrames owns all media playback and reads data-start, data-media-start, and data-volume to schedule it.
  • Fix: don’t touch playback at all; use GSAP for visual animation only.
// Broken — conflicts with framework media sync
document.getElementById("el-video").play();
document.getElementById("el-audio").currentTime = 5;
 
// Fixed — let the framework drive media; animate visuals only
tl.to("#el-video", { opacity: 1, duration: 0.5 }, 0);

3. Composition duration shorter than the video (cuts off early)

  • Breaks: video plays a few seconds then stops; timeline shows 8-10s even though the source is minutes long.
  • Why: composition duration equals the GSAP timeline duration, not the video’s data-duration. If your last tween ends at 8s, the composition is 8s.
  • Fix: extend the timeline with a zero-duration tween at the target end time; confirm with npx hyperframes compositions.
tl.to("#lower-third", { left: -640, duration: 0.6 }, 7.2);
// Extend the timeline to 283s to match the video length
tl.set({}, {}, 283);

4. Missing class="clip" on timed elements

  • Breaks: elements stay visible the whole time, ignoring data-start / data-duration.
  • Why: class="clip" is what tells the runtime to manage an element’s visibility lifecycle; without it the element is always rendered.
  • Fix: add class="clip" to every timed element. This is the one mistake the linter catchesnpx hyperframes lint flags timed elements missing it.

5. Oversized source images

  • Breaks: preview stutters on image-heavy scenes; render slows down.
  • Why: Chrome decodes images to raw RGBA bitmaps sized width × height × 4 bytes — independent of file size on disk. A 7000×5000 JPEG is ~140MB decoded even at 2MB on disk, and the compositor resamples that huge texture every frame.
  • Fix: downscale sources to at most ~2× the canvas (3840×2160 is plenty for a 1080p comp).
mkdir -p assets/resized
mogrify -path assets/resized -resize 3840x3840\> assets/*.jpg

6. Heavy backdrop-filter blur stacks

  • Breaks: specific scenes drop to 5-10fps while the rest is fine.
  • Why: each backdrop-filter: blur() layer makes the compositor sample behind the element, run a blur kernel, and re-composite — every frame. Stacked layers multiply the cost (8 layers/side = 16 passes/frame).
  • Fix: keep stacks to 2-3 tuned radii per region, avoid radii above 64px on large areas, or pre-render a static blur to a PNG and overlay it as a normal <img>.
/* Expensive: 8 stacked layers */    /* Cheaper: 3 hand-picked radii */
.pb-1 { backdrop-filter: blur(1px); }   .pb-1 { backdrop-filter: blur(4px); }
/* ...up to blur(128px) */               .pb-2 { backdrop-filter: blur(16px); }
                                         .pb-3 { backdrop-filter: blur(48px); }

7. Expected HDR output but got SDR

  • Breaks: render looks like SDR; ffprobe reports color_transfer=bt709.
  • Why: by default HyperFrames only switches to HDR encoding when a source <video> or <img> carries BT.2020 / PQ / HLG color metadata.
  • Fix: check sources with ffprobe -v error -show_streams source.mp4 | grep color_transfer; render with --format mp4 (HDR needs MP4, not MOV/WebM); force it with --hdr; and drop any --sdr flag when HDR sources are present.

8. Timeline key doesn’t match data-composition-id

  • Breaks: animations don’t play; the composition appears static.
  • Why: the key registered on window.__timelines[...] must match the root element’s data-composition-id exactly. A mismatch means the runtime never finds the timeline.
  • Fix: register under the same id used in the HTML.
// HTML: <div data-composition-id="my-video">
window.__timelines["root"] = tl;       // broken — key mismatch
window.__timelines["my-video"] = tl;   // fixed — matches data-composition-id

Debugging checklist (page’s recommended order): (1) run npx hyperframes lint; (2) confirm the timeline is registered and the key matches data-composition-id; (3) confirm GSAP animates only visual props (opacity, transform, color); (4) confirm the timeline is long enough (tl.set({}, {}, DURATION)); (5) open the browser console — runtime errors surface as [Browser:ERROR]; (6) fall through to Troubleshooting for environment/render issues.

Troubleshooting

Environment, tooling, and rendering issues (symptom → cause → solution).

  • “No composition found” → the directory has no valid composition / the root element lacks data-composition-id. Run npx hyperframes init from an example, or give the root data-composition-id (plus data-start / data-width / data-height).
  • “FFmpeg not found” → local rendering needs FFmpeg. Install (brew install ffmpeg, sudo apt install ffmpeg, or the Windows download + PATH), verify with ffmpeg -version, then npx hyperframes doctor. Can’t install it? Render in Docker (npx hyperframes render --docker) — the image bundles FFmpeg.
  • Lint errorsnpx hyperframes lint reports the structural set: missing data-composition-id, missing class="clip", overlapping timelines on the same data-track-index, unmuted video (muted required unless data-has-audio="true"), and deprecated attribute names (data-layer / data-end are replaced — see HyperFrames HTML Schema).
  • Preview not updating → you’re editing the wrong index.html, or the cache is stale. Check the preview-server terminal, restart npx hyperframes preview, hard-refresh (Ctrl/Cmd+Shift+R), clear the browser cache for unreflected CSS.
  • Preview stutters / low frame rate (but the mp4 is fine) → individual frames exceed the 16-33ms paint budget; render hides this because it captures one frame at a time. Usual culprits, most-to-least: stacked backdrop-filter: blur() (radii > 32px), >4K images in small regions, filter: blur() / drop-shadow() on large elements, many animated box-shadow / text-shadow. Diagnose in Chrome DevTools → Performance (look for long “Composite Layers” / “Paint” tasks). Workaround: npx hyperframes render --quality draft and watch the output. See HyperFrames Rendering.
  • Render looks different from preview → font availability, Chrome version, or system compositing differs across machines. Use --docker for deterministic output.
  • Docker mode fails to start → check docker info. Common causes: daemon not running; permission denied (add your user to the docker group and restart the shell); image pull fails (first render downloads the HyperFrames image — check connectivity).
  • Render is slow--quality draft during dev; npx hyperframes benchmark to find the optimal worker count; compare --no-browser-gpu; --gpu for hardware-accelerated encoding (local only); drop to --fps 24 if 30 isn’t needed; prune unnecessary elements / overly complex animation.
  • Still stucknpx hyperframes info for system + project details, check the GitHub issues, and open a new one with that output plus repro steps.

Prevention

Two non-interactive CLI gates catch most failures before a render, which makes them ideal to wire into an agent’s pre-render step (see HyperFrames Packages & CLI):

  • npx hyperframes lint — structural validation. Catches missing data-composition-id, missing class="clip" on timed elements, overlapping clips on a data-track-index, unmuted video elements, and deprecated attribute names. It does not catch the runtime/visual pitfalls above (media-playback control, timeline-length truncation, video-dimension animation, perf blur/image cost) — those need the debugging checklist.
  • npx hyperframes doctor — environment validation. Checks Node.js version, FFmpeg availability, Docker status, and other requirements. Resolve doctor findings before rendering.
  • npx hyperframes compositions — sanity-check the resolved duration of each composition; a shorter-than-expected number means the timeline needs extending (mistake #3).

Agent rule of thumb: doctor once per environment, then lint + compositions before every render; treat a clean lint as necessary-but-not-sufficient because the highest-frequency mistakes are the ones lint can’t see.

Try It

  • Before any render in an agent loop, run npx hyperframes lint && npx hyperframes compositions and only proceed on a clean lint with the expected duration.
  • Reproduce the #1 mistake to learn the failure signature: animate width/height on a raw <video>, watch frames freeze, then wrap it in a <div> and animate the wrapper.
  • If a composition renders static, jump straight to the timeline key — grep your script for window.__timelines[ and confirm the key equals the root’s data-composition-id.
  • For HDR work, gate the pipeline on ffprobe ... | grep color_transfer and pass --format mp4 --hdr.
  • Add npx hyperframes doctor to project onboarding so FFmpeg/Docker gaps surface before the first render, not during it.