* 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
Seeks in open-GOP streams restarted at the previous IDR because gop.rs asked
is_random_access, which only accepted NAL type 5. Use the splitter's rule
(sei_has_recovery_point, now exported by filmcraft-mpegts). Test counts the
samples decoded by a seek into the last GOP of the open-GOP fixture.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scan and read copied the 32-byte head of every packet and every pack, PSM
and PES packet (up to 64 KB) with to_vec before looking at it. They now
borrow the window; the borrows end before the next window access. Output is
unchanged: the new large-stream test (two streams, over 2 MiB, MPEG-1 and
MPEG-2 packs, cuts inside the last packets) passes on the old and the new
code, as do the existing round-trip and truncation tests. The allocation
saving follows from the removed to_vec calls; it is not measured (the
workspace forbids unsafe, so there is no counting allocator).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A window refill resized the buffer to at least 1 MiB although read_some can
only fill what the file still holds. Near the end of the file, where the
previous window was shorter, that zeroed about a megabyte for every packet
whose length ran past the end. The buffer is now capped at the bytes left.
Window contents are unchanged: proved against the old refill for many
lengths, positions and sizes, and the zeroed byte count is asserted (at most
1000 against about 500 MiB for the old refill in the tail scenario).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One table lookup per byte instead of eight shift steps. Proved equal to the
old bitwise loop (kept in the test as the reference) for every single byte,
random inputs of 0..4096 bytes, the CRC-32/MPEG-2 check value and the
section-plus-own-CRC-is-zero property.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Unit.pos for transport streams is a byte offset (scan stores off + k *
packet_size), not a packet index; frame_bytes carried the doc line of
frame_info above its own.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Nothing outside one test read them. No behaviour change; the existing
header tests cover parse.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: an H.264/HEVC unit was one PES packet with a timestamp, so
muxers that put several access units in one PES gave one merged unit
(several frames under one PTS, key/disposable OR-combined). H.264 `key`
meant IDR only, so streams with non-IDR I pictures and recovery point
SEI (broadcast, camcorder, cut TS) had no key units and could not be
opened ("no random-access picture in the video stream").
Fix: split units at access-unit boundaries (H.264 7.4.1.2.3, H.265
7.4.2.4.4: AUD/parameter sets/SEI/first slice after a VCL NAL). The
first access unit of a PES keeps its PTS/DTS, later ones have none.
An H.264 unit with a recovery point SEI (payload type 6) is key. The
decoder already starts at a non-IDR picture (frame_num gap concealment),
so crates/codecs needs no change.
Tests: synthetic Annex B in TS (recovery key, several AUs per PES for
H.264 and HEVC, slice-only splitting, unchanged one-AU-per-PES), and
ffmpeg open-GOP / intra-refresh fixtures cut at a recovery point.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: in ts::scan every packet-parse failure re-read a whole 4096-packet
block from an offset that advanced about one packet. packet() returned None
both for a bad sync byte and for an adaptation_field_length that overruns the
packet, so aligned packets with a bad adaptation field cost one ~770 KB read
each (a 564 KB file made ByteSource::read_at fetch 846 MB). The fallback
offset (`pos + got`) also overshot by k packets, skipping unread data, and the
five-packet sync window only looked at the first packet_size start positions.
Fix: packet() distinguishes Bad::Sync from Bad::Adaptation. A bad adaptation
field means "no payload, grid intact": count a warning and go on with the next
packet. A real sync loss is repaired inside the loaded block (resync(): first
start whose five sync bytes line up, any distance); the block is refilled
from the file at the exact new offset only when it is used up. read() skips a
bad-adaptation packet instead of ending the unit.
Tests: read-volume tests with a counting ByteSource (bad adaptation fields,
sync byte every 189 bytes), and a resync test with a garbage prefix and
mid-stream garbage around the block boundary for 188- and 192-byte packets.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: frame_info indexed byte 6 of an AC-3 header that is only guaranteed
6 bytes long (panic), and gave E-AC-3 fscod2 3 a sample rate of 0. The PES
modes of Splitter never pruned their chunk list. read_unit reserved up to
4 GiB up front, the unit table was uncapped and 4-byte E-AC-3 "frames"
amplified input 16x. PesAsm kept passing continuation packets of a non-PES
unit start, and an adaptation-field-only unit start destroyed the PES in
progress.
Fix: b.get(6)? and None for the reserved rate; no chunk bookkeeping where
positions come from feed(); EAC3_MIN_FRAME = 8; MAX_UNITS (2^23) per stream
with a warning; read_unit preallocates at most 1 MiB and refuses units over
128 MiB; PesAsm gets a dropping state and ignores payload-less unit starts.
Regression tests for each.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: packet_len accepted an MPEG-1 pack header (b[4]>>4==2) with
only 6 bytes available, but scan reads the SCR from head[4..9]. A file
ending inside the last pack header (00 00 01 BA 21 00 00) panicked with
"range end index 9 out of range".
Fix: packet_len requires the full 12-byte MPEG-1 pack header; a shorter
tail is not a packet and takes the normal resync path. The MPEG-2 branch
already needs 14 bytes (b.get(13)) before scan reads 10, so it is fine.
Tests: truncate a valid MPEG-1 and MPEG-2 program stream at every byte
length, plus the minimal 7-byte input; both panicked before the fix.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: files with E-AC-3 audio (MP4 ec-3, MOV, Matroska A_EAC3, MPEG-TS)
imported silent: filmcraft-ac3 refused bsid 11-16.
Fix: clean-room E-AC-3 decoding from ATSC A/52:2012 Annex E in filmcraft-ac3:
bsi and audfrm syntax, 1/2/3/6-block syncframes, reduced sample rates,
frame-based exponent strategies (exponents reused across short syncframes),
SNR offset strategies, default band structures, spectral extension
(translation, notch filter, noise blending). Independent substream 0 is
decoded; dependent substreams and other programs are skipped (7.1 plays as
its 5.1 core). AHT and enhanced coupling are refused as unsupported. The
audio blocks share the AC-3 path; AC-3 output is unchanged. filmcraft-codecs
routes E-AC-3 in MP4/MOV, Matroska and TS to the decoder; the TS splitter
joins dependent substream frames to their access unit.
Tests: ffmpeg oracle for raw E-AC-3 (64.8-91.0 dB SNR) and in MP4, MOV,
Matroska and TS (79.5-91.3 dB, random access identical), mutation fuzz,
synthetic frames for headers, substream skipping, refusals and exponent
reuse across 1-block syncframes, spectral extension unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AviSource maps AVI streams onto the decoders, opened from bytes or a
streaming reader (Media Browser probe):
- Video: Motion JPEG, H.264 and HEVC (Annex B, our decoders) and
uncompressed BGR24 / BGRX32 / YUY2 / UYVY / I420 / YV12 / NV12 / grey
(new RawVideoDecoder). Empty "drop frame" chunks hold the picture
before. AVI has no timestamps: H.264 with B-frames is stored in decode
order, so the presentation order comes from the pictures' order counts
(mxf::avc_presentation_order), keeping the slots the pictures occupy.
A writer that kept a fine time base (ffmpeg copying an MP4's 1/600)
fills the slots between pictures with empty chunks; the reported frame
rate is the one the pictures play at. DivX / Xvid and DV are named and
report that they have no decoder; their sound plays.
- Audio: PCM (integer and float) from the chunks; MPEG audio and AC-3
frames, which AVI chunks need not follow, found again across chunk
boundaries with the new filmcraft_mpegts::frame_bytes.
- Motion JPEG frames without Huffman tables (the AVI1 convention) decode
with the standard ITU-T T.81 Annex K tables (also for MOV).
Tests against ffmpeg's decode of the same files: H.264 with B-frames
bit-exact under random access, also copied from a 1/600 MP4;
uncompressed video and PCM bit-exact; JPEG and MP3 / MP2 / AC-3 within
decoder tolerance; drop frames; DivX; JPEG without tables equal to the
same frame with them. Engine: an AVI imports, makes a sequence, plays
picture and sound, and probes without reading it whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust/clippy 1.99 added `chunks_exact_to_as_chunks` and tightened
`needless_late_init` and `drain_collect`, failing `clippy -D warnings`
on main.
- `chunks_exact(N)` / `chunks_exact_mut(N)` with a constant N become
`as_chunks::<N>()` / `as_chunks_mut::<N>()` (fixed-size arrays, so
several byte reads drop their per-element indexing). relink's
fingerprint uses the `(chunks, remainder)` split; its hash output is
unchanged.
- Late-initialised `let`s in ac3 bit allocation and opus CELT
bands/rate become `let x = if/match …`.
- `dpb.drain(..).collect()` becomes `std::mem::take(&mut dpb)`.
No behaviour change; `cargo xtask ci` passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mpegts: a PES unit is built before it is stored (no `as_mut().expect`); stream maps are
drained with `let … else` instead of `remove().expect`.
- ogg: completing a packet and reading the page granule no longer expect.
- mxf writer: the picture descriptor uses `let … else` on the picture configuration.
- matroska: the `Lacing::None` arm returns the single frame instead of `unreachable!`.
- Each crate denies unwrap/expect/panic/unimplemented/todo/unreachable outside tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>