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>
* interchange: '..' climbs out of a relative document folder (#3998)
join popped a '..' only while the base had more than one part, which kept
an absolute path at its root but also kept a relative folder's first part:
`edit` + `../media/a.mov` gave `edit/media/a.mov`. A relative base now loses
any folder to '..' and keeps a leading '..' past its start; an absolute base
still stops at its root.
* engine: a document given by a relative path has its media resolved once (#4001)
interchange::import passed the document's folder as given, so a document
opened by a relative path gave media paths relative to the working directory,
and MediaPool::follow then took them from the document's folder a second time
(edit/edit/sub/a.mov). The folder now comes from the document's absolute path,
as doc_path::resolve already takes it.
* interchange: a '..' after a base that ends in '..' is kept (#3998)
std::path::absolute keeps '..' on POSIX, so a document opened as
`edit/sub/../cut.xml` (or `../cut.xml`) gives a base that ends in '..'.
join treated that as the root of an absolute path and dropped the media
path's own '..', looking for `edit/sub/../media/b.mov`. Now only a real
root (`/`, `C:`) absorbs a '..'.
* interchange: a document folder's own '..' is resolved before its media paths (#3998)
Keeping a '..' after a base that ends in '..' let a media path climb above
the root: importing ../cut.xml from /work gave /work/../../media/b.mov,
which craft-docpath refuses. join now resolves the base's own '.' and '..'
first with the same rule as the media path's (take off the last folder,
keep it past the start of a relative path, drop it at an absolute root), so
both /work/edit/.. and /work/.. with ../media/b.mov give /media/b.mov.
* interchange: join reads a folder's root as craft-docpath does, and drops empty names (#3998)
join counted one root part, so `..` could take the share off
`\\srv\share\..\edit`, and it kept empty names, so `./` + `clip.mov` gave
`/clip.mov` and a repeated separator gave `..` an empty name to remove.
The folder's root is now read as craft-docpath reads it (none, `/`, `C:\`,
drive-relative `C:`, `\\host\share`, `//host/share`, `\\?\C:`,
`\\?\UNC\host\share`). Below it, empty names and `.` are dropped and `..`
takes off the last name, is dropped at an absolute root and kept past the
start of a relative folder. One table-driven test covers the path forms.
Found while testing crates/interchange/src/common.rs:36 (percent_decode, used by file_url_to_path and resolve_url)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/interchange/src/edl.rs:~90 (parse_channels, the `p.starts_with('A')` arm)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* interchange: EDL and OMF export reports name the video tracks they leave out (#4132)
* edl: report no video exported when the chosen track is empty
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* edl: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The OTIO importer took the sequence rate from global_start_time or the
first item with a source_range, whatever its rate. A gap or clip at rate
48000 is valid OTIO, so the sequence came out at 48000 fps and the
engine refused the import. Both now skip a rate no sequence can run at.
* interchange: EDL media relinked with Align Timecode lands on the file's own time (#3990)
An EDL's source times are timecodes, but its media items had no start
timecode, so Align Timecode had nothing to align from and a clip relinked
after an offline import kept its timecode as media time (the file's last
frame). EDL media now starts at timecode 0, and the relink shift reads each
timecode at its own file's rate, as the import-time rebase does.
* engine: EDL relink test checks the placeholder is listed missing, not its offline flag (#3990)
#4000 stops flagging EDL clips named only by clip name offline, so the setup asks media.findMissing instead, which lists the item either way.
* engine: the EDL relink test also runs with 25 fps media (#3990)
make_movie_at takes the frame rate. Under a 24 fps EDL a 25 fps file's timecode must be read at 25 fps: with the old shift (old_rate.tick_of(b - a)) the clip lands at -149 s instead of 1 s.
An EDL line with no `* SOURCE FILE:` guesses its file from the clip name in
the EDL's folder. When that file wasn't there the importer flagged the clip
offline, which Link Media reads as "made offline": the folder search, Relink
Others and the prompt on open all skip it. It is now a missing file, as an
unfound `* SOURCE FILE:` is.
A project file can hold any i64 sequence start timecode and any u32 audio
channel count and sample rate (load-time validation checks neither). The EDL
writer added the start timecode to every record frame unchecked; the AAF and
OMF writers multiplied channels by bytes per sample and by the sample rate,
and wav_header did the same in u16. Channel counts, frame counts and block
sizes were then cut down with `as` (65 536 channels became 0 in OMF and in
the embedded WAVE essence), and aiff_header's chunk sizes wrapped. Debug
builds panicked; release builds wrote wrapped values. All of it saturates
now, as does frames_round's next frame. Regression tests for EDL, AAF and
OMF; found by mutation-fuzzing project files.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* interchange: AAF top-level composition carries the registered Usage_TopLevel (#3986)
The usage code was 060e2b34.04010105.0d011201.02030000, which is in no
SMPTE register. Usage_TopLevel is 060e2b34.04010101.0d010102.01010700
in the Labels register. LibAAF looks for it and finds no sequence
without it. Files with the old code still import through the reader's
fallback to the composition no other one uses.
* interchange: AAF import reads the old FilmCraft top-level label as top-level too (#3986)
With the registered Usage_TopLevel recognised, a composition tagged with
the label FilmCraft 0.16 and earlier wrote was dropped whenever another
composition in the file carried the registered one. Both now count.
Found while testing crates/interchange/src/edl.rs:118 (parse_event_line, num_at parsing as i64) and :498 (import_event, tick_of(l2.dur))
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* M3: Add shared custom colors and complete marker editing
* #1435: refine native colors and range endpoint flags
Use the macOS Colors panel for custom label colors and reuse the established egui picker on other hosts. Preserve draft confirmation, exact sRGB values, native text shortcuts and project ownership. Compact the swatches and center the add control, split range glyphs across their endpoints, localize the new controls, protect newer marker edits from stale dialogs, and cover the behavior with rendered UI and hit-test regressions.
Co-authored-by: Codex <noreply@openai.com>
* #1435: preserve imported palettes and marker double-click ownership
Union saved OTIO palettes across timelines with deduplication, bounded capacity,
first-name preservation and explicit overflow warnings. Keep null authors
consistent with omitted fields, and consume marker double-clicks before the
work-area gesture so opening the editor does not alter project history.
Correct the CSV URL column and schema documentation. Add six regressions for
palette retention/capacity, author handling, and physical overlapping gestures.
Validation: full cargo xtask ci (3666 passed, 45 skipped), 42 rendered UI tests,
and strict workspace/all-target Clippy on Rust 1.99.1.
Co-authored-by: Codex <noreply@openai.com>
* #1435: retain OTIO swatch names and generator labels
Ignore orphan name entries when retaining saved palette names so input ordering
does not hide valid names after the first 64 entries. Keep names bounded by the
retained palette and preserve existing valid names.
Include generator preset labels alongside custom label colors in OTIO metadata.
Add regressions for orphan-prefixed name maps and three generator kinds with
preset/custom colors through actual export/import round trips.
Validation: both regressions failed before fixes; all 72 interchange tests and
full cargo xtask ci pass (3668 tests, 45 skipped), plus strict workspace/all-target
Clippy on Rust 1.99.1.
Co-authored-by: Codex <noreply@openai.com>
* #1435: preserve distinct OTIO generator item identity
Use exported FilmCraft item IDs for generator media identity so identical
content can retain distinct source names, labels and custom colors. Repeated
references to one source item still share a single imported media entry.
Preserve content-based fallback for missing or invalid item IDs. Cover black
video, bars and tone, and color mattes with distinct items/shared references,
plus legacy and malformed identity metadata.
Validation: distinct-generator regression failed before fix; legacy compatibility
passed. All 74 interchange tests and final full cargo xtask ci pass (3670 tests,
45 skipped); strict workspace/all-target Clippy also passes on Rust 1.99.1.
Co-authored-by: Codex <noreply@openai.com>
* #1435: align Edit Marker layout and add safe marker navigation
Give the editor a wide aligned form, larger Notes area, side action column,
and grouped palette with vertical marker types. Expand the Web Link field
with the form and keep actions accessible in small windows.
Previous/Next saves valid edits as one Undo step and stays with the original
sequence/source owner, including coincident markers. Invalid drafts stay open;
unchanged navigation and Done add no history. Keep native/shared color pickers,
Author, Notes, URL validation and immediate marker creation.
Validation: all 45 rendered sequence/marker UI tests pass, including five new
navigation/owner/validation/layout regressions. Native macOS layout, picker
Cancel and exact single-step Undo restoration verified. Full standard cargo
xtask ci passes 3675 tests (45 skipped), doctests and all remaining gates;
strict Rust 1.99.1 workspace/all-target Clippy also passes.
Co-authored-by: Codex <noreply@openai.com>
* #1435: refine Edit Marker proportions and action alignment
Narrow the dialog to 680 logical pixels and keep the core order Name,
Time/Duration, Notes, Color and marker type. Place Author after the type-specific
options as secondary metadata. Preserve our palette, custom picker and range controls.
Use consistently sized, centered rounded action buttons with a primary Done action.
Shorten Delete Marker to Delete and remove the navigation helper and unused translations.
Keep the existing navigation save/validation/Undo semantics and automation IDs.
Validation: all 45 rendered marker/sequence UI tests pass, including small-window
action accessibility. Actual macOS layout and no-op Previous/Next/Cancel verified.
Full standard cargo xtask ci passes 3675 tests (45 skipped), doctests, formatting,
Clippy, translations, layers, assets and 49 WASM crates. Strict Rust 1.99.1
workspace/all-target Clippy passes separately.
Co-authored-by: Codex <noreply@openai.com>
* #1435: tighten marker layout and validate text editing
Use a narrower aligned form with Author below Name, bounded scrolling Notes,
left-aligned colors, centered actions and an inline Web Link URL/open icon.
Remove the legacy Flash Cue choice while preserving imported marker types.
Constrain timecode typing/paste to complete numeric colon-separated fields,
validate drop-frame labels and frame bounds, and disable Done for invalid timing.
Preserve high-frame-rate timecodes and point-marker semantics when Duration is
unchecked. Route native/menu clipboard and text Undo commands to focused fields;
explicit engine commands retain project Undo and selection semantics.
Validation: standard cargo xtask ci passes 3679 tests (45 standard skips),
doctests, fmt, strict workspace/all-target Clippy, i18n, layers, 69 assets and
49 WASM crates. Also passed 48 rendered marker/sequence tests and strict Rust
1.99.1 Clippy. Actual macOS clipboard, bounded Notes, point save and exact Undo
were checked. Windows/Linux desktop interaction remains pending.
Co-authored-by: Codex <noreply@openai.com>
* M0: Wait for frame-worker recovery before asserting
Do not treat a nonempty request queue as proof of worker death in the
panic-recovery regression. Keep waiting for all injected faults and enough
post-fault completions, then drain the queue without adding more seeks.
Retain the exact fault-count, successful-recovery and empty-queue assertions
and bound the wait with a two-minute deadline.
The inherited test failed twice in independent fork CI. It did not reproduce
locally: baseline passed 20 normal and 10 delayed-report runs; the repair
passed 50 normal and 10 delayed-report runs. No product or marker UI changes.
Validation: standard cargo xtask ci passed all 4485 tests (52 standard skips),
doctests, formatting, strict workspace/all-target Clippy, i18n, layers,
69 assets and 49 WASM crates.
Assisted-by: OpenAI Codex
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <codex@openai.com>
Import standalone video elements as picture-only clips, retaining media
paths, timing, adjustments, markers, transitions and connected placement.
Warn when a video references an unsupported generator or source.
Add synthetic importer and engine regressions covering zero-duration still
assets, missing-media relinking, save/reopen, undo/redo and rendered pixels.
Document the FCPXML 1.10 specification and supported behavior.
Validation: interchange tests and the engine slideshow regression pass,
as do scoped Clippy, formatting, layers, assets and WASM. Full CI on Rust
1.95 stops at the existing ProRes nonminimal_bool lint; unrelated CI
repairs are excluded from this change.
The reporter's original XML imports all 55 slides, 54 transitions and
55 media items in a temporary local check; reporter files are not included.
Fixes#2837
Co-authored-by: Codex <noreply@openai.com>
* 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
Bounded item_path, item_media, and base_item in interchange helpers
and ale::export_items with a 16-step bound matching crates/project.
Cyclic subclip chains (self-referencing or mutual cycles) and overly
deep chains now terminate safely without causing a stack overflow abort.
Closes#2788
Found while testing crates/interchange/src/common.rs:68
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/interchange/src/xml.rs:7 (escape, used by start_tag for attributes)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CodeRabbit review: import_with returns early for AAF and OMF, so the
shared bound_start_timecodes pass never ran for them and an AAF with an
absurd start timecode kept it. The AAF and OMF importers now run the
pass themselves (the test fails without it for AAF; OMF carries no
sequence start timecode).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>