1#![allow(dead_code)]
2#[derive(Debug, Clone, PartialEq)]
6pub enum OutputConstraint {
7 MaxVideoBitrate(u64),
9 MinVideoBitrate(u64),
11 MaxAudioBitrate(u64),
13 ExactWidth(u32),
15 ExactHeight(u32),
17 MaxFileSizeBytes(u64),
19 DurationWithinTolerance {
21 expected: f64,
23 tolerance: f64,
25 },
26 HasAudio,
28 HasVideo,
30}
31
32impl OutputConstraint {
33 #[must_use]
35 pub fn constraint_name(&self) -> &'static str {
36 match self {
37 Self::MaxVideoBitrate(_) => "max_video_bitrate",
38 Self::MinVideoBitrate(_) => "min_video_bitrate",
39 Self::MaxAudioBitrate(_) => "max_audio_bitrate",
40 Self::ExactWidth(_) => "exact_width",
41 Self::ExactHeight(_) => "exact_height",
42 Self::MaxFileSizeBytes(_) => "max_file_size_bytes",
43 Self::DurationWithinTolerance { .. } => "duration_within_tolerance",
44 Self::HasAudio => "has_audio",
45 Self::HasVideo => "has_video",
46 }
47 }
48
49 #[must_use]
51 pub fn is_bitrate_constraint(&self) -> bool {
52 matches!(
53 self,
54 Self::MaxVideoBitrate(_) | Self::MinVideoBitrate(_) | Self::MaxAudioBitrate(_)
55 )
56 }
57
58 #[must_use]
60 pub fn is_stream_presence(&self) -> bool {
61 matches!(self, Self::HasAudio | Self::HasVideo)
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct OutputViolation {
68 pub constraint: OutputConstraint,
70 pub description: String,
72 pub blocking: bool,
74}
75
76impl OutputViolation {
77 #[must_use]
79 pub fn new(
80 constraint: OutputConstraint,
81 description: impl Into<String>,
82 blocking: bool,
83 ) -> Self {
84 Self {
85 constraint,
86 description: description.into(),
87 blocking,
88 }
89 }
90
91 #[must_use]
93 pub fn is_critical(&self) -> bool {
94 self.blocking
95 }
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct OutputFileInfo {
101 pub video_bitrate: u64,
103 pub audio_bitrate: u64,
105 pub width: u32,
107 pub height: u32,
109 pub file_size_bytes: u64,
111 pub duration_seconds: f64,
113 pub has_audio: bool,
115 pub has_video: bool,
117}
118
119#[derive(Debug, Default)]
121pub struct OutputVerifier {
122 constraints: Vec<OutputConstraint>,
123}
124
125impl OutputVerifier {
126 #[must_use]
128 pub fn new() -> Self {
129 Self::default()
130 }
131
132 pub fn add_constraint(&mut self, constraint: OutputConstraint) {
134 self.constraints.push(constraint);
135 }
136
137 #[must_use]
139 pub fn check_file(&self, info: &OutputFileInfo) -> OutputVerifyReport {
140 let mut violations = Vec::new();
141
142 for constraint in &self.constraints {
143 if let Some(v) = self.evaluate(constraint, info) {
144 violations.push(v);
145 }
146 }
147
148 OutputVerifyReport { violations }
149 }
150
151 fn evaluate(
152 &self,
153 constraint: &OutputConstraint,
154 info: &OutputFileInfo,
155 ) -> Option<OutputViolation> {
156 match constraint {
157 OutputConstraint::MaxVideoBitrate(max) => {
158 if info.video_bitrate > *max {
159 Some(OutputViolation::new(
160 constraint.clone(),
161 format!(
162 "video bitrate {} bps exceeds limit {} bps",
163 info.video_bitrate, max
164 ),
165 true,
166 ))
167 } else {
168 None
169 }
170 }
171 OutputConstraint::MinVideoBitrate(min) => {
172 if info.video_bitrate < *min {
173 Some(OutputViolation::new(
174 constraint.clone(),
175 format!(
176 "video bitrate {} bps below minimum {} bps",
177 info.video_bitrate, min
178 ),
179 false,
180 ))
181 } else {
182 None
183 }
184 }
185 OutputConstraint::MaxAudioBitrate(max) => {
186 if info.audio_bitrate > *max {
187 Some(OutputViolation::new(
188 constraint.clone(),
189 format!(
190 "audio bitrate {} bps exceeds limit {} bps",
191 info.audio_bitrate, max
192 ),
193 true,
194 ))
195 } else {
196 None
197 }
198 }
199 OutputConstraint::ExactWidth(w) => {
200 if info.width == *w {
201 None
202 } else {
203 Some(OutputViolation::new(
204 constraint.clone(),
205 format!("width {} != required {}", info.width, w),
206 true,
207 ))
208 }
209 }
210 OutputConstraint::ExactHeight(h) => {
211 if info.height == *h {
212 None
213 } else {
214 Some(OutputViolation::new(
215 constraint.clone(),
216 format!("height {} != required {}", info.height, h),
217 true,
218 ))
219 }
220 }
221 OutputConstraint::MaxFileSizeBytes(max) => {
222 if info.file_size_bytes > *max {
223 Some(OutputViolation::new(
224 constraint.clone(),
225 format!(
226 "file size {} bytes exceeds limit {} bytes",
227 info.file_size_bytes, max
228 ),
229 true,
230 ))
231 } else {
232 None
233 }
234 }
235 OutputConstraint::DurationWithinTolerance {
236 expected,
237 tolerance,
238 } => {
239 let diff = (info.duration_seconds - expected).abs();
240 if diff > *tolerance {
241 Some(OutputViolation::new(
242 constraint.clone(),
243 format!(
244 "duration {:.3}s differs from expected {:.3}s by {:.3}s (tolerance {:.3}s)",
245 info.duration_seconds, expected, diff, tolerance
246 ),
247 false,
248 ))
249 } else {
250 None
251 }
252 }
253 OutputConstraint::HasAudio => {
254 if info.has_audio {
255 None
256 } else {
257 Some(OutputViolation::new(
258 constraint.clone(),
259 "no audio track present".to_string(),
260 true,
261 ))
262 }
263 }
264 OutputConstraint::HasVideo => {
265 if info.has_video {
266 None
267 } else {
268 Some(OutputViolation::new(
269 constraint.clone(),
270 "no video track present".to_string(),
271 true,
272 ))
273 }
274 }
275 }
276 }
277}
278
279#[derive(Debug)]
281pub struct OutputVerifyReport {
282 violations: Vec<OutputViolation>,
283}
284
285impl OutputVerifyReport {
286 #[must_use]
288 pub fn violations(&self) -> &[OutputViolation] {
289 &self.violations
290 }
291
292 #[must_use]
294 pub fn blocking_violations(&self) -> Vec<&OutputViolation> {
295 self.violations.iter().filter(|v| v.is_critical()).collect()
296 }
297
298 #[must_use]
300 pub fn is_ok(&self) -> bool {
301 self.violations.is_empty()
302 }
303
304 #[must_use]
306 pub fn is_deliverable(&self) -> bool {
307 self.blocking_violations().is_empty()
308 }
309
310 #[must_use]
312 pub fn violation_count(&self) -> usize {
313 self.violations.len()
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 fn base_info() -> OutputFileInfo {
322 OutputFileInfo {
323 video_bitrate: 5_000_000,
324 audio_bitrate: 128_000,
325 width: 1920,
326 height: 1080,
327 file_size_bytes: 100_000_000,
328 duration_seconds: 60.0,
329 has_audio: true,
330 has_video: true,
331 }
332 }
333
334 #[test]
335 fn test_constraint_name_max_video_bitrate() {
336 assert_eq!(
337 OutputConstraint::MaxVideoBitrate(5_000_000).constraint_name(),
338 "max_video_bitrate"
339 );
340 }
341
342 #[test]
343 fn test_constraint_is_bitrate_constraint() {
344 assert!(OutputConstraint::MaxVideoBitrate(0).is_bitrate_constraint());
345 assert!(OutputConstraint::MinVideoBitrate(0).is_bitrate_constraint());
346 assert!(!OutputConstraint::HasAudio.is_bitrate_constraint());
347 }
348
349 #[test]
350 fn test_constraint_is_stream_presence() {
351 assert!(OutputConstraint::HasAudio.is_stream_presence());
352 assert!(OutputConstraint::HasVideo.is_stream_presence());
353 assert!(!OutputConstraint::ExactWidth(1920).is_stream_presence());
354 }
355
356 #[test]
357 fn test_violation_is_critical() {
358 let v = OutputViolation::new(OutputConstraint::HasAudio, "no audio", true);
359 assert!(v.is_critical());
360 let v2 = OutputViolation::new(OutputConstraint::MinVideoBitrate(0), "low", false);
361 assert!(!v2.is_critical());
362 }
363
364 #[test]
365 fn test_verifier_no_violations_on_pass() {
366 let mut v = OutputVerifier::new();
367 v.add_constraint(OutputConstraint::MaxVideoBitrate(10_000_000));
368 v.add_constraint(OutputConstraint::ExactWidth(1920));
369 let report = v.check_file(&base_info());
370 assert!(report.is_ok());
371 }
372
373 #[test]
374 fn test_verifier_max_video_bitrate_violation() {
375 let mut v = OutputVerifier::new();
376 v.add_constraint(OutputConstraint::MaxVideoBitrate(4_000_000));
377 let report = v.check_file(&base_info());
378 assert!(!report.is_ok());
379 assert_eq!(report.violation_count(), 1);
380 }
381
382 #[test]
383 fn test_verifier_exact_width_violation() {
384 let mut v = OutputVerifier::new();
385 v.add_constraint(OutputConstraint::ExactWidth(3840));
386 let report = v.check_file(&base_info());
387 assert!(!report.is_deliverable());
388 }
389
390 #[test]
391 fn test_verifier_has_audio_missing() {
392 let mut info = base_info();
393 info.has_audio = false;
394 let mut v = OutputVerifier::new();
395 v.add_constraint(OutputConstraint::HasAudio);
396 let report = v.check_file(&info);
397 assert!(!report.is_ok());
398 assert_eq!(report.blocking_violations().len(), 1);
399 }
400
401 #[test]
402 fn test_verifier_has_video_ok() {
403 let mut v = OutputVerifier::new();
404 v.add_constraint(OutputConstraint::HasVideo);
405 let report = v.check_file(&base_info());
406 assert!(report.is_ok());
407 }
408
409 #[test]
410 fn test_verifier_file_size_violation() {
411 let mut v = OutputVerifier::new();
412 v.add_constraint(OutputConstraint::MaxFileSizeBytes(50_000_000));
413 let report = v.check_file(&base_info());
414 assert!(!report.is_ok());
415 }
416
417 #[test]
418 fn test_verifier_duration_within_tolerance_ok() {
419 let mut v = OutputVerifier::new();
420 v.add_constraint(OutputConstraint::DurationWithinTolerance {
421 expected: 60.0,
422 tolerance: 1.0,
423 });
424 let report = v.check_file(&base_info());
425 assert!(report.is_ok());
426 }
427
428 #[test]
429 fn test_verifier_duration_outside_tolerance() {
430 let mut info = base_info();
431 info.duration_seconds = 58.0;
432 let mut v = OutputVerifier::new();
433 v.add_constraint(OutputConstraint::DurationWithinTolerance {
434 expected: 60.0,
435 tolerance: 0.5,
436 });
437 let report = v.check_file(&info);
438 assert!(!report.is_ok());
439 assert!(report.is_deliverable());
441 }
442
443 #[test]
444 fn test_verifier_min_video_bitrate_advisory() {
445 let mut v = OutputVerifier::new();
446 v.add_constraint(OutputConstraint::MinVideoBitrate(8_000_000));
447 let report = v.check_file(&base_info());
448 assert!(!report.is_ok());
449 assert!(report.is_deliverable()); }
451
452 #[test]
453 fn test_report_blocking_vs_total() {
454 let mut v = OutputVerifier::new();
455 v.add_constraint(OutputConstraint::MaxVideoBitrate(4_000_000)); v.add_constraint(OutputConstraint::MinVideoBitrate(8_000_000)); let report = v.check_file(&base_info());
458 assert_eq!(report.violation_count(), 2);
459 assert_eq!(report.blocking_violations().len(), 1);
460 }
461}