use std::process::Command;
use super::manifest::{GoldenOracle, VerifiedShape, VERIFIED_SHAPES};
use super::{from_cli_args, linked_profile_verified, CliError};
use crate::core::container_info::{get_duration_us, get_metadata};
use crate::core::context::ffmpeg_context::FfmpegContext;
use crate::core::context::input::Input;
use crate::core::context::output::Output;
use crate::core::stream_info::{find_all_stream_infos, StreamInfo};
const DURATION_TOLERANCE_US: i64 = 200_000;
const FIXTURE_TITLE: &str = "cli compat golden fixture";
const PRESEED_LEN: usize = 8 * 1024 * 1024;
fn preseed(path: &str) {
std::fs::write(path, vec![0xABu8; PRESEED_LEN]).unwrap();
}
fn assert_truncated(label: &str, path: &str) {
let len = std::fs::metadata(path)
.unwrap_or_else(|e| panic!("{label}: output {path} missing: {e}"))
.len() as usize;
assert!(
len < PRESEED_LEN,
"{label}: {path} is {len} bytes, not smaller than the {PRESEED_LEN}-byte sentinel — the -y overwrite did not truncate"
);
}
fn tmp_dir(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("ez_ffmpeg_cli_goldens_{}", std::process::id()));
let dir = dir.join(name);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run_context(context: FfmpegContext, scenario: &str) {
let scheduler = context.start().unwrap();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(scheduler.wait());
});
match rx.recv_timeout(std::time::Duration::from_secs(180)) {
Ok(result) => result.unwrap_or_else(|e| panic!("{scenario} failed: {e}")),
Err(_) => panic!("{scenario} did not finish within 180s"),
}
}
enum CliGate {
Run { bin: String },
Skip(String),
}
fn cli_gate() -> CliGate {
use std::io::ErrorKind;
let pinned = std::env::var("EZ_FFMPEG_CLI").ok();
let strict = pinned.is_some();
let bin = pinned.unwrap_or_else(|| "ffmpeg".to_string());
let out = match Command::new(&bin).arg("-version").output() {
Err(e) if e.kind() == ErrorKind::NotFound && !strict => {
return CliGate::Skip(format!("no `{bin}` on PATH"));
}
Err(e) => panic!("ffmpeg CLI probe `{bin} -version` failed: {e}"),
Ok(o) if !o.status.success() => panic!("`{bin} -version` exited with {}", o.status),
Ok(o) => o,
};
let banner = String::from_utf8_lossy(&out.stdout).into_owned();
let cli_avcodec = parse_lib_version(&banner, "libavcodec")
.unwrap_or_else(|| panic!("no libavcodec line in `{bin} -version` output"));
let cli_avformat = parse_lib_version(&banner, "libavformat")
.unwrap_or_else(|| panic!("no libavformat line in `{bin} -version` output"));
let ours_avcodec = major_minor(unsafe { ffmpeg_sys_next::avcodec_version() });
let ours_avformat = major_minor(unsafe { ffmpeg_sys_next::avformat_version() });
if cli_avcodec != ours_avcodec || cli_avformat != ours_avformat {
let msg = format!(
"`{bin}` links avcodec {}.{} / avformat {}.{}, the crate links {}.{} / {}.{} — \
semantic parity is only defined on the same library line",
cli_avcodec.0,
cli_avcodec.1,
cli_avformat.0,
cli_avformat.1,
ours_avcodec.0,
ours_avcodec.1,
ours_avformat.0,
ours_avformat.1
);
if strict {
panic!("{msg} (point EZ_FFMPEG_CLI at a matching binary)");
}
return CliGate::Skip(msg);
}
CliGate::Run { bin }
}
fn major_minor(v: u32) -> (u32, u32) {
(v >> 16, (v >> 8) & 0xff)
}
fn canonical_encoders(shape: &VerifiedShape) -> Vec<&'static str> {
shape
.canonical_argv
.iter()
.enumerate()
.filter(|(_, token)| **token == "-c:v" || **token == "-c:a")
.filter_map(|(i, _)| shape.canonical_argv.get(i + 1).copied())
.filter(|codec| *codec != "copy")
.collect()
}
fn missing_linked_encoder(shape: &VerifiedShape) -> Option<&'static str> {
let encoders = crate::core::codec::get_encoders();
canonical_encoders(shape)
.into_iter()
.find(|name| !encoders.iter().any(|e| e.codec_name == *name))
}
fn parse_lib_version(banner: &str, lib: &str) -> Option<(u32, u32)> {
let line = banner.lines().find(|l| l.trim_start().starts_with(lib))?;
let runtime = line.rsplit('/').next()?;
let digits: String = runtime
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.')
.collect();
let mut it = digits.split('.').filter(|s| !s.is_empty());
Some((it.next()?.parse().ok()?, it.next()?.parse().ok()?))
}
fn run_reference(bin: &str, args: &[String], cwd: Option<&std::path::Path>) {
let mut command = Command::new(bin);
command.args(args);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let out = command
.output()
.unwrap_or_else(|e| panic!("failed to spawn `{bin}`: {e}"));
assert!(
out.status.success(),
"reference `{bin} {}` failed:\n{}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
fn av_fixture() -> String {
let path = std::env::temp_dir()
.join(format!("ez_ffmpeg_cli_goldens_{}", std::process::id()))
.join("fixture_av.mp4");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let path = path.to_string_lossy().into_owned();
if std::path::Path::new(&path).exists() {
return path;
}
run_context(
FfmpegContext::builder()
.input(Input::from("testsrc2=size=320x240:rate=30:duration=16").set_format("lavfi"))
.input(
Input::from("sine=frequency=440:sample_rate=44100:duration=16").set_format("lavfi"),
)
.output(
Output::from(path.as_str())
.set_video_codec("mpeg4")
.set_video_filter("negate=enable='lt(mod(t,2),1)'")
.set_audio_codec("aac")
.add_metadata("title", FIXTURE_TITLE),
)
.build()
.unwrap(),
"av fixture",
);
path
}
fn multi_audio_fixture() -> String {
let path = std::env::temp_dir()
.join(format!("ez_ffmpeg_cli_goldens_{}", std::process::id()))
.join("fixture_multi_audio.mp4");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let path = path.to_string_lossy().into_owned();
if std::path::Path::new(&path).exists() {
return path;
}
run_context(
FfmpegContext::builder()
.input(Input::from("testsrc2=size=320x240:rate=30:duration=16").set_format("lavfi"))
.input(
Input::from("sine=frequency=440:sample_rate=44100:duration=16").set_format("lavfi"),
)
.input(
Input::from("anullsrc=channel_layout=stereo:sample_rate=48000").set_format("lavfi"),
)
.output(
Output::from(path.as_str())
.set_video_codec("mpeg4")
.set_audio_codec("aac")
.set_recording_time_us(16_000_000)
.add_stream_map("0:v")
.add_stream_map("1:a")
.add_stream_map("2:a")
.add_metadata("title", FIXTURE_TITLE),
)
.build()
.unwrap(),
"multi-audio fixture",
);
let channel_counts: Vec<i32> = find_all_stream_infos(&path)
.unwrap()
.iter()
.filter_map(|info| match info {
StreamInfo::Audio { nb_channels, .. } => Some(*nb_channels),
_ => None,
})
.collect();
assert_eq!(
channel_counts,
vec![1, 2],
"fixture must be mono-then-stereo"
);
path
}
fn fixture_for(shape_id: &str) -> String {
match shape_id {
"V3" => multi_audio_fixture(),
_ => av_fixture(),
}
}
struct StreamSummary {
kind: &'static str,
codec: String,
width: i32,
height: i32,
sample_rate: i32,
channels: i32,
bit_rate: i64,
}
fn summarize(path: &str) -> Vec<StreamSummary> {
find_all_stream_infos(path)
.unwrap_or_else(|e| panic!("probing {path} failed: {e}"))
.into_iter()
.map(|info| match info {
StreamInfo::Video {
codec_name,
width,
height,
..
} => StreamSummary {
kind: "video",
codec: codec_name,
width,
height,
sample_rate: 0,
channels: 0,
bit_rate: 0,
},
StreamInfo::Audio {
codec_name,
sample_rate,
nb_channels,
bit_rate,
..
} => StreamSummary {
kind: "audio",
codec: codec_name,
width: 0,
height: 0,
sample_rate,
channels: nb_channels,
bit_rate,
},
other => StreamSummary {
kind: "other",
codec: format!("{other:?}"),
width: 0,
height: 0,
sample_rate: 0,
channels: 0,
bit_rate: 0,
},
})
.collect()
}
fn container_title(path: &str) -> Option<String> {
get_metadata(path)
.unwrap_or_else(|e| panic!("metadata probe of {path} failed: {e}"))
.into_iter()
.find(|(key, _)| key.eq_ignore_ascii_case("title"))
.map(|(_, value)| value)
}
fn assert_same_media(label: &str, ours: &str, theirs: &str) {
let a = summarize(ours);
let b = summarize(theirs);
assert_eq!(
a.len(),
b.len(),
"{label}: stream count differs ({ours} vs {theirs})"
);
for (i, (x, y)) in a.iter().zip(&b).enumerate() {
assert_eq!(x.kind, y.kind, "{label}: stream {i} type differs");
assert_eq!(x.codec, y.codec, "{label}: stream {i} codec differs");
assert_eq!(
(x.width, x.height),
(y.width, y.height),
"{label}: stream {i} geometry differs"
);
assert_eq!(
(x.sample_rate, x.channels),
(y.sample_rate, y.channels),
"{label}: stream {i} audio layout differs"
);
}
let da = get_duration_us(ours).unwrap();
let db = get_duration_us(theirs).unwrap();
assert!(
(da - db).abs() <= DURATION_TOLERANCE_US,
"{label}: duration differs beyond tolerance: {da}us vs {db}us"
);
assert_eq!(
container_title(ours),
container_title(theirs),
"{label}: implicitly copied container title differs"
);
}
struct GoldenRun {
ours_output: String,
theirs_output: String,
emitted_dir: std::path::PathBuf,
emitted_output: String,
bin: String,
}
fn materialize(shape: &VerifiedShape, fixture: &str, dir: &std::path::Path) -> Vec<String> {
let canonical_input = canonical_input(shape);
let canonical_output = shape.canonical_argv.last().unwrap();
shape
.canonical_argv
.iter()
.map(|token| {
if *token == canonical_input {
fixture.to_string()
} else if token == canonical_output || token.contains("%03d") {
dir.join(token).to_string_lossy().into_owned()
} else {
token.to_string()
}
})
.collect()
}
fn canonical_input(shape: &VerifiedShape) -> &'static str {
let position = shape
.canonical_argv
.iter()
.position(|token| *token == "-i")
.expect("canonical argv has -i");
shape.canonical_argv[position + 1]
}
fn run_shape(shape: &VerifiedShape) -> Option<GoldenRun> {
if !linked_profile_verified() {
let args: Vec<String> = shape.canonical_argv.iter().map(|s| s.to_string()).collect();
match from_cli_args(&args) {
Err(CliError::UnverifiedRuntimeProfile { .. }) => {
eprintln!(
"{}: linked profile not verified; asserted the typed profile failure",
shape.id
);
return None;
}
Ok(_) => panic!("{}: runtime must refuse a non-verified profile", shape.id),
Err(other) => panic!(
"{}: expected UnverifiedRuntimeProfile on this linked build, got: {other}",
shape.id
),
}
}
let bin = match cli_gate() {
CliGate::Run { bin } => bin,
CliGate::Skip(reason) => {
eprintln!("skipping {}: {reason}", shape.id);
return None;
}
};
if std::env::var("EZ_FFMPEG_CLI").is_err() {
if let Some(missing) = missing_linked_encoder(shape) {
eprintln!(
"skipping {}: the linked FFmpeg lacks the `{missing}` encoder named by the \
shape's canonical command; the PATH-discovered `{bin}` arms the semantic \
goldens only on a fully provisioned build (set EZ_FFMPEG_CLI to make this \
lane strict)",
shape.id
);
return None;
}
}
let fixture = fixture_for(shape.id);
let canonical_output = shape.canonical_argv.last().unwrap().to_string();
let ours_dir = tmp_dir(&format!("{}_ours", shape.id));
let theirs_dir = tmp_dir(&format!("{}_theirs", shape.id));
let ours_args = materialize(shape, &fixture, &ours_dir);
let theirs_args = materialize(shape, &fixture, &theirs_dir);
let ours_output = ours_dir
.join(&canonical_output)
.to_string_lossy()
.into_owned();
let theirs_output = theirs_dir
.join(&canonical_output)
.to_string_lossy()
.into_owned();
preseed(&ours_output);
preseed(&theirs_output);
let context = from_cli_args(&ours_args).unwrap_or_else(|e| {
panic!(
"{}: from_cli_args rejected the canonical command: {e}",
shape.id
)
});
run_context(context, shape.id);
run_reference(&bin, &theirs_args, None);
assert_truncated(&format!("{} ours", shape.id), &ours_output);
assert_truncated(&format!("{} theirs", shape.id), &theirs_output);
let emitted_dir = tmp_dir(&format!("{}_emitted", shape.id));
let emitted_input = emitted_dir.join(canonical_input(shape));
if !emitted_input.exists() {
std::fs::copy(&fixture, &emitted_input).unwrap();
}
let emitted_output = emitted_dir
.join(&canonical_output)
.to_string_lossy()
.into_owned();
preseed(&emitted_output);
let example = example_binary(shape.emitted_example);
let out = Command::new(&example)
.current_dir(&emitted_dir)
.output()
.unwrap_or_else(|e| panic!("failed to run emitted program {example:?}: {e}"));
assert!(
out.status.success(),
"emitted program {} failed:\n{}",
shape.emitted_example,
String::from_utf8_lossy(&out.stderr)
);
assert_truncated(&format!("{} emitted", shape.id), &emitted_output);
assert_same_media(
&format!("{} ours-vs-cli", shape.id),
&ours_output,
&theirs_output,
);
assert_same_media(
&format!("{} emitted-vs-cli", shape.id),
&emitted_output,
&theirs_output,
);
Some(GoldenRun {
ours_output,
theirs_output,
emitted_dir,
emitted_output,
bin,
})
}
fn example_binary(name: &str) -> std::path::PathBuf {
let deps_dir = std::env::current_exe().unwrap();
let debug_dir = deps_dir.parent().unwrap().parent().unwrap();
let path = debug_dir
.join("examples")
.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
if path.exists() {
return path;
}
let status = Command::new(env!("CARGO"))
.args(["build", "--features", "cli", "--example", name])
.status()
.expect("failed to spawn cargo to build the emitted example");
assert!(status.success(), "building example {name} failed");
assert!(
path.exists(),
"example binary {path:?} still missing after build"
);
path
}
#[test]
fn verified_shapes_pass_their_semantic_goldens() {
for shape in VERIFIED_SHAPES {
let Some(run) = run_shape(shape) else {
continue;
};
match shape.oracle {
GoldenOracle::Transcode => oracle_transcode(&run),
GoldenOracle::Clip => oracle_clip(&run),
GoldenOracle::AudioExtract => oracle_audio_extract(&run),
GoldenOracle::Thumbnail => oracle_thumbnail(&run),
GoldenOracle::Scale => oracle_scale(&run),
GoldenOracle::Hls => oracle_hls(&run),
}
}
}
#[test]
fn canonical_encoder_extraction_matches_the_manifest() {
let by_id = |id: &str| {
canonical_encoders(VERIFIED_SHAPES.iter().find(|shape| shape.id == id).unwrap())
};
assert_eq!(by_id("V1"), vec!["libx264", "aac"]);
assert_eq!(by_id("V3"), vec!["aac"]);
assert_eq!(by_id("V4"), vec!["mjpeg"]);
for shape in VERIFIED_SHAPES {
assert!(
!canonical_encoders(shape).is_empty(),
"{}: every verified canonical command names its encoders explicitly",
shape.id
);
}
}
#[test]
fn v1_crf_preset_ride_the_lowered_plan_and_emission() {
let shape = VERIFIED_SHAPES
.iter()
.find(|shape| shape.id == "V1")
.unwrap();
let args: Vec<String> = shape.canonical_argv.iter().map(|s| s.to_string()).collect();
let ir = super::parse::parse(&args).unwrap();
let lowered = super::lower::lower(&ir);
assert_eq!(
lowered.output.video_codec_opts,
vec![
("crf".to_string(), "23".to_string()),
("preset".to_string(), "fast".to_string()),
],
"the lowered plan must carry -crf and -preset as encoder options"
);
let code = super::emit_rust_code_from_args(shape.canonical_argv).unwrap();
assert!(
code.contains(".set_video_codec_opt(\"crf\", \"23\")"),
"emitted program lost the -crf call:\n{code}"
);
assert!(
code.contains(".set_video_codec_opt(\"preset\", \"fast\")"),
"emitted program lost the -preset call:\n{code}"
);
}
fn oracle_transcode(run: &GoldenRun) {
let ours = summarize(&run.ours_output);
assert_eq!(ours.len(), 2);
assert_eq!(ours[0].codec, "h264");
assert_eq!(ours[1].codec, "aac");
assert_eq!(
container_title(&run.ours_output).as_deref(),
Some(FIXTURE_TITLE),
"the fixture title must be implicitly copied"
);
let sei_carries = |path: &str, marker: &[u8]| {
let bytes = std::fs::read(path).unwrap();
bytes.windows(marker.len()).any(|w| w == marker)
};
for (lane, path) in [
("theirs", &run.theirs_output),
("ours", &run.ours_output),
("emitted", &run.emitted_output),
] {
assert!(
sei_carries(path, b"crf=23.0"),
"V1 {lane}: x264 SEI does not carry the crf marker"
);
}
let probe_dir = tmp_dir("V1_crf_probe");
let probe_out = probe_dir.join("crf30.mp4").to_string_lossy().into_owned();
let fixture = fixture_for("V1");
let probe_args = [
"-i", &fixture, "-c:v", "libx264", "-crf", "30", "-preset", "fast", "-c:a", "aac",
"-y", &probe_out,
];
let context = from_cli_args(&probe_args).expect("V1 crf probe must pass the gates");
run_context(context, "V1 crf probe");
assert!(
sei_carries(&probe_out, b"crf=30.0"),
"V1 crf probe: -crf 30 never reached the encoder"
);
assert!(
!sei_carries(&probe_out, b"crf=23.0"),
"V1 crf probe: encoder ran at the crf default despite -crf 30"
);
}
fn oracle_clip(run: &GoldenRun) {
let duration = get_duration_us(&run.ours_output).unwrap();
assert!(
(3_500_000..=4_500_000).contains(&duration),
"clip duration {duration}us outside the expected ~4s window"
);
}
fn oracle_audio_extract(run: &GoldenRun) {
let ours = summarize(&run.ours_output);
assert_eq!(ours.len(), 1, "-vn output must be audio-only");
assert_eq!(ours[0].codec, "aac");
assert_eq!(
(ours[0].channels, ours[0].sample_rate),
(1, 44100),
"the DEFAULT-disposition bonus must select the mono track"
);
let theirs = summarize(&run.theirs_output);
let emitted = summarize(&run.emitted_output);
let anchor = theirs[0].bit_rate;
assert!(
(80_000..=210_000).contains(&anchor),
"V3 reference audio bitrate {anchor} outside the -b:a 192k window"
);
for (lane, bit_rate) in [("ours", ours[0].bit_rate), ("emitted", emitted[0].bit_rate)] {
assert!(
(bit_rate - anchor).abs() * 100 <= anchor * 15,
"V3 {lane}: audio bitrate {bit_rate} deviates more than 15% from the reference \
CLI's {anchor} — the -b:a 192k lowering did not reach the encoder"
);
}
let fixture_b = std::env::temp_dir()
.join(format!("ez_ffmpeg_cli_goldens_{}", std::process::id()))
.join("fixture_disposition.mp4");
let fixture_b = fixture_b.to_string_lossy().into_owned();
if !std::path::Path::new(&fixture_b).exists() {
run_reference(
&run.bin,
&[
"-y",
"-v",
"error",
"-f",
"lavfi",
"-i",
"testsrc2=size=320x240:rate=30:duration=4",
"-f",
"lavfi",
"-i",
"anullsrc=channel_layout=stereo:sample_rate=48000",
"-f",
"lavfi",
"-i",
"sine=frequency=440:sample_rate=44100:duration=4",
"-map",
"0:v",
"-map",
"1:a",
"-map",
"2:a",
"-disposition:a:0",
"0",
"-disposition:a:1",
"default",
"-c:v",
"mpeg4",
"-c:a",
"aac",
"-t",
"4",
&fixture_b,
]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
None,
);
}
let out_ours = tmp_dir("V3b_ours")
.join("out.m4a")
.to_string_lossy()
.into_owned();
let out_theirs = tmp_dir("V3b_theirs")
.join("out.m4a")
.to_string_lossy()
.into_owned();
let args = |out: &str| -> Vec<String> {
[
"-i", &fixture_b, "-vn", "-c:a", "aac", "-b:a", "192k", "-y", out,
]
.iter()
.map(|s| s.to_string())
.collect()
};
let context = from_cli_args(&args(&out_ours)).expect("V3 fixture B must pass the gates");
run_context(context, "golden v3 fixture B");
run_reference(&run.bin, &args(&out_theirs), None);
assert_same_media("V3 fixture B", &out_ours, &out_theirs);
let ours_b = summarize(&out_ours);
assert_eq!(
(ours_b[0].channels, ours_b[0].sample_rate),
(1, 44100),
"selection must follow the DEFAULT bonus to the non-first, non-most-channels track"
);
}
fn oracle_thumbnail(run: &GoldenRun) {
let ours = summarize(&run.ours_output);
assert_eq!(ours.len(), 1, "-an single-frame output must be video-only");
assert_eq!(ours[0].codec, "mjpeg");
assert_eq!((ours[0].width, ours[0].height), (320, 240));
let (ours_px, ours_dims) = decode_rgb(&run.ours_output);
let (theirs_px, theirs_dims) = decode_rgb(&run.theirs_output);
let (emitted_px, emitted_dims) = decode_rgb(&run.emitted_output);
assert_eq!(ours_dims, theirs_dims, "thumbnail geometry differs");
assert_eq!(
emitted_dims, theirs_dims,
"emitted thumbnail geometry differs"
);
assert_eq!(
ours_px, theirs_px,
"thumbnails must decode to identical pixels"
);
assert_eq!(
emitted_px, theirs_px,
"emitted thumbnail pixels must match the CLI"
);
if cli_avcodec_triple(&run.bin) == Some(linked_avcodec_triple()) {
let ours_bytes = std::fs::read(&run.ours_output).unwrap();
let theirs_bytes = std::fs::read(&run.theirs_output).unwrap();
assert_eq!(
ours_bytes, theirs_bytes,
"on the exact same libavcodec build the thumbnail must be byte-identical"
);
} else {
eprintln!(
"golden_v4_thumbnail: byte comparison skipped (libavcodec micro differs); \
pixel parity asserted instead"
);
}
}
fn oracle_scale(run: &GoldenRun) {
let ours = summarize(&run.ours_output);
assert_eq!((ours[0].width, ours[0].height), (1280, 960));
}
fn oracle_hls(run: &GoldenRun) {
let ours_playlist = run.ours_output.clone();
let theirs_playlist = run.theirs_output.clone();
let emitted_playlist = run.emitted_output.clone();
let ours = parse_playlist(&ours_playlist);
let theirs = parse_playlist(&theirs_playlist);
let emitted = parse_playlist(&emitted_playlist);
for (label, playlist) in [("ours", &ours), ("theirs", &theirs), ("emitted", &emitted)] {
assert!(
playlist.vod,
"{label}: playlist must declare EXT-X-PLAYLIST-TYPE:VOD"
);
assert!(
playlist.endlist,
"{label}: playlist must end with EXT-X-ENDLIST"
);
}
assert!(
ours.segments.len() >= 2,
"the 16s fixture must produce multiple segments, got {:?}",
ours.segments
);
assert_eq!(
ours.segments, theirs.segments,
"segment topology/naming must match the CLI"
);
assert_eq!(
emitted.segments, theirs.segments,
"emitted segment topology must match"
);
assert_eq!(
ours.target_duration, theirs.target_duration,
"EXT-X-TARGETDURATION differs"
);
assert_eq!(
emitted.target_duration, theirs.target_duration,
"emitted EXT-X-TARGETDURATION differs"
);
assert_eq!(
ours.target_duration,
Some(6),
"hls_time 6 on the 1s-IDR fixture must yield target duration 6"
);
assert_eq!(
ours.durations.len(),
theirs.durations.len(),
"EXTINF count differs"
);
assert_eq!(
emitted.durations.len(),
theirs.durations.len(),
"emitted EXTINF count differs"
);
for (i, (a, b)) in ours.durations.iter().zip(&theirs.durations).enumerate() {
assert!(
(a - b).abs() <= 0.2,
"segment {i} duration differs: {a} vs {b}"
);
}
for (i, (a, b)) in emitted.durations.iter().zip(&theirs.durations).enumerate() {
assert!(
(a - b).abs() <= 0.2,
"emitted segment {i} duration differs: {a} vs {b}"
);
}
let expected = [6.0, 6.0, 4.0];
assert_eq!(
ours.durations.len(),
expected.len(),
"expected a 6/6/4 split"
);
for (i, (a, e)) in ours.durations.iter().zip(&expected).enumerate() {
assert!(
(a - e).abs() <= 0.2,
"segment {i}: expected ~{e}s on the keyframe-dense fixture, got {a}"
);
}
let ours_dir = std::path::Path::new(&ours_playlist).parent().unwrap();
for seg in &ours.segments {
let seg_path = ours_dir.join(seg);
let len = std::fs::metadata(&seg_path)
.unwrap_or_else(|e| panic!("crate segment {seg_path:?} missing: {e}"))
.len();
assert!(len > 0, "crate segment {seg_path:?} is empty");
}
for seg in &emitted.segments {
let seg_path = run.emitted_dir.join(seg);
assert!(seg_path.exists(), "emitted segment {seg_path:?} missing");
}
}
#[test]
fn vf_execution_rejects_multi_video_inputs() {
let dir = tmp_dir("multi_video");
let fixture = dir.join("two_streams.mp4").to_string_lossy().into_owned();
if !std::path::Path::new(&fixture).exists() {
run_context(
FfmpegContext::builder()
.input(Input::from("testsrc2=size=320x240:rate=30:duration=1").set_format("lavfi"))
.input(Input::from("testsrc2=size=160x120:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(fixture.as_str())
.set_video_codec("mpeg4")
.add_stream_map("0:v")
.add_stream_map("1:v"),
)
.build()
.unwrap(),
"two-video fixture",
);
}
let out = dir.join("scaled.mp4").to_string_lossy().into_owned();
let args: Vec<String> = [
"-i",
fixture.as_str(),
"-vf",
"scale=64:-2",
"-c:v",
"libx264",
"-crf",
"23",
"-preset",
"fast",
"-c:a",
"aac",
"-y",
out.as_str(),
]
.iter()
.map(|s| s.to_string())
.collect();
match from_cli_args(&args) {
Ok(_) => panic!("a -vf command over a two-video-stream input must be rejected"),
Err(CliError::AmbiguousFilterSource { video_streams }) => {
assert_eq!(video_streams, 2);
}
Err(CliError::UnverifiedRuntimeProfile { .. }) if !linked_profile_verified() => {}
Err(other) => panic!("expected AmbiguousFilterSource, got: {other}"),
}
}
#[test]
fn vf_uniqueness_is_checked_on_the_executed_opening() {
let dir = tmp_dir("mutable_input");
let path = dir.join("mutable.mp4").to_string_lossy().into_owned();
run_context(
FfmpegContext::builder()
.input(Input::from("testsrc2=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(Output::from(path.as_str()).set_video_codec("mpeg4"))
.build()
.unwrap(),
"single-video first content",
);
let observed = find_all_stream_infos(&path)
.unwrap()
.into_iter()
.filter(|info| matches!(info, StreamInfo::Video { .. }))
.count();
assert_eq!(observed, 1, "precondition: the path starts unique");
let two_video = dir.join("two_video_src.mp4").to_string_lossy().into_owned();
if !std::path::Path::new(&two_video).exists() {
run_context(
FfmpegContext::builder()
.input(Input::from("testsrc2=size=320x240:rate=30:duration=1").set_format("lavfi"))
.input(Input::from("testsrc2=size=160x120:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(two_video.as_str())
.set_video_codec("mpeg4")
.add_stream_map("0:v")
.add_stream_map("1:v"),
)
.build()
.unwrap(),
"two-video replacement content",
);
}
std::fs::copy(&two_video, &path).unwrap();
let out = dir.join("scaled.mp4").to_string_lossy().into_owned();
let args: Vec<String> = [
"-i",
path.as_str(),
"-vf",
"scale=64:-2",
"-c:v",
"libx264",
"-crf",
"23",
"-preset",
"fast",
"-c:a",
"aac",
"-y",
out.as_str(),
]
.iter()
.map(|s| s.to_string())
.collect();
match from_cli_args(&args) {
Ok(_) => panic!("the mutated two-video input must be rejected"),
Err(CliError::AmbiguousFilterSource { video_streams }) => {
assert_eq!(video_streams, 2);
}
Err(CliError::UnverifiedRuntimeProfile { .. }) if !linked_profile_verified() => {}
Err(other) => panic!("expected AmbiguousFilterSource, got: {other}"),
}
}
#[test]
fn vf_uniqueness_gate_opens_the_input_exactly_once() {
let dir = tmp_dir("single_opening");
let fixture = dir
.join("single_opening_probe.mp4")
.to_string_lossy()
.into_owned();
if !std::path::Path::new(&fixture).exists() {
run_context(
FfmpegContext::builder()
.input(Input::from("testsrc2=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(Output::from(fixture.as_str()).set_video_codec("mpeg4"))
.build()
.unwrap(),
"single-opening fixture",
);
}
let opens_of = |path: &str| {
crate::core::context::ffmpeg_context::open_input::INPUT_OPEN_LOG
.lock()
.unwrap()
.iter()
.filter(|url| url.as_str() == path)
.count()
};
let before = opens_of(&fixture);
let out = dir.join("scaled.mp4").to_string_lossy().into_owned();
let args: Vec<String> = [
"-i",
fixture.as_str(),
"-vf",
"scale=64:-2",
"-c:v",
"libx264",
"-crf",
"23",
"-preset",
"fast",
"-c:a",
"aac",
"-y",
out.as_str(),
]
.iter()
.map(|s| s.to_string())
.collect();
let have_libx264 = crate::core::codec::get_encoders()
.iter()
.any(|e| e.codec_name == "libx264");
match from_cli_args(&args) {
Ok(context) => {
drop(context);
assert!(linked_profile_verified());
assert_eq!(
opens_of(&fixture) - before,
1,
"the uniqueness gate must ride the pipeline's single opening"
);
}
Err(CliError::UnverifiedRuntimeProfile { .. }) if !linked_profile_verified() => {
assert_eq!(opens_of(&fixture) - before, 0);
}
Err(CliError::Build(err)) if linked_profile_verified() && !have_libx264 => {
assert!(
matches!(
&err,
crate::error::Error::OpenOutput(
crate::error::OpenOutputError::EncoderUnavailable { name }
) if name == "libx264"
),
"expected EncoderUnavailable(libx264) on this build, got: {err}"
);
assert_eq!(
opens_of(&fixture) - before,
1,
"encoder resolution happens after the single opening"
);
}
Err(other) => panic!("single-video -vf build failed unexpectedly: {other}"),
}
}
fn decode_rgb(path: &str) -> (Vec<u8>, (u32, u32)) {
let frames = crate::core::frame_export::FrameExtractor::new(path)
.collect_frames()
.unwrap_or_else(|e| panic!("decoding {path} failed: {e}"));
assert_eq!(frames.len(), 1, "{path} must contain exactly one frame");
let frame = &frames[0];
(frame.as_bytes().to_vec(), (frame.width(), frame.height()))
}
fn cli_avcodec_triple(bin: &str) -> Option<(u32, u32, u32)> {
let out = Command::new(bin).arg("-version").output().ok()?;
let banner = String::from_utf8_lossy(&out.stdout).into_owned();
let line = banner
.lines()
.find(|l| l.trim_start().starts_with("libavcodec"))?;
let runtime = line.rsplit('/').next()?;
let digits: String = runtime
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.')
.collect();
let mut it = digits.split('.').filter(|s| !s.is_empty());
Some((
it.next()?.parse().ok()?,
it.next()?.parse().ok()?,
it.next()?.parse().ok()?,
))
}
fn linked_avcodec_triple() -> (u32, u32, u32) {
let v = unsafe { ffmpeg_sys_next::avcodec_version() };
(v >> 16, (v >> 8) & 0xff, v & 0xff)
}
struct Playlist {
vod: bool,
endlist: bool,
target_duration: Option<u32>,
durations: Vec<f64>,
segments: Vec<String>,
}
fn parse_playlist(path: &str) -> Playlist {
let text =
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("playlist {path} unreadable: {e}"));
assert!(
text.starts_with("#EXTM3U"),
"{path} is not an m3u8 playlist"
);
let mut playlist = Playlist {
vod: false,
endlist: false,
target_duration: None,
durations: Vec::new(),
segments: Vec::new(),
};
for line in text.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("#EXT-X-PLAYLIST-TYPE:") {
playlist.vod = rest == "VOD";
} else if line == "#EXT-X-ENDLIST" {
playlist.endlist = true;
} else if let Some(rest) = line.strip_prefix("#EXT-X-TARGETDURATION:") {
playlist.target_duration = rest.parse().ok();
} else if let Some(rest) = line.strip_prefix("#EXTINF:") {
let secs = rest.trim_end_matches(',');
playlist.durations.push(secs.parse().unwrap_or(f64::NAN));
} else if !line.is_empty() && !line.starts_with('#') {
let name = line.rsplit('/').next().unwrap_or(line).to_string();
playlist.segments.push(name);
}
}
playlist
}