* 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
* 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
* fix(cli): snapshot reads https video in the Desktop app instead of failing at ffprobe
* fix(cli): give a remote clip download the same 30 s a stalled host had before
* fix(producer): a capture that landed every frame is not failed for the work after its last frame
* chore(producer): drop a comment the ratchet counts
* perf(producer): the audio true-peak check seeks to each segment instead of decoding up to it
Each parallel segment scan decoded the AAC from the start and trimmed after, so the last one decoded the whole file. It now seeks to a whole second one second before its segment and trims relative to it: the same peak, measured in about half the time.
* test(producer): the true-peak check's ffmpeg tests run in the integration lane
* fix(core): a font declared in a linked stylesheet no longer trips the bundler's font warning
* fix(core): drop the guard's explanatory comment
* fix(core): bundled fonts named only in a CSS font shorthand load
* fix(core): a font shorthand loads faces but never fails the cloud system-font check
* test(producer): regenerate static-volume-future-set for the font shorthand face
* test(producer): regenerate two more fixtures for the font shorthand face
* 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
* 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
* perf(producer): drawElement films keep their parallel workers instead of dropping to one
* perf(producer): shorter inversion floor comment
* test(producer): inversion suites opt into the single-worker inversion
* 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
* 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
* fix(producer): stop HDR video windows after the first from repeating one frame
A raw HDR window seeks to a whole second, then trims its lead-in frames.
trim keeps their timestamps, and the raw muxer writes none, so when the
input has an audio track FFmpeg's default CFR sync pads from pts 0 with
copies of the window's first frame and -frames:v cuts the real ones off.
Phone footage always has audio, so HLG clips rendered at ~half their frames.
Pass frames through as the filter graph emits them.
Fixes#6556
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(producer): hold the first frame until a delayed HDR video starts
Window slots counted from the first decoded frame, so a video stream that
starts after its audio played early by its lead. Slots now count from the
extraction start and show the first frame until the stream begins, as they
did before windowed extraction (#6304) and as Chrome does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(producer): add an HDR fixture whose clips carry audio and span extraction windows
Neither HDR fixture had an audio track, which is how #6556 shipped. Window A
checks frames after the first extraction window are not repeats; window B
checks a video stream that starts 0.5 s after its audio holds its first frame
until the picture starts. The baseline was generated in Docker and the fixture
fails against main.
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(producer): a taken file-server port fails the render, and the late grading test runs in its lane
* test(producer): run the taken-port file-server test under Node, where listen errors are events
* test(producer): every *.integration.test.ts must sit in the integration lane
* fix(producer): drop the explanatory comment on the file-server error handler
* 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
* 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
* test(producer): the stall tests jump fake time to the next timer instead of a second at a time
A 400 s scenario took 400 steps of 50 real event-loop turns plus file IO, which crept
past the unit timeout on a loaded runner. Idle windows now cost one step.
* test(producer): stub the memory sampler so the stall tests' timer jumps skip the idle windows
The sampler is a faked 250 ms interval, so it was the next timer on every jump and the
jump was never longer than 250 ms.
* 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
* 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.
* 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(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
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>