1use crate::{
4 AudioCodec, Container, EncodeError, HardwareEncoder, Preset, ProgressCallback, VideoCodec,
5};
6use std::path::PathBuf;
7
8pub struct EncoderBuilder {
29 pub(crate) path: PathBuf,
31
32 pub(crate) container: Option<Container>,
34
35 pub(crate) video_width: Option<u32>,
38 pub(crate) video_height: Option<u32>,
40 pub(crate) video_fps: Option<f64>,
42 pub(crate) video_codec: VideoCodec,
44 pub(crate) video_bitrate: Option<u64>,
46 pub(crate) video_quality: Option<u32>,
48 pub(crate) preset: Preset,
50 pub(crate) hardware_encoder: HardwareEncoder,
52
53 pub(crate) audio_sample_rate: Option<u32>,
56 pub(crate) audio_channels: Option<u32>,
58 pub(crate) audio_codec: AudioCodec,
60 pub(crate) audio_bitrate: Option<u64>,
62
63 pub(crate) progress_callback: Option<Box<dyn ProgressCallback>>,
66}
67
68impl std::fmt::Debug for EncoderBuilder {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("EncoderBuilder")
71 .field("path", &self.path)
72 .field("container", &self.container)
73 .field("video_width", &self.video_width)
74 .field("video_height", &self.video_height)
75 .field("video_fps", &self.video_fps)
76 .field("video_codec", &self.video_codec)
77 .field("video_bitrate", &self.video_bitrate)
78 .field("video_quality", &self.video_quality)
79 .field("preset", &self.preset)
80 .field("hardware_encoder", &self.hardware_encoder)
81 .field("audio_sample_rate", &self.audio_sample_rate)
82 .field("audio_channels", &self.audio_channels)
83 .field("audio_codec", &self.audio_codec)
84 .field("audio_bitrate", &self.audio_bitrate)
85 .field(
86 "progress_callback",
87 &self.progress_callback.as_ref().map(|_| "<callback>"),
88 )
89 .finish()
90 }
91}
92
93impl EncoderBuilder {
94 pub fn new(path: PathBuf) -> Result<Self, EncodeError> {
104 if path.parent().is_none() {
106 return Err(EncodeError::InvalidConfig {
107 reason: "Output path must have a parent directory".to_string(),
108 });
109 }
110
111 Ok(Self {
112 path,
113 container: None,
114 video_width: None,
115 video_height: None,
116 video_fps: None,
117 video_codec: VideoCodec::default(),
118 video_bitrate: None,
119 video_quality: None,
120 preset: Preset::default(),
121 hardware_encoder: HardwareEncoder::default(),
122 audio_sample_rate: None,
123 audio_channels: None,
124 audio_codec: AudioCodec::default(),
125 audio_bitrate: None,
126 progress_callback: None,
127 })
128 }
129
130 #[must_use]
140 pub fn video(mut self, width: u32, height: u32, fps: f64) -> Self {
141 self.video_width = Some(width);
142 self.video_height = Some(height);
143 self.video_fps = Some(fps);
144 self
145 }
146
147 #[must_use]
153 pub fn video_codec(mut self, codec: VideoCodec) -> Self {
154 self.video_codec = codec;
155 self
156 }
157
158 #[must_use]
164 pub fn video_bitrate(mut self, bitrate: u64) -> Self {
165 self.video_bitrate = Some(bitrate);
166 self
167 }
168
169 #[must_use]
178 pub fn video_quality(mut self, crf: u32) -> Self {
179 self.video_quality = Some(crf);
180 self
181 }
182
183 #[must_use]
189 pub fn preset(mut self, preset: Preset) -> Self {
190 self.preset = preset;
191 self
192 }
193
194 #[must_use]
200 pub fn hardware_encoder(mut self, hw: HardwareEncoder) -> Self {
201 self.hardware_encoder = hw;
202 self
203 }
204
205 #[must_use]
214 pub fn audio(mut self, sample_rate: u32, channels: u32) -> Self {
215 self.audio_sample_rate = Some(sample_rate);
216 self.audio_channels = Some(channels);
217 self
218 }
219
220 #[must_use]
226 pub fn audio_codec(mut self, codec: AudioCodec) -> Self {
227 self.audio_codec = codec;
228 self
229 }
230
231 #[must_use]
237 pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
238 self.audio_bitrate = Some(bitrate);
239 self
240 }
241
242 #[must_use]
252 pub fn container(mut self, container: Container) -> Self {
253 self.container = Some(container);
254 self
255 }
256
257 #[must_use]
281 pub fn on_progress<F>(mut self, callback: F) -> Self
282 where
283 F: FnMut(&crate::Progress) + Send + 'static,
284 {
285 self.progress_callback = Some(Box::new(callback));
286 self
287 }
288
289 #[must_use]
328 pub fn progress_callback<C: ProgressCallback + 'static>(mut self, callback: C) -> Self {
329 self.progress_callback = Some(Box::new(callback));
330 self
331 }
332
333 pub fn build(self) -> Result<crate::VideoEncoder, EncodeError> {
344 self.validate()?;
346
347 crate::VideoEncoder::from_builder(self)
349 }
350
351 fn validate(&self) -> Result<(), EncodeError> {
353 let has_video =
355 self.video_width.is_some() && self.video_height.is_some() && self.video_fps.is_some();
356 let has_audio = self.audio_sample_rate.is_some() && self.audio_channels.is_some();
357
358 if !has_video && !has_audio {
359 return Err(EncodeError::InvalidConfig {
360 reason: "At least one video or audio stream must be configured".to_string(),
361 });
362 }
363
364 if has_video {
366 if let Some(width) = self.video_width
367 && (width == 0 || width % 2 != 0)
368 {
369 return Err(EncodeError::InvalidConfig {
370 reason: format!("Video width must be non-zero and even, got {width}"),
371 });
372 }
373
374 if let Some(height) = self.video_height
375 && (height == 0 || height % 2 != 0)
376 {
377 return Err(EncodeError::InvalidConfig {
378 reason: format!("Video height must be non-zero and even, got {height}"),
379 });
380 }
381
382 if let Some(fps) = self.video_fps
383 && fps <= 0.0
384 {
385 return Err(EncodeError::InvalidConfig {
386 reason: format!("Video FPS must be positive, got {fps}"),
387 });
388 }
389
390 if let Some(quality) = self.video_quality
391 && quality > 51
392 {
393 return Err(EncodeError::InvalidConfig {
394 reason: format!("Video quality (CRF) must be 0-51, got {quality}"),
395 });
396 }
397 }
398
399 if has_audio {
401 if let Some(sample_rate) = self.audio_sample_rate
402 && sample_rate == 0
403 {
404 return Err(EncodeError::InvalidConfig {
405 reason: "Audio sample rate must be non-zero".to_string(),
406 });
407 }
408
409 if let Some(channels) = self.audio_channels
410 && channels == 0
411 {
412 return Err(EncodeError::InvalidConfig {
413 reason: "Audio channels must be non-zero".to_string(),
414 });
415 }
416 }
417
418 Ok(())
419 }
420
421 pub fn build_audio(self) -> Result<crate::AudioEncoder, EncodeError> {
427 crate::AudioEncoder::from_builder(self)
428 }
429}
430
431#[cfg(test)]
432#[allow(clippy::unwrap_used)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_builder_video_only() {
439 let builder = EncoderBuilder::new("output.mp4".into())
440 .unwrap()
441 .video(1920, 1080, 30.0)
442 .video_codec(VideoCodec::H264)
443 .video_bitrate(8_000_000);
444
445 assert_eq!(builder.video_width, Some(1920));
446 assert_eq!(builder.video_height, Some(1080));
447 assert_eq!(builder.video_fps, Some(30.0));
448 assert_eq!(builder.video_codec, VideoCodec::H264);
449 assert_eq!(builder.video_bitrate, Some(8_000_000));
450 }
451
452 #[test]
453 fn test_builder_audio_only() {
454 let builder = EncoderBuilder::new("output.mp3".into())
455 .unwrap()
456 .audio(48000, 2)
457 .audio_codec(AudioCodec::Mp3)
458 .audio_bitrate(192_000);
459
460 assert_eq!(builder.audio_sample_rate, Some(48000));
461 assert_eq!(builder.audio_channels, Some(2));
462 assert_eq!(builder.audio_codec, AudioCodec::Mp3);
463 assert_eq!(builder.audio_bitrate, Some(192_000));
464 }
465
466 #[test]
467 fn test_builder_both_streams() {
468 let builder = EncoderBuilder::new("output.mp4".into())
469 .unwrap()
470 .video(1920, 1080, 30.0)
471 .audio(48000, 2);
472
473 assert_eq!(builder.video_width, Some(1920));
474 assert_eq!(builder.audio_sample_rate, Some(48000));
475 }
476
477 #[test]
478 fn test_builder_preset() {
479 let builder = EncoderBuilder::new("output.mp4".into())
480 .unwrap()
481 .video(1920, 1080, 30.0)
482 .preset(Preset::Fast);
483
484 assert_eq!(builder.preset, Preset::Fast);
485 }
486
487 #[test]
488 fn test_builder_hardware_encoder() {
489 let builder = EncoderBuilder::new("output.mp4".into())
490 .unwrap()
491 .video(1920, 1080, 30.0)
492 .hardware_encoder(HardwareEncoder::Nvenc);
493
494 assert_eq!(builder.hardware_encoder, HardwareEncoder::Nvenc);
495 }
496
497 #[test]
498 fn test_builder_container() {
499 let builder = EncoderBuilder::new("output.video".into())
500 .unwrap()
501 .video(1920, 1080, 30.0)
502 .container(Container::Mp4);
503
504 assert_eq!(builder.container, Some(Container::Mp4));
505 }
506
507 #[test]
508 fn test_validate_no_streams() {
509 let builder = EncoderBuilder::new("output.mp4".into()).unwrap();
510 let result = builder.validate();
511 assert!(result.is_err());
512 }
513
514 #[test]
515 fn test_validate_odd_width() {
516 let builder = EncoderBuilder::new("output.mp4".into())
517 .unwrap()
518 .video(1921, 1080, 30.0);
519 let result = builder.validate();
520 assert!(result.is_err());
521 }
522
523 #[test]
524 fn test_validate_odd_height() {
525 let builder = EncoderBuilder::new("output.mp4".into())
526 .unwrap()
527 .video(1920, 1081, 30.0);
528 let result = builder.validate();
529 assert!(result.is_err());
530 }
531
532 #[test]
533 fn test_validate_invalid_fps() {
534 let builder = EncoderBuilder::new("output.mp4".into())
535 .unwrap()
536 .video(1920, 1080, -1.0);
537 let result = builder.validate();
538 assert!(result.is_err());
539 }
540
541 #[test]
542 fn test_validate_invalid_quality() {
543 let builder = EncoderBuilder::new("output.mp4".into())
544 .unwrap()
545 .video(1920, 1080, 30.0)
546 .video_quality(100);
547 let result = builder.validate();
548 assert!(result.is_err());
549 }
550
551 #[test]
552 fn test_validate_valid_config() {
553 let builder = EncoderBuilder::new("output.mp4".into())
554 .unwrap()
555 .video(1920, 1080, 30.0);
556 let result = builder.validate();
557 assert!(result.is_ok());
558 }
559}