* fix(preview): the proxy copy for the video at the playhead is made ahead of the rest
* test(preview): the resolveProxy assertions carry the priority option
* fix(preview): the proxy copy asked for last goes first, and a repeat ask moves it to the front
* refactor(preview): the proxy wait loop moves its header and progress handling into helpers
* fix(studio-server): preview loads root-path media, images and CSS from the project, as render does
* fix(studio-server): preview writes root paths project-relative, the form Studio reads back
* test(studio): record each edit-accuracy shard's file writes and reloads when HF_WRITE_TRACE is set
* style(studio): format the write trace
* fix(core): hash the write trace with sha256
* style(studio): wrap the trace hash helper
* style(studio): format the trace hash helper
* fix(studio): turn the reload log on in the Studio page the trace watches
* fix(core): text a script splits into letters shows once and carries the variable's value
* fix(core): Studio's mounted and swapped scenes read bound text, and instance values load first
* test(core): the scoped variables table is emitted compact in head
* fix(core): retiming a parked scene in the preview takes effect without a reload
* fix(core): an edit that leaves a parked scene without a start or length in seconds reloads instead
* fix(core): a video after a cut starts early by its measured start delay, so it plays in sync
* test(player): playbench reports how far the picture after a cut runs behind the playhead
* fix(core): a looping video no longer skews the learned video start delay
* fix(core): the video start delay is learned in less code and capped at 100 ms under test
* fix(core): the npm package no longer ships the unminified media sync runtime
* style(core): a two-line comment for the media clip's volume keyframes
* style(core): the moved media clip type keeps only the comments its names do not say
Chrome's profile lockfile is held for a moment after the browser exits, so removing the
capture profile threw EBUSY and a finished capture exited 1. The profile and staging
removals now go through a core helper that retries transient locks and, for cleanup of
finished work, warns instead of failing.
* fix(player): each take plays its own audio when the browser blocks autoplay
* fix(player): a parent audio proxy of a video clip is seeked when its lag holds, not rate-chased
* style(core): keep the steering policy's comment share where it was
* fix(core): a video no longer runs ahead of the playhead or past its out-point
* fix(core): keep a looping video playing when the playhead wraps before it
* fix(core): audio clips join on time and their volume lane follows the audio
An incoming <audio> clip is now pre-seeked to its in-point like a video, and
a playing audio element that drifts is steered by rate instead of seeked
while it sounds. A routed clip's volume lane and fades are re-aimed at the
element's own position whenever it starts, is seeked or changes rate.
* fix(core): sounding audio is nudged at most 10%, and a larger lag seeks under a short dip
Speech audibly changes tempo past a 10% rate nudge, and a bare seek clicks.
Audio that holds a lag rate cannot close in about a second is seeked under a
15 ms dip on its routed graph; an unrouted element is seeked plainly. Video
keeps its steps.
* fix(core): a routed clip's automation has one owner, its element, looping included
A rate change re-aimed a routed clip's lane on the context clock and again
when the element took the new rate. Only the element re-aims it now. A
looping element's wrapped position is placed on the pass the transport is in,
so a looping bed's lane follows it too.
* style(core): wrap a steering comment to the line limit
* refactor(core): one drift policy for playing media, exported for the parent proxy
The steering steps, the audio cap and limit, and the steering state move to
runtime/mediaSteer.ts as steerMedia(el, offset, baseRate, settled) and
forgetSteering(el), exported as @hyperframes/core/runtime/media-steer so the
player's parent-frame proxy uses the same policy instead of a second one.
* style(core): trim the drift policy's comments to the package's share
* fix(core): the dipped seek lands on the frame-start media time
Routes the one currentTime write this branch adds through mediaElementTime,
so Chrome's truncated currentTime cannot land it a frame early.
* test(core): expect the plain audio seek at the frame-start media time
* fix(core): preview seeks land on the composition's own frame rate
* fix(studio): frame step, frame readout and ruler use the composition's frame rate
* fix(studio): an end trim previews the clip's last frame at the composition's frame rate
* refactor(studio): ruler labels follow the ticks' frame rate and fps parsing splits by form
* style(studio): format the ruler label helper and its test
* chore(inspector): raise the package size budget to the measured size plus headroom
* fix(check): a mask that cuts descenders and a shared scene's own selector are handled
* test(core): update the page-build goldens for the scoped selector rewrite
* fix(core): a shared scene's authored-id selector rewrites to the mount without a second pattern
* fix(render): a PNG's colour tags no longer change its pixels in preview, check, snapshot or render
* fix(render): the play and present server also serves a PNG without its colour tags
* fix(render): bundled pages inline a PNG without its colour tags too
* feat(core): one function gives the media time that shows the frame starting at an instant
* fix(core): paused preview and render show the same, correct video frame
* fix(core): register the media-clock subpath in its descriptor and keep the reason beside the test
* fix(core): media seeks lead by 10 us so Chrome's timestamp rounding never shows the prior frame
* fix(core): generated pages lead media seeks by a literal, on every generator branch
* fix(producer): a composition with no timeline-registering script no longer waits 45 s at setup
The render waited the full timeout for every composition that never registers a timeline, such as one driven by canvas or requestAnimationFrame.
* fix(producer): module and importing scripts still count as able to register a timeline
* fix(producer): a failed script keeps the readiness wait, and the page scan runs once
* fix(core): a composition behind a wrapper takes its host's start
Fixes#4334.
* fix(core): a composition inside a timed wrapper starts with the wrapper
The nearest host or timed wrapper above a mounted root sets its start, so its clock starts when the wrapper shows it, matching how CSS animations already pick their clip.
* test(producer): re-pin the test classification digest
* fix(producer): deduplicate scene font faces before embedding
* fix(producer): address font deduplication review
* docs(help): document render font-face deduplication
The troubleshooting guide this section originally targeted was folded
into docs/help.mdx on main, so the section now lives under the render
troubleshooting entries there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(core): the fonts gate starts loading declared faces no laid-out text uses yet
Before composition scripts run, waitForFonts forces one layout and awaits
document.fonts.ready. A face that only script-built text uses has not started
loading at that point, so ready does not wait for it and the script measures
the fallback face. Start every face still unloaded after the forced layout,
under the existing timeout; a load that rejects falls through to the same
timeout warning instead of stopping the scripts.
Refs #5112
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(core): keep the fonts gate's comment share level; cover a face that never settles
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The off-canvas indicators (always mounted) and the Layers panel rebuild their layer list every
100 ms while a scrub mutates the preview, and each rebuild read the computed style of every element
of every scene, on screen or not. The runtime already hides everything inside a clip its visibility
pass hid, so those subtrees hold no layer: the walk now skips a clip marked data-hf-clip-hidden
with an inline visibility of hidden. The marker constant moves to studio-preview-mark so the runtime
and Studio share one owner.
* feat(studio-server): a copy being made says how far it has got, and can be retried
* feat(core): the preview reports its lighter copies' progress and retries failed ones
* feat(studio): the preview quality and its lighter copies' progress live in the player store
* feat(studio): every host forwards the preview quality, and the choice is kept per computer
* feat(studio): the preview quality knows if a person or the app chose it
* fix(core): the first report of the lighter copies is not held back by the throttle
* test(studio): the preview quality preference test writes under the real key
* fix(core): a quality change reports its copies at once, not after the throttle
* style: format the preview quality files
* feat(core): report how playback is going, and what the preview holds, to the host
* fix(studio): the preview quality levels come from a browser-safe core entry
* style: format the new entry's files
* style: drop two comments the file's ratchet and the citation check refuse
* feat(core): tell which lower levels would give a video a lighter copy
* fix(studio): drop the old level's playback health when the level changes
* feat(studio-server): preview facts carry the probed codec of every video
* refactor(studio): route the preview's reports through a table for the complexity gate
* fix(core): a copy's retry flag is sent on the first ask only, not on every poll
* fix(core): type the health report and restore the player hook's complexity directive
* style: cut four comments the comment ratchet refuses
* fix(studio): keep playerStore.ts within its 600-line cap
* fix(core): a scene mounting does not restart the playback health window
* fix(core): the playback health window restarts only when the display scale changes
* fix(core): a repeated display scale no longer restarts the playback health window
* fix(studio): route the preview's reports by an explicit list of message types
* fix(core): preview audio fades play as ramps on the audio clock, so cuts no longer click
data-fade-in / data-fade-out were folded into one volume per 50 ms sync tick and applied as
a step (el.volume, or gain.value in the Web Audio transport), so a 20 ms fade never played
and each clip started and stopped at near full level: a click at every cut. The export
applies the fades (ffmpeg afade) and does not click.
The transport now books each clip's fades on a gain of its own when it schedules the
clip, for both decoded buffers and captured media elements: a linear ramp from where the
clip starts sounding up to its fade-in, and down to silence at its end. The sync tick hands
the transport the level without the fade (onElementVolume's new fifth argument, sent only
where a fade applies), so a booked fade is not applied twice. The plain el.volume fallback
keeps its stepped fade.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): one doc block on setElementVolume
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): keep the comment share where it was
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): booked fades hold through pre-roll, follow rate changes, skip unbounded ends
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: raise the producer unpacked budget to the measured size plus 1%
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(core): tell which video files deserve a lighter preview copy, and at which size
* feat(studio-server): serve lighter copies of heavy playable files, a keyframe every second
* feat(core): the preview plays lighter copies of heavy videos below full quality
* style: shorten two comments
* style: shorten a comment
* style: shorten a comment
* fix(core): format the preview quality files and split the copy sizing for the complexity gate
* style: format the studio-server preview files
* fix(core): type the copy job's level as a preview quality
* chore(producer): raise the package size budget for the preview runtime growth
* fix(core): pressing play puts a sped-up or looping video at the right moment in one seek
Pressing play, or seeking while playing, placed every video as if it played at 1x from its start and never looped. The media sync that runs right after knew better, saw a gap of seconds on a 2x or looping clip, and seeked again: two decoder resets per press. Both now read one function for where a media element belongs in its source at a given time, so the first placement is the only seek.
* fix(core): play keeps a video's own speed and skips seeking a video already in place
Setting the transport rate wrote that rate onto every media element, so a 1.15x clip played at 1x until the next media sync put it back; Studio sends a rate on every Play. The media sync already sets each element to its own rate times the transport rate, so the rate change now leaves elements alone and lets that sync apply it. Pressing Play also no longer seeks a video that is already at its source time, since every seek flushes the decoder.
* fix(core): pause and play leave media alone that another feature or the slideshow is driving
Play and pause placed every in-window video at the playhead, including a video the grading preview or a Studio scrub holds while paused, and slide media while the slideshow drives media itself. They now ask the media sync which clips are the runtime's to move.
* refactor(core): trim comments the comment-share check rejects
* perf(studio): music beat analysis no longer freezes the page or runs during playback
* fix(core): media-treatment --analyze finishes on long HDR clips
Grade analysis decoded the clip from the start and kept every frame until the
fps filter had emitted five samples. Past 50 s the filter rate floors at 0.1
fps, so it always decoded the first 40 s at full rate and resolution: a
3-minute 4K60 10-bit HLG clip timed out at 15 s, and every long clip was
graded from its first 40 s only.
Each sample now seeks to the middle of one fifth of the clip and decodes one
frame. The timeout applies per sample and its error names the sample time and
HYPERFRAMES_ANALYZE_TIMEOUT_MS.
* refactor(core): name the keyframe seek instead of commenting it
* fix(core): count one frame per grade sample on ffmpeg 8
* fix(sdk): preserve font and image declarations after object edits
* fix(studio): preserve variable object defaults when editing declarations
* fix(studio): show font and image values in bind warnings
* fix(skills): load dotLottie through its browser ESM entry
* chore: prepare conflict resolution against current main
* fix(skills): retain local Lottie setup after syncing main
* chore: prepare conflict resolution against current main
* fix(skills): retain browser dotlottie setup in current runtime guidance
* chore: prepare conflict resolution against current main
* fix(skills): refresh dotLottie fingerprints against current main
* chore: prepare conflict resolution against current main
* chore: merge main and refresh dotLottie skill manifest
* chore: prepare conflict resolution against current main
* fix(skills): keep Lottie setup guidance in its reference
* fix(core): a timeline set that hides a timed clip hides it in preview and render
* fix(core): tell a script's hidden clip from the runtime's by the declaration's priority
* fix(core): a scene a script hides takes its timed children with it
A root composition with no data-duration took its own GSAP timeline's length as its visibility
window, so when media made the composition longer, the whole frame went black from the end of
that timeline in preview and render. The root's window is now open-ended, bounded by the
composition's length. In a framed preview, the duration the runtime stamps on GSAP targets for
Studio's timeline no longer hides the clips inside them either: the ancestor chain check now
skips discovery stamps, as the clip's own visibility pass already does.
* fix(core): a self-closed audio group no longer hides the clips after it
HTML does not close a custom element on `/>`, so `<hf-audio-group id="sfx" />` takes in every
later sibling, and the runtime's inert style (`display:none`) hid them in preview, snapshot and
render. The style is now `display:contents`: an empty group still makes no box, and anything
inside one renders as written. A lint warning names the elements a group holds, and the docs and
mixing reference show the group closed.
* style(core): cut the audio group comments to the comment length limits
* fix(core): a muted audio group still hides what sits inside it
A muted bus (`data-hidden`) is hidden by an inline display:none that the bus rule outranked, so
clips inside a muted self-closed bus kept painting while their audio and video frames dropped.