//! craft-media's oracle-test helpers: the generic ones from `craft-media-testkit`, and the MP4 //! fixtures and ffprobe queries the container tests share. #![allow(dead_code)] pub use craft_media_testkit::*; use serde_json::Value; use std::path::{Path, PathBuf}; use std::process::Command; /// Fixtures of the `isobmff` tests. pub fn fixture_dir() -> PathBuf { fixture_subdir("isobmff") } pub fn out_dir() -> PathBuf { let d = fixture_dir().join("out"); std::fs::create_dir_all(&d).unwrap(); d } const V: &[&str] = &["-f", "lavfi", "-i", "testsrc2=size=160x120:rate=25"]; const A: &[&str] = &["-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000"]; /// (fixture name, ffmpeg arguments after the global options, output file name derived from the name). pub fn fixture_specs() -> Vec<(&'static str, Vec<&'static str>)> { let cat = |parts: &[&[&'static str]]| parts.concat(); vec![ ("h264_bframes.mp4", cat(&[V, A, &["-t", "2", "-c:v", "libx264", "-bf", "2", "-g", "12", "-pix_fmt", "yuv420p", "-c:a", "aac"]])), ("h264.mov", cat(&[V, A, &["-t", "2", "-c:v", "libx264", "-bf", "3", "-g", "25", "-pix_fmt", "yuv420p", "-c:a", "pcm_s16le"]])), ( "hevc.mp4", cat(&[V, &["-t", "2", "-c:v", "libx265", "-tag:v", "hvc1", "-x265-params", "log-level=error:bframes=3", "-pix_fmt", "yuv420p"]]), ), ("prores.mov", cat(&[V, A, &["-t", "1", "-c:v", "prores_ks", "-profile:v", "3", "-c:a", "pcm_s24le"]])), ("prores4444.mov", cat(&[V, &["-t", "0.5", "-c:v", "prores_ks", "-profile:v", "4", "-pix_fmt", "yuva444p10le"]])), ("mjpeg.mov", cat(&[V, &["-t", "1", "-c:v", "mjpeg", "-q:v", "5"]])), ( "dnxhd.mov", vec![ "-f", "lavfi", "-i", "testsrc2=size=1280x720:rate=25", "-t", "0.2", "-c:v", "dnxhd", "-profile:v", "dnxhr_lb", "-pix_fmt", "yuv422p", ], ), ("pcm_f32.mov", cat(&[A, &["-t", "1", "-c:a", "pcm_f32le"]])), ("pcm_s16be.mov", cat(&[A, &["-t", "1", "-c:a", "pcm_s16be"]])), ("aac.mp4", cat(&[A, &["-t", "2", "-c:a", "aac", "-b:a", "96k"]])), ("aac_stereo44.m4a", vec!["-f", "lavfi", "-i", "sine=frequency=440:sample_rate=44100", "-t", "1.5", "-ac", "2", "-c:a", "aac"]), ( "frag.mp4", cat(&[ V, A, &[ "-t", "3", "-c:v", "libx264", "-bf", "2", "-g", "25", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "frag_keyframe+empty_moov", ], ]), ), ( "frag_dash.mp4", cat(&[ V, &[ "-t", "3", "-c:v", "libx264", "-bf", "2", "-g", "25", "-pix_fmt", "yuv420p", "-movflags", "frag_keyframe+empty_moov+default_base_moof", ], ]), ), ("faststart.mp4", cat(&[V, A, &["-t", "2", "-c:v", "libx264", "-bf", "2", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "+faststart"]])), ( "tmcd_2997df.mov", vec!["-f", "lavfi", "-i", "testsrc2=size=160x120:rate=30000/1001", "-t", "1", "-c:v", "mjpeg", "-timecode", "01:00:00;00"], ), ( "tmcd_23976.mov", vec![ "-f", "lavfi", "-i", "testsrc2=size=160x120:rate=24000/1001", "-t", "1", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-timecode", "01:00:00:00", ], ), ( "h264_2997.mp4", vec!["-f", "lavfi", "-i", "testsrc2=size=160x120:rate=30000/1001", "-t", "2", "-c:v", "libx264", "-bf", "2", "-pix_fmt", "yuv420p"], ), ("flac.mp4", cat(&[A, &["-t", "1", "-c:a", "flac"]])), ("alac.m4a", cat(&[A, &["-t", "1", "-c:a", "alac"]])), ("opus.mp4", cat(&[A, &["-t", "1", "-c:a", "libopus"]])), ("ac3.mp4", cat(&[A, &["-t", "1", "-c:a", "ac3"]])), ("vp9.mp4", cat(&[V, &["-t", "1", "-c:v", "libvpx-vp9", "-deadline", "realtime", "-cpu-used", "8"]])), ("av1.mp4", cat(&[V, &["-t", "1", "-c:v", "libsvtav1", "-preset", "12"]])), ( "colr_pasp.mov", cat(&[ V, &[ "-t", "0.5", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709", "-aspect", "16:9", ], ]), ), ] } fn run_ffmpeg(ff: &Path, args: &[&str], out: &Path) -> Option { let tmp = temp_path(out); let mut cmd = Command::new(ff); cmd.args(["-v", "error", "-y", "-nostdin"]).args(args).arg(&tmp); run_generator(&mut cmd, &tmp, out, oracles_required()) } /// Path to a fixture, generating it with ffmpeg if needed. `None` after printing `SKIPPED` if /// ffmpeg is missing or the encoder isn't available (a failure with `CRAFT_MEDIA_REQUIRE_ORACLES=1`). pub fn fixture(name: &str) -> Option { let ff = ffmpeg()?; let path = fixture_dir().join(name); if path.exists() { return Some(path); } if name == "editlist_cut.mp4" { let src = fixture("h264_bframes.mp4")?; let s = src.to_str().expect("fixture path is UTF-8").to_string(); return run_ffmpeg(&ff, &["-ss", "0.5", "-i", &s, "-c", "copy"], &path); } let (_, args) = fixture_specs().into_iter().find(|(n, _)| *n == name).unwrap_or_else(|| panic!("unknown fixture {name}")); run_ffmpeg(&ff, &args, &path) } pub fn all_fixture_names() -> Vec<&'static str> { let mut v: Vec<&str> = fixture_specs().iter().map(|(n, _)| *n).collect(); v.push("editlist_cut.mp4"); v } /// ffprobe's JSON for `path`, or `None` after printing `SKIPPED` (a failure with /// `CRAFT_MEDIA_REQUIRE_ORACLES=1`). pub fn probe_json(path: &Path, what: &[&str]) -> Option { let fp = ffprobe()?; let o = match Command::new(fp).args(["-v", "error", "-of", "json"]).args(what).arg(path).output() { Ok(o) if o.status.success() => o, Ok(o) => return skip(&format!("ffprobe failed on {}: {}", path.display(), String::from_utf8_lossy(&o.stderr).trim())), Err(e) => return skip(&format!("could not run ffprobe: {e}")), }; match serde_json::from_slice(&o.stdout) { Ok(v) => Some(v), Err(e) => skip(&format!("ffprobe output for {} is not JSON: {e}", path.display())), } } /// ffprobe's packets for `path`, or `None` after a skipped probe (see [`probe_json`]). pub fn packets(path: &Path) -> Option> { probe_json(path, &["-show_packets"]).map(|v| v["packets"].as_array().cloned().unwrap_or_default()) } /// ffprobe's streams for `path`, or `None` after a skipped probe (see [`probe_json`]). pub fn streams(path: &Path) -> Option> { probe_json(path, &["-show_streams"]).map(|v| v["streams"].as_array().cloned().unwrap_or_default()) } pub fn int(v: &Value) -> Option { match v { Value::Number(n) => n.as_i64(), Value::String(s) => s.parse().ok(), _ => None, } } /// Decode a file with ffmpeg to the null muxer; returns stderr (empty on success). pub fn ffmpeg_decode_errors(path: &Path) -> Option { let ff = ffmpeg()?; let o = Command::new(ff).args(["-v", "error", "-nostdin", "-i"]).arg(path).args(["-f", "null", "-"]).output().ok()?; let mut s = String::from_utf8_lossy(&o.stderr).into_owned(); if !o.status.success() { s.push_str("\n(ffmpeg exited with failure)"); } Some(s) }