* 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
* fix(cli): a Windows browser that will not start says where and why, not just spawn UNKNOWN
* fix(engine): the launch diagnosis names a browser file Windows refuses to run
* fix(engine): the launch diagnosis names a browser file Windows refuses to run (engine part)
* perf(engine,cli): a machine's GPU encoder probe runs once, not on every render
* refactor(engine): one shared probe per process, no answer kept on disk
* ci(windows): probe that a render opens no visible window
* ci(windows): the probe names what opened each window
* ci(windows): the probe spawns each binary alone, with and without detached
* ci(windows): the probe logs the processes started during the render
* ci(windows): the probe also spawns Chrome one consoleless hop down
* ci(windows): the probe logs each child started without windowsHide
* ci(windows): the probe logs every child process with its windowsHide and detached
* fix(engine): Chrome keeps the CLI's console on Windows, so a render opens no window
* fix(cli): every child the CLI starts on Windows is hidden unless it opts out
* ci(windows): the probe bisects Chrome's flags and keeps each command's output
* ci(windows): the probe names the Chrome process behind each window
* ci(windows): the probe loads a page in Chrome directly and one hidden hop down
* ci(windows): the probe prints the Chrome main command line and renders with the reporter's flags
* ci(windows): the probe narrows the CLI's Chrome arguments to the ones that open a window
* ci(windows): the probe launches the CLI's Chrome arguments one hidden hop down
* ci(windows): the probe launches Chrome through the engine's browser pool
* ci(windows): the probe varies what the engine passes to Puppeteer
* fix(engine): renders on Windows launch Chrome with a Puppeteer that keeps it attached
* refactor(cli): hideChildConsoles copies its arguments and keeps one comment line
* fix(producer): HDR sources are tone-mapped for formats that cannot carry HDR
* refactor(producer): one owner for which output formats can carry HDR
* refactor(producer): drop comments the helper names already carry
* fix(engine): SDR clips are not lifted to HDR when the render tone-maps to SDR
* fix(engine): extraction waits out the decode up to a deep seek in a long-GOP video
* chore(engine): shorten the keyframe gap doc comment
* fix(engine): cancelling an extraction during the keyframe probe reports a cancel
* fix(engine): a segment stopped by another's failure reads as stopped, not cancelled
* feat(cli): render --prores-profile picks ProRes 4444, hq, 422, lt or proxy for MOV
* fix(cli): drop a comment line that raised the comment share
* fix(cli): the render plan args type carries --prores-profile
A first page of a fresh Chrome stalled past vitest's 10 s hook limit on a contended Windows runner and failed the first test of its file. The first page is now opened inside beforeAll, with the launch and first-page times logged, and the browser project's hooks get the 30 s the CLI's Chrome project uses.
Audio effects launched Chrome with its own two flags and no retry, so inside
an agent's macOS seatbelt Chrome died at startup with "Target closed" while
the CLI's own browser launch recovered. The sandbox launch policy now lives
in the engine; the engine's Chrome launch retries once in one process over a
pipe when the sandbox refuses it, and audio effects use the shared Chrome
flags in software GPU mode. The CLI imports the same policy from the engine.
* fix: phone videos with an undecodable first audio track check, preview and render
* fix(engine): a rate-lane clip bakes its ramp from the decodable audio track
* test: phone-clip tests end ffprobe options before the input
The start probe waited for the shell to exit after SIGTERM, so a shell that stalled under load blocked the whole process. Kill it outright at the timeout.
The GPU probe's answer is kept for the whole process and the first caller's probe is the
one every later caller waits on. When that first caller cancelled it (a prelaunched
browser dropped, a stopping check), the probe died with "This operation was aborted",
and that cancellation was kept as the answer: every browser the process launched after
it ran in software mode, with a log line blaming the GPU probe. A cancelled probe is now
forgotten and the next caller probes again.
* perf(producer): a parallel chunk checks BeginFrame on its first worker, with no separate browser
* style(producer): format
* fix(producer): a chunk whose first worker dies before its check settles instead of hanging
* fix(cli): a Chrome that fails to start says how it ended, and a slow start says so
A failed launch only reported "Protocol error (Target.setDiscoverTargets): Target closed". It now
runs the browser once more on its own with the same flags and reports its exit code (Windows
codes named: missing runtime, out of memory, security software), its last stderr lines, or that
it stayed up. The CLI and the engine's render launch both use it. A start slower than 5 s prints
"Chrome is still starting...", and a --timeout that looks like seconds says it is milliseconds.
* chore(cli): cut the Chrome launch diagnosis comments to the comment limit
* fix(cli): the Chrome startup check runs Chrome as puppeteer does and can be cancelled
The check started the browser with only the caller's flags, so a full Chrome ran headful and
was blamed for a missing X server. It now uses puppeteer's default arguments (headless
included). The check is stopped when the CLI is stopped, and the GPU probe, whose failure
falls back to software anyway, skips it.
* fix(cli): a Chrome launch error is never replaced by a failure to explain it
The startup diagnosis asks puppeteer for its default arguments before re-running
Chrome. If that call fails, its error used to replace the launch error the caller
needed. The launch error is now reported as it was. The core package size budget
rises to cover the new chromeStartup module (about 4.7 KB unpacked).
* refactor(engine): the Chrome startup diagnosis lives in the engine, not core
Both callers launch Chrome through the engine (the CLI already imports it), so the
engine owns the diagnosis. Core's package size budget is back to its old value.