Skip to main content

jugar_probar/video_quality/
types.rs

1//! Types for video quality verification.
2//!
3//! Provides structures for video probe results, quality expectations,
4//! and verification reports.
5
6use serde::{Deserialize, Serialize};
7
8/// Video probe result from ffprobe.
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct VideoProbe {
11    /// Video codec name (e.g., "h264", "hevc", "prores")
12    pub codec: String,
13    /// Width in pixels
14    pub width: u32,
15    /// Height in pixels
16    pub height: u32,
17    /// Frame rate as a fraction (e.g., "24/1", "30000/1001")
18    pub fps_fraction: String,
19    /// Frame rate as a float
20    pub fps: f64,
21    /// Duration in seconds
22    pub duration_secs: f64,
23    /// Bitrate in bits per second (0 if unavailable)
24    pub bitrate_bps: u64,
25    /// Pixel format (e.g., "yuv420p")
26    pub pixel_format: String,
27    /// Audio codec (None if no audio stream)
28    pub audio_codec: Option<String>,
29    /// Audio sample rate (None if no audio stream)
30    pub audio_sample_rate: Option<u32>,
31    /// Audio channels (None if no audio stream)
32    pub audio_channels: Option<u32>,
33}
34
35/// Expected video properties for validation.
36#[derive(Clone, Debug)]
37pub struct VideoExpectations {
38    /// Expected width (None = skip check)
39    pub width: Option<u32>,
40    /// Expected height (None = skip check)
41    pub height: Option<u32>,
42    /// Expected FPS (None = skip check)
43    pub fps: Option<f64>,
44    /// Expected codec (None = skip check)
45    pub codec: Option<String>,
46    /// Minimum duration in seconds (None = skip check)
47    pub min_duration_secs: Option<f64>,
48    /// Maximum duration in seconds (None = skip check)
49    pub max_duration_secs: Option<f64>,
50    /// Whether audio stream must be present
51    pub require_audio: bool,
52    /// FPS tolerance for comparison (default: 0.01)
53    pub fps_tolerance: f64,
54}
55
56impl Default for VideoExpectations {
57    fn default() -> Self {
58        Self {
59            width: None,
60            height: None,
61            fps: None,
62            codec: None,
63            min_duration_secs: None,
64            max_duration_secs: None,
65            require_audio: false,
66            fps_tolerance: 0.01,
67        }
68    }
69}
70
71impl VideoExpectations {
72    /// Set expected resolution.
73    #[must_use]
74    pub const fn with_resolution(mut self, width: u32, height: u32) -> Self {
75        self.width = Some(width);
76        self.height = Some(height);
77        self
78    }
79
80    /// Set expected FPS.
81    #[must_use]
82    pub fn with_fps(mut self, fps: f64) -> Self {
83        self.fps = Some(fps);
84        self
85    }
86
87    /// Set expected codec.
88    #[must_use]
89    pub fn with_codec(mut self, codec: impl Into<String>) -> Self {
90        self.codec = Some(codec.into());
91        self
92    }
93
94    /// Set minimum duration.
95    #[must_use]
96    pub fn with_min_duration(mut self, secs: f64) -> Self {
97        self.min_duration_secs = Some(secs);
98        self
99    }
100
101    /// Set maximum duration.
102    #[must_use]
103    pub fn with_max_duration(mut self, secs: f64) -> Self {
104        self.max_duration_secs = Some(secs);
105        self
106    }
107
108    /// Require audio stream.
109    #[must_use]
110    pub const fn with_require_audio(mut self, require: bool) -> Self {
111        self.require_audio = require;
112        self
113    }
114}
115
116/// Video quality verification report.
117#[derive(Clone, Debug, Serialize)]
118pub struct VideoQualityReport {
119    /// Source file path
120    pub source: String,
121    /// Overall verdict
122    pub verdict: VideoVerdict,
123    /// Probe results
124    pub probe: VideoProbe,
125    /// Individual check results
126    pub checks: Vec<VideoCheck>,
127    /// Number of passed checks
128    pub passed_count: usize,
129    /// Total number of checks
130    pub total_count: usize,
131}
132
133/// Individual video quality check result.
134#[derive(Clone, Debug, Serialize)]
135pub struct VideoCheck {
136    /// Check name
137    pub name: String,
138    /// Expected value
139    pub expected: String,
140    /// Actual value
141    pub actual: String,
142    /// Whether the check passed
143    pub passed: bool,
144}
145
146/// Overall video quality verdict.
147#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
148pub enum VideoVerdict {
149    /// All checks passed
150    Pass,
151    /// One or more checks failed
152    Fail,
153    /// Could not probe video
154    ProbeError,
155}
156
157impl std::fmt::Display for VideoVerdict {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            Self::Pass => write!(f, "PASS"),
161            Self::Fail => write!(f, "FAIL"),
162            Self::ProbeError => write!(f, "PROBE ERROR"),
163        }
164    }
165}
166
167#[cfg(test)]
168#[allow(clippy::unwrap_used)]
169mod tests {
170    use super::*;
171
172    fn sample_probe() -> VideoProbe {
173        VideoProbe {
174            codec: "h264".to_string(),
175            width: 1920,
176            height: 1080,
177            fps_fraction: "24/1".to_string(),
178            fps: 24.0,
179            duration_secs: 120.0,
180            bitrate_bps: 5_000_000,
181            pixel_format: "yuv420p".to_string(),
182            audio_codec: Some("aac".to_string()),
183            audio_sample_rate: Some(48000),
184            audio_channels: Some(2),
185        }
186    }
187
188    #[test]
189    fn test_video_verdict_display() {
190        assert_eq!(VideoVerdict::Pass.to_string(), "PASS");
191        assert_eq!(VideoVerdict::Fail.to_string(), "FAIL");
192        assert_eq!(VideoVerdict::ProbeError.to_string(), "PROBE ERROR");
193    }
194
195    #[test]
196    fn test_video_verdict_equality() {
197        assert_eq!(VideoVerdict::Pass, VideoVerdict::Pass);
198        assert_ne!(VideoVerdict::Pass, VideoVerdict::Fail);
199    }
200
201    #[test]
202    fn test_expectations_default() {
203        let exp = VideoExpectations::default();
204        assert!(exp.width.is_none());
205        assert!(exp.height.is_none());
206        assert!(exp.fps.is_none());
207        assert!(!exp.require_audio);
208    }
209
210    #[test]
211    fn test_expectations_builders() {
212        let exp = VideoExpectations::default()
213            .with_resolution(1920, 1080)
214            .with_fps(24.0)
215            .with_codec("h264")
216            .with_min_duration(10.0)
217            .with_max_duration(300.0)
218            .with_require_audio(true);
219        assert_eq!(exp.width, Some(1920));
220        assert_eq!(exp.height, Some(1080));
221        assert!((exp.fps.unwrap() - 24.0).abs() < f64::EPSILON);
222        assert_eq!(exp.codec.as_deref(), Some("h264"));
223        assert!((exp.min_duration_secs.unwrap() - 10.0).abs() < f64::EPSILON);
224        assert!((exp.max_duration_secs.unwrap() - 300.0).abs() < f64::EPSILON);
225        assert!(exp.require_audio);
226    }
227
228    #[test]
229    fn test_probe_serialization() {
230        let probe = sample_probe();
231        let json = serde_json::to_string(&probe).unwrap();
232        assert!(json.contains("\"codec\":\"h264\""));
233        assert!(json.contains("\"width\":1920"));
234    }
235
236    #[test]
237    fn test_probe_deserialization() {
238        let json = r#"{
239            "codec": "h264",
240            "width": 1920,
241            "height": 1080,
242            "fps_fraction": "24/1",
243            "fps": 24.0,
244            "duration_secs": 120.0,
245            "bitrate_bps": 5000000,
246            "pixel_format": "yuv420p",
247            "audio_codec": "aac",
248            "audio_sample_rate": 48000,
249            "audio_channels": 2
250        }"#;
251        let probe: VideoProbe = serde_json::from_str(json).unwrap();
252        assert_eq!(probe.codec, "h264");
253        assert_eq!(probe.width, 1920);
254    }
255
256    #[test]
257    fn test_video_check() {
258        let check = VideoCheck {
259            name: "resolution".to_string(),
260            expected: "1920x1080".to_string(),
261            actual: "1920x1080".to_string(),
262            passed: true,
263        };
264        assert!(check.passed);
265    }
266
267    #[test]
268    fn test_video_quality_report_serialization() {
269        let report = VideoQualityReport {
270            source: "test.mp4".to_string(),
271            verdict: VideoVerdict::Pass,
272            probe: sample_probe(),
273            checks: vec![VideoCheck {
274                name: "codec".to_string(),
275                expected: "h264".to_string(),
276                actual: "h264".to_string(),
277                passed: true,
278            }],
279            passed_count: 1,
280            total_count: 1,
281        };
282        let json = serde_json::to_string(&report).unwrap();
283        assert!(json.contains("\"verdict\":\"Pass\""));
284    }
285}