* fix(engine): GPU renders use the NVENC preset names the host's ffmpeg lists
* fix(engine): one ffmpeg stdout reader for encoder detection and preset listing
* ci(engine): raise the engine unpacked size budget for the NVENC preset reader
* fix(engine): passthrough output follows the page frames and holds a short video's last frame
* test(engine): a film whose audio outlasts its video still qualifies for passthrough
* test(engine): passthrough encodes one output frame per page frame when the video is shorter
* test(engine): end the ffprobe options before the file in the passthrough frame count test
* feat(engine): composite a source video inside the streaming encoder
* feat(engine): opt-in flag and rule for compositing a lone full-timeline video
* feat(producer): opt-in video layer passthrough for a lone full-canvas video
* feat(producer): launch the probe, drop its page and capture PNG when passthrough is on
* refactor(engine): trim the passthrough doc comments to one line
* fix(producer): refuse passthrough when a wrapper or sub-composition paints a background
* chore(producer): mark the in-page layout check as one deliberate unit
* fix(engine): a video left out of capture no longer blocks static-frame dedup
* refactor(engine): shorten two long comments in the static-frame eligibility scan
* refactor(engine): one builder for the video frame filter chain
* refactor(engine): keep the filter chain builder module-private until it has a caller
* fix(producer): a failed video extraction names which clip files failed
* fix(producer): the extraction source label hides scheme payloads and drive paths
* style(producer): format the extraction source label
* fix(engine): scale static-frame verification budget with the job
* fix(engine): keep comment share flat
* fix(engine): drop added comment
* fix(engine): time seek in verify budget, state the policy
* fix(producer): render telemetry reports beyond-viewport capture as the workers used it
* fix(producer): apply the sessions' beyond-viewport value at the summary, on every capture path
* chore(engine): drop a comment so the types file's comment share does not rise
* fix(producer): auto renders with color-graded media stay SDR instead of promoting to HDR
* fix(producer): keep auto renders SDR when a graded canvas is present
* fix(cli): capture's public-address check uses the engine's list; 192.0/16 is not all reserved
Only 192.0.0.0/24 and 192.0.2.0/24 are special-use. The capture proxy blocked all of
192.0.0.0/16, so sites on WordPress VIP (192.0.64.0/18, e.g. nasa.gov at 192.0.66.108)
failed every capture with net::ERR_TUNNEL_CONNECTION_FAILED. isPublicIPv4 now reads the
engine's isBlockedNetworkHost, so capture and asset downloads share one list.
— Jerrai (cutroom-debug)
* refactor(engine): one special-use network list, in a light subpath capture imports
Importing all of @hyperframes/engine into capture's publicNetwork slowed the lookup child's
start-up past publicNetwork.process.test's 6 s mid-lookup exit. The list and
isBlockedNetworkHost move to engine/src/utils/networkPolicy.ts, exported as
@hyperframes/engine/network-policy; urlDownloader and the engine index read it there.
— Jerrai (cutroom-debug)
* test(engine): pin 192.0.0.0/24 and 192.0.2.0/24 blocked, the rest of 192.0/16 public
— Jerrai (cutroom-debug)
* fix(cli): alias the engine's network-policy subpath in the CLI bundle
esbuild's root alias rewrote @hyperframes/engine/network-policy to src/index.ts/network-policy;
the exact-subpath list now carries it. The publicNetwork docstring is cut to keep the file's
comment share under its base.
— Jerrai (cutroom-debug)
Frames ffmpeg has written now reach the render's stage progress, so a long source video no longer leaves the render silent between extract_video and capture.
* fix(engine): a video clip starting between frames shows its media on the right frame
* chore(engine): trim the helper comment to one line
* chore(engine): drop the helper comment
* test(engine): extract once per start in the off-grid clip test
* fix(engine): a looped clip keeps its loop origin when the start is off the frame grid
* test(engine): three off-grid starts cover the failure modes and keep the flake gate under its limit
* fix(cli): a missing registry item names the closest ones, and a blocked browser says how to allow it
* fix(cli): let closestNames carry its rule in its name
* fix(engine): a video on a non-square pixel grid renders at its display shape
* chore(engine): drop comments the names already carry
* fix(engine): a rotated clip with a non-square pixel grid keeps its display shape
* test(engine): the frame probes end their options with -- like every ffprobe call
* fix(engine): a render no longer waits for a sub-composition timeline no script can register
* fix(engine): the timeline wait matches a twice-mounted id and a template-literal key
* fix(engine): late-starting videos play to their last frame
A video whose first frame is after timestamp 0 could end early (frozen
tail) or late: ffprobe's stream duration does not run from the first
frame in every container (fragmented MP4, Matroska). Such a video's end
is now read from its last packet, once, where its facts are probed.
* test(engine): the WebM-with-audio case measures its first frame from the file
* test(engine): the first-frame probe ends its arguments with the input file
* fix(engine): long films start capturing at once instead of after a full effect scan
* fix(engine): give the effect scan two minutes, under the app's stall limit
* fix(engine): show effect scan progress so a long scan reads as preparing, not stalled
* perf(cli): give render the heap its auto workers need in small containers
Auto worker sizing caps capture workers at (V8 heap - 1024 MB) / 640 MB.
Node sizes its default heap from visible memory, so in a 10 GB container
the heap is 2,096 MB and every render captures with one worker, whatever
the core count.
When no --workers is given, no --docker, no heap was set by hand, and the
heap is the only bound holding workers back, render relaunches itself once
with --max-old-space-size set to what CPU and memory allow (capped at half
of memory). The engine exposes heapMbForAutoWorkers() for this.
* fix(cli): count a heap-relaunched render once, keep detached renders alive
The relaunched child skips the CLI's command telemetry, update and skills
notices, and app-history notice; the parent already records and prints
them. A detached render no longer gets SIGHUP forwarded, which its
default handler would treat as a kill. A heap set as --max_old_space_size
counts as set by hand.
* fix(engine): keep the desktop app's per-worker heap figure at 640 MB
Inside Electron the heap is a hard 4,192 MB cage, and that is where the
field OOM behind the old figure happened (#5149: 5 auto workers on a heavy
export). The new 320 MB figure is measured only in a system Node, so the
desktop app keeps 640 MB (4 workers there) until a heavy desktop export is
measured. Containers and plain Node keep 320.
* fix(engine): know the packaged desktop app by its client tag, which runs the CLI on plain Node
* fix(producer): a BeginFrame capture that draws no first frame in 20 s switches to screenshot
* test(cli): cover the Chrome launch failure messages a failed render prints
An inert <hf-color-group> holds a lane's data-color-grading; <video>, <img>
and <canvas> join with data-color-group. A member's own grading overrides
its group's. The runtime discovers members, regrades them when the group's
grading or a member's data-color-group changes, and ungrades them when the
group element or its grading goes, drawing only members on screen.
Render keeps group-graded video at full size like directly graded video,
and lint accepts (and still validates) grading on hf-color-group.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): late-starting MP4 and WebM videos render every frame the preview shows
The browser counts an MP4 or WebM from timestamp 0, but FFmpeg seeks from the file's
earliest stream start. A file whose first frame comes after 0 (a fragment cut from a
longer stream, an edit-list offset, a WebM with a start offset) was extracted early and
short, so renders tripped the video coverage gate. Every video seek now maps media time
to FFmpeg's timeline, and the video begins where the browser shows it.
* fix(engine): audio from late-starting MP4 and WebM plays when the preview plays it
The mixer seeked and trimmed audio by FFmpeg's clock, which starts at the file's earliest
stream. In an MP4 or WebM whose streams all start after 0, the browser plays that audio
later, so the render heard it early by the same amount. The mixer now maps media time onto
FFmpeg's clock with the same offset frame extraction uses.
* fix(engine): a late-starting file's picture shows on the clock its audio plays on
* test(producer): vfr-screen-recording baseline shows each frame at its timestamp