Skip to main content

oximedia_transcode/
builder.rs

1//! Fluent builder API for creating transcode configurations.
2
3use crate::{
4    AbrLadder, MultiPassMode, NormalizationConfig, PresetConfig, QualityConfig, QualityMode,
5    RateControlMode, Result, TranscodeConfig, TranscodeError,
6};
7
8/// Fluent builder for creating transcode configurations.
9pub struct TranscodeBuilder {
10    config: TranscodeConfig,
11}
12
13impl TranscodeBuilder {
14    /// Creates a new transcode builder.
15    #[must_use]
16    pub fn new() -> Self {
17        Self {
18            config: TranscodeConfig::default(),
19        }
20    }
21
22    /// Sets the input file path.
23    #[must_use]
24    pub fn input(mut self, path: impl Into<String>) -> Self {
25        self.config.input = Some(path.into());
26        self
27    }
28
29    /// Sets the output file path.
30    #[must_use]
31    pub fn output(mut self, path: impl Into<String>) -> Self {
32        self.config.output = Some(path.into());
33        self
34    }
35
36    /// Sets the video codec.
37    #[must_use]
38    pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
39        self.config.video_codec = Some(codec.into());
40        self
41    }
42
43    /// Sets the audio codec.
44    #[must_use]
45    pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
46        self.config.audio_codec = Some(codec.into());
47        self
48    }
49
50    /// Sets the video bitrate in bits per second.
51    #[must_use]
52    pub fn video_bitrate(mut self, bitrate: u64) -> Self {
53        self.config.video_bitrate = Some(bitrate);
54        self
55    }
56
57    /// Sets the audio bitrate in bits per second.
58    #[must_use]
59    pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
60        self.config.audio_bitrate = Some(bitrate);
61        self
62    }
63
64    /// Sets the output resolution.
65    #[must_use]
66    pub fn resolution(mut self, width: u32, height: u32) -> Self {
67        self.config.width = Some(width);
68        self.config.height = Some(height);
69        self
70    }
71
72    /// Sets the output frame rate.
73    #[must_use]
74    pub fn frame_rate(mut self, num: u32, den: u32) -> Self {
75        self.config.frame_rate = Some((num, den));
76        self
77    }
78
79    /// Sets the multi-pass encoding mode.
80    #[must_use]
81    pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
82        self.config.multi_pass = Some(mode);
83        self
84    }
85
86    /// Sets the quality mode.
87    #[must_use]
88    pub fn quality(mut self, mode: QualityMode) -> Self {
89        self.config.quality_mode = Some(mode);
90        self
91    }
92
93    /// Enables audio normalization with the default standard (EBU R128).
94    #[must_use]
95    pub fn normalize_audio(mut self) -> Self {
96        self.config.normalize_audio = true;
97        self
98    }
99
100    /// Sets a specific loudness standard for audio normalization.
101    #[must_use]
102    pub fn loudness_standard(mut self, standard: crate::LoudnessStandard) -> Self {
103        self.config.loudness_standard = Some(standard);
104        self.config.normalize_audio = true;
105        self
106    }
107
108    /// Enables or disables hardware acceleration.
109    #[must_use]
110    pub fn hw_accel(mut self, enable: bool) -> Self {
111        self.config.hw_accel = enable;
112        self
113    }
114
115    /// Enables or disables metadata preservation.
116    #[must_use]
117    pub fn preserve_metadata(mut self, enable: bool) -> Self {
118        self.config.preserve_metadata = enable;
119        self
120    }
121
122    /// Sets the subtitle mode.
123    #[must_use]
124    pub fn subtitles(mut self, mode: crate::SubtitleMode) -> Self {
125        self.config.subtitle_mode = Some(mode);
126        self
127    }
128
129    /// Sets the chapter mode.
130    #[must_use]
131    pub fn chapters(mut self, mode: crate::ChapterMode) -> Self {
132        self.config.chapter_mode = Some(mode);
133        self
134    }
135
136    /// Applies a preset configuration.
137    #[must_use]
138    pub fn preset(mut self, preset: PresetConfig) -> Self {
139        if let Some(codec) = preset.video_codec {
140            self.config.video_codec = Some(codec);
141        }
142        if let Some(codec) = preset.audio_codec {
143            self.config.audio_codec = Some(codec);
144        }
145        if let Some(bitrate) = preset.video_bitrate {
146            self.config.video_bitrate = Some(bitrate);
147        }
148        if let Some(bitrate) = preset.audio_bitrate {
149            self.config.audio_bitrate = Some(bitrate);
150        }
151        if let Some(width) = preset.width {
152            self.config.width = Some(width);
153        }
154        if let Some(height) = preset.height {
155            self.config.height = Some(height);
156        }
157        if let Some(fps) = preset.frame_rate {
158            self.config.frame_rate = Some(fps);
159        }
160        if let Some(mode) = preset.quality_mode {
161            self.config.quality_mode = Some(mode);
162        }
163        self
164    }
165
166    /// Builds the transcode configuration.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if the configuration is invalid.
171    pub fn build(self) -> Result<TranscodeConfig> {
172        // Validate required fields
173        if self.config.input.is_none() {
174            return Err(TranscodeError::InvalidInput(
175                "Input path is required".to_string(),
176            ));
177        }
178
179        if self.config.output.is_none() {
180            return Err(TranscodeError::InvalidOutput(
181                "Output path is required".to_string(),
182            ));
183        }
184
185        Ok(self.config)
186    }
187
188    /// Builds and validates the configuration, consuming the builder.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the configuration is invalid or validation fails.
193    pub fn validate(self) -> Result<TranscodeConfig> {
194        let config = self.build()?;
195
196        // Perform additional validation
197        use crate::validation::{InputValidator, OutputValidator};
198
199        if let Some(ref input) = config.input {
200            InputValidator::validate_path(input)?;
201        }
202
203        if let Some(ref output) = config.output {
204            OutputValidator::validate_path(output, true)?;
205        }
206
207        if let Some(ref codec) = config.video_codec {
208            OutputValidator::validate_codec(codec)?;
209        }
210
211        if let Some(ref codec) = config.audio_codec {
212            OutputValidator::validate_codec(codec)?;
213        }
214
215        if let (Some(width), Some(height)) = (config.width, config.height) {
216            OutputValidator::validate_resolution(width, height)?;
217        }
218
219        if let Some((num, den)) = config.frame_rate {
220            OutputValidator::validate_frame_rate(num, den)?;
221        }
222
223        Ok(config)
224    }
225}
226
227impl Default for TranscodeBuilder {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233/// Advanced builder with fluent API for complex scenarios.
234pub struct AdvancedTranscodeBuilder {
235    #[allow(dead_code)]
236    builder: TranscodeBuilder,
237    quality_config: Option<QualityConfig>,
238    #[allow(dead_code)]
239    normalization_config: Option<NormalizationConfig>,
240    #[allow(dead_code)]
241    abr_ladder: Option<AbrLadder>,
242}
243
244#[allow(dead_code)]
245impl AdvancedTranscodeBuilder {
246    /// Creates a new advanced builder.
247    #[must_use]
248    pub fn new() -> Self {
249        Self {
250            builder: TranscodeBuilder::new(),
251            quality_config: None,
252            normalization_config: None,
253            abr_ladder: None,
254        }
255    }
256
257    /// Sets the input file.
258    #[must_use]
259    pub fn input(mut self, path: impl Into<String>) -> Self {
260        self.builder = self.builder.input(path);
261        self
262    }
263
264    /// Sets the output file.
265    #[must_use]
266    pub fn output(mut self, path: impl Into<String>) -> Self {
267        self.builder = self.builder.output(path);
268        self
269    }
270
271    /// Sets the quality configuration.
272    #[must_use]
273    #[allow(dead_code)]
274    pub fn quality_config(mut self, config: QualityConfig) -> Self {
275        self.quality_config = Some(config);
276        self
277    }
278
279    /// Sets the normalization configuration.
280    #[allow(dead_code)]
281    #[must_use]
282    pub fn normalization_config(mut self, config: NormalizationConfig) -> Self {
283        self.normalization_config = Some(config);
284        self
285    }
286
287    /// Sets the ABR ladder for adaptive streaming.
288    #[allow(dead_code)]
289    #[must_use]
290    pub fn abr_ladder(mut self, ladder: AbrLadder) -> Self {
291        self.abr_ladder = Some(ladder);
292        self
293    }
294
295    /// Sets constant quality mode with CRF.
296    #[must_use]
297    pub fn crf(mut self, value: u8) -> Self {
298        if let Some(ref mut config) = self.quality_config {
299            config.rate_control = RateControlMode::Crf(value);
300        } else {
301            let mut config = QualityConfig::default();
302            config.rate_control = RateControlMode::Crf(value);
303            self.quality_config = Some(config);
304        }
305        self
306    }
307
308    /// Sets constant bitrate mode.
309    #[must_use]
310    pub fn cbr(mut self, bitrate: u64) -> Self {
311        if let Some(ref mut config) = self.quality_config {
312            config.rate_control = RateControlMode::Cbr(bitrate);
313        } else {
314            let mut config = QualityConfig::default();
315            config.rate_control = RateControlMode::Cbr(bitrate);
316            self.quality_config = Some(config);
317        }
318        self.builder = self.builder.video_bitrate(bitrate);
319        self
320    }
321
322    /// Sets variable bitrate mode.
323    #[must_use]
324    pub fn vbr(mut self, target: u64, max: u64) -> Self {
325        if let Some(ref mut config) = self.quality_config {
326            config.rate_control = RateControlMode::Vbr { target, max };
327        } else {
328            let mut config = QualityConfig::default();
329            config.rate_control = RateControlMode::Vbr { target, max };
330            self.quality_config = Some(config);
331        }
332        self.builder = self.builder.video_bitrate(target);
333        self
334    }
335
336    /// Builds the configuration.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error if the configuration is invalid.
341    pub fn build(self) -> Result<TranscodeConfig> {
342        self.builder.build()
343    }
344}
345
346impl Default for AdvancedTranscodeBuilder {
347    fn default() -> Self {
348        Self::new()
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_builder_basic() {
358        let config = TranscodeBuilder::new()
359            .input("/tmp/input.mp4")
360            .output("/tmp/output.mp4")
361            .video_codec("vp9")
362            .audio_codec("opus")
363            .build()
364            .expect("should succeed in test");
365
366        assert_eq!(config.input, Some("/tmp/input.mp4".to_string()));
367        assert_eq!(config.output, Some("/tmp/output.mp4".to_string()));
368        assert_eq!(config.video_codec, Some("vp9".to_string()));
369        assert_eq!(config.audio_codec, Some("opus".to_string()));
370    }
371
372    #[test]
373    fn test_builder_missing_input() {
374        let result = TranscodeBuilder::new().output("/tmp/output.mp4").build();
375
376        assert!(result.is_err());
377    }
378
379    #[test]
380    fn test_builder_missing_output() {
381        let result = TranscodeBuilder::new().input("/tmp/input.mp4").build();
382
383        assert!(result.is_err());
384    }
385
386    #[test]
387    fn test_builder_with_resolution() {
388        let config = TranscodeBuilder::new()
389            .input("/tmp/input.mp4")
390            .output("/tmp/output.mp4")
391            .resolution(1920, 1080)
392            .build()
393            .expect("should succeed in test");
394
395        assert_eq!(config.width, Some(1920));
396        assert_eq!(config.height, Some(1080));
397    }
398
399    #[test]
400    fn test_builder_with_quality() {
401        let config = TranscodeBuilder::new()
402            .input("/tmp/input.mp4")
403            .output("/tmp/output.mp4")
404            .quality(QualityMode::High)
405            .build()
406            .expect("should succeed in test");
407
408        assert_eq!(config.quality_mode, Some(QualityMode::High));
409    }
410
411    #[test]
412    fn test_builder_with_multipass() {
413        let config = TranscodeBuilder::new()
414            .input("/tmp/input.mp4")
415            .output("/tmp/output.mp4")
416            .multi_pass(MultiPassMode::TwoPass)
417            .build()
418            .expect("should succeed in test");
419
420        assert_eq!(config.multi_pass, Some(MultiPassMode::TwoPass));
421    }
422
423    #[test]
424    fn test_builder_with_normalization() {
425        let config = TranscodeBuilder::new()
426            .input("/tmp/input.mp4")
427            .output("/tmp/output.mp4")
428            .normalize_audio()
429            .build()
430            .expect("should succeed in test");
431
432        assert!(config.normalize_audio);
433    }
434
435    #[test]
436    fn test_advanced_builder_crf() {
437        let config = AdvancedTranscodeBuilder::new()
438            .input("/tmp/input.mp4")
439            .output("/tmp/output.mp4")
440            .crf(23)
441            .build()
442            .expect("should succeed in test");
443
444        assert_eq!(config.input, Some("/tmp/input.mp4".to_string()));
445        assert_eq!(config.output, Some("/tmp/output.mp4".to_string()));
446    }
447
448    #[test]
449    fn test_advanced_builder_cbr() {
450        let config = AdvancedTranscodeBuilder::new()
451            .input("/tmp/input.mp4")
452            .output("/tmp/output.mp4")
453            .cbr(5_000_000)
454            .build()
455            .expect("should succeed in test");
456
457        assert_eq!(config.video_bitrate, Some(5_000_000));
458    }
459
460    #[test]
461    fn test_advanced_builder_vbr() {
462        let config = AdvancedTranscodeBuilder::new()
463            .input("/tmp/input.mp4")
464            .output("/tmp/output.mp4")
465            .vbr(5_000_000, 8_000_000)
466            .build()
467            .expect("should succeed in test");
468
469        assert_eq!(config.video_bitrate, Some(5_000_000));
470    }
471}