Skip to main content

ff_encode/
builder.rs

1//! Encoder builder implementation.
2
3use crate::{
4    AudioCodec, Container, EncodeError, HardwareEncoder, Preset, ProgressCallback, VideoCodec,
5};
6use std::path::PathBuf;
7
8/// Builder for constructing a [`VideoEncoder`](crate::VideoEncoder) or [`AudioEncoder`](crate::AudioEncoder).
9///
10/// Provides a fluent API for configuring encoding parameters before
11/// creating the actual encoder instance.
12///
13/// # Examples
14///
15/// ```ignore
16/// use ff_encode::{VideoEncoder, VideoCodec, AudioCodec, Preset};
17///
18/// let encoder = VideoEncoder::create("output.mp4")?
19///     .video(1920, 1080, 30.0)
20///     .video_codec(VideoCodec::H264)
21///     .video_bitrate(8_000_000)
22///     .preset(Preset::Medium)
23///     .audio(48000, 2)
24///     .audio_codec(AudioCodec::Aac)
25///     .audio_bitrate(192_000)
26///     .build()?;
27/// ```
28pub struct EncoderBuilder {
29    /// Output file path
30    pub(crate) path: PathBuf,
31
32    /// Container format (auto-detected if None)
33    pub(crate) container: Option<Container>,
34
35    // Video settings
36    /// Video width in pixels
37    pub(crate) video_width: Option<u32>,
38    /// Video height in pixels
39    pub(crate) video_height: Option<u32>,
40    /// Video frame rate (frames per second)
41    pub(crate) video_fps: Option<f64>,
42    /// Video codec
43    pub(crate) video_codec: VideoCodec,
44    /// Video bitrate in bits per second
45    pub(crate) video_bitrate: Option<u64>,
46    /// Video quality (CRF: 0-51, lower is higher quality)
47    pub(crate) video_quality: Option<u32>,
48    /// Encoding preset
49    pub(crate) preset: Preset,
50    /// Hardware encoder
51    pub(crate) hardware_encoder: HardwareEncoder,
52
53    // Audio settings
54    /// Audio sample rate in Hz
55    pub(crate) audio_sample_rate: Option<u32>,
56    /// Number of audio channels
57    pub(crate) audio_channels: Option<u32>,
58    /// Audio codec
59    pub(crate) audio_codec: AudioCodec,
60    /// Audio bitrate in bits per second
61    pub(crate) audio_bitrate: Option<u64>,
62
63    // Callbacks
64    /// Progress callback handler
65    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    /// Create a new encoder builder with the given output path.
95    ///
96    /// # Arguments
97    ///
98    /// * `path` - Output file path
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the output path is invalid.
103    pub fn new(path: PathBuf) -> Result<Self, EncodeError> {
104        // Validate path has a parent directory
105        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    // === Video settings ===
131
132    /// Configure video stream settings.
133    ///
134    /// # Arguments
135    ///
136    /// * `width` - Video width in pixels
137    /// * `height` - Video height in pixels
138    /// * `fps` - Frame rate in frames per second
139    #[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    /// Set video codec.
148    ///
149    /// # Arguments
150    ///
151    /// * `codec` - Video codec to use
152    #[must_use]
153    pub fn video_codec(mut self, codec: VideoCodec) -> Self {
154        self.video_codec = codec;
155        self
156    }
157
158    /// Set video bitrate in bits per second.
159    ///
160    /// # Arguments
161    ///
162    /// * `bitrate` - Target bitrate in bps (e.g., `8_000_000` for 8 Mbps)
163    #[must_use]
164    pub fn video_bitrate(mut self, bitrate: u64) -> Self {
165        self.video_bitrate = Some(bitrate);
166        self
167    }
168
169    /// Set video quality using CRF (Constant Rate Factor).
170    ///
171    /// # Arguments
172    ///
173    /// * `crf` - Quality value (0-51, lower is higher quality)
174    ///
175    /// Note: CRF mode typically produces better quality than constant bitrate
176    /// for the same file size, but the final file size is less predictable.
177    #[must_use]
178    pub fn video_quality(mut self, crf: u32) -> Self {
179        self.video_quality = Some(crf);
180        self
181    }
182
183    /// Set encoding preset (speed vs quality tradeoff).
184    ///
185    /// # Arguments
186    ///
187    /// * `preset` - Encoding preset
188    #[must_use]
189    pub fn preset(mut self, preset: Preset) -> Self {
190        self.preset = preset;
191        self
192    }
193
194    /// Set hardware encoder.
195    ///
196    /// # Arguments
197    ///
198    /// * `hw` - Hardware encoder to use
199    #[must_use]
200    pub fn hardware_encoder(mut self, hw: HardwareEncoder) -> Self {
201        self.hardware_encoder = hw;
202        self
203    }
204
205    // === Audio settings ===
206
207    /// Configure audio stream settings.
208    ///
209    /// # Arguments
210    ///
211    /// * `sample_rate` - Sample rate in Hz (e.g., 48000)
212    /// * `channels` - Number of channels (1 = mono, 2 = stereo)
213    #[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    /// Set audio codec.
221    ///
222    /// # Arguments
223    ///
224    /// * `codec` - Audio codec to use
225    #[must_use]
226    pub fn audio_codec(mut self, codec: AudioCodec) -> Self {
227        self.audio_codec = codec;
228        self
229    }
230
231    /// Set audio bitrate in bits per second.
232    ///
233    /// # Arguments
234    ///
235    /// * `bitrate` - Target bitrate in bps (e.g., `192_000` for 192 kbps)
236    #[must_use]
237    pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
238        self.audio_bitrate = Some(bitrate);
239        self
240    }
241
242    // === Container settings ===
243
244    /// Set container format explicitly.
245    ///
246    /// # Arguments
247    ///
248    /// * `container` - Container format
249    ///
250    /// Note: Usually not needed as the format is auto-detected from file extension.
251    #[must_use]
252    pub fn container(mut self, container: Container) -> Self {
253        self.container = Some(container);
254        self
255    }
256
257    // === Callbacks ===
258
259    /// Set progress callback using a closure or function.
260    ///
261    /// This is a convenience method for simple progress callbacks.
262    /// For cancellation support, use `progress_callback()` instead.
263    ///
264    /// # Arguments
265    ///
266    /// * `callback` - Closure or function to call with progress updates
267    ///
268    /// # Examples
269    ///
270    /// ```ignore
271    /// use ff_encode::VideoEncoder;
272    ///
273    /// let encoder = VideoEncoder::create("output.mp4")?
274    ///     .video(1920, 1080, 30.0)
275    ///     .on_progress(|progress| {
276    ///         println!("Progress: {:.1}%", progress.percent());
277    ///     })
278    ///     .build()?;
279    /// ```
280    #[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    /// Set progress callback using a trait object.
290    ///
291    /// Use this method when you need cancellation support or want to
292    /// use a custom struct implementing `ProgressCallback`.
293    ///
294    /// # Arguments
295    ///
296    /// * `callback` - Progress callback handler
297    ///
298    /// # Examples
299    ///
300    /// ```ignore
301    /// use ff_encode::{VideoEncoder, ProgressCallback, Progress};
302    /// use std::sync::Arc;
303    /// use std::sync::atomic::{AtomicBool, Ordering};
304    ///
305    /// struct CancellableProgress {
306    ///     cancelled: Arc<AtomicBool>,
307    /// }
308    ///
309    /// impl ProgressCallback for CancellableProgress {
310    ///     fn on_progress(&mut self, progress: &Progress) {
311    ///         println!("Progress: {:.1}%", progress.percent());
312    ///     }
313    ///
314    ///     fn should_cancel(&self) -> bool {
315    ///         self.cancelled.load(Ordering::Relaxed)
316    ///     }
317    /// }
318    ///
319    /// let cancelled = Arc::new(AtomicBool::new(false));
320    /// let encoder = VideoEncoder::create("output.mp4")?
321    ///     .video(1920, 1080, 30.0)
322    ///     .progress_callback(CancellableProgress {
323    ///         cancelled: cancelled.clone()
324    ///     })
325    ///     .build()?;
326    /// ```
327    #[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    // === Build ===
334
335    /// Build the encoder.
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if:
340    /// - The configuration is invalid
341    /// - The output file cannot be created
342    /// - No suitable encoder is found for the requested codec
343    pub fn build(self) -> Result<crate::VideoEncoder, EncodeError> {
344        // Validate configuration
345        self.validate()?;
346
347        // Build encoder
348        crate::VideoEncoder::from_builder(self)
349    }
350
351    /// Validate the builder configuration.
352    fn validate(&self) -> Result<(), EncodeError> {
353        // Check if at least one stream is configured
354        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        // Validate video settings
365        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        // Validate audio settings
400        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    /// Build an audio-only encoder with the configured settings.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if the configuration is invalid or encoder creation fails.
426    pub fn build_audio(self) -> Result<crate::AudioEncoder, EncodeError> {
427        crate::AudioEncoder::from_builder(self)
428    }
429}
430
431#[cfg(test)]
432// Tests are allowed to use unwrap() for simplicity
433#[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}