Skip to main content

oximedia_proxy/generate/
optimizer.rs

1//! Proxy generation optimizer for automatic settings adjustment.
2
3use super::settings::ProxyGenerationSettings;
4use crate::Result;
5
6/// Proxy generation optimizer.
7pub struct ProxyOptimizer {
8    /// Target file size in bytes (optional).
9    target_size: Option<u64>,
10
11    /// Target bitrate in bits per second (optional).
12    target_bitrate: Option<u64>,
13
14    /// Maximum encoding time in seconds (optional).
15    max_encoding_time: Option<f64>,
16}
17
18impl ProxyOptimizer {
19    /// Create a new proxy optimizer.
20    #[must_use]
21    pub const fn new() -> Self {
22        Self {
23            target_size: None,
24            target_bitrate: None,
25            max_encoding_time: None,
26        }
27    }
28
29    /// Set target file size.
30    #[must_use]
31    pub const fn with_target_size(mut self, size: u64) -> Self {
32        self.target_size = Some(size);
33        self
34    }
35
36    /// Set target bitrate.
37    #[must_use]
38    pub const fn with_target_bitrate(mut self, bitrate: u64) -> Self {
39        self.target_bitrate = Some(bitrate);
40        self
41    }
42
43    /// Set maximum encoding time.
44    #[must_use]
45    pub const fn with_max_encoding_time(mut self, time: f64) -> Self {
46        self.max_encoding_time = Some(time);
47        self
48    }
49
50    /// Optimize settings for the given constraints.
51    pub fn optimize(
52        &self,
53        base_settings: ProxyGenerationSettings,
54        input_duration: f64,
55    ) -> Result<ProxyGenerationSettings> {
56        let mut settings = base_settings;
57
58        // Optimize for target bitrate
59        if let Some(target_bitrate) = self.target_bitrate {
60            settings.bitrate = target_bitrate;
61        }
62
63        // Optimize for target file size
64        if let Some(target_size) = self.target_size {
65            // Calculate required bitrate: (target_size * 8) / duration
66            let required_bitrate = (target_size as f64 * 8.0 / input_duration) as u64;
67
68            // Reserve 10% for audio and container overhead
69            settings.bitrate = (required_bitrate as f64 * 0.9) as u64;
70            settings.audio_bitrate = (required_bitrate as f64 * 0.1) as u64;
71        }
72
73        // Optimize for encoding time
74        if let Some(_max_time) = self.max_encoding_time {
75            // Use faster encoding presets for time constraints
76            settings.quality_preset = "ultrafast".to_string();
77            settings.threads = num_cpus();
78        }
79
80        settings.validate()?;
81        Ok(settings)
82    }
83
84    /// Estimate output size for given settings.
85    #[must_use]
86    pub fn estimate_output_size(&self, settings: &ProxyGenerationSettings, duration: f64) -> u64 {
87        // Video size: (bitrate * duration) / 8
88        let video_size = (settings.bitrate as f64 * duration / 8.0) as u64;
89
90        // Audio size: (audio_bitrate * duration) / 8
91        let audio_size = (settings.audio_bitrate as f64 * duration / 8.0) as u64;
92
93        // Container overhead (approximately 5%)
94        let overhead = ((video_size + audio_size) as f64 * 0.05) as u64;
95
96        video_size + audio_size + overhead
97    }
98
99    /// Estimate encoding time for given settings.
100    #[must_use]
101    pub fn estimate_encoding_time(&self, settings: &ProxyGenerationSettings, duration: f64) -> f64 {
102        // Base encoding speed (realtime factor)
103        let base_speed = match settings.quality_preset.as_str() {
104            "ultrafast" => 10.0, // 10x realtime
105            "veryfast" => 5.0,   // 5x realtime
106            "fast" => 3.0,       // 3x realtime
107            "medium" => 1.5,     // 1.5x realtime
108            "slow" => 0.5,       // 0.5x realtime
109            _ => 1.0,            // 1x realtime
110        };
111
112        // Adjust for scale factor (smaller = faster)
113        let scale_adjustment = 1.0 / (settings.scale_factor as f64);
114
115        // Adjust for threads
116        let thread_adjustment: f64 = if settings.threads == 0 {
117            num_cpus::get() as f64
118        } else {
119            settings.threads as f64
120        };
121
122        // Calculate estimated time
123        let denominator = base_speed * thread_adjustment / scale_adjustment;
124        duration / denominator
125    }
126}
127
128impl Default for ProxyOptimizer {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134#[allow(dead_code)]
135fn num_cpus() -> u32 {
136    std::thread::available_parallelism()
137        .map(|n| n.get() as u32)
138        .unwrap_or(1)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_optimizer_creation() {
147        let optimizer = ProxyOptimizer::new()
148            .with_target_size(100_000_000)
149            .with_target_bitrate(5_000_000);
150
151        assert_eq!(optimizer.target_size, Some(100_000_000));
152        assert_eq!(optimizer.target_bitrate, Some(5_000_000));
153    }
154
155    #[test]
156    fn test_optimize_for_size() {
157        let optimizer = ProxyOptimizer::new().with_target_size(50_000_000); // 50 MB
158
159        let base_settings = ProxyGenerationSettings::quarter_res_h264();
160        let optimized = optimizer
161            .optimize(base_settings, 60.0)
162            .expect("should succeed in test");
163
164        // Check that bitrate was adjusted for target size
165        assert!(optimized.bitrate > 0);
166    }
167
168    #[test]
169    fn test_estimate_output_size() {
170        let optimizer = ProxyOptimizer::new();
171        let settings = ProxyGenerationSettings::quarter_res_h264();
172
173        let estimated_size = optimizer.estimate_output_size(&settings, 60.0);
174        assert!(estimated_size > 0);
175
176        // For 60 seconds at 2 Mbps video + 128 kbps audio
177        // Should be approximately: (2000000 + 128000) * 60 / 8 = ~16 MB
178        assert!(estimated_size > 10_000_000);
179        assert!(estimated_size < 20_000_000);
180    }
181
182    #[test]
183    fn test_estimate_encoding_time() {
184        let optimizer = ProxyOptimizer::new();
185        let settings = ProxyGenerationSettings::quarter_res_h264();
186
187        let estimated_time = optimizer.estimate_encoding_time(&settings, 60.0);
188        assert!(estimated_time > 0.0);
189
190        // Medium preset should take less than realtime for quarter res
191        assert!(estimated_time < 60.0);
192    }
193
194    #[test]
195    fn test_optimize_for_time() {
196        let optimizer = ProxyOptimizer::new().with_max_encoding_time(10.0);
197
198        let base_settings = ProxyGenerationSettings::quarter_res_h264();
199        let optimized = optimizer
200            .optimize(base_settings, 60.0)
201            .expect("should succeed in test");
202
203        // Should use ultrafast preset for time optimization
204        assert_eq!(optimized.quality_preset, "ultrafast");
205        assert!(optimized.threads > 0);
206    }
207}