#767: import MXF (OP1a / OP-Atom) footage
Problem: the Media Browser lists .mxf files, but File > Import's dialog filtered them out and media::is_importable refused them, although the pinned FilmCraft codecs behind crates/media already demux MXF (AVC, MPEG-2 / XDCAM / D-10, VC-3 / DNxHD, ProRes, PCM audio) by content. Fix: add "mxf" to VIDEO_EXTENSIONS and to the File > Import and Set Proxy dialog filters. Tests check MPEG-2 4:2:2 long-GOP + 24-bit PCM and DNxHD 36 MXF against ffmpeg frames (>35 dB, measured 64-67 dB), audio level, and that the Import dialog offers MXF. AVI stays open: FilmCraft has no AVI demuxer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9a7d3e7699
commit
f0bd916543
|
|
@ -7,8 +7,8 @@
|
|||
//! memory-budgeted LRU frame cache, one decoder per source, and the decoder kept positioned for
|
||||
//! sequential playback. Also audio ([`MediaPool::audio_samples`]) and thumbnails.
|
||||
//!
|
||||
//! Video containers (MP4/MOV, Matroska/WebM) and codecs (H.264, HEVC, VP9, AV1, ProRes, DNxHD,
|
||||
//! MJPEG; AAC, Opus, PCM, MP3/FLAC/Vorbis) come from FilmCraft's pure-Rust crates (git dependency,
|
||||
//! Video containers (MP4/MOV, Matroska/WebM, MXF OP1a / OP-Atom) and codecs (H.264, HEVC, VP9,
|
||||
//! AV1, ProRes, DNxHD, MPEG-2, MJPEG; AAC, Opus, PCM, MP3/FLAC/Vorbis) come from FilmCraft's pure-Rust crates (git dependency,
|
||||
//! pinned; `plan/adr/0001`). FilmCraft types never leave this crate. Stills use the `image` crate
|
||||
//! (PNG, JPEG, GIF, WebP, TIFF, BMP, OpenEXR); Photoshop documents (merged image or one layer) come
|
||||
//! from `effectcraft-psd` and SVG from `effectcraft-svg` (rasterised at any scale).
|
||||
|
|
@ -55,7 +55,7 @@ pub const STILL_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "ti
|
|||
/// File extensions recognised as audio-only files.
|
||||
pub const AUDIO_EXTENSIONS: &[&str] = filmcraft_media::AUDIO_EXTENSIONS;
|
||||
/// File extensions recognised as movies.
|
||||
pub const VIDEO_EXTENSIONS: &[&str] = &["mp4", "m4v", "mov", "mkv", "webm"];
|
||||
pub const VIDEO_EXTENSIONS: &[&str] = &["mp4", "m4v", "mov", "mkv", "webm", "mxf"];
|
||||
|
||||
/// File extensions recognised as 3D models (Advanced 3D model layers).
|
||||
pub const MODEL_EXTENSIONS: &[&str] = &["gltf", "glb", "obj"];
|
||||
|
|
|
|||
|
|
@ -379,6 +379,97 @@ fn vp9_webm_frames() {
|
|||
}
|
||||
}
|
||||
|
||||
/// MXF OP1a as broadcast cameras write it: long-GOP MPEG-2 4:2:2 (XDCAM-like, B-frames) with
|
||||
/// 24-bit PCM audio.
|
||||
fn mpeg2_mxf() -> Option<PathBuf> {
|
||||
ffmpeg_fixture(
|
||||
"testsrc2_mpeg2_pcm.mxf",
|
||||
&[
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"testsrc2=size=1280x720:rate=25:duration=2",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=440:sample_rate=48000:duration=2",
|
||||
"-c:v",
|
||||
"mpeg2video",
|
||||
"-pix_fmt",
|
||||
"yuv422p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-g",
|
||||
"12",
|
||||
"-bf",
|
||||
"2",
|
||||
"-c:a",
|
||||
"pcm_s24le",
|
||||
"-ac",
|
||||
"2",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// MXF OP1a with intra-only DNxHD 36 (Avid's offline codec).
|
||||
fn dnxhd_mxf() -> Option<PathBuf> {
|
||||
ffmpeg_fixture(
|
||||
"testsrc2_dnxhd.mxf",
|
||||
&["-f", "lavfi", "-i", "testsrc2=size=1920x1080:rate=25:duration=1", "-c:v", "dnxhd", "-b:v", "36M", "-pix_fmt", "yuv422p"],
|
||||
)
|
||||
}
|
||||
|
||||
/// MXF is importable footage (After Effects imports it), whatever the extension's case.
|
||||
#[test]
|
||||
fn mxf_is_importable() {
|
||||
assert!(effectcraft_media::is_importable(Path::new("/media/A001C003.mxf")));
|
||||
assert!(effectcraft_media::is_importable(Path::new("/media/A001C003.MXF")));
|
||||
assert!(!effectcraft_media::is_importable(Path::new("/media/A001C003.avi")), "no AVI demuxer yet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mxf_mpeg2_with_pcm_audio() {
|
||||
let Some(p) = mpeg2_mxf() else { return };
|
||||
let f = probe(&p).expect("probe");
|
||||
assert_eq!(f.kind, FootageKind::Video);
|
||||
assert_eq!((f.width, f.height), (1280, 720));
|
||||
assert_eq!(f.frame_rate, FrameRate::FPS_25);
|
||||
assert!((f.duration.seconds() - 2.0).abs() < 0.1, "duration {:?}", f.duration.seconds());
|
||||
assert!(f.has_video && f.has_audio);
|
||||
eprintln!("mxf mpeg-2: codec {:?}", f.codec);
|
||||
let pool = MediaPool::new();
|
||||
// sequential, a backwards seek, a jump into a GOP (B-frames reordered), the last frame
|
||||
for n in [0usize, 1, 2, 3, 30, 7, 49] {
|
||||
let img = pool.frame_at(&f, t_of(f.frame_rate, n as i64)).expect("frame");
|
||||
let db = psnr(&img, &reference_frame(&p, n));
|
||||
eprintln!("mxf mpeg-2 frame {n}: {db:.2} dB");
|
||||
assert!(db > 35.0, "frame {n}: {db:.2} dB");
|
||||
}
|
||||
let s = pool.audio_samples(&f, Tick(TICKS_PER_SECOND / 2), 4800, 48_000);
|
||||
assert_eq!(s.len(), 9600);
|
||||
let rms = (s.iter().map(|v| v * v).sum::<f32>() / s.len() as f32).sqrt();
|
||||
// ffmpeg's sine source upmixed to stereo: −24 dBFS RMS
|
||||
assert!((rms - 0.0625).abs() < 0.01, "rms {rms}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mxf_dnxhd_frames() {
|
||||
let Some(p) = dnxhd_mxf() else { return };
|
||||
let f = probe(&p).expect("probe");
|
||||
assert_eq!(f.kind, FootageKind::Video);
|
||||
assert_eq!((f.width, f.height), (1920, 1080));
|
||||
assert_eq!(f.frame_rate, FrameRate::FPS_25);
|
||||
assert!(!f.has_audio);
|
||||
eprintln!("mxf dnxhd: codec {:?}", f.codec);
|
||||
let pool = MediaPool::new();
|
||||
for n in [0usize, 12, 24, 3] {
|
||||
let img = pool.frame_at(&f, t_of(f.frame_rate, n as i64)).expect("frame");
|
||||
let db = psnr(&img, &reference_frame(&p, n));
|
||||
eprintln!("mxf dnxhd frame {n}: {db:.2} dB");
|
||||
assert!(db > 35.0, "frame {n}: {db:.2} dB");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn png_sequence_import() {
|
||||
let Some(first) = png_sequence() else { return };
|
||||
|
|
|
|||
|
|
@ -1213,7 +1213,7 @@ fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> Option<Res
|
|||
"keys.rpfCameraImport" => ("path", Ask::Open(&["json", "csv", "txt"])),
|
||||
"essential.exportTemplate" => ("path", Ask::Save("Template.ectemplate")),
|
||||
"essential.importTemplate" => ("path", Ask::Open(&["ectemplate"])),
|
||||
"file.setProxy" => ("path", Ask::Open(&["mp4", "mov", "m4v", "mkv", "webm", "png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "exr"])),
|
||||
"file.setProxy" => ("path", Ask::Open(&["mp4", "mov", "m4v", "mkv", "webm", "mxf", "png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "exr"])),
|
||||
_ => return None,
|
||||
};
|
||||
if params.get(key).is_some()
|
||||
|
|
@ -1225,8 +1225,8 @@ fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> Option<Res
|
|||
return None;
|
||||
}
|
||||
const MEDIA: &[&str] = &[
|
||||
"mp4", "mov", "m4v", "mkv", "webm", "png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "exr", "wav", "aif", "aiff", "mp3", "flac", "ogg",
|
||||
"opus", "svg", "pdf", "ai", "eps", "psd", "psb", "gltf", "glb", "obj", "json", "csv", "tsv",
|
||||
"mp4", "mov", "m4v", "mkv", "webm", "mxf", "png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "exr", "wav", "aif", "aiff", "mp3", "flac",
|
||||
"ogg", "opus", "svg", "pdf", "ai", "eps", "psd", "psb", "gltf", "glb", "obj", "json", "csv", "tsv",
|
||||
];
|
||||
// A host whose modal dialogs would run a nested event loop inside this frame (macOS: a
|
||||
// drag over the window then re-entered winit's event handler and aborted, #511) shows the
|
||||
|
|
|
|||
|
|
@ -204,3 +204,24 @@ fn relink_restores_decoded_pixels_and_invalidates_viewer_content() {
|
|||
assert_eq!(s.render(cid, Tick::ZERO, RenderOpts::default()).data, expected.data);
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// #767: File ▸ Import ▸ File… offers MXF footage (OP1a / OP-Atom from broadcast cameras and
|
||||
/// Avid), which the Media Browser lists and the importer reads.
|
||||
#[test]
|
||||
fn import_dialog_offers_mxf() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
let mut app = EffectcraftApp::new(Session::default());
|
||||
let offered = Rc::new(RefCell::new(Vec::<String>::new()));
|
||||
let seen = offered.clone();
|
||||
app.hooks.pick_files = Some(Box::new(move |exts: &[&str]| {
|
||||
*seen.borrow_mut() = exts.iter().map(|e| e.to_string()).collect();
|
||||
Vec::new()
|
||||
}));
|
||||
let result = effectcraft_ui_egui::menus::invoke(&mut app, &egui::Context::default(), "file.import", json!({})).unwrap();
|
||||
assert!(result.is_null(), "cancelled: nothing imported");
|
||||
let offered = offered.borrow();
|
||||
assert!(offered.iter().any(|e| e == "mxf"), "offered {offered:?}");
|
||||
assert!(offered.iter().any(|e| e == "mov"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ trips and renders, checked by tests, not against After Effects), **partial** (na
|
|||
| AV1 | R | yes | yes (MP4 `av01`, WebM) | beyond After Effects' export; no B-frames / CDEF |
|
||||
| Apple ProRes (422 family, 4444) | R/W | yes | yes | good; 4444 colour tag wrong (#340); no ProRes RAW |
|
||||
| WebM / Matroska (VP9, AV1, Opus) | R (limited) | yes, incl. VP9 alpha | yes | beyond After Effects |
|
||||
| MXF (OP1a / OP-Atom) | R/W | none (FilmCraft reads it; not wired) | none | Media Browser lists it |
|
||||
| MXF (OP1a / OP-Atom) | R/W | yes (FilmCraft's demuxer: AVC, MPEG-2 incl. XDCAM / D-10, VC-3 / DNxHD, ProRes; PCM audio) | none | partial: import (#767), checked against ffmpeg; no MXF output |
|
||||
| AVI | R | none | none | Media Browser lists it |
|
||||
| DNxHD / DNxHR | R/W | unverified (FilmCraft decodes it; reachable only inside `.mov`) | none | — |
|
||||
| BRAW, R3D, ARRIRAW | R (plug-ins / SDKs) | none | none | proprietary SDKs |
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ are grouped by area; the issue tracker has the rest.
|
|||
| 14 | **Object Matte** (26.2, disk cache in 26.5) | Not present; MobileSAM is already in `effectcraft-segment` and could back it | The new one-click roto; Roto Brush covers part of it | 15–30 h | G7 · [target-app-parity.md](target-app-parity.md) |
|
||||
| 15 | **Hardware video decode and encode** | No VideoToolbox, NVENC, Quick Sync or VA-API; #498 asks for it; FilmCraft has VideoToolbox / NVENC / VA-API paths to borrow | 4K playback and export speed | 25–45 h | G5/G6 · [hardware-parity.md](hardware-parity.md) |
|
||||
| 16 | **Localization depth** | Menus in 6 languages; panels in 4; effect names and ~1,650 effect parameter names untranslated; French, German, Korean, Italian, Russian (After Effects ships them) absent; #599 Hangul boxes on macOS | After Effects is used heavily in Japan, China, Korea, Brazil, Europe | 60–110 h + native review | G9 · [localization-parity.md](localization-parity.md) |
|
||||
| 17 | **Media Browser lists files import refuses** | `media_browser.rs` lists TGA, DPX, HDR, AVI, MXF; `media::is_importable` has none of them; #431 any image imports as a sequence; #324 MP3 goes silent | Confusing failures on common VFX formats (DPX, TGA) | 15–30 h | G6 · [file-format-parity.md](file-format-parity.md) |
|
||||
| 17 | **Media Browser lists files import refuses** | `media_browser.rs` lists TGA, DPX, HDR, AVI; `media::is_importable` has none of them (MXF OP1a / OP-Atom imports since #767; FilmCraft has no AVI demuxer); #431 any image imports as a sequence; #324 MP3 goes silent | Confusing failures on common VFX formats (DPX, TGA) | 15–30 h | G6 · [file-format-parity.md](file-format-parity.md) |
|
||||
| 18 | **Relative paths and relinking** | Projects store absolute footage paths; no relink when a project moves | Moving or sharing a project breaks it | 6–12 h | G4 · [file-format-parity.md](file-format-parity.md) |
|
||||
| 19 | **Plug-in ecosystem** | No After Effects SDK hosting (#600, #327 OpenFX, #334, #376 plug-in manager); our WebAssembly API has no third-party plug-ins | Many pro projects depend on Trapcode, Element, Saber… | 40–80 h for API growth; SDK hosting needs an owner decision | G8 · [scripting-parity.md](scripting-parity.md) |
|
||||
| 20 | **Multi-monitor and video output** | #486 multi-screen, floating panels on other screens, Mercury Transmit to video hardware (we have a second preview window only) | Two-screen setups are standard for professionals | 10–20 h | G2 · [hardware-parity.md](hardware-parity.md) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue