oximedia_transcode/
builder.rs1use crate::{
4 AbrLadder, MultiPassMode, NormalizationConfig, PresetConfig, QualityConfig, QualityMode,
5 RateControlMode, Result, TranscodeConfig, TranscodeError,
6};
7
8pub struct TranscodeBuilder {
10 config: TranscodeConfig,
11}
12
13impl TranscodeBuilder {
14 #[must_use]
16 pub fn new() -> Self {
17 Self {
18 config: TranscodeConfig::default(),
19 }
20 }
21
22 #[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 #[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 #[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 #[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 #[must_use]
52 pub fn video_bitrate(mut self, bitrate: u64) -> Self {
53 self.config.video_bitrate = Some(bitrate);
54 self
55 }
56
57 #[must_use]
59 pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
60 self.config.audio_bitrate = Some(bitrate);
61 self
62 }
63
64 #[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 #[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 #[must_use]
81 pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
82 self.config.multi_pass = Some(mode);
83 self
84 }
85
86 #[must_use]
88 pub fn quality(mut self, mode: QualityMode) -> Self {
89 self.config.quality_mode = Some(mode);
90 self
91 }
92
93 #[must_use]
95 pub fn normalize_audio(mut self) -> Self {
96 self.config.normalize_audio = true;
97 self
98 }
99
100 #[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 #[must_use]
110 pub fn hw_accel(mut self, enable: bool) -> Self {
111 self.config.hw_accel = enable;
112 self
113 }
114
115 #[must_use]
117 pub fn preserve_metadata(mut self, enable: bool) -> Self {
118 self.config.preserve_metadata = enable;
119 self
120 }
121
122 #[must_use]
124 pub fn subtitles(mut self, mode: crate::SubtitleMode) -> Self {
125 self.config.subtitle_mode = Some(mode);
126 self
127 }
128
129 #[must_use]
131 pub fn chapters(mut self, mode: crate::ChapterMode) -> Self {
132 self.config.chapter_mode = Some(mode);
133 self
134 }
135
136 #[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 pub fn build(self) -> Result<TranscodeConfig> {
172 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 pub fn validate(self) -> Result<TranscodeConfig> {
194 let config = self.build()?;
195
196 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
233pub 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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}