Skip to main content

oximedia_transcode/
multipass.rs

1//! Multi-pass encoding controller for optimal quality encoding.
2
3use crate::{Result, TranscodeError};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7/// Multi-pass encoding mode.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9pub enum MultiPassMode {
10    /// Single-pass encoding (fastest, lower quality).
11    #[default]
12    SinglePass,
13    /// Two-pass encoding (good balance of speed and quality).
14    TwoPass,
15    /// Three-pass encoding (best quality, slowest).
16    ThreePass,
17}
18
19/// Multi-pass encoding configuration.
20#[derive(Debug, Clone)]
21pub struct MultiPassConfig {
22    /// Encoding mode.
23    pub mode: MultiPassMode,
24    /// Statistics file path for pass data.
25    pub stats_file: PathBuf,
26    /// Current pass number.
27    pub current_pass: u32,
28    /// Whether to keep statistics files after encoding.
29    pub keep_stats: bool,
30    /// Target bitrate for multi-pass encoding.
31    pub target_bitrate: Option<u64>,
32    /// Maximum bitrate for constrained encoding.
33    pub max_bitrate: Option<u64>,
34}
35
36impl MultiPassMode {
37    /// Returns the total number of passes for this mode.
38    #[must_use]
39    pub fn pass_count(self) -> u32 {
40        match self {
41            Self::SinglePass => 1,
42            Self::TwoPass => 2,
43            Self::ThreePass => 3,
44        }
45    }
46
47    /// Checks if this mode requires statistics files.
48    #[must_use]
49    pub fn requires_stats(self) -> bool {
50        !matches!(self, Self::SinglePass)
51    }
52
53    /// Gets a human-readable description of the mode.
54    #[must_use]
55    pub fn description(self) -> &'static str {
56        match self {
57            Self::SinglePass => "Single-pass encoding (fast, good quality)",
58            Self::TwoPass => "Two-pass encoding (balanced speed and quality)",
59            Self::ThreePass => "Three-pass encoding (slow, best quality)",
60        }
61    }
62}
63
64impl MultiPassConfig {
65    /// Creates a new multi-pass configuration.
66    ///
67    /// # Arguments
68    ///
69    /// * `mode` - The multi-pass mode to use
70    /// * `stats_file` - Path where statistics will be stored
71    pub fn new(mode: MultiPassMode, stats_file: impl Into<PathBuf>) -> Self {
72        Self {
73            mode,
74            stats_file: stats_file.into(),
75            current_pass: 1,
76            keep_stats: false,
77            target_bitrate: None,
78            max_bitrate: None,
79        }
80    }
81
82    /// Sets the target bitrate for multi-pass encoding.
83    #[must_use]
84    pub fn with_target_bitrate(mut self, bitrate: u64) -> Self {
85        self.target_bitrate = Some(bitrate);
86        self
87    }
88
89    /// Sets the maximum bitrate for constrained encoding.
90    #[must_use]
91    pub fn with_max_bitrate(mut self, bitrate: u64) -> Self {
92        self.max_bitrate = Some(bitrate);
93        self
94    }
95
96    /// Sets whether to keep statistics files after encoding.
97    #[must_use]
98    pub fn keep_stats(mut self, keep: bool) -> Self {
99        self.keep_stats = keep;
100        self
101    }
102
103    /// Gets the statistics file path for a specific pass.
104    #[must_use]
105    pub fn stats_file_for_pass(&self, pass: u32) -> PathBuf {
106        if self.mode.pass_count() == 1 {
107            return self.stats_file.clone();
108        }
109
110        let mut path = self.stats_file.clone();
111        let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("stats");
112        let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("log");
113
114        path.set_file_name(format!("{stem}_pass{pass}.{ext}"));
115        path
116    }
117
118    /// Checks if a pass is an analysis pass (no output).
119    #[must_use]
120    pub fn is_analysis_pass(&self, pass: u32) -> bool {
121        if self.mode == MultiPassMode::SinglePass {
122            return false;
123        }
124
125        // In multi-pass encoding, all passes except the last are analysis
126        pass < self.mode.pass_count()
127    }
128
129    /// Gets the encoder flags for a specific pass.
130    #[must_use]
131    pub fn encoder_flags_for_pass(&self, pass: u32) -> Vec<String> {
132        let mut flags = Vec::new();
133
134        match self.mode {
135            MultiPassMode::SinglePass => {
136                // No special flags needed
137            }
138            MultiPassMode::TwoPass => {
139                if pass == 1 {
140                    flags.push("pass=1".to_string());
141                    flags.push(format!(
142                        "stats={}",
143                        self.stats_file_for_pass(pass).display()
144                    ));
145                } else {
146                    flags.push("pass=2".to_string());
147                    flags.push(format!("stats={}", self.stats_file_for_pass(1).display()));
148                }
149            }
150            MultiPassMode::ThreePass => match pass {
151                1 => {
152                    flags.push("pass=1".to_string());
153                    flags.push(format!(
154                        "stats={}",
155                        self.stats_file_for_pass(pass).display()
156                    ));
157                }
158                2 => {
159                    flags.push("pass=2".to_string());
160                    flags.push(format!(
161                        "stats={}",
162                        self.stats_file_for_pass(pass).display()
163                    ));
164                }
165                3 => {
166                    flags.push("pass=3".to_string());
167                    flags.push(format!("stats={}", self.stats_file_for_pass(1).display()));
168                    flags.push(format!("stats2={}", self.stats_file_for_pass(2).display()));
169                }
170                _ => {}
171            },
172        }
173
174        flags
175    }
176
177    /// Validates the multi-pass configuration.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if the configuration is invalid.
182    pub fn validate(&self) -> Result<()> {
183        if self.mode.requires_stats() {
184            let parent = self.stats_file.parent().ok_or_else(|| {
185                TranscodeError::MultiPassError("Invalid stats file path".to_string())
186            })?;
187
188            if !parent.exists() {
189                return Err(TranscodeError::MultiPassError(format!(
190                    "Stats directory does not exist: {}",
191                    parent.display()
192                )));
193            }
194        }
195
196        if let (Some(target), Some(max)) = (self.target_bitrate, self.max_bitrate) {
197            if target > max {
198                return Err(TranscodeError::MultiPassError(
199                    "Target bitrate cannot exceed max bitrate".to_string(),
200                ));
201            }
202        }
203
204        Ok(())
205    }
206
207    /// Cleans up statistics files.
208    pub fn cleanup(&self) -> Result<()> {
209        if !self.keep_stats && self.mode.requires_stats() {
210            for pass in 1..=self.mode.pass_count() {
211                let stats_file = self.stats_file_for_pass(pass);
212                if stats_file.exists() {
213                    std::fs::remove_file(&stats_file)?;
214                }
215            }
216        }
217        Ok(())
218    }
219}
220
221/// Multi-pass encoder controller.
222pub struct MultiPassEncoder {
223    config: MultiPassConfig,
224}
225
226impl MultiPassEncoder {
227    /// Creates a new multi-pass encoder controller.
228    #[must_use]
229    pub fn new(config: MultiPassConfig) -> Self {
230        Self { config }
231    }
232
233    /// Gets the total number of passes.
234    #[must_use]
235    pub fn pass_count(&self) -> u32 {
236        self.config.mode.pass_count()
237    }
238
239    /// Checks if more passes are needed.
240    #[must_use]
241    pub fn has_more_passes(&self) -> bool {
242        self.config.current_pass < self.pass_count()
243    }
244
245    /// Advances to the next pass.
246    pub fn next_pass(&mut self) {
247        if self.has_more_passes() {
248            self.config.current_pass += 1;
249        }
250    }
251
252    /// Gets the current pass number.
253    #[must_use]
254    pub fn current_pass(&self) -> u32 {
255        self.config.current_pass
256    }
257
258    /// Gets the encoder flags for the current pass.
259    #[must_use]
260    pub fn current_encoder_flags(&self) -> Vec<String> {
261        self.config.encoder_flags_for_pass(self.config.current_pass)
262    }
263
264    /// Checks if the current pass is an analysis pass.
265    #[must_use]
266    pub fn is_current_analysis_pass(&self) -> bool {
267        self.config.is_analysis_pass(self.config.current_pass)
268    }
269
270    /// Resets the encoder to the first pass.
271    pub fn reset(&mut self) {
272        self.config.current_pass = 1;
273    }
274
275    /// Cleans up statistics files.
276    pub fn cleanup(&self) -> Result<()> {
277        self.config.cleanup()
278    }
279}
280
281/// Builder for multi-pass configuration.
282#[allow(dead_code)]
283pub struct MultiPassConfigBuilder {
284    mode: MultiPassMode,
285    stats_file: Option<PathBuf>,
286    keep_stats: bool,
287    target_bitrate: Option<u64>,
288    max_bitrate: Option<u64>,
289}
290
291#[allow(dead_code)]
292impl MultiPassConfigBuilder {
293    /// Creates a new builder with the specified mode.
294    #[must_use]
295    pub fn new(mode: MultiPassMode) -> Self {
296        Self {
297            mode,
298            stats_file: None,
299            keep_stats: false,
300            target_bitrate: None,
301            max_bitrate: None,
302        }
303    }
304
305    /// Sets the statistics file path.
306    #[must_use]
307    pub fn stats_file(mut self, path: impl Into<PathBuf>) -> Self {
308        self.stats_file = Some(path.into());
309        self
310    }
311
312    /// Sets whether to keep statistics files.
313    #[must_use]
314    pub fn keep_stats(mut self, keep: bool) -> Self {
315        self.keep_stats = keep;
316        self
317    }
318
319    /// Sets the target bitrate.
320    #[must_use]
321    pub fn target_bitrate(mut self, bitrate: u64) -> Self {
322        self.target_bitrate = Some(bitrate);
323        self
324    }
325
326    /// Sets the maximum bitrate.
327    #[must_use]
328    pub fn max_bitrate(mut self, bitrate: u64) -> Self {
329        self.max_bitrate = Some(bitrate);
330        self
331    }
332
333    /// Builds the multi-pass configuration.
334    ///
335    /// # Errors
336    ///
337    /// Returns an error if the stats file is required but not set.
338    pub fn build(self) -> Result<MultiPassConfig> {
339        let stats_file = if self.mode.requires_stats() {
340            self.stats_file.ok_or_else(|| {
341                TranscodeError::MultiPassError(
342                    "Stats file required for multi-pass encoding".to_string(),
343                )
344            })?
345        } else {
346            self.stats_file
347                .unwrap_or_else(|| PathBuf::from("/tmp/stats.log"))
348        };
349
350        let mut config = MultiPassConfig::new(self.mode, stats_file);
351        config.keep_stats = self.keep_stats;
352        config.target_bitrate = self.target_bitrate;
353        config.max_bitrate = self.max_bitrate;
354
355        config.validate()?;
356        Ok(config)
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_multipass_mode_pass_count() {
366        assert_eq!(MultiPassMode::SinglePass.pass_count(), 1);
367        assert_eq!(MultiPassMode::TwoPass.pass_count(), 2);
368        assert_eq!(MultiPassMode::ThreePass.pass_count(), 3);
369    }
370
371    #[test]
372    fn test_multipass_mode_requires_stats() {
373        assert!(!MultiPassMode::SinglePass.requires_stats());
374        assert!(MultiPassMode::TwoPass.requires_stats());
375        assert!(MultiPassMode::ThreePass.requires_stats());
376    }
377
378    #[test]
379    fn test_multipass_config_stats_file() {
380        let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
381        assert_eq!(
382            config.stats_file_for_pass(1),
383            PathBuf::from("/tmp/stats_pass1.log")
384        );
385        assert_eq!(
386            config.stats_file_for_pass(2),
387            PathBuf::from("/tmp/stats_pass2.log")
388        );
389    }
390
391    #[test]
392    fn test_multipass_config_is_analysis_pass() {
393        let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
394        assert!(config.is_analysis_pass(1));
395        assert!(!config.is_analysis_pass(2));
396    }
397
398    #[test]
399    fn test_multipass_encoder_flow() {
400        let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
401        let mut encoder = MultiPassEncoder::new(config);
402
403        assert_eq!(encoder.current_pass(), 1);
404        assert!(encoder.has_more_passes());
405        assert!(encoder.is_current_analysis_pass());
406
407        encoder.next_pass();
408
409        assert_eq!(encoder.current_pass(), 2);
410        assert!(!encoder.has_more_passes());
411        assert!(!encoder.is_current_analysis_pass());
412    }
413
414    #[test]
415    fn test_multipass_encoder_reset() {
416        let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
417        let mut encoder = MultiPassEncoder::new(config);
418
419        encoder.next_pass();
420        assert_eq!(encoder.current_pass(), 2);
421
422        encoder.reset();
423        assert_eq!(encoder.current_pass(), 1);
424    }
425
426    #[test]
427    fn test_multipass_config_builder() {
428        let config = MultiPassConfigBuilder::new(MultiPassMode::TwoPass)
429            .stats_file("/tmp/test_stats.log")
430            .target_bitrate(5_000_000)
431            .max_bitrate(8_000_000)
432            .keep_stats(true)
433            .build()
434            .expect("should succeed in test");
435
436        assert_eq!(config.mode, MultiPassMode::TwoPass);
437        assert_eq!(config.target_bitrate, Some(5_000_000));
438        assert_eq!(config.max_bitrate, Some(8_000_000));
439        assert!(config.keep_stats);
440    }
441
442    #[test]
443    fn test_encoder_flags_two_pass() {
444        let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
445
446        let flags1 = config.encoder_flags_for_pass(1);
447        assert!(flags1.contains(&"pass=1".to_string()));
448
449        let flags2 = config.encoder_flags_for_pass(2);
450        assert!(flags2.contains(&"pass=2".to_string()));
451    }
452
453    #[test]
454    fn test_single_pass_no_stats() {
455        let config = MultiPassConfig::new(MultiPassMode::SinglePass, "/tmp/stats.log");
456        assert!(!config.is_analysis_pass(1));
457        assert_eq!(config.encoder_flags_for_pass(1).len(), 0);
458    }
459}