#![allow(unsafe_code)]
#![allow(unsafe_op_in_unsafe_fn)]
#![allow(clippy::cast_possible_truncation)]
use std::ffi::CString;
use std::path::Path;
use crate::FilterError;
use crate::analysis::LoudnessResult;
pub(super) unsafe fn measure_loudness_unsafe(path: &Path) -> Result<LoudnessResult, FilterError> {
macro_rules! bail {
($graph:ident, $reason:expr) => {{
let mut g = $graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(FilterError::AnalysisFailed {
reason: format!("{}", $reason),
});
}};
}
let path_str = path.to_string_lossy();
let amovie_args =
CString::new(format!("filename={path_str}")).map_err(|_| FilterError::AnalysisFailed {
reason: "path contains null byte".to_string(),
})?;
let graph = ff_sys::avfilter_graph_alloc();
if graph.is_null() {
return Err(FilterError::AnalysisFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let amovie_filt = ff_sys::avfilter_get_by_name(c"amovie".as_ptr());
if amovie_filt.is_null() {
bail!(graph, "filter not found: amovie");
}
let mut src_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut src_ctx,
amovie_filt,
c"loudness_src".as_ptr(),
amovie_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("amovie create_filter failed code={ret}"));
}
let ebur128_filt = ff_sys::avfilter_get_by_name(c"ebur128".as_ptr());
if ebur128_filt.is_null() {
bail!(graph, "filter not found: ebur128");
}
let mut meas_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut meas_ctx,
ebur128_filt,
c"loudness_ebur128".as_ptr(),
c"metadata=1:peak=true".as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("ebur128 create_filter failed code={ret}"));
}
let abuffersink_filt = ff_sys::avfilter_get_by_name(c"abuffersink".as_ptr());
if abuffersink_filt.is_null() {
bail!(graph, "filter not found: abuffersink");
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sink_ctx,
abuffersink_filt,
c"loudness_sink".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("abuffersink create_filter failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(src_ctx, 0, meas_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link src→ebur128 failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(meas_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link ebur128→sink failed code={ret}")
);
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let mut integrated_lufs = f32::NEG_INFINITY;
let mut lra_lu: f32 = 0.0;
let mut true_peak_dbtp = f32::NEG_INFINITY;
loop {
let raw_frame = ff_sys::av_frame_alloc();
if raw_frame.is_null() {
break;
}
let ret = ff_sys::av_buffersink_get_frame(sink_ctx, raw_frame);
if ret < 0 {
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
break;
}
read_f32_meta(raw_frame, c"lavfi.r128.I".as_ptr(), &mut integrated_lufs);
read_f32_meta(raw_frame, c"lavfi.r128.LRA".as_ptr(), &mut lra_lu);
read_f32_meta(
raw_frame,
c"lavfi.r128.true_peak".as_ptr(),
&mut true_peak_dbtp,
);
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
}
let mut g = graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
log::debug!(
"loudness analysis complete integrated_lufs={integrated_lufs:.1} \
lra_lu={lra_lu:.1} true_peak_dbtp={true_peak_dbtp:.1}"
);
Ok(LoudnessResult {
integrated_lufs,
lra: lra_lu,
true_peak_dbtp,
})
}
unsafe fn read_f32_meta(
frame: *mut ff_sys::AVFrame,
key: *const std::os::raw::c_char,
out: &mut f32,
) {
let entry = ff_sys::av_dict_get((*frame).metadata, key, std::ptr::null(), 0);
if !entry.is_null()
&& let Ok(s) = std::ffi::CStr::from_ptr((*entry).value).to_str()
&& let Ok(v) = s.parse::<f32>()
{
*out = v;
}
}
pub(super) unsafe fn compute_ssim_unsafe(
reference: &Path,
distorted: &Path,
) -> Result<f32, FilterError> {
let ref_count = probe_video_frame_count(reference);
let dist_count = probe_video_frame_count(distorted);
if let (Some(r), Some(d)) = (ref_count, dist_count)
&& (r - d).abs() > 1
{
return Err(FilterError::AnalysisFailed {
reason: format!("frame count mismatch: reference={r} distorted={d}"),
});
}
macro_rules! bail {
($graph:ident, $reason:expr) => {{
let mut g = $graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(FilterError::AnalysisFailed {
reason: format!("{}", $reason),
});
}};
}
let ref_str = reference.to_string_lossy();
let dist_str = distorted.to_string_lossy();
let ref_args =
CString::new(format!("filename={ref_str}")).map_err(|_| FilterError::AnalysisFailed {
reason: "reference path contains null byte".to_string(),
})?;
let dist_args =
CString::new(format!("filename={dist_str}")).map_err(|_| FilterError::AnalysisFailed {
reason: "distorted path contains null byte".to_string(),
})?;
let graph = ff_sys::avfilter_graph_alloc();
if graph.is_null() {
return Err(FilterError::AnalysisFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let movie_filt = ff_sys::avfilter_get_by_name(c"movie".as_ptr());
if movie_filt.is_null() {
bail!(graph, "filter not found: movie");
}
let mut ref_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ref_ctx,
movie_filt,
c"ssim_ref".as_ptr(),
ref_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("movie(reference) create_filter failed code={ret}")
);
}
let mut dist_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut dist_ctx,
movie_filt,
c"ssim_dist".as_ptr(),
dist_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("movie(distorted) create_filter failed code={ret}")
);
}
let ssim_filt = ff_sys::avfilter_get_by_name(c"ssim".as_ptr());
if ssim_filt.is_null() {
bail!(graph, "filter not found: ssim");
}
let mut ssim_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ssim_ctx,
ssim_filt,
c"ssim_compute".as_ptr(),
c"eof_action=endall".as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("ssim create_filter failed code={ret}"));
}
let buffersink_filt = ff_sys::avfilter_get_by_name(c"buffersink".as_ptr());
if buffersink_filt.is_null() {
bail!(graph, "filter not found: buffersink");
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sink_ctx,
buffersink_filt,
c"ssim_sink".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("buffersink create_filter failed code={ret}"));
}
let ret = ff_sys::avfilter_link(ref_ctx, 0, ssim_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link ref→ssim[0] failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(dist_ctx, 0, ssim_ctx, 1);
if ret < 0 {
bail!(
graph,
format!("avfilter_link dist→ssim[1] failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(ssim_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link ssim→sink failed code={ret}"));
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let mut ssim_sum: f64 = 0.0;
let mut frame_count: u64 = 0;
loop {
let raw_frame = ff_sys::av_frame_alloc();
if raw_frame.is_null() {
break;
}
let ret = ff_sys::av_buffersink_get_frame(sink_ctx, raw_frame);
if ret < 0 {
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
break;
}
let mut ssim_val = 0.0f32;
read_f32_meta(raw_frame, c"lavfi.ssim.All".as_ptr(), &mut ssim_val);
if ssim_val > 0.0 {
ssim_sum += f64::from(ssim_val);
frame_count += 1;
}
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
}
let mut g = graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
if frame_count == 0 {
return Err(FilterError::AnalysisFailed {
reason: "no frames were compared (empty or incompatible inputs)".to_string(),
});
}
#[allow(clippy::cast_precision_loss)]
let mean_ssim = (ssim_sum / frame_count as f64) as f32;
log::debug!("ssim complete mean={mean_ssim:.6} frames={frame_count}");
Ok(mean_ssim)
}
pub(super) unsafe fn compute_psnr_unsafe(
reference: &Path,
distorted: &Path,
) -> Result<f32, FilterError> {
let ref_count = probe_video_frame_count(reference);
let dist_count = probe_video_frame_count(distorted);
if let (Some(r), Some(d)) = (ref_count, dist_count)
&& (r - d).abs() > 1
{
return Err(FilterError::AnalysisFailed {
reason: format!("frame count mismatch: reference={r} distorted={d}"),
});
}
macro_rules! bail {
($graph:ident, $reason:expr) => {{
let mut g = $graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(FilterError::AnalysisFailed {
reason: format!("{}", $reason),
});
}};
}
let ref_str = reference.to_string_lossy();
let dist_str = distorted.to_string_lossy();
let ref_args =
CString::new(format!("filename={ref_str}")).map_err(|_| FilterError::AnalysisFailed {
reason: "reference path contains null byte".to_string(),
})?;
let dist_args =
CString::new(format!("filename={dist_str}")).map_err(|_| FilterError::AnalysisFailed {
reason: "distorted path contains null byte".to_string(),
})?;
let graph = ff_sys::avfilter_graph_alloc();
if graph.is_null() {
return Err(FilterError::AnalysisFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let movie_filt = ff_sys::avfilter_get_by_name(c"movie".as_ptr());
if movie_filt.is_null() {
bail!(graph, "filter not found: movie");
}
let mut ref_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ref_ctx,
movie_filt,
c"psnr_ref".as_ptr(),
ref_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("movie(reference) create_filter failed code={ret}")
);
}
let mut dist_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut dist_ctx,
movie_filt,
c"psnr_dist".as_ptr(),
dist_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("movie(distorted) create_filter failed code={ret}")
);
}
let psnr_filt = ff_sys::avfilter_get_by_name(c"psnr".as_ptr());
if psnr_filt.is_null() {
bail!(graph, "filter not found: psnr");
}
let mut psnr_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut psnr_ctx,
psnr_filt,
c"psnr_compute".as_ptr(),
c"eof_action=endall".as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("psnr create_filter failed code={ret}"));
}
let buffersink_filt = ff_sys::avfilter_get_by_name(c"buffersink".as_ptr());
if buffersink_filt.is_null() {
bail!(graph, "filter not found: buffersink");
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sink_ctx,
buffersink_filt,
c"psnr_sink".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("buffersink create_filter failed code={ret}"));
}
let ret = ff_sys::avfilter_link(ref_ctx, 0, psnr_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link ref→psnr[0] failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(dist_ctx, 0, psnr_ctx, 1);
if ret < 0 {
bail!(
graph,
format!("avfilter_link dist→psnr[1] failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(psnr_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link psnr→sink failed code={ret}"));
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let mut psnr_sum: f64 = 0.0;
let mut frame_count: u64 = 0;
loop {
let raw_frame = ff_sys::av_frame_alloc();
if raw_frame.is_null() {
break;
}
let ret = ff_sys::av_buffersink_get_frame(sink_ctx, raw_frame);
if ret < 0 {
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
break;
}
let mut psnr_val = f32::NEG_INFINITY; read_f32_meta(raw_frame, c"lavfi.psnr.psnr.y".as_ptr(), &mut psnr_val);
if psnr_val > f32::NEG_INFINITY {
psnr_sum += f64::from(psnr_val);
frame_count += 1;
}
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
}
let mut g = graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
if frame_count == 0 {
return Err(FilterError::AnalysisFailed {
reason: "no frames were compared (empty or incompatible inputs)".to_string(),
});
}
#[allow(clippy::cast_precision_loss)]
let mean_psnr = (psnr_sum / frame_count as f64) as f32;
log::debug!("psnr complete mean={mean_psnr:.3} frames={frame_count}");
Ok(mean_psnr)
}
unsafe fn probe_video_frame_count(path: &Path) -> Option<i64> {
let mut fmt_ctx = ff_sys::avformat::open_input(path).ok()?;
let _ = ff_sys::avformat::find_stream_info(fmt_ctx);
let nb_streams = (*fmt_ctx).nb_streams;
let mut result: Option<i64> = None;
for i in 0..nb_streams {
let stream = *(*fmt_ctx).streams.add(i as usize);
if (*(*stream).codecpar).codec_type != ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO {
continue;
}
if (*stream).nb_frames > 0 {
result = Some((*stream).nb_frames);
} else {
let dur = (*stream).duration;
let tb = (*stream).time_base;
let fps = (*stream).r_frame_rate;
#[allow(clippy::cast_precision_loss)]
if dur != ff_sys::AV_NOPTS_VALUE && tb.den > 0 && fps.den > 0 {
let dur_secs = dur as f64 * f64::from(tb.num) / f64::from(tb.den);
let fps_val = f64::from(fps.num) / f64::from(fps.den);
result = Some((dur_secs * fps_val).round() as i64);
}
}
break; }
ff_sys::avformat::close_input(std::ptr::addr_of_mut!(fmt_ctx));
result
}