Skip to main content

oximedia_proxy/
smart_proxy.rs

1//! Intelligent proxy selection and recommendation.
2//!
3//! This module analyses the editing context (NLE software, resolution, network
4//! conditions) and recommends the most appropriate proxy configuration for a
5//! given set of source files.
6
7#![allow(dead_code)]
8
9use crate::transcode_queue::ProxySpec;
10use serde::{Deserialize, Serialize};
11
12/// The NLE (Non-Linear Editor) software being used.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum EditingSoftware {
15    /// DaVinci Resolve (Blackmagic Design).
16    Resolve,
17    /// Adobe Premiere Pro.
18    Premiere,
19    /// Avid Media Composer.
20    Avid,
21    /// Apple Final Cut Pro.
22    FinalCut,
23    /// VEGAS Pro (Magix).
24    Vegas,
25    /// Kdenlive (KDE).
26    Kdenlive,
27}
28
29impl EditingSoftware {
30    /// The proxy codec preferred by this software.
31    #[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/// Information about an editing session's context.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct EditingContext {
47    /// NLE software in use.
48    pub software: EditingSoftware,
49    /// Project/sequence resolution (width, height).
50    pub resolution: (u32, u32),
51    /// Codecs the editing system can decode in real-time.
52    pub codec_support: Vec<String>,
53    /// Available network bandwidth in Mbit/s (for shared storage workflows).
54    pub network_speed_mbps: f32,
55}
56
57impl EditingContext {
58    /// Create a new editing context.
59    #[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/// Source file specification used when choosing a proxy.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SourceSpec {
78    /// File path.
79    pub path: String,
80    /// Source resolution (width, height).
81    pub resolution: (u32, u32),
82    /// Source codec identifier.
83    pub codec: String,
84    /// Source bitrate in kbit/s.
85    pub bitrate_kbps: u32,
86    /// Source frame rate.
87    pub fps: f32,
88}
89
90impl SourceSpec {
91    /// Create a new source spec.
92    #[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
110/// Recommends proxy specifications based on editing context.
111pub struct ProxyRecommender;
112
113impl ProxyRecommender {
114    /// Recommend a list of proxy specs for the given sources and editing context.
115    ///
116    /// The recommendation strategy:
117    /// 1. Use the codec preferred by the editing software.
118    /// 2. Target the editing context resolution (downscaling if the source is larger).
119    /// 3. Adjust bitrate based on network speed: slower networks → lower bitrate.
120    #[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        // Determine target resolution: use context resolution, but do not upscale
125        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                // Do not upscale
132                let target_w = ctx_w.min(src_w);
133                let target_h = ctx_h.min(src_h);
134
135                // Bitrate: scale by pixel area ratio vs context resolution,
136                // then clamp based on network speed
137                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                // Cap bitrate to what the network can sustain (rough heuristic:
142                // leave 50% of bandwidth headroom, convert Mbit/s → kbit/s)
143                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                // Use the software's preferred codec, unless not supported
151                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                    // Fall back to first supported codec
160                    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/// Result of a proxy–context compatibility check.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct CompatibilityResult {
176    /// Whether the proxy is fully compatible with the editing context.
177    pub compatible: bool,
178    /// Non-fatal warnings (e.g. sub-optimal codec choice).
179    pub warnings: Vec<String>,
180    /// Compatibility score (0.0 = incompatible, 1.0 = perfect).
181    pub score: f32,
182}
183
184impl CompatibilityResult {
185    /// Create a fully compatible result with no warnings.
186    #[must_use]
187    pub fn ok() -> Self {
188        Self {
189            compatible: true,
190            warnings: vec![],
191            score: 1.0,
192        }
193    }
194
195    /// Create an incompatible result.
196    #[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
206/// Checks whether a proxy spec is compatible with an editing context.
207pub struct ProxyCompatibilityChecker;
208
209impl ProxyCompatibilityChecker {
210    /// Check compatibility and return a detailed result.
211    #[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        // 1. Codec check
217        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        // 2. Resolution check: proxy should not exceed context resolution
227        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        // 3. Bitrate vs network check
238        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        // 4. Codec support list (if provided)
248        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
269/// Estimates disk storage required for a batch of proxy files.
270pub struct ProxyStorageEstimator;
271
272impl ProxyStorageEstimator {
273    /// Estimate the total storage in gigabytes.
274    ///
275    /// # Arguments
276    /// * `source_count` – Number of source files.
277    /// * `avg_duration_mins` – Average duration of each source file in minutes.
278    /// * `spec` – Proxy specification (bitrate drives the estimate).
279    #[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        // bitrate_kbps * 1000 bits/kbit → bits/s
285        // * 60 seconds/minute * avg_duration_mins → total bits
286        // / 8 → bytes, / 1e9 → gigabytes
287        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        // Network: 1 Mbit/s → max 500 kbps usable
350        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        // Network 1 Mbit/s → max 500 kbps
410        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        // 8000 kbps * 1000 * 60 * 5 / 8 / 1e9 * 100 = 30 GB
421        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}