1#![allow(dead_code)]
8
9use crate::transcode_queue::ProxySpec;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum EditingSoftware {
15 Resolve,
17 Premiere,
19 Avid,
21 FinalCut,
23 Vegas,
25 Kdenlive,
27}
28
29impl EditingSoftware {
30 #[must_use]
32 pub fn preferred_proxy_codec(self) -> &'static str {
33 match self {
34 Self::Resolve => "prores_proxy",
35 Self::Premiere => "h264",
36 Self::Avid => "dnxhd",
37 Self::FinalCut => "prores_proxy",
38 Self::Vegas => "h264",
39 Self::Kdenlive => "h264",
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct EditingContext {
47 pub software: EditingSoftware,
49 pub resolution: (u32, u32),
51 pub codec_support: Vec<String>,
53 pub network_speed_mbps: f32,
55}
56
57impl EditingContext {
58 #[must_use]
60 pub fn new(
61 software: EditingSoftware,
62 resolution: (u32, u32),
63 codec_support: Vec<String>,
64 network_speed_mbps: f32,
65 ) -> Self {
66 Self {
67 software,
68 resolution,
69 codec_support,
70 network_speed_mbps,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SourceSpec {
78 pub path: String,
80 pub resolution: (u32, u32),
82 pub codec: String,
84 pub bitrate_kbps: u32,
86 pub fps: f32,
88}
89
90impl SourceSpec {
91 #[must_use]
93 pub fn new(
94 path: impl Into<String>,
95 resolution: (u32, u32),
96 codec: impl Into<String>,
97 bitrate_kbps: u32,
98 fps: f32,
99 ) -> Self {
100 Self {
101 path: path.into(),
102 resolution,
103 codec: codec.into(),
104 bitrate_kbps,
105 fps,
106 }
107 }
108}
109
110pub struct ProxyRecommender;
112
113impl ProxyRecommender {
114 #[must_use]
121 pub fn recommend(context: &EditingContext, source_specs: &[SourceSpec]) -> Vec<ProxySpec> {
122 let preferred_codec = context.software.preferred_proxy_codec().to_string();
123
124 let (ctx_w, ctx_h) = context.resolution;
126
127 source_specs
128 .iter()
129 .map(|src| {
130 let (src_w, src_h) = src.resolution;
131 let target_w = ctx_w.min(src_w);
133 let target_h = ctx_h.min(src_h);
134
135 let area_ratio =
138 (target_w * target_h) as f32 / (src_w.max(1) * src_h.max(1)) as f32;
139 let base_bitrate_kbps = (src.bitrate_kbps as f32 * area_ratio) as u32;
140
141 let max_net_kbps = (context.network_speed_mbps * 1000.0 * 0.5) as u32;
144 let bitrate_kbps = if max_net_kbps > 0 {
145 base_bitrate_kbps.min(max_net_kbps)
146 } else {
147 base_bitrate_kbps
148 };
149
150 let codec = if context
152 .codec_support
153 .iter()
154 .any(|c| c == preferred_codec.as_str())
155 || context.codec_support.is_empty()
156 {
157 preferred_codec.clone()
158 } else {
159 context
161 .codec_support
162 .first()
163 .cloned()
164 .unwrap_or_else(|| "h264".to_string())
165 };
166
167 ProxySpec::new((target_w, target_h), codec, bitrate_kbps.max(500))
168 })
169 .collect()
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct CompatibilityResult {
176 pub compatible: bool,
178 pub warnings: Vec<String>,
180 pub score: f32,
182}
183
184impl CompatibilityResult {
185 #[must_use]
187 pub fn ok() -> Self {
188 Self {
189 compatible: true,
190 warnings: vec![],
191 score: 1.0,
192 }
193 }
194
195 #[must_use]
197 pub fn incompatible(reason: impl Into<String>) -> Self {
198 Self {
199 compatible: false,
200 warnings: vec![reason.into()],
201 score: 0.0,
202 }
203 }
204}
205
206pub struct ProxyCompatibilityChecker;
208
209impl ProxyCompatibilityChecker {
210 #[must_use]
212 pub fn check(proxy: &ProxySpec, context: &EditingContext) -> CompatibilityResult {
213 let mut warnings = Vec::new();
214 let mut score = 1.0f32;
215
216 let preferred = context.software.preferred_proxy_codec();
218 if proxy.codec != preferred {
219 warnings.push(format!(
220 "Proxy codec '{}' is not the preferred codec '{}' for {:?}",
221 proxy.codec, preferred, context.software
222 ));
223 score -= 0.2;
224 }
225
226 let (p_w, p_h) = proxy.resolution;
228 let (c_w, c_h) = context.resolution;
229 if p_w > c_w || p_h > c_h {
230 warnings.push(format!(
231 "Proxy resolution {}×{} exceeds editing resolution {}×{}",
232 p_w, p_h, c_w, c_h
233 ));
234 score -= 0.2;
235 }
236
237 let max_net_kbps = (context.network_speed_mbps * 1000.0 * 0.5) as u32;
239 if max_net_kbps > 0 && proxy.bitrate_kbps > max_net_kbps {
240 warnings.push(format!(
241 "Proxy bitrate {} kbps exceeds safe network limit {} kbps",
242 proxy.bitrate_kbps, max_net_kbps
243 ));
244 score -= 0.3;
245 }
246
247 if !context.codec_support.is_empty()
249 && !context
250 .codec_support
251 .iter()
252 .any(|c| c == proxy.codec.as_str())
253 {
254 warnings.push(format!(
255 "Proxy codec '{}' is not in the supported codec list",
256 proxy.codec
257 ));
258 score -= 0.3;
259 }
260
261 CompatibilityResult {
262 compatible: score > 0.4,
263 warnings,
264 score: score.clamp(0.0, 1.0),
265 }
266 }
267}
268
269pub struct ProxyStorageEstimator;
271
272impl ProxyStorageEstimator {
273 #[must_use]
280 pub fn estimate_gb(source_count: u32, avg_duration_mins: f32, spec: &ProxySpec) -> f64 {
281 if source_count == 0 || avg_duration_mins <= 0.0 {
282 return 0.0;
283 }
284 let bits_per_file = spec.bitrate_kbps as f64 * 1_000.0 * 60.0 * avg_duration_mins as f64;
288 let bytes_per_file = bits_per_file / 8.0;
289 let total_bytes = bytes_per_file * source_count as f64;
290 total_bytes / 1_000_000_000.0
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn make_context(software: EditingSoftware, res: (u32, u32), net: f32) -> EditingContext {
299 EditingContext::new(software, res, vec![], net)
300 }
301
302 #[test]
303 fn test_editing_software_preferred_codec() {
304 assert_eq!(
305 EditingSoftware::Resolve.preferred_proxy_codec(),
306 "prores_proxy"
307 );
308 assert_eq!(EditingSoftware::Premiere.preferred_proxy_codec(), "h264");
309 assert_eq!(EditingSoftware::Avid.preferred_proxy_codec(), "dnxhd");
310 assert_eq!(
311 EditingSoftware::FinalCut.preferred_proxy_codec(),
312 "prores_proxy"
313 );
314 assert_eq!(EditingSoftware::Vegas.preferred_proxy_codec(), "h264");
315 assert_eq!(EditingSoftware::Kdenlive.preferred_proxy_codec(), "h264");
316 }
317
318 #[test]
319 fn test_recommender_codec_matches_software() {
320 let ctx = make_context(EditingSoftware::Avid, (1920, 1080), 1000.0);
321 let src = vec![SourceSpec::new(
322 "/a.mov",
323 (3840, 2160),
324 "h264",
325 100_000,
326 25.0,
327 )];
328 let recs = ProxyRecommender::recommend(&ctx, &src);
329 assert_eq!(recs.len(), 1);
330 assert_eq!(recs[0].codec, "dnxhd");
331 }
332
333 #[test]
334 fn test_recommender_does_not_upscale() {
335 let ctx = make_context(EditingSoftware::Premiere, (3840, 2160), 1000.0);
336 let src = vec![SourceSpec::new(
337 "/b.mov",
338 (1920, 1080),
339 "h264",
340 10_000,
341 25.0,
342 )];
343 let recs = ProxyRecommender::recommend(&ctx, &src);
344 assert_eq!(recs[0].resolution, (1920, 1080));
345 }
346
347 #[test]
348 fn test_recommender_network_cap() {
349 let ctx = make_context(EditingSoftware::Premiere, (1920, 1080), 1.0);
351 let src = vec![SourceSpec::new(
352 "/c.mov",
353 (1920, 1080),
354 "h264",
355 100_000,
356 25.0,
357 )];
358 let recs = ProxyRecommender::recommend(&ctx, &src);
359 assert!(recs[0].bitrate_kbps <= 500);
360 }
361
362 #[test]
363 fn test_recommender_empty_sources() {
364 let ctx = make_context(EditingSoftware::Resolve, (1920, 1080), 100.0);
365 let recs = ProxyRecommender::recommend(&ctx, &[]);
366 assert!(recs.is_empty());
367 }
368
369 #[test]
370 fn test_compatibility_check_perfect() {
371 let proxy = ProxySpec::new((1920, 1080), "prores_proxy", 10_000);
372 let ctx = EditingContext::new(
373 EditingSoftware::Resolve,
374 (1920, 1080),
375 vec!["prores_proxy".to_string()],
376 1000.0,
377 );
378 let result = ProxyCompatibilityChecker::check(&proxy, &ctx);
379 assert!(result.compatible);
380 assert!(result.warnings.is_empty());
381 assert!((result.score - 1.0).abs() < f32::EPSILON);
382 }
383
384 #[test]
385 fn test_compatibility_check_wrong_codec() {
386 let proxy = ProxySpec::new((1920, 1080), "h264", 10_000);
387 let ctx = EditingContext::new(
388 EditingSoftware::Avid,
389 (1920, 1080),
390 vec!["dnxhd".to_string()],
391 1000.0,
392 );
393 let result = ProxyCompatibilityChecker::check(&proxy, &ctx);
394 assert!(!result.warnings.is_empty());
395 assert!(result.score < 1.0);
396 }
397
398 #[test]
399 fn test_compatibility_check_resolution_too_large() {
400 let proxy = ProxySpec::new((3840, 2160), "h264", 5_000);
401 let ctx = make_context(EditingSoftware::Premiere, (1920, 1080), 1000.0);
402 let result = ProxyCompatibilityChecker::check(&proxy, &ctx);
403 assert!(!result.warnings.is_empty());
404 assert!(result.score < 1.0);
405 }
406
407 #[test]
408 fn test_compatibility_check_bitrate_too_high() {
409 let proxy = ProxySpec::new((1280, 720), "h264", 50_000);
411 let ctx = make_context(EditingSoftware::Premiere, (1920, 1080), 1.0);
412 let result = ProxyCompatibilityChecker::check(&proxy, &ctx);
413 assert!(!result.warnings.is_empty());
414 }
415
416 #[test]
417 fn test_storage_estimator_basic() {
418 let spec = ProxySpec::new((1920, 1080), "h264", 8_000);
419 let gb = ProxyStorageEstimator::estimate_gb(100, 5.0, &spec);
420 assert!((gb - 30.0).abs() < 0.01);
422 }
423
424 #[test]
425 fn test_storage_estimator_zero_files() {
426 let spec = ProxySpec::new((1920, 1080), "h264", 8_000);
427 let gb = ProxyStorageEstimator::estimate_gb(0, 5.0, &spec);
428 assert!((gb - 0.0).abs() < f64::EPSILON);
429 }
430
431 #[test]
432 fn test_storage_estimator_zero_duration() {
433 let spec = ProxySpec::new((1920, 1080), "h264", 8_000);
434 let gb = ProxyStorageEstimator::estimate_gb(10, 0.0, &spec);
435 assert!((gb - 0.0).abs() < f64::EPSILON);
436 }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct ResolutionVariant {
446 pub label: String,
448 pub scale: f32,
450 pub resolution: (u32, u32),
452 pub codec: String,
454 pub bitrate_kbps: u32,
456}
457
458impl ResolutionVariant {
459 #[must_use]
464 pub fn from_source(
465 label: impl Into<String>,
466 source: (u32, u32),
467 scale: f32,
468 codec: impl Into<String>,
469 base_bitrate_kbps: u32,
470 ) -> Self {
471 let (sw, sh) = source;
472 let w = ((sw as f32 * scale) as u32) & !1; let h = ((sh as f32 * scale) as u32) & !1;
474 let actual_scale = (w * h) as f32 / ((sw * sh).max(1) as f32);
475 let bitrate = (base_bitrate_kbps as f32 * actual_scale) as u32;
476 Self {
477 label: label.into(),
478 scale,
479 resolution: (w.max(2), h.max(2)),
480 codec: codec.into(),
481 bitrate_kbps: bitrate.max(200),
482 }
483 }
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct MultiResolutionProxy {
489 pub source_path: String,
491 pub quarter: ResolutionVariant,
493 pub half: ResolutionVariant,
495 pub full: ResolutionVariant,
497}
498
499impl MultiResolutionProxy {
500 #[must_use]
506 pub fn from_source(
507 source_path: impl Into<String>,
508 source_resolution: (u32, u32),
509 codec: impl Into<String>,
510 base_bitrate_kbps: u32,
511 ) -> Self {
512 let codec = codec.into();
513 let path = source_path.into();
514 Self {
515 source_path: path,
516 quarter: ResolutionVariant::from_source(
517 "quarter",
518 source_resolution,
519 0.5, &codec,
521 base_bitrate_kbps,
522 ),
523 half: ResolutionVariant::from_source(
524 "half",
525 source_resolution,
526 0.707, &codec,
528 base_bitrate_kbps,
529 ),
530 full: ResolutionVariant::from_source(
531 "full",
532 source_resolution,
533 1.0,
534 &codec,
535 base_bitrate_kbps,
536 ),
537 }
538 }
539}
540
541pub struct DisplayAwareSelector;
547
548impl DisplayAwareSelector {
549 #[must_use]
556 pub fn select<'a>(
557 proxy: &'a MultiResolutionProxy,
558 display: (u32, u32),
559 ) -> &'a ResolutionVariant {
560 let display_area = display.0 as u64 * display.1 as u64;
561 let (qw, qh) = proxy.quarter.resolution;
562 let quarter_area = qw as u64 * qh as u64;
563 let (hw, hh) = proxy.half.resolution;
564 let half_area = hw as u64 * hh as u64;
565
566 if display_area <= quarter_area {
567 &proxy.quarter
568 } else if display_area <= half_area {
569 &proxy.half
570 } else {
571 &proxy.full
572 }
573 }
574
575 #[must_use]
577 pub fn select_label(proxy: &MultiResolutionProxy, display: (u32, u32)) -> &str {
578 Self::select(proxy, display).label.as_str()
579 }
580}
581
582#[cfg(test)]
583mod multi_res_tests {
584 use super::*;
585
586 fn make_proxy() -> MultiResolutionProxy {
587 MultiResolutionProxy::from_source("/src/4k.mov", (3840, 2160), "h264", 50_000)
588 }
589
590 #[test]
591 fn test_multi_res_variants_created() {
592 let p = make_proxy();
593 assert_eq!(p.quarter.resolution, (1920, 1080));
595 let (hw, hh) = p.half.resolution;
597 assert!(hw > 1920 && hw < 3840);
598 assert!(hh > 1080 && hh < 2160);
599 assert_eq!(p.full.resolution, (3840, 2160));
601 }
602
603 #[test]
604 fn test_variant_label() {
605 let p = make_proxy();
606 assert_eq!(p.quarter.label, "quarter");
607 assert_eq!(p.half.label, "half");
608 assert_eq!(p.full.label, "full");
609 }
610
611 #[test]
612 fn test_variant_scale() {
613 let p = make_proxy();
614 assert!((p.quarter.scale - 0.5).abs() < 1e-3);
615 assert!((p.full.scale - 1.0).abs() < 1e-3);
616 }
617
618 #[test]
619 fn test_display_aware_selects_quarter_for_small_display() {
620 let p = make_proxy();
621 let label = DisplayAwareSelector::select_label(&p, (960, 540));
623 assert_eq!(label, "quarter");
624 }
625
626 #[test]
627 fn test_display_aware_selects_half_for_medium_display() {
628 let p = make_proxy();
629 let label = DisplayAwareSelector::select_label(&p, (2000, 1200));
632 assert_eq!(label, "half");
633 }
634
635 #[test]
636 fn test_display_aware_selects_full_for_large_display() {
637 let p = make_proxy();
638 let label = DisplayAwareSelector::select_label(&p, (3840, 2160));
640 assert_eq!(label, "full");
641 }
642
643 #[test]
644 fn test_display_aware_exact_quarter_area() {
645 let p = make_proxy();
646 let (qw, qh) = p.quarter.resolution;
647 let label = DisplayAwareSelector::select_label(&p, (qw, qh));
649 assert_eq!(label, "quarter");
650 }
651
652 #[test]
653 fn test_bitrate_scales_with_area() {
654 let p = make_proxy();
655 assert!(p.quarter.bitrate_kbps < p.half.bitrate_kbps);
657 assert!(p.half.bitrate_kbps < p.full.bitrate_kbps);
658 }
659
660 #[test]
661 fn test_select_returns_reference() {
662 let p = make_proxy();
663 let variant = DisplayAwareSelector::select(&p, (640, 360));
664 assert!(!variant.label.is_empty());
665 }
666
667 #[test]
668 fn test_multi_res_codec_propagated() {
669 let p = MultiResolutionProxy::from_source("/a.mov", (1920, 1080), "prores_proxy", 20_000);
670 assert_eq!(p.quarter.codec, "prores_proxy");
671 assert_eq!(p.half.codec, "prores_proxy");
672 assert_eq!(p.full.codec, "prores_proxy");
673 }
674
675 #[test]
676 fn test_resolution_variant_from_source_even_dimensions() {
677 let v = ResolutionVariant::from_source("half", (1001, 999), 0.5, "h264", 10_000);
679 assert_eq!(v.resolution.0 % 2, 0);
680 assert_eq!(v.resolution.1 % 2, 0);
681 }
682}