* fix(export): an image-sequence export reports the stills it wrote
An export to `a.png` as an image sequence writes `a000.png`, `a001.png` and so on, but the
job's result, `file.exportMedia`'s `path` and the CLI's "exported" line all named `a.png`,
which does not exist.
The report now names the first still, the name `file.import` and `probe --image-sequence`
take for a sequence. `file.exportMedia` and `export.quick` name it as `path` up front, from
the frame count `export.resolve` uses, and add `lastPath`. The CLI prints both. A single
file keeps its path.
Fixes#4009
* fix(automation): a cancelled image-sequence export is cleaned up by its stills' name
Since `file.exportMedia` names an image sequence's first still as `path`, the MCP cancel
clean-up took `a000` as the stem: it left `a001.png` and every later still, and the `a.srt`
sidecar, behind. The result now also carries `basePath`, the name the stills are numbered
from, and the clean-up goes by it.
* fix(automation): an image-sequence cancel clean-up keeps the base name and finds .jpeg stills
An image-sequence export numbers every still, a single frame included, so a file under its base
name (`a.png`) is never its own: cancelling one deleted a user's `a.png` if it was saved during
the export. The stills of a `.jpeg` sequence were left behind instead, since the extension was
read as a format name, and `jpeg` names Motion-JPEG there; `file.exportMedia` keeps `.jpeg`
for a JPEG sequence.
* fix(automation): a cancelled image sequence removes only the stills it numbered
The clean-up matched any `<stem><3+ digits>.<ext>` written during the export, so cancelling an
export to `a.jpeg` could delete `a1000.jpeg`, the first still of an export to `a1.jpeg` in the
same folder. It now takes the digits and the last index from the result's `lastPath` and removes
only those names: `a000.jpeg` to `a120.jpeg` for a 121-frame export.
* G13: TikTok, Instagram Reels and YouTube Shorts export presets
Three Web & Social presets for short-form vertical video: H.264
1080×1920 (9:16), scaled to fill, at the sequence's frame rate, AAC
48 kHz stereo, with Loudness Normalization on at −14 LUFS and a −1 dBTP
ceiling, the level the platforms play at; 10, 8 and 12 Mbps VBR. They
share a short_form() helper; Social Vertical is unchanged.
short_form_presets_are_vertical_and_loudness_normalized checks size,
rate, scaling, bitrate, audio and loudness. Real exports measured
−14.1 LUFS from both a −19 and a −8.4 LUFS source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G13: the short-form presets test checks each maximum bitrate
CodeRabbit on #3721: assert max_bitrate_kbps (12, 10 and 16 Mbps) and
the VBR 1 pass mode next to the target bitrate, so a change to the
ceiling alone fails the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Export: the time left counts down between batches; the status bar shows it once a second, in place
An export finishes its frames in batches of up to 16. The estimate measured the speed to the
latest reading, so between batches the speed sagged and the time left grew, then fell when the
next batch landed: about ±13 % on a heavy 4K60 export, enough to flip between 1:04:50 and 56:11.
It now measures the speed between batch arrivals and counts down from the last one; a batch three
intervals late falls back to the speed up to now, so a stall still makes the estimate grow.
The status bar redrew its text every frame, right-aligned against the bar, in Inter's proportional
digits, so "Exporting… 46%" moved sideways with every change of the time left. The text now
changes at most once a second, its digits sit in cells of equal width, and its room only grows
while the job runs (from the start it fits 100 % and a time under an hour).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: no "0 s left" while the last batch is late; the status text has room for any time left from the start
The countdown between batches reached zero when the last batch took longer than the speed said,
and showed "0 s left" until it was three intervals late; it now gives way to the speed up to now as
soon as it runs out. The status bar's room was for a time under an hour, so an export's first
estimate over an hour still moved the text once; it is now for 99:59:59 from the start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: the queue's and the Progress panel's job text change once a second, with digits of one width
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui-egui: tabular_label is read by screen readers as a label
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #3459: finalize MP4 video exports with fast-start metadata
Use the existing fast-start muxer for MP4 video, support reading buffered outputs, and retain readable reserved proxy handles. Add disk, memory, reserved-handle and I/O failure regressions.
Implemented with OpenAI Codex assistance.
* #3459: assert decoded audio RMS in fast-start exports
Use the existing RMS helper to check the expected tone level across every output mode and container. Contributor reports the updated tests passing.
Implemented with OpenAI Codex assistance.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Every source keeps up to 384 MiB of decoded frames plus its live decoders
for the whole session, and the 1 GiB process budget only trims caches that
were idle while another cache asks for frames. The 8/16-bit plane shelves
(192 MiB each) were never trimmed. A long-lived process (MCP) therefore
held 2-3 GiB after an export and ran out of memory on the second one.
Add filmcraft_codecs::release_idle(), which empties every cache nobody is
decoding on (try-locked, so users are untouched), and
filmcraft_frame::pool::trim_planes(). A standalone export calls both next
to trim_f32 when its pipeline drops; parts of a batch keep them.
* #3396: preserve binary transparency in animated GIF exports
Enable Include Alpha Channel for GIF with an explicit 128/255 threshold. Cover transparent, opaque and partial-alpha pixels, frame disposal and the Export UI. Document the behavior and add the requested username-only contributor credit.
Co-Authored-By: Codex <noreply@openai.com>
* #3396: clarify when the GIF alpha threshold applies
Begin the export hint with "When enabled," and wrap the longer label so its explanation remains visible. Alpha behavior and threshold wording remain unchanged.
Co-Authored-By: Codex <noreply@openai.com>
* #3396: cover GIF transparency during palette quantization
Round-trip two frames with more than 256 post-conversion RGBA colors, a moving isolated transparent pixel, and opaque black and near-black neighbors. Require exact alpha and bounded RGB error. Document commands for the GIF and export test suites; leave the encoder unchanged.
Co-Authored-By: Codex <noreply@openai.com>
---------
Co-authored-by: Codex <noreply@openai.com>
make_venc walked the registered encoder factories with find_map, and
`Some(Err(..))` is a `Some`: a factory that failed ended the walk, and no
later factory was asked. That is wrong as soon as more than one factory
could take the export.
For H.264 the software encoder is the documented fallback (Export >
Hardware encoding: Auto: "use the hardware encoder when the system has one
and it takes the settings; otherwise the software encoder"). A hardware
factory that *declines* already answers None, so the walk continues; but a
factory that *failed* answered Some(Err(..)) and the export died instead
of falling back.
For H.265 there is no software encoder at all, so the next factory may be
the only one that can take the export. With a second hardware encoder
registered (Media Foundation, #2488) this is not hypothetical: on an NVIDIA
Windows machine the H.265 export failed with "opening the NVENC session
failed" and the Media Foundation encoder, which does work there, was never
asked.
The walk now continues past a factory that failed and reports the first
failure when no factory takes the export, so the message a user sees is
unchanged.
Co-authored-by: OpenCode (MiniMax M3.1 Flash) <noreply@opencode.ai>
The convert stage counter stopped growing once the render threads
converted every frame ahead, which failed
determinism_tests::an_export_adds_up_the_wall_time_of_its_stages
under nextest (one process per test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The H.264 encoder's RGBA -> 4:2:0 conversion runs on the render
threads (VideoEncoder::prepare_ahead), which are idle while a batch is
encoded; encode() picks the planes up by picture index and converts a
picture itself when there are none (then on the encoder's own pool,
not the busy global one).
- rgba_to_yuv420_8 uses per-code tables of the same products, rounds
without roundf and works row by row so the divisions vectorise; the
output is pinned against the float formula for all 2^24 colours.
- Full-size opaque 8-bit Y'CbCr -> linear conversion tabulates R' and
B' per code pair with the expressions of the per-pixel loop.
- over_black_rgba8 fetches the sRGB table once per picture.
bench_export (1080p H.264 + AAC to H.264 + AAC, Ryzen 16 threads, 7
interleaved rounds) with the encoder change: median 7.86 s -> 6.14 s
(-21.9 %), min 7.78 s -> 6.02 s (-22.7 %). Same file (MD5) as
before, and the same for a ProRes export of the clip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the test modules in whisper/tokenizer.rs and export/pipeline.rs to the
end of their files. Group the output path and optional open file in proxies.rs
into a Dest struct so transcode_with_file takes 7 parameters. Behaviour is unchanged.
* chore: resolve SonarCloud maintainability issues
* fix: repair CI on the Sonar maintainability refactor
- h264: the coefficient scratch is i32, so the method path is <[i32]>::first_chunk_mut
- hevc: Px::w32 takes self, so keep the closure over the &P from slice::get
- tts: upsamples is a method, not an importable item
- vp8: drop the unused B_DC_PRED/DC_PRED imports; cargo fmt the import lists
- ui-egui: editing no longer needs to be mutable
- packaging: keep literal printf formats (shellcheck SC2059)
* fix: restore the mutable binding in hot_number
* Tests: one integration-test binary per crate
Each crate with several tests/*.rs files keeps them as modules of tests/it/,
so cargo links one test executable per crate instead of one per file. Files
that change process-wide state or time a workload stay binaries of their own.
* Profiles: line tables for workspace crates, no debug info for dependencies
* Windows ARM64: nightly and on release pushes instead of every push to main
* xtask ci: clippy and tests in the dev profile
The release profile's thin LTO and single codegen unit made linking the test
binaries take most of the gate. The codec and audio crates stay at opt-level 3
in the dev profile, so their tests run at release speed.
* xtask ci: nextest, doctests, no examples, one wasm check
Tests run with cargo nextest when it is installed (cargo test otherwise) and
build with --lib --bins --tests, so examples aren't linked; doctests run
separately. The wasm check keeps going past the first failing crate. VP8
fixture generation locks across processes, since nextest runs each test in
its own.
* VP9 synthetic fixtures: write aside and rename
A test in another process could read a fixture ffmpeg was still writing.
* Fuzz tests: a third of each sweep by default, all of it with FILMCRAFT_FUZZ_ITERS
Every seed, mutation kind and thread count still runs. Header bytes each get
one overwrite value and two bit flips in rotation, so every value and bit
index still occurs across the region. FILMCRAFT_FUZZ_ITERS=N runs the whole
sweep plus N random rounds per seed, as before.
* Platform fallback tests: one failure point per hybrid path
The failure-anywhere test injected a failure at four points around every
restart point; the hybrid's paths differ only in the first GOP, around the
second and last restart points, at the end and never. The replay test needs
the first two GOPs.
* Export tests: shorter ranges with the same checks
The bitrate test measures 0.5–1.5 s, where each preset reaches its own
target (on 0–3 s the simple demo left two presets at the same rate). The
every-preset test exports three frames per preset.
* Audio tests: effects in parallel, a table-driven FFT for the Opus quality metric
The extreme-settings test keeps both settings and the 40 s tail for every
effect, one thread each. The Opus quality metric's FFT builds its twiddles
once and allocates nothing per transform (same algorithm and summation order).
* CI: dev-profile gate with nextest
xtask ci no longer builds with --release, so the release-profile overrides
go; nextest is installed for the test step.
* CI: apt packages through the cached install-apt-packages action
* Review fixes: fuzz tests with a panic hook in their own binaries, loud fixture lock
The fuzz tests of av1, h264, hevc, mpeg2v, vp8 and vp9 count panics through
a process-wide hook, so a panic in another test of the same process would
fail them; they are binaries of their own again. The VP8 fixture lock fails
the test instead of skipping it silently, and the full-sweep doc comments say
how many truncation points the full run samples.
* Fuzz tests: rotate the default subset per run (FILMCRAFT_FUZZ_SEED replays one)
* platform tests: decode-switch tests in their own binaries, one NVENC lock
* security(proxies): reserve outputs atomically
* proxies: ReservedOutput::file returns an Option instead of expect()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ColorSignal::apply_to wrote the sample entry's colr box only for HDR, so
SDR ProRes, DNxHR and APV MOVs carried no colour that Apple's media stack
reads (it takes their colour only from colr, not from the codecs' frame
headers): QuickTime Player and Final Cut saw no primaries, transfer or
matrix. SDR exports now get colr with the export's colour too (nclc in
MOV, nclx in MP4); HDR metadata stays PQ-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bound outer export batches by actual renderer-pool capacity before nested Rayon work can steal a sibling frame. Cover image sequences, GIF, stepped export and overlapping audio with the original one-renderer regression.
Addresses #1956.
Full original GPU contribution preserved at `preserve/861-gpu-export-original` (674e8cd68d).
Co-authored-by: Codex <noreply@openai.com>
`ExportSettings::max_threads` (`maxThreads` in JSON, default 0 = all cores)
makes `export()` run on a dedicated rayon pool of that size when it is below the
pool the export is called in. Batch size, render-ahead, the renderer pool and the
parallel work of the software encoders follow it because they ask rayon for the
number of threads. A cap at or above the current pool changes nothing.
The commands that resolve export settings accept it as a flat parameter or inside
`settings`. The output does not depend on it. The decoders' and the H.264
encoder's own thread pools are not counted yet.
- AudioSettings gains `float`; `bits` takes 16, 24 or 32. `SampleFormat` (Int16 / Int24 /
Int32 / Float32) and `ExportSettings::supports_sample_format` decide what each codec and
container stores: WAV, AIFF and QuickTime PCM all four, MXF integers, FLAC integers.
`validate` refuses float without 32 bits and float in FLAC or MXF.
- WAV float is WAVE_FORMAT_IEEE_FLOAT (EXTENSIBLE for more than two channels) with a `fact`
chunk; AIFF float is AIFF-C `fl32`; QuickTime float is `lpcm` with the float flag; MXF writes
32-bit integers. Float keeps the mix as it is, peaks above 0 dBFS included; 16 and 24 bit
write the same bytes as before.
- FLAC encoder: 8-32 bits. 32-bit stereo is coded independently (a side channel would need 33
bits), the sample range check no longer overflows, and 32-bit frames point at STREAMINFO for
the size, which symphonia 0.5 (FilmCraft's own FLAC import) and ffmpeg both read.
- Export mode: Sample Size shows 16 bit, 24 bit, 32 bit and 32 bit float like Audio Format
does, greying out what the format cannot store with the reason on hover; a size that stops
being available (float after switching to FLAC) becomes 32-bit integer. es, fr, pt-br, id,
ja and zh-cn strings.
- Tests: writer layouts and ffprobe codecs for every new format, round trips through ffmpeg
and FilmCraft's own readers in WAV, AIFF, MOV, MXF and FLAC, headroom of a +6 dB tone,
refused combinations, full-scale 32-bit FLAC through ffmpeg (MD5 too) and symphonia, and a
UI test of the selector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nzevSsTNZxHPpUngfGTAw
Keep FLAC beside main's new AAC Audio format (21 formats, both audio-only
presets), use is_mp4_with / is_mp4_video in audio_codec() and the Audio
Format picker so MP4 (incl. AV1) offers AAC or FLAC, and add flacLevel to
main's export.media params.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: OpenEXR sequences (#806) were written uncompressed only, because the
workspace had no deflate encoder; the files are 2-4x larger than necessary and
Premiere offers an EXR compression choice.
Fix: a small clean-room DEFLATE (RFC 1951) encoder in a zlib (RFC 1950)
wrapper in filmcraft-export (greedy LZ77 with bounded hash chains; stored,
fixed or dynamic Huffman per block, whichever is smallest; length-limited
codes). The EXR writer gains RLE, ZIPS (1 line) and ZIP (16 lines) chunks with
the format's byte interleave + delta predictor; chunks that do not shrink are
stored raw. New ExportSettings::exr_compression (default None, as before),
`exrCompression` param on file.exportMedia, Compression combo in Export mode
(es/fr/ja/zh-cn strings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the GIF exporter gave every frame the same delay of
1000·den/num ms, which the encoder truncates to whole centiseconds.
At 24 fps each 41.67 ms frame became 4 cs, so a 4.0 s sequence played
in 3.84 s (4 % fast); other non-integer rates drifted the same way.
Fix: each frame's delay now runs from its rounded start time to the
next frame's (gif_delay_cs), so delays alternate (4, 4, 5, 4, 4, 4 cs
at 24 fps) and add up to the exact duration. Checked i128 math also
replaces the unchecked `(1000 * den) as u32` cast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With NVENC registered on Linux, `filmcraft-cli --test cli` aborts in 2 of 45 runs on an RTX 3090
(`malloc(): mismatching next->prev_size`, `malloc(): unaligned fastbin chunk detected`) right
after an export finishes; 0 of 30 runs fail with this reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere's "AAC Audio" format has no FilmCraft equivalent. AAC
was only written inside H.264 / H.265 / AV1 movies; the only audio-only
exports were Waveform Audio and AIFF.
Fix: a new `aac` export format ("AAC Audio") that encodes the sequence
mix with the existing AAC-LC encoder into an MPEG-4 audio file (.m4a,
encoder delay in an edit list), or a raw ADTS stream when the path ends
in .aac. Bitrate, sample rate, channels and loudness normalisation follow
the audio settings. Adds an "AAC Audio 48 kHz 320 kbps" Audio Only
preset, CLI extension guessing (.m4a / .aac), es/fr/ja/zh-cn strings, and
updates G13 (also noting ProRes 4444 / XQ is already in the export UI).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere offers OpenEXR as an image-sequence export format;
FilmCraft could not write EXR at all (docs/gaps.md G13).
Fix: a new export format (id exr, "OpenEXR") writing numbered .exr stills,
with a preset, the Include Alpha Channel option, the file.exportMedia format
list and es/ja/zh-cn strings. The writer in crates/export/src/raster.rs
follows the public OpenEXR file layout: version 2 single-part scan-line
file, half-float B/G/R(/A) channels, no compression, one scan line per
chunk, Rec.709 chromaticities; values are scene-linear (the pipeline's
transfer undone) with premultiplied alpha, as EXR defines. No new
dependency. ZIP / PIZ / DWA compression stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere offers JPEG, Targa and DPX as image-sequence export
formats; FilmCraft only wrote PNG, TIFF and BMP sequences (docs/gaps.md G13).
Fix: three new export formats (ids jpg, tga, dpx), numbered like the other
sequences, with presets, the export UI (JPEG quality slider, Targa alpha)
and es/ja/zh-cn strings. JPEG uses the image crate at the export quality;
Targa (TGA 2.0, uncompressed 24/32-bit) and DPX (SMPTE ST 268, 10-bit RGB
method A, BT.709) are written by a small hostile-input-safe writer in
crates/export/src/raster.rs. Format::is_image_sequence replaces the
hand-kept format lists. OpenEXR stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NVENC HEVC encoder (Main 8-bit SDR and Main 10 HDR PQ / HLG) was gated to
Windows. It now also runs on 64-bit Linux, on the CUDA device the H.264 encoder
already uses: the H.265 format and HDR probes are registered there and the
HEVC tests run on both systems.
NVENC registration on Linux now happens after the VA-API probe. Started beside
it, the background HEVC probe's first cuInit could deadlock with
libva-nvidia-driver, which calls cuInit from its library constructor under the
loader lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With GPU rendering on, a frame the planner hands back whole (an adjustment
layer, a complex transition, an HDR / wide-gamut sequence) was still rendered
by render_sequence, but inside RendererPool::with: as many frames at a time as
there are pooled renderers (2 to 4) instead of one per export thread. A
timeline graded on an adjustment layer exported slower with Auto than with Off.
Pipeline::frame asks the planner first (plan::is_cpu_frame, the test
plan_frame itself makes) and renders such a frame on the CPU directly. It
counts as a fallback now, not as a GPU frame. The picture does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Format::Av1 in export settings and jobs, SampleEntry::av1 (av01 + av1C) in
isobmff, and the Export panel's Format list, profile and encoder rows. No
encoder ships here: like H.265, AV1 is listed greyed out until a platform
encoder registers a probe, and a job without one fails with "AV1 encoder not
available yet". Split out of #409; the VA-API AV1 encoder follows on main's
vaapi::va design.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>