use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum VideoDetector {
Black {
min_duration_s: f64,
pixel_th: f64,
picture_th: f64,
},
Scene { threshold_pct: f64 },
Crop { limit: u32, round: u32, reset: u32 },
}
#[derive(Debug, Clone, PartialEq)]
pub enum AudioDetector {
Silence {
noise_db: f64,
min_duration_s: f64,
mono: bool,
},
Ebur128 { true_peak: bool },
}
impl VideoDetector {
pub(crate) fn filter_name(&self) -> &'static str {
match self {
VideoDetector::Black { .. } => "blackdetect",
VideoDetector::Scene { .. } => "scdet",
VideoDetector::Crop { .. } => "cropdetect",
}
}
pub(crate) fn to_filter(&self) -> String {
match *self {
VideoDetector::Black {
min_duration_s,
pixel_th,
picture_th,
} => format!("blackdetect=d={min_duration_s}:pix_th={pixel_th}:pic_th={picture_th}"),
VideoDetector::Scene { threshold_pct } => {
format!("scdet=threshold={threshold_pct}:sc_pass=0")
}
VideoDetector::Crop {
limit,
round,
reset,
} => format!("cropdetect=limit={limit}:round={round}:reset={reset}"),
}
}
pub(crate) fn validate(&self) -> Result<()> {
let in_range = |v: f64, lo: f64, hi: f64, what: &str| -> Result<()> {
if v.is_finite() && v >= lo && v <= hi {
Ok(())
} else {
Err(Error::InvalidRecipeArg(format!(
"{what} must be in {lo}..={hi}, got {v}"
)))
}
};
match *self {
VideoDetector::Black {
min_duration_s,
pixel_th,
picture_th,
} => {
if !min_duration_s.is_finite() || min_duration_s < 0.0 {
return Err(Error::InvalidRecipeArg(format!(
"blackdetect min_duration_s must be finite and >= 0, got {min_duration_s}"
)));
}
in_range(pixel_th, 0.0, 1.0, "blackdetect pixel_th")?;
in_range(picture_th, 0.0, 1.0, "blackdetect picture_th")?;
}
VideoDetector::Scene { threshold_pct } => {
in_range(threshold_pct, 0.0, 100.0, "scene threshold_pct")?;
}
VideoDetector::Crop {
limit,
round,
reset,
} => {
for (v, what) in [
(limit, "cropdetect limit"),
(round, "cropdetect round"),
(reset, "cropdetect reset"),
] {
if v > i32::MAX as u32 {
return Err(Error::InvalidRecipeArg(format!(
"{what} must be <= {}, got {v}",
i32::MAX
)));
}
}
}
}
Ok(())
}
}
impl AudioDetector {
pub(crate) fn filter_name(&self) -> &'static str {
match self {
AudioDetector::Silence { .. } => "silencedetect",
AudioDetector::Ebur128 { .. } => "ebur128",
}
}
pub(crate) fn to_filter(&self) -> String {
match *self {
AudioDetector::Silence {
noise_db,
min_duration_s,
mono,
} => {
let mut s = format!("silencedetect=noise={noise_db}dB:d={min_duration_s}");
if mono {
s.push_str(":mono=1");
}
s
}
AudioDetector::Ebur128 { true_peak } => {
if true_peak {
"ebur128=metadata=1:peak=true".to_string()
} else {
"ebur128=metadata=1".to_string()
}
}
}
}
pub(crate) fn validate(&self) -> Result<()> {
if let AudioDetector::Silence {
noise_db,
min_duration_s,
..
} = *self
{
if !noise_db.is_finite() {
return Err(Error::InvalidRecipeArg(format!(
"silencedetect noise_db must be finite, got {noise_db}"
)));
}
if !min_duration_s.is_finite() || min_duration_s < 0.0 {
return Err(Error::InvalidRecipeArg(format!(
"silencedetect min_duration_s must be finite and >= 0, got {min_duration_s}"
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn black_filter_string() {
let d = VideoDetector::Black {
min_duration_s: 0.1,
pixel_th: 0.1,
picture_th: 0.98,
};
assert_eq!(d.to_filter(), "blackdetect=d=0.1:pix_th=0.1:pic_th=0.98");
}
#[test]
fn scene_filter_uses_sc_pass_zero() {
let d = VideoDetector::Scene { threshold_pct: 10.0 };
assert_eq!(d.to_filter(), "scdet=threshold=10:sc_pass=0");
}
#[test]
fn crop_filter_string() {
let d = VideoDetector::Crop {
limit: 24,
round: 16,
reset: 0,
};
assert_eq!(d.to_filter(), "cropdetect=limit=24:round=16:reset=0");
}
#[test]
fn silence_filter_requires_db_suffix() {
let d = AudioDetector::Silence {
noise_db: -30.0,
min_duration_s: 0.5,
mono: false,
};
assert_eq!(d.to_filter(), "silencedetect=noise=-30dB:d=0.5");
}
#[test]
fn silence_mono_adds_flag() {
let d = AudioDetector::Silence {
noise_db: -30.0,
min_duration_s: 0.5,
mono: true,
};
assert_eq!(d.to_filter(), "silencedetect=noise=-30dB:d=0.5:mono=1");
}
#[test]
fn ebur128_peak_toggle() {
assert_eq!(
AudioDetector::Ebur128 { true_peak: false }.to_filter(),
"ebur128=metadata=1"
);
assert_eq!(
AudioDetector::Ebur128 { true_peak: true }.to_filter(),
"ebur128=metadata=1:peak=true"
);
}
#[test]
fn validate_rejects_non_finite() {
assert!(VideoDetector::Black {
min_duration_s: f64::NAN,
pixel_th: 0.1,
picture_th: 0.98,
}
.validate()
.is_err());
assert!(VideoDetector::Scene {
threshold_pct: f64::INFINITY,
}
.validate()
.is_err());
assert!(AudioDetector::Silence {
noise_db: f64::NAN,
min_duration_s: 0.5,
mono: false,
}
.validate()
.is_err());
assert!(VideoDetector::Scene { threshold_pct: 101.0 }.validate().is_err());
assert!(VideoDetector::Black {
min_duration_s: -1.0,
pixel_th: 0.1,
picture_th: 0.98,
}
.validate()
.is_err());
assert!(VideoDetector::Black {
min_duration_s: 0.1,
pixel_th: 1.5,
picture_th: 0.98,
}
.validate()
.is_err());
assert!(VideoDetector::Black {
min_duration_s: 0.1,
pixel_th: 0.1,
picture_th: 0.98,
}
.validate()
.is_ok());
}
}