#![deny(missing_docs)]
use std::io::Write;
extern crate ffmpeg_next as ffmpeg;
use anyhow::Result;
const CRATE_ENV_VAR: &str = "TWENTY_TWENTY";
#[track_caller]
pub fn assert_image<P: AsRef<std::path::Path>>(path: P, actual: &image::DynamicImage, threshold: f64) {
if let Err(e) = assert_image_impl(path, actual, threshold) {
panic!("assertion failed: {e}")
}
}
#[track_caller]
pub fn assert_h264_frame<P: AsRef<std::path::Path>>(path: P, actual: &[u8], threshold: f64) {
match h264_frame_to_image(actual) {
Ok(image) => {
if let Err(e) = assert_image_impl(path, &image, threshold) {
panic!("assertion failed: {e}")
}
}
Err(e) => {
panic!("could not convert H.264 frame to image: {e}")
}
}
}
pub(crate) fn h264_frame_to_image(data: &[u8]) -> Result<image::DynamicImage> {
ffmpeg::init()?;
let temp_file_name = std::env::temp_dir().join(format!("{}.h264", uuid::Uuid::new_v4()));
let mut temp_file = std::fs::File::create(&temp_file_name)?;
temp_file.write_all(data)?;
let ictx = ffmpeg::format::input(&temp_file_name).map_err(|e| anyhow::anyhow!(e))?;
let input = ictx
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or(ffmpeg::Error::StreamNotFound)?;
let context = ffmpeg::codec::context::Context::from_parameters(input.parameters())?;
let mut video_decoder = context.decoder().video()?;
let mut video_frame = ffmpeg::frame::Video::empty();
let packet = ffmpeg::packet::Packet::copy(data);
video_decoder.send_packet(&packet)?;
video_decoder.receive_frame(&mut video_frame)?;
video_decoder.flush();
let pixel_format = video_frame.format();
if pixel_format != ffmpeg::format::Pixel::RGB24 {
let mut converted_video = ffmpeg::frame::Video::empty();
video_frame
.converter(ffmpeg::format::Pixel::RGB24)?
.run(&video_frame, &mut converted_video)?;
video_frame = converted_video;
}
video_frame.set_format(ffmpeg::format::Pixel::RGB24);
let Some(raw) = image::RgbImage::from_raw(video_frame.width(), video_frame.height(), video_frame.data(0).to_vec()) else {
anyhow::bail!("could not parse image from raw");
};
Ok(image::DynamicImage::ImageRgb8(raw))
}
pub(crate) fn assert_image_impl<P: AsRef<std::path::Path>>(
path: P,
actual: &image::DynamicImage,
threshold: f64,
) -> Result<(), String> {
let path = path.as_ref();
let var = std::env::var_os(CRATE_ENV_VAR);
let overwrite = var.as_deref().and_then(std::ffi::OsStr::to_str) == Some("overwrite");
if overwrite {
if let Err(e) = actual.save_with_format(path, image::ImageFormat::Png) {
panic!("unable to write image to {}: {}", path.display(), e);
}
} else {
let expected = match image::io::Reader::open(path) {
Ok(s) => s.decode().expect("decoding image from path failed"),
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => image::DynamicImage::new_rgba16(actual.width(), actual.height()),
_ => panic!("unable to read contents of {}: {}", path.display(), e),
},
};
let result = match image_compare::rgba_hybrid_compare(&expected.to_rgba8(), &actual.to_rgba8()) {
Ok(result) => result,
Err(err) => {
panic!("could not compare the images {err}")
}
};
if result.score < threshold {
return Err(format!(
r#"image (`{}`) score is `{}` which is less than threshold `{}`
set {}=overwrite if these changes are intentional"#,
path.display(),
result.score,
threshold,
CRATE_ENV_VAR
));
}
}
Ok(())
}