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    fn tmp_in() -> String {
357        std::env::temp_dir()
358            .join("oximedia-transcode-builder-input.mp4")
359            .to_string_lossy()
360            .into_owned()
361    }
362
363    fn tmp_out() -> String {
364        std::env::temp_dir()
365            .join("oximedia-transcode-builder-output.mp4")
366            .to_string_lossy()
367            .into_owned()
368    }
369
370    #[test]
371    fn test_builder_basic() {
372        let (ti, to) = (tmp_in(), tmp_out());
373        let config = TranscodeBuilder::new()
374            .input(&ti)
375            .output(&to)
376            .video_codec("vp9")
377            .audio_codec("opus")
378            .build()
379            .expect("should succeed in test");
380
381        assert_eq!(config.input, Some(ti));
382        assert_eq!(config.output, Some(to));
383        assert_eq!(config.video_codec, Some("vp9".to_string()));
384        assert_eq!(config.audio_codec, Some("opus".to_string()));
385    }
386
387    #[test]
388    fn test_builder_missing_input() {
389        let result = TranscodeBuilder::new().output(tmp_out()).build();
390
391        assert!(result.is_err());
392    }
393
394    #[test]
395    fn test_builder_missing_output() {
396        let result = TranscodeBuilder::new().input(tmp_in()).build();
397
398        assert!(result.is_err());
399    }
400
401    #[test]
402    fn test_builder_with_resolution() {
403        let config = TranscodeBuilder::new()
404            .input(tmp_in())
405            .output(tmp_out())
406            .resolution(1920, 1080)
407            .build()
408            .expect("should succeed in test");
409
410        assert_eq!(config.width, Some(1920));
411        assert_eq!(config.height, Some(1080));
412    }
413
414    #[test]
415    fn test_builder_with_quality() {
416        let config = TranscodeBuilder::new()
417            .input(tmp_in())
418            .output(tmp_out())
419            .quality(QualityMode::High)
420            .build()
421            .expect("should succeed in test");
422
423        assert_eq!(config.quality_mode, Some(QualityMode::High));
424    }
425
426    #[test]
427    fn test_builder_with_multipass() {
428        let config = TranscodeBuilder::new()
429            .input(tmp_in())
430            .output(tmp_out())
431            .multi_pass(MultiPassMode::TwoPass)
432            .build()
433            .expect("should succeed in test");
434
435        assert_eq!(config.multi_pass, Some(MultiPassMode::TwoPass));
436    }
437
438    #[test]
439    fn test_builder_with_normalization() {
440        let config = TranscodeBuilder::new()
441            .input(tmp_in())
442            .output(tmp_out())
443            .normalize_audio()
444            .build()
445            .expect("should succeed in test");
446
447        assert!(config.normalize_audio);
448    }
449
450    #[test]
451    fn test_advanced_builder_crf() {
452        let (ti, to) = (tmp_in(), tmp_out());
453        let config = AdvancedTranscodeBuilder::new()
454            .input(&ti)
455            .output(&to)
456            .crf(23)
457            .build()
458            .expect("should succeed in test");
459
460        assert_eq!(config.input, Some(ti));
461        assert_eq!(config.output, Some(to));
462    }
463
464    #[test]
465    fn test_advanced_builder_cbr() {
466        let config = AdvancedTranscodeBuilder::new()
467            .input(tmp_in())
468            .output(tmp_out())
469            .cbr(5_000_000)
470            .build()
471            .expect("should succeed in test");
472
473        assert_eq!(config.video_bitrate, Some(5_000_000));
474    }
475
476    #[test]
477    fn test_advanced_builder_vbr() {
478        let config = AdvancedTranscodeBuilder::new()
479            .input(tmp_in())
480            .output(tmp_out())
481            .vbr(5_000_000, 8_000_000)
482            .build()
483            .expect("should succeed in test");
484
485        assert_eq!(config.video_bitrate, Some(5_000_000));
486    }
487}