With no colour tags, FilmCraft decodes with BT.709 transfer and
primaries and the matrix by frame size, and probe and media.colorInfo
showed those values with nothing saying they were defaults, so an
untagged file looked the same as one tagged BT.709.
VideoStreamInfo gets color_assumed (optional, left out when unknown):
which of the matrix, transfer and primaries no tag in the file gave.
MP4/MOV and Matroska take it from the same sources as the colour, APV
from its colour description, and AVI, MXF and Y4M mark all three.
probe prints it as video.color_assumed and media.colorInfo as assumed,
next to the unchanged metadata.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* media: name Apple Intermediate Codec and say why a clip is unreadable
Part of #3646. A MOV in Apple Intermediate Codec (fourcc icod, the
editing codec of iMovie and Final Cut) imported with its codec named
"unknown", and its frames failed with "no decoder for unknown video".
Only the log showed that error: the Project panel's badge said
"Unreadable" and nothing more.
- codecs: codec_label names icod "Apple Intermediate Codec". The
"no decoder" errors of make_video_decoder and software_video_decoder
use that label for sample entries the parser keeps as Unknown, so
the error reads "no decoder for Apple Intermediate Codec video".
Other codecs keep their short names in that error.
- ui-egui: an Unreadable badge shows on hover the error the media pool
recorded, on cards (Icon and Freeform view) and in List view, which
now registers its badge as project.item.<id>.offline as cards do.
This covers every cause the pool records, such as "H.264: field
pictures", not only this codec.
- docs: an Apple Intermediate Codec row in codec-parity.md, and the
hover in project-files.md.
FilmCraft still has no AIC decoder. Apple publishes no specification
for it, so a clean-room decoder (AGENTS.md section 2) needs a
maintainer ruling, and #3646 stays open for that.
Tests: apple_intermediate_codec_is_named_though_nothing_decodes_it in
filmcraft-codecs (the label and all three errors), and
unreadable_badge_says_why_on_hover in the ui-egui it suite (both views).
Developed with an AI assistant (Árbol, running Muse).
* ui: keep the List view's badges beside a long name (#3646)
In List view, a name that left less than 40 px of its column hid the
item's offline icon and proxy chip, and with them the Unreadable
badge's automation id and the reason it shows on hover. The badges now
keep their room at the end of the Name column, and a long name is cut
short before them. Only a column too narrow for the badges leaves them
out. Rows that showed their badges before, and rows without badges,
draw as they did.
unreadable_badge_says_why_on_hover imports a file whose name is wider
than the Name column, so its List view pass covers this case. Found by
CodeRabbit in its review of the pull request.
Developed with an AI assistant (Árbol, running Muse).
* codecs, ui: doc comments for codec_label and media_badge (#3646)
Both functions gained a role with the Apple Intermediate Codec change:
codec_label also names the codec in the no-decoder errors, and
media_badge records why an Unreadable file cannot be read. Their doc
comments say so.
Developed with an AI assistant (Árbol, running Muse).
* ui: end a List-view name cut short with "…" and show it in full on hover (#3646)
Keeping the badges' room at the end of the Name column can cut short a name
that used to fit, and the cut left no sign of it. The List view now draws the
name with the cards' elided_name, so a cut name ends in "…", and hovering it
shows the whole name, as the cards already do. Names that fit draw as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* codecs: an MP4 edit rounded to the movie timescale keeps the last frame
Edit durations are whole movie-timescale units (milliseconds in ffmpeg,
x264 and FilmCraft's own exports), so a clip whose length is not a whole
number of milliseconds imported one frame short: 320 frames at 24 fps
(13.3333 s, written as 13 333) read as 319. When the last edit plays the
media to the end of its last picture (within one unit), the clip now lasts
exactly that long.
* codecs: an edit exactly one movie-timescale unit short of the media is a trim
Rounding to the movie timescale is always less than one unit off, so a
difference of exactly one unit (a one-frame trim when the movie and track
timescales are equal) keeps the edit's own length.
* codecs: compare the edit with the media exactly, and never across a whole picture
The comparison now runs in the two timescales themselves (Tick flooring
could put a one-unit trim at 23/23 under the bound), and a difference of a
whole last picture or more is a trim even when a coarse movie unit (1/24 s
against 1/30 s pictures) is longer than that.
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.
* fix(codecs): frame lookup inside a frame with one timestamp unit per frame
MP4 and Matroska look a frame up by rounding the requested time to the
nearest timestamp unit (#73). With a unit of a frame (MP4 timescale =
frame rate, Matroska TimestampScale = frame duration) half a unit is
half a frame, so from every frame's middle on the next frame was shown.
With a unit of half a frame or more the request now moves back to the
start of its frame (nominal rate) before rounding; finer units keep
#73's round-to-nearest.
generated with Claude Code
* fix(codecs): snap coarse-unit lookups only on constant-rate tracks
A timestamp unit of half a frame or more is no proof of a constant
rate: a variable-rate track (timescale 50, samples 2, 1, 2 units long)
has a frame starting between two nominal frames, and moving a request
at that start back to its nominal frame showed the frame before.
The snapping is now decided once per track at open (gop::
snap_coarse_units): only when every sample stamp is where the snapped
lookup lands for its nearest nominal frame. Other tracks keep the
lookup by the nearest unit (#73). Regression test for MP4 and Matroska.
generated with Claude Code
* #2798: contain Symphonia audio decoder panics
Guard decoder creation, packet conversion and standalone imports. Keep
panicked decoders disabled across reset and reject partial imports while
preserving ordinary corrupt-packet recovery. Add synthetic regression
and hostile-header tests plus crate documentation.
Refs #2798; H.264/HEVC worker containment remains separate.
Co-authored-by: Codex <noreply@openai.com>
* #2798: document panic strategy limits of audio guards
Clarify that decoder panics become CodecError::Decode only with panic=unwind. Document the default wasm32 abort behavior and retain the process-abort and allocation-failure caveats.
Co-authored-by: Codex <noreply@openai.com>
* codecs README: drop the PR's local test log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rayon_frame_requests_while_decoding_on_rayon_never_deadlock waited 120 s
at a time and checked for progress only when a wait ran out. When the
requests stopped just after one was answered at the start of a wait, the
next check still saw progress, and the test reported the deadlock about
240 s after the last answer.
- wait_while_answered polls every second and fails once 120 s have
passed since the answered count last grew.
- a_stall_right_after_an_answer_is_reported_once_the_limit_has_passed
checks it with a 2 s limit: one answer after 50 ms, then none.
* 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
* security(media): bound generator frames, imported audio dimensions, and render images
* fix(media): unwrap render result in demo_frames example
* fix(render, codecs): guard zero-sized images, check post-scale sequence pixels, and bound combined MXF channels
* fix(codecs): annotate channels as u32 in MXF sound track combination
* fix(render): preserve exact output_size scaling calculation for renderable_sequence validation
* fix(media,render): reject unsupported audio dimensions and invalid render scale
Sample rates above MAX_AUDIO_SAMPLE_RATE were clamped without resampling, so a
768 kHz stream was reported as 384 kHz and played at half speed. Reject them
instead, and drop the stream at the four codec call sites rather than
advertising dimensions the samples do not match.
output_size now treats a non-finite or non-positive scale as 1.0 instead of
casting it to zero and collapsing the frame to 1x1.
* fix(media): keep Matroska audio tracks and their metadata together; guard empty images
A track whose advertised sample rate or channel count is refused is dropped before the track list is built, so atracks[k] and audio_streams[k] stay the same track. over_black_with_alpha_rgba8 and Image::transformed return early for an empty image instead of chunking by zero.
gop::tests::rayon_frame_requests_while_decoding_on_rayon_never_deadlock
gave its 3,840 frame requests 120 s in total. On a loaded machine they
take longer while every request is still answered. With six copies of
the test running at once, all 12 runs on `main` failed after 121 to
123 s with "frame requests deadlocked".
The test now counts answered requests and fails when none is answered
for 120 s. The same 12 runs pass, in 166 to 259 s. With the #480 fix in
`isolated` turned off, the test still fails ("frame requests deadlocked
after 927 frames").
Seeking used to build a new decoder (a new rayon pool) and drop the old one,
whose queued picture jobs kept decoding and competed with the new position.
Decoder::reset forgets the stream like a new decoder but keeps the worker
threads; jobs of the old position stop at their next macroblock row and still
publish all their rows, so nothing waiting on them blocks. The codecs H.264
decoder resets with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Build: use craft-media for bitstream, isobmff and matroska
filmcraft-bitstream, filmcraft-isobmff and filmcraft-matroska re-export
craft_media::{bitstream, isobmff, matroska} from craft-libs. The packages
and their public paths stay, so dependent crates build unchanged. The
integration tests in crates/isobmff/tests and crates/matroska/tests run
against the re-exports.
* Build: pin craft-media at 9db732e
craft-media spells the Matroska colour types in US English: Colour is
Color and VideoInfo::colour is VideoInfo::color. The spec names
element_ids::COLOUR, COLOUR_SPACE and VpcConfig::colour_primaries stay.
* matroska: the oracle skips the duration of a track's last frame
Ported from craft-media: the last audio or video frame of a track cut by
-t is shorter than a codec frame. ffmpeg 8 writes a shortened
BlockDuration and reports the codec frame duration; a remux by our writer
stores DefaultDuration and ffprobe reports the shortened one. Subtitle
cue durations are still compared. This fixes oracle::mjpeg_ac3 with
ffmpeg 8. The HDR test is named hevc_hdr_color, as in craft-media.
* matroska: laced_remux laces every audio track and checks timestamps
craft-media laces only tracks with a DefaultDuration. The remux gives
each audio track without one its mean packet spacing, so Vorbis, FLAC
and PCM lace too, and compares every packet's timestamp with the
source's. Ported from craft-media's oracle_matroska.rs.
* codecs: the Matroska audio timing test laces its Xiph case
craft-media laces only tracks with a DefaultDuration. The test's
Matroska helper sets the AAC access unit's duration (1024 samples,
rounded to the nanosecond), and the test checks that the Xiph-laced file
has laced blocks and the unlaced one has none.
* Build: repin craft-media to f97c84d27 (NAL length-size checks)
craft-libs#245 is on storytold/craft-libs main, so the dependency points there.
* Build: repin craft-media to f0ad6c1e1 (QuickTime v2 PCM entry limits)
* 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
* codecs: walk open-GOP leading pictures back in a loop, not by recursion
MxfVideo::sync_before and MpegVideo::sync_before recursed one GOP back
per leading picture, so a crafted index whose every GOP leads its key
frame recursed once per GOP: tens of thousands of frames for a small
file, a plausible stack overflow in dev builds.
Both now use gop::open_gop_sync, a loop over the same steps. It stops
when the index's sync sample lies past the picture instead of looping,
and reads pts and keys with get().
Fixes#2803
* gop: keep decode_start's doc comment on decode_start
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>
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>
MOV / MP4 files with uncompressed video were reported unreadable: the
fourccs were recognised as video but no decoder factory took them (the
raw decoder was wired up for AVI only). A raw_factory now decodes
`raw ` at depth 24 (packed RGB, unpadded rows), `2vuy` (UYVY 4:2:2)
and `v210` (10-bit 4:2:2, shown at 8 bits), and the media info names
them. fl32 audio in these files already played.
Fixes#2340
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A DTS (or TrueHD, or any other undecodable) audio track in an MKV showed
up as a normal stream and simply played silent. Label it
"<codec> (unsupported)" in the media info as MPEG and MXF already do,
name DTS / TrueHD instead of their codec ids, and say in the decode error
that FilmCraft has no decoder for it. The DTS decoder itself stays open.
Refs #1422
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>