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.
* test(producer): probe a variable-bound clip under a host with values
* fix(producer): probe a variable-bound clip its mount or defaults rebind
* style(producer): variable media fixtures in small steps
* test(producer): list the real-render variable mount test in the integration lane
* style(producer): keep probeStage's comment share at the base
* feat(cli): capture can route its traffic through an opt-in upstream proxy
HYPERFRAMES_CAPTURE_PROXY=http://host:port sends the capture browser proxy's
forwarded requests and CONNECT tunnels, and every publicRequest download,
to that proxy by hostname instead of dialing a checked public IPv4. Unset,
behaviour is unchanged.
* fix(cli): keep capture comment share flat and close the new CodeQL findings
Test fixtures record what they receive instead of echoing request data, and
capture --video strips line breaks from a failed download's message before
logging it.
* fix(cli): proxied HTTP downloads send no URL fragment, and a malformed proxy credential is refused
The absolute-form request to the upstream used the URL's href, so a fragment such as
sprite.svg#icon went on the request line, which the pinned path never sends and strict proxies
refuse. A malformed percent-escape in the proxy credentials threw a bare URIError instead of the
usual "not a valid proxy URL" error. The test servers label their echoes text/plain.
* fix(cli): strip line breaks from capture --video errors in the form CodeQL recognizes
* fix(cli): capture --video logs errors through a recognized newline sanitizer
---------
Co-authored-by: Jerrai <jerrai@heygen.com>
* fix(engine): renders on a Mac decode a clip in software when the media engine refuses it
* refactor(engine): name the software decode retry condition instead of commenting it
render, check and snapshot open project history, which looks for a running
preview by probing 100 loopback ports and 5190. Behind an egress fence every
probe is refused and reported. Setting HYPERFRAMES_NO_PREVIEW_DISCOVERY=1 makes
historyDeps.findServer return null without a request, so history opens directly.
Refs #6204
* 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
* perf(aws-lambda): each chunk logs where its seconds went
The chunk_capture event now carries the handler's setup time and the renderer's own stage timings (plan check, browser boot, capture, encode, total), which the renderer already measured but the logs dropped. Lets a fleet run be split into start-up, capture and tail per chunk.
* perf(aws-lambda): chunk logs also time the Chrome path resolution
On the container image the compressed Chrome may be unpacked on every cold chunk; chromeMs separates that from the plan download in setupMs.
* feat(cli): write one diagnostic report when a command stays silent for 60 s
* feat(cli): load the hang report only when it is asked for
* fix(cli): the hang report is written without announcing itself on stderr
* fix(cli): the hang report leaves out environment values or is not written, and is written once
* refactor(cli): shorten the hang report comment to the line limit
* fix(cli): history show --diff works without git on PATH
* fix(cli): unifiedDiff passes lint and fallow: blocks, shared direction rule, split test
* refactor(cli): split the diff walk and its backtrack into small steps
* fix(lint): a media src whose file name holds # or ? says to write it percent-encoded
* refactor(lint): the url-safe hint encodes with the shared encodeUrlPath
* fix(engine): a stalled BeginFrame capture resumes from the first unwritten frame
A wedged worker used to throw away every frame already written and restart the
whole render on a screenshot session. Fresh BeginFrame workers now continue from
the first unwritten frame (once; a second stall still takes the screenshot
fallback), and the stall message names each worker's in-flight frame and step.
Force-closing the wedged Chrome no longer crashes the CLI with an unhandled
pipe ECONNRESET.
* chore(engine): cut the stall-resume comments to the comment limit
The explanations live in the PR body.
* fix(studio): a timeline edit that lands on the clip's own values saves nothing instead of failing
* chore: re-run checks against the updated PR description
* feat(engine): encoders cap peak bitrate and place keyframes on a set interval
* style(engine): shorten the rate-cap comment
* style(engine): drop the doc lines the comment ratchet flagged
* feat(producer): mp4 renders honour a keyframe interval and a peak bitrate cap on every encoder path
* style(producer): shorten the delivery encoder file headers
* style(producer): drop a comment line from the segment manifest
* style(producer): shorten the integration test header
* test: delivery encoder tests follow the ffprobe argv contract and run in the integration lane
* feat: the render request, server and Lambda plan carry the keyframe interval and peak bitrate cap
* fix(studio): the Studio render route passes the keyframe interval and bitrate cap to the encoder
* chore(studio-server): drop a comment the helper's name already says
* feat(cli): render and lambda render take --keyframe-interval, --max-bitrate and --buffer-size
* 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(engine): a scratch folder Windows refuses to delete no longer fails a finished audio mix
* fix(engine): audio scratch files Windows refuses to delete no longer fail a render
* fix(engine): a rename Windows keeps refusing falls back to a copy so the audio mix finishes
* fix(engine): audio scratch left behind is counted in the render log
* style(engine): wrap the long rename fallback test call
* refactor(engine): split the rename fallback decision out of the retry loop
* fix(engine): the rename copy fallback is opt-in and scratch leftovers are counted per render
* style(engine): keep renameWithRetry's comment share at the base
* style(engine): drop the blank line left by the comment cut
* fix(producer): compile probes each unique media source once however many elements use it
* test(producer): run the probe-count test in the integration lane
* 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
* chore(cli): tag the studio download link with where it was shown
The terminal hint, hyperframes open and the browser editor button now add
src=cli or src=browser-editor to the download link. The download route
already records src, so these downloads stop landing in no source.
* chore(cli): drop the DownloadSource doc comment the type name already says
* chore(cli): leave the no-build fallback link untagged, since only the download route reads src
/guides/open-design was folded into design-tools in August and the 4K
guide was retitled "Render in 4K"; Google stopped showing us for "open
design" and "4k render". The titles name them again.
* docs(studio-app): name the Windows app's words, keys and settings folder
The app says PC where a Mac says Mac and takes Ctrl where a Mac takes Cmd;
its settings live in %APPDATA%\HyperFrames. Cut out object here needs the
Mac image engine (Apple silicon, macOS 14+); Remove background runs on any
computer.
* docs(guides): split picture and video cut out requirements
Pictures cut out on Apple silicon with macOS 14+ or Intel with macOS
13.3+ (imageModels.mjs DARWIN_FLOOR); video cut out needs Apple silicon
(imageIpc.mjs models.video checks arm64).
* fix(cli): check and snapshot fail fast on a missing browser path and say while Chrome downloads
* fix(cli): a non-interactive Chrome download gives up after one stall, naming the env var and doctor
Mintlify search keywords on the FAQ (pricing, price, cost, free...) and
the skills guide (npx skills, skills add...). Last week pricing searches
found a clicked result 8% of the time and npx skills 0%.
* fix(studio): a clip that cannot be read gives up after two retries with one typed line
* fix(studio): a clip whose stream breaks mid-read also fails with the typed line
The segmented route had no drawElement recovery: a suspect small frame, a
missing paint record, a renderer stall or a failed self-verification failed
the whole render, where the streaming and disk paths re-render via
screenshot. Catch those errors around the segmented stage, replan with
forceScreenshot (segmentation kept), close the probe and retry once.
The retry discards the segment directory and manifest, including segments
resumed from an earlier run: the plan hash does not cover the capture mode,
drawElement and screenshot segments are not byte-identical, and segmented
drawElement segments are never self-verified.
A screenshot retry whose first segment encoder cannot spawn now keeps
forceScreenshot when it drops to plain streaming.
— Via
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(producer): open a fresh segmented session when the probe has risk frames
Since #6318 the gate-check probe is planned with deFrameFallback: true, so
it can carry deRiskFrames. The segmented stage reuses that probe for its
first session and captures every frame with captureFrameToBuffer, which
cannot hand a risk frame to a screenshot session: the first one throws
"frame needs screenshot capture, which this capture path cannot hand over"
and the segmented route has no drawElement catch, so the render fails.
Close such a probe and open a fresh segmented session instead; those are
created with deFrameFallback: false and keep the whole-render screenshot
fallback.
— Via
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(producer): name the risk-frame probe condition instead of commenting it
The comment ratchet does not allow a file's comment share to rise.
— Via
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(engine): a failed drawElement worker encode takes the fresh-session recovery
The encode result is awaited after its frame was produced.
Its failure reached the stage as a plain error, so the render ended.
It now surfaces as a drawElement capture failure, like a failed produce.
* fix(engine): drop the comments from the encode failure wrapper
* feat(docs): download links carry the docs visitor's PostHog id
hyperframes.dev counts each Studio download in PostHog, but it cannot read the
docs' posthog cookie (another domain), so a download clicked here lands under
an id with none of this visit's pageviews. A docs script now adds the id as
ph_id when a download link is clicked, and puts the link back afterwards so a
copied link never carries it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(docs): forward the PostHog id only to the download URL, and only with consent
The id now goes only to https://hyperframes.dev/studio/download, the one
address the docs link to, never another subdomain or plain http. It is not
forwarded when PostHog is not running on the page, when the visitor opted out
of PostHog capture, or when the browser sends Do Not Track or Global Privacy
Control.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds server.json for registry.modelcontextprotocol.io under the
io.github.heygen-com namespace, pointing at HeyGen's hosted server.
Today the registry lists only a third-party paid clone.
The hyperframes package had no homepage, so npm linked the GitHub
readme. The publish job rewrites only the name, so this field reaches
npm on the next release.
* 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(cli): the Windows Chrome check launches Chrome instead of trusting chrome.exe --version
* refactor(cli): shorten the Windows Chrome version comment to fit the comment ratchet
* test(cli): pin the platform in the Unix Chrome preflight tests so Windows runs them as written
* fix(cli): the Windows Chrome check reads the file version, not chrome.exe --version
* fix(cli): Windows preview and catch-up records move to LOCALAPPDATA, a refused log no longer blocks
* fix(cli): a refused preview log falls back to the null device and drops the extra comments
* fix(cli): Windows catch-up record goes under LOCALAPPDATA/hyperframes like the other records
* fix(cli): the check and snapshot project lock works where hard links are refused
* refactor(cli): drop the project lock comment to fit the comment ratchet
* 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