1#![allow(dead_code)]
2
3use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct Resolution {
15 pub width: u32,
17 pub height: u32,
19}
20
21impl Resolution {
22 pub const fn new(width: u32, height: u32) -> Self {
24 Self { width, height }
25 }
26
27 pub const fn pixel_count(&self) -> u64 {
29 self.width as u64 * self.height as u64
30 }
31
32 #[allow(clippy::cast_precision_loss)]
34 pub fn ratio_to(&self, other: &Resolution) -> f64 {
35 if other.pixel_count() == 0 {
36 return 0.0;
37 }
38 self.pixel_count() as f64 / other.pixel_count() as f64
39 }
40}
41
42impl std::fmt::Display for Resolution {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "{}x{}", self.width, self.height)
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct MediaInfo {
51 pub path: String,
53 pub resolution: Resolution,
55 pub bitrate_bps: u64,
57 pub frame_rate: f64,
59 pub duration_ms: u64,
61 pub codec: String,
63 pub file_size_bytes: u64,
65}
66
67impl MediaInfo {
68 pub fn new(path: &str) -> Self {
70 Self {
71 path: path.to_string(),
72 resolution: Resolution::new(0, 0),
73 bitrate_bps: 0,
74 frame_rate: 0.0,
75 duration_ms: 0,
76 codec: String::new(),
77 file_size_bytes: 0,
78 }
79 }
80
81 pub fn with_resolution(mut self, w: u32, h: u32) -> Self {
83 self.resolution = Resolution::new(w, h);
84 self
85 }
86
87 pub fn with_bitrate(mut self, bps: u64) -> Self {
89 self.bitrate_bps = bps;
90 self
91 }
92
93 pub fn with_frame_rate(mut self, fps: f64) -> Self {
95 self.frame_rate = fps;
96 self
97 }
98
99 pub fn with_duration_ms(mut self, ms: u64) -> Self {
101 self.duration_ms = ms;
102 self
103 }
104
105 pub fn with_codec(mut self, codec: &str) -> Self {
107 self.codec = codec.to_string();
108 self
109 }
110
111 pub fn with_file_size(mut self, bytes: u64) -> Self {
113 self.file_size_bytes = bytes;
114 self
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct ComparisonResult {
121 pub proxy: MediaInfo,
123 pub original: MediaInfo,
125 pub resolution_ratio: f64,
127 pub bitrate_ratio: f64,
129 pub frame_rate_match: bool,
131 pub duration_match: bool,
133 pub size_ratio: f64,
135 pub duration_diff_ms: i64,
137}
138
139#[derive(Debug, Clone)]
141pub struct ComparisonTolerance {
142 pub frame_rate_tolerance: f64,
144 pub duration_tolerance_ms: u64,
146 pub max_resolution_ratio: f64,
148 pub min_resolution_ratio: f64,
150}
151
152impl Default for ComparisonTolerance {
153 fn default() -> Self {
154 Self {
155 frame_rate_tolerance: 0.01,
156 duration_tolerance_ms: 100,
157 max_resolution_ratio: 1.0,
158 min_resolution_ratio: 0.01,
159 }
160 }
161}
162
163#[derive(Debug)]
165pub struct ProxyCompareEngine {
166 tolerance: ComparisonTolerance,
168}
169
170impl ProxyCompareEngine {
171 pub fn new() -> Self {
173 Self {
174 tolerance: ComparisonTolerance::default(),
175 }
176 }
177
178 pub fn with_tolerance(tolerance: ComparisonTolerance) -> Self {
180 Self { tolerance }
181 }
182
183 #[allow(clippy::cast_precision_loss)]
185 pub fn compare(&self, proxy: &MediaInfo, original: &MediaInfo) -> ComparisonResult {
186 let resolution_ratio = proxy.resolution.ratio_to(&original.resolution);
187
188 let bitrate_ratio = if original.bitrate_bps > 0 {
189 proxy.bitrate_bps as f64 / original.bitrate_bps as f64
190 } else {
191 0.0
192 };
193
194 let frame_rate_match =
195 (proxy.frame_rate - original.frame_rate).abs() <= self.tolerance.frame_rate_tolerance;
196
197 let duration_diff_ms = proxy.duration_ms as i64 - original.duration_ms as i64;
198 let duration_match =
199 (duration_diff_ms.unsigned_abs()) <= self.tolerance.duration_tolerance_ms;
200
201 let size_ratio = if original.file_size_bytes > 0 {
202 proxy.file_size_bytes as f64 / original.file_size_bytes as f64
203 } else {
204 0.0
205 };
206
207 ComparisonResult {
208 proxy: proxy.clone(),
209 original: original.clone(),
210 resolution_ratio,
211 bitrate_ratio,
212 frame_rate_match,
213 duration_match,
214 size_ratio,
215 duration_diff_ms,
216 }
217 }
218
219 pub fn passes_qc(&self, result: &ComparisonResult) -> bool {
221 result.frame_rate_match
222 && result.duration_match
223 && result.resolution_ratio >= self.tolerance.min_resolution_ratio
224 && result.resolution_ratio <= self.tolerance.max_resolution_ratio
225 }
226
227 pub fn compare_batch(&self, pairs: &[(MediaInfo, MediaInfo)]) -> Vec<ComparisonResult> {
229 pairs
230 .iter()
231 .map(|(proxy, original)| self.compare(proxy, original))
232 .collect()
233 }
234
235 #[allow(clippy::cast_precision_loss)]
237 pub fn aggregate_stats(results: &[ComparisonResult]) -> ComparisonStats {
238 if results.is_empty() {
239 return ComparisonStats::default();
240 }
241 let total = results.len();
242 let frame_rate_matches = results.iter().filter(|r| r.frame_rate_match).count();
243 let duration_matches = results.iter().filter(|r| r.duration_match).count();
244 let avg_resolution_ratio: f64 =
245 results.iter().map(|r| r.resolution_ratio).sum::<f64>() / total as f64;
246 let avg_bitrate_ratio: f64 =
247 results.iter().map(|r| r.bitrate_ratio).sum::<f64>() / total as f64;
248 let avg_size_ratio: f64 = results.iter().map(|r| r.size_ratio).sum::<f64>() / total as f64;
249
250 ComparisonStats {
251 total,
252 frame_rate_matches,
253 duration_matches,
254 avg_resolution_ratio,
255 avg_bitrate_ratio,
256 avg_size_ratio,
257 }
258 }
259
260 pub fn group_by_codec(results: &[ComparisonResult]) -> HashMap<String, Vec<usize>> {
262 let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
263 for (i, r) in results.iter().enumerate() {
264 groups.entry(r.proxy.codec.clone()).or_default().push(i);
265 }
266 groups
267 }
268}
269
270impl Default for ProxyCompareEngine {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276#[derive(Debug, Clone, Default)]
278pub struct ComparisonStats {
279 pub total: usize,
281 pub frame_rate_matches: usize,
283 pub duration_matches: usize,
285 pub avg_resolution_ratio: f64,
287 pub avg_bitrate_ratio: f64,
289 pub avg_size_ratio: f64,
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn make_original() -> MediaInfo {
298 MediaInfo::new("/src/clip.mxf")
299 .with_resolution(3840, 2160)
300 .with_bitrate(100_000_000)
301 .with_frame_rate(23.976)
302 .with_duration_ms(60_000)
303 .with_codec("ProRes")
304 .with_file_size(750_000_000)
305 }
306
307 fn make_proxy() -> MediaInfo {
308 MediaInfo::new("/proxy/clip.mp4")
309 .with_resolution(1920, 1080)
310 .with_bitrate(5_000_000)
311 .with_frame_rate(23.976)
312 .with_duration_ms(60_000)
313 .with_codec("H264")
314 .with_file_size(37_500_000)
315 }
316
317 #[test]
318 fn test_resolution_pixel_count() {
319 let r = Resolution::new(1920, 1080);
320 assert_eq!(r.pixel_count(), 2_073_600);
321 }
322
323 #[test]
324 fn test_resolution_ratio() {
325 let proxy = Resolution::new(1920, 1080);
326 let original = Resolution::new(3840, 2160);
327 let ratio = proxy.ratio_to(&original);
328 assert!((ratio - 0.25).abs() < 0.001);
329 }
330
331 #[test]
332 fn test_resolution_ratio_zero() {
333 let a = Resolution::new(1920, 1080);
334 let zero = Resolution::new(0, 0);
335 assert!((a.ratio_to(&zero) - 0.0).abs() < f64::EPSILON);
336 }
337
338 #[test]
339 fn test_resolution_display() {
340 let r = Resolution::new(1920, 1080);
341 assert_eq!(format!("{r}"), "1920x1080");
342 }
343
344 #[test]
345 fn test_compare_frame_rate_match() {
346 let engine = ProxyCompareEngine::new();
347 let result = engine.compare(&make_proxy(), &make_original());
348 assert!(result.frame_rate_match);
349 }
350
351 #[test]
352 fn test_compare_duration_match() {
353 let engine = ProxyCompareEngine::new();
354 let result = engine.compare(&make_proxy(), &make_original());
355 assert!(result.duration_match);
356 assert_eq!(result.duration_diff_ms, 0);
357 }
358
359 #[test]
360 fn test_compare_resolution_ratio() {
361 let engine = ProxyCompareEngine::new();
362 let result = engine.compare(&make_proxy(), &make_original());
363 assert!((result.resolution_ratio - 0.25).abs() < 0.001);
364 }
365
366 #[test]
367 fn test_compare_bitrate_ratio() {
368 let engine = ProxyCompareEngine::new();
369 let result = engine.compare(&make_proxy(), &make_original());
370 assert!((result.bitrate_ratio - 0.05).abs() < 0.001);
371 }
372
373 #[test]
374 fn test_compare_size_ratio() {
375 let engine = ProxyCompareEngine::new();
376 let result = engine.compare(&make_proxy(), &make_original());
377 assert!((result.size_ratio - 0.05).abs() < 0.001);
378 }
379
380 #[test]
381 fn test_passes_qc_default() {
382 let engine = ProxyCompareEngine::new();
383 let result = engine.compare(&make_proxy(), &make_original());
384 assert!(engine.passes_qc(&result));
385 }
386
387 #[test]
388 fn test_fails_qc_frame_rate_mismatch() {
389 let engine = ProxyCompareEngine::new();
390 let proxy = make_proxy().with_frame_rate(30.0);
391 let result = engine.compare(&proxy, &make_original());
392 assert!(!result.frame_rate_match);
393 assert!(!engine.passes_qc(&result));
394 }
395
396 #[test]
397 fn test_fails_qc_duration_mismatch() {
398 let engine = ProxyCompareEngine::new();
399 let proxy = make_proxy().with_duration_ms(65_000);
400 let result = engine.compare(&proxy, &make_original());
401 assert!(!result.duration_match);
402 }
403
404 #[test]
405 fn test_compare_batch() {
406 let engine = ProxyCompareEngine::new();
407 let pairs = vec![
408 (make_proxy(), make_original()),
409 (make_proxy(), make_original()),
410 ];
411 let results = engine.compare_batch(&pairs);
412 assert_eq!(results.len(), 2);
413 }
414
415 #[test]
416 fn test_aggregate_stats() {
417 let engine = ProxyCompareEngine::new();
418 let results = vec![
419 engine.compare(&make_proxy(), &make_original()),
420 engine.compare(&make_proxy(), &make_original()),
421 ];
422 let stats = ProxyCompareEngine::aggregate_stats(&results);
423 assert_eq!(stats.total, 2);
424 assert_eq!(stats.frame_rate_matches, 2);
425 assert_eq!(stats.duration_matches, 2);
426 }
427
428 #[test]
429 fn test_aggregate_stats_empty() {
430 let stats = ProxyCompareEngine::aggregate_stats(&[]);
431 assert_eq!(stats.total, 0);
432 }
433
434 #[test]
435 fn test_group_by_codec() {
436 let engine = ProxyCompareEngine::new();
437 let results = vec![
438 engine.compare(&make_proxy(), &make_original()),
439 engine.compare(&make_proxy().with_codec("VP9"), &make_original()),
440 ];
441 let groups = ProxyCompareEngine::group_by_codec(&results);
442 assert!(groups.contains_key("H264"));
443 assert!(groups.contains_key("VP9"));
444 }
445
446 #[test]
447 fn test_custom_tolerance() {
448 let tolerance = ComparisonTolerance {
449 frame_rate_tolerance: 1.0,
450 duration_tolerance_ms: 5000,
451 max_resolution_ratio: 1.0,
452 min_resolution_ratio: 0.001,
453 };
454 let engine = ProxyCompareEngine::with_tolerance(tolerance);
455 let proxy = make_proxy().with_frame_rate(24.0);
456 let result = engine.compare(&proxy, &make_original());
457 assert!(result.frame_rate_match);
458 }
459
460 #[test]
461 fn test_default_engine() {
462 let engine = ProxyCompareEngine::default();
463 assert!((engine.tolerance.frame_rate_tolerance - 0.01).abs() < f64::EPSILON);
464 }
465}