use std::path::{Path, PathBuf};
use ff_decode::{DecodeError, HardwareAccel, VideoDecoder};
use ff_format::PixelFormat;
mod fixtures;
use fixtures::assets_dir;
struct TempDir(PathBuf);
impl TempDir {
fn new(name: &str) -> Self {
let dir = std::env::temp_dir().join(name);
std::fs::create_dir_all(&dir).expect("failed to create temp dir");
Self(dir)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn make_exr_sequence(dir: &Path, count: usize) -> Option<PathBuf> {
let src = assets_dir().join("img/hello-triangle.png");
for i in 1..=count {
let dst = dir.join(format!("frame{i:04}.exr"));
let status = std::process::Command::new("ffmpeg")
.args([
"-y",
"-i",
src.to_str()?,
"-vf",
"format=gbrpf32le",
"-frames:v",
"1",
dst.to_str()?,
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.ok()?;
if !status.success() {
return None; }
}
Some(dir.join("frame%04d.exr"))
}
#[test]
fn exr_sequence_should_decode_all_frames() {
let tmp = TempDir::new("ff_decode_exr_seq_count");
let pattern = match make_exr_sequence(tmp.path(), 3) {
Some(p) => p,
None => {
println!("Skipping: EXR encoder not available in this FFmpeg build");
return;
}
};
let mut decoder = match VideoDecoder::open(&pattern)
.hardware_accel(HardwareAccel::None)
.build()
{
Ok(d) => d,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let mut count = 0usize;
loop {
match decoder.decode_one() {
Ok(Some(_)) => count += 1,
Ok(None) => break,
Err(e) => panic!("decode error: {e}"),
}
}
assert_eq!(count, 3, "expected 3 frames from a 3-image EXR sequence");
}
#[test]
fn exr_sequence_frame_should_have_gbrpf32le_pixel_format() {
let tmp = TempDir::new("ff_decode_exr_seq_fmt");
let pattern = match make_exr_sequence(tmp.path(), 1) {
Some(p) => p,
None => {
println!("Skipping: EXR encoder not available in this FFmpeg build");
return;
}
};
let mut decoder = match VideoDecoder::open(&pattern)
.hardware_accel(HardwareAccel::None)
.build()
{
Ok(d) => d,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = match decoder.decode_one() {
Ok(Some(f)) => f,
Ok(None) => panic!("expected at least one frame"),
Err(e) => panic!("decode error: {e}"),
};
assert_eq!(
frame.format(),
PixelFormat::Gbrpf32le,
"EXR frames should decode as gbrpf32le"
);
}
#[test]
fn exr_sequence_frame_should_have_positive_dimensions() {
let tmp = TempDir::new("ff_decode_exr_seq_dims");
let pattern = match make_exr_sequence(tmp.path(), 1) {
Some(p) => p,
None => {
println!("Skipping: EXR encoder not available in this FFmpeg build");
return;
}
};
let mut decoder = match VideoDecoder::open(&pattern)
.hardware_accel(HardwareAccel::None)
.build()
{
Ok(d) => d,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = match decoder.decode_one() {
Ok(Some(f)) => f,
Ok(None) => panic!("expected at least one frame"),
Err(e) => panic!("decode error: {e}"),
};
assert!(frame.width() > 0, "frame width must be positive");
assert!(frame.height() > 0, "frame height must be positive");
}
#[test]
fn exr_sequence_frame_should_have_three_planes() {
let tmp = TempDir::new("ff_decode_exr_seq_planes");
let pattern = match make_exr_sequence(tmp.path(), 1) {
Some(p) => p,
None => {
println!("Skipping: EXR encoder not available in this FFmpeg build");
return;
}
};
let mut decoder = match VideoDecoder::open(&pattern)
.hardware_accel(HardwareAccel::None)
.build()
{
Ok(d) => d,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = match decoder.decode_one() {
Ok(Some(f)) => f,
Ok(None) => panic!("expected at least one frame"),
Err(e) => panic!("decode error: {e}"),
};
assert_eq!(frame.num_planes(), 3, "gbrpf32le must have 3 planes");
for i in 0..3 {
let plane = frame.plane(i).expect("plane {i} must exist");
assert!(!plane.is_empty(), "plane {i} must contain data");
}
}
#[test]
fn exr_missing_decoder_should_return_decoder_unavailable_not_panic() {
let tmp = TempDir::new("ff_decode_exr_seq_nocodec");
let pattern = match make_exr_sequence(tmp.path(), 1) {
Some(p) => p,
None => {
println!("Skipping: EXR encoder not available — cannot create test fixture");
return;
}
};
match VideoDecoder::open(&pattern)
.hardware_accel(HardwareAccel::None)
.build()
{
Ok(_) => {
}
Err(DecodeError::DecoderUnavailable { .. }) => {
}
Err(e) => panic!("unexpected error when opening EXR sequence: {e}"),
}
}
#[test]
fn png_sequence_still_works_after_exr_support() {
let tmp = TempDir::new("ff_decode_exr_regression_png");
let src = assets_dir().join("img/hello-triangle.png");
for i in 1..=2u32 {
let dst = tmp.path().join(format!("frame{i:04}.png"));
std::fs::copy(&src, &dst).expect("failed to copy PNG");
}
let pattern = tmp.path().join("frame%04d.png");
let mut decoder = match VideoDecoder::open(&pattern)
.hardware_accel(HardwareAccel::None)
.build()
{
Ok(d) => d,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let mut count = 0usize;
loop {
match decoder.decode_one() {
Ok(Some(_)) => count += 1,
Ok(None) => break,
Err(e) => panic!("decode error: {e}"),
}
}
assert_eq!(count, 2, "PNG sequence must still decode 2 frames");
}