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}