use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct VideoProbe {
pub codec: String,
pub width: u32,
pub height: u32,
pub fps_fraction: String,
pub fps: f64,
pub duration_secs: f64,
pub bitrate_bps: u64,
pub pixel_format: String,
pub audio_codec: Option<String>,
pub audio_sample_rate: Option<u32>,
pub audio_channels: Option<u32>,
}
#[derive(Clone, Debug)]
pub struct VideoExpectations {
pub width: Option<u32>,
pub height: Option<u32>,
pub fps: Option<f64>,
pub codec: Option<String>,
pub min_duration_secs: Option<f64>,
pub max_duration_secs: Option<f64>,
pub require_audio: bool,
pub fps_tolerance: f64,
}
impl Default for VideoExpectations {
fn default() -> Self {
Self {
width: None,
height: None,
fps: None,
codec: None,
min_duration_secs: None,
max_duration_secs: None,
require_audio: false,
fps_tolerance: 0.01,
}
}
}
impl VideoExpectations {
#[must_use]
pub const fn with_resolution(mut self, width: u32, height: u32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
#[must_use]
pub fn with_fps(mut self, fps: f64) -> Self {
self.fps = Some(fps);
self
}
#[must_use]
pub fn with_codec(mut self, codec: impl Into<String>) -> Self {
self.codec = Some(codec.into());
self
}
#[must_use]
pub fn with_min_duration(mut self, secs: f64) -> Self {
self.min_duration_secs = Some(secs);
self
}
#[must_use]
pub fn with_max_duration(mut self, secs: f64) -> Self {
self.max_duration_secs = Some(secs);
self
}
#[must_use]
pub const fn with_require_audio(mut self, require: bool) -> Self {
self.require_audio = require;
self
}
}
#[derive(Clone, Debug, Serialize)]
pub struct VideoQualityReport {
pub source: String,
pub verdict: VideoVerdict,
pub probe: VideoProbe,
pub checks: Vec<VideoCheck>,
pub passed_count: usize,
pub total_count: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct VideoCheck {
pub name: String,
pub expected: String,
pub actual: String,
pub passed: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum VideoVerdict {
Pass,
Fail,
ProbeError,
}
impl std::fmt::Display for VideoVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pass => write!(f, "PASS"),
Self::Fail => write!(f, "FAIL"),
Self::ProbeError => write!(f, "PROBE ERROR"),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn sample_probe() -> VideoProbe {
VideoProbe {
codec: "h264".to_string(),
width: 1920,
height: 1080,
fps_fraction: "24/1".to_string(),
fps: 24.0,
duration_secs: 120.0,
bitrate_bps: 5_000_000,
pixel_format: "yuv420p".to_string(),
audio_codec: Some("aac".to_string()),
audio_sample_rate: Some(48000),
audio_channels: Some(2),
}
}
#[test]
fn test_video_verdict_display() {
assert_eq!(VideoVerdict::Pass.to_string(), "PASS");
assert_eq!(VideoVerdict::Fail.to_string(), "FAIL");
assert_eq!(VideoVerdict::ProbeError.to_string(), "PROBE ERROR");
}
#[test]
fn test_video_verdict_equality() {
assert_eq!(VideoVerdict::Pass, VideoVerdict::Pass);
assert_ne!(VideoVerdict::Pass, VideoVerdict::Fail);
}
#[test]
fn test_expectations_default() {
let exp = VideoExpectations::default();
assert!(exp.width.is_none());
assert!(exp.height.is_none());
assert!(exp.fps.is_none());
assert!(!exp.require_audio);
}
#[test]
fn test_expectations_builders() {
let exp = VideoExpectations::default()
.with_resolution(1920, 1080)
.with_fps(24.0)
.with_codec("h264")
.with_min_duration(10.0)
.with_max_duration(300.0)
.with_require_audio(true);
assert_eq!(exp.width, Some(1920));
assert_eq!(exp.height, Some(1080));
assert!((exp.fps.unwrap() - 24.0).abs() < f64::EPSILON);
assert_eq!(exp.codec.as_deref(), Some("h264"));
assert!((exp.min_duration_secs.unwrap() - 10.0).abs() < f64::EPSILON);
assert!((exp.max_duration_secs.unwrap() - 300.0).abs() < f64::EPSILON);
assert!(exp.require_audio);
}
#[test]
fn test_probe_serialization() {
let probe = sample_probe();
let json = serde_json::to_string(&probe).unwrap();
assert!(json.contains("\"codec\":\"h264\""));
assert!(json.contains("\"width\":1920"));
}
#[test]
fn test_probe_deserialization() {
let json = r#"{
"codec": "h264",
"width": 1920,
"height": 1080,
"fps_fraction": "24/1",
"fps": 24.0,
"duration_secs": 120.0,
"bitrate_bps": 5000000,
"pixel_format": "yuv420p",
"audio_codec": "aac",
"audio_sample_rate": 48000,
"audio_channels": 2
}"#;
let probe: VideoProbe = serde_json::from_str(json).unwrap();
assert_eq!(probe.codec, "h264");
assert_eq!(probe.width, 1920);
}
#[test]
fn test_video_check() {
let check = VideoCheck {
name: "resolution".to_string(),
expected: "1920x1080".to_string(),
actual: "1920x1080".to_string(),
passed: true,
};
assert!(check.passed);
}
#[test]
fn test_video_quality_report_serialization() {
let report = VideoQualityReport {
source: "test.mp4".to_string(),
verdict: VideoVerdict::Pass,
probe: sample_probe(),
checks: vec![VideoCheck {
name: "codec".to_string(),
expected: "h264".to_string(),
actual: "h264".to_string(),
passed: true,
}],
passed_count: 1,
total_count: 1,
};
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("\"verdict\":\"Pass\""));
}
}