Skip to main content

ff_decode/video/builder/
mod.rs

1//! Video decoder builder for constructing video decoders with custom configuration.
2//!
3//! This module provides the [`VideoDecoderBuilder`] type which enables fluent
4//! configuration of video decoders. Use [`VideoDecoder::open()`] to start building.
5//!
6//! # Examples
7//!
8//! ```ignore
9//! use ff_decode::{VideoDecoder, HardwareAccel};
10//! use ff_format::PixelFormat;
11//!
12//! let decoder = VideoDecoder::open("video.mp4")?
13//!     .output_format(PixelFormat::Rgba)
14//!     .hardware_accel(HardwareAccel::Auto)
15//!     .thread_count(4)
16//!     .build()?;
17//! ```
18
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21use std::time::Duration;
22
23use ff_format::{ContainerInfo, NetworkOptions, PixelFormat, VideoStreamInfo};
24
25use crate::HardwareAccel;
26use crate::error::DecodeError;
27use crate::video::decoder_inner::VideoDecoderInner;
28use ff_common::FramePool;
29
30mod decode;
31mod format;
32mod hw;
33mod network;
34mod scale;
35
36/// Requested output scale for decoded frames.
37///
38/// Controls how `libswscale` resizes the frame in the same pass as pixel-format
39/// conversion. The last setter wins — calling `output_width()` after
40/// `output_size()` replaces the earlier setting.
41///
42/// Both width and height are rounded up to the nearest even number if needed,
43/// because most pixel formats (e.g. `yuv420p`) require even dimensions.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) enum OutputScale {
46    /// Scale to an exact width × height.
47    Exact {
48        /// Target width in pixels.
49        width: u32,
50        /// Target height in pixels.
51        height: u32,
52    },
53    /// Scale to the given width; compute height to preserve aspect ratio.
54    FitWidth(u32),
55    /// Scale to the given height; compute width to preserve aspect ratio.
56    FitHeight(u32),
57}
58
59/// Builder for configuring and constructing a [`VideoDecoder`].
60///
61/// This struct provides a fluent interface for setting up decoder options
62/// before opening a video file. It is created by calling [`VideoDecoder::open()`].
63///
64/// # Examples
65///
66/// ## Basic Usage
67///
68/// ```ignore
69/// use ff_decode::VideoDecoder;
70///
71/// let decoder = VideoDecoder::open("video.mp4")?
72///     .build()?;
73/// ```
74///
75/// ## With Custom Format
76///
77/// ```ignore
78/// use ff_decode::VideoDecoder;
79/// use ff_format::PixelFormat;
80///
81/// let decoder = VideoDecoder::open("video.mp4")?
82///     .output_format(PixelFormat::Rgba)
83///     .build()?;
84/// ```
85///
86/// ## With Hardware Acceleration
87///
88/// ```ignore
89/// use ff_decode::{VideoDecoder, HardwareAccel};
90///
91/// let decoder = VideoDecoder::open("video.mp4")?
92///     .hardware_accel(HardwareAccel::Nvdec)
93///     .build()?;
94/// ```
95///
96/// ## With Frame Pool
97///
98/// ```ignore
99/// use ff_decode::{VideoDecoder, FramePool};
100/// use std::sync::Arc;
101///
102/// let pool: Arc<dyn FramePool> = create_frame_pool();
103/// let decoder = VideoDecoder::open("video.mp4")?
104///     .frame_pool(pool)
105///     .build()?;
106/// ```
107#[derive(Debug)]
108pub struct VideoDecoderBuilder {
109    /// Path to the media file
110    path: PathBuf,
111    /// Output pixel format (None = use source format)
112    output_format: Option<PixelFormat>,
113    /// Output scale (None = use source dimensions)
114    output_scale: Option<OutputScale>,
115    /// Hardware acceleration setting
116    hardware_accel: HardwareAccel,
117    /// Number of decoding threads (0 = auto)
118    thread_count: usize,
119    /// Optional frame pool for memory reuse
120    frame_pool: Option<Arc<dyn FramePool>>,
121    /// Frame rate override for image sequences (default 25 fps when path contains `%`).
122    frame_rate: Option<u32>,
123    /// Network options for URL-based sources (RTMP, RTSP, HTTP, etc.).
124    network_opts: Option<NetworkOptions>,
125}
126
127impl VideoDecoderBuilder {
128    /// Creates a new builder for the specified file path.
129    ///
130    /// This is an internal constructor; use [`VideoDecoder::open()`] instead.
131    pub(crate) fn new(path: PathBuf) -> Self {
132        Self {
133            path,
134            output_format: None,
135            output_scale: None,
136            hardware_accel: HardwareAccel::Auto,
137            thread_count: 0,
138            frame_pool: None,
139            frame_rate: None,
140            network_opts: None,
141        }
142    }
143
144    /// Returns the configured file path.
145    #[must_use]
146    pub fn path(&self) -> &Path {
147        &self.path
148    }
149
150    /// Returns the configured output format, if any.
151    #[must_use]
152    pub fn get_output_format(&self) -> Option<PixelFormat> {
153        self.output_format
154    }
155
156    /// Returns the configured hardware acceleration mode.
157    #[must_use]
158    pub fn get_hardware_accel(&self) -> HardwareAccel {
159        self.hardware_accel
160    }
161
162    /// Returns the configured thread count.
163    #[must_use]
164    pub fn get_thread_count(&self) -> usize {
165        self.thread_count
166    }
167
168    /// Builds the decoder with the configured options.
169    ///
170    /// This method opens the media file, initializes the decoder context,
171    /// and prepares for frame decoding.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if:
176    /// - The file cannot be found ([`DecodeError::FileNotFound`])
177    /// - The file contains no video stream ([`DecodeError::NoVideoStream`])
178    /// - The codec is not supported ([`DecodeError::UnsupportedCodec`])
179    /// - Hardware acceleration is unavailable ([`DecodeError::HwAccelUnavailable`])
180    /// - Other `FFmpeg` errors occur ([`DecodeError::Ffmpeg`])
181    ///
182    /// # Examples
183    ///
184    /// ```ignore
185    /// use ff_decode::VideoDecoder;
186    ///
187    /// let decoder = VideoDecoder::open("video.mp4")?
188    ///     .build()?;
189    ///
190    /// // Start decoding
191    /// for result in &mut decoder {
192    ///     let frame = result?;
193    ///     // Process frame...
194    /// }
195    /// ```
196    pub fn build(self) -> Result<VideoDecoder, DecodeError> {
197        // Validate output scale dimensions before opening the file.
198        // FitWidth / FitHeight aspect-ratio dimensions are resolved at decode time
199        // from the actual source dimensions, so we only reject an explicit zero here.
200        if let Some(scale) = self.output_scale {
201            let (w, h) = match scale {
202                OutputScale::Exact { width, height } => (width, height),
203                OutputScale::FitWidth(w) => (w, 1), // height will be derived
204                OutputScale::FitHeight(h) => (1, h), // width will be derived
205            };
206            if w == 0 || h == 0 {
207                return Err(DecodeError::InvalidOutputDimensions {
208                    width: w,
209                    height: h,
210                });
211            }
212        }
213
214        // Image-sequence patterns contain '%' — the literal path does not exist.
215        // Network URLs must also skip the file-existence check.
216        let path_str = self.path.to_str().unwrap_or("");
217        let is_image_sequence = path_str.contains('%');
218        let is_network_url = crate::network::is_url(path_str);
219        if !is_image_sequence && !is_network_url && !self.path.exists() {
220            return Err(DecodeError::FileNotFound {
221                path: self.path.clone(),
222            });
223        }
224
225        // Create the decoder inner
226        let (inner, stream_info, container_info) = VideoDecoderInner::new(
227            &self.path,
228            self.output_format,
229            self.output_scale,
230            self.hardware_accel,
231            self.thread_count,
232            self.frame_rate,
233            self.frame_pool.clone(),
234            self.network_opts,
235        )?;
236
237        Ok(VideoDecoder {
238            path: self.path,
239            frame_pool: self.frame_pool,
240            inner,
241            stream_info,
242            container_info,
243            fused: false,
244        })
245    }
246}
247
248/// A video decoder for extracting frames from media files.
249///
250/// The decoder provides frame-by-frame access to video content with support
251/// for seeking, hardware acceleration, and format conversion.
252///
253/// # Construction
254///
255/// Use [`VideoDecoder::open()`] to create a builder, then call [`VideoDecoderBuilder::build()`]:
256///
257/// ```ignore
258/// use ff_decode::VideoDecoder;
259/// use ff_format::PixelFormat;
260///
261/// let decoder = VideoDecoder::open("video.mp4")?
262///     .output_format(PixelFormat::Rgba)
263///     .build()?;
264/// ```
265///
266/// # Frame Decoding
267///
268/// Frames can be decoded one at a time or using the built-in iterator:
269///
270/// ```ignore
271/// // Decode one frame
272/// if let Some(frame) = decoder.decode_one()? {
273///     println!("Frame at {:?}", frame.timestamp().as_duration());
274/// }
275///
276/// // Iterator form — VideoDecoder implements Iterator directly
277/// for result in &mut decoder {
278///     let frame = result?;
279///     // Process frame...
280/// }
281/// ```
282///
283/// # Seeking
284///
285/// The decoder supports efficient seeking:
286///
287/// ```ignore
288/// use ff_decode::SeekMode;
289/// use std::time::Duration;
290///
291/// // Seek to 30 seconds (keyframe)
292/// decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;
293///
294/// // Seek to exact frame
295/// decoder.seek(Duration::from_secs(30), SeekMode::Exact)?;
296/// ```
297pub struct VideoDecoder {
298    /// Path to the media file
299    path: PathBuf,
300    /// Optional frame pool for memory reuse
301    frame_pool: Option<Arc<dyn FramePool>>,
302    /// Internal decoder state
303    inner: VideoDecoderInner,
304    /// Video stream information
305    stream_info: VideoStreamInfo,
306    /// Container-level metadata
307    container_info: ContainerInfo,
308    /// Set to `true` after a decoding error; causes [`Iterator::next`] to return `None`.
309    fused: bool,
310}
311
312impl VideoDecoder {
313    /// Opens a media file and returns a builder for configuring the decoder.
314    ///
315    /// This is the entry point for creating a decoder. The returned builder
316    /// allows setting options before the decoder is fully initialized.
317    ///
318    /// # Arguments
319    ///
320    /// * `path` - Path to the media file to decode.
321    ///
322    /// # Examples
323    ///
324    /// ```ignore
325    /// use ff_decode::VideoDecoder;
326    ///
327    /// // Simple usage
328    /// let decoder = VideoDecoder::open("video.mp4")?
329    ///     .build()?;
330    ///
331    /// // With options
332    /// let decoder = VideoDecoder::open("video.mp4")?
333    ///     .output_format(PixelFormat::Rgba)
334    ///     .hardware_accel(HardwareAccel::Auto)
335    ///     .build()?;
336    /// ```
337    ///
338    /// # Note
339    ///
340    /// This method does not validate that the file exists or is a valid
341    /// media file. Validation occurs when [`VideoDecoderBuilder::build()`] is called.
342    pub fn open(path: impl AsRef<Path>) -> VideoDecoderBuilder {
343        VideoDecoderBuilder::new(path.as_ref().to_path_buf())
344    }
345
346    // =========================================================================
347    // Information Methods
348    // =========================================================================
349
350    /// Returns the video stream information.
351    ///
352    /// This contains metadata about the video stream including resolution,
353    /// frame rate, codec, and color characteristics.
354    #[must_use]
355    pub fn stream_info(&self) -> &VideoStreamInfo {
356        &self.stream_info
357    }
358
359    /// Returns the video width in pixels.
360    #[must_use]
361    pub fn width(&self) -> u32 {
362        self.stream_info.width()
363    }
364
365    /// Returns the video height in pixels.
366    #[must_use]
367    pub fn height(&self) -> u32 {
368        self.stream_info.height()
369    }
370
371    /// Returns the frame rate in frames per second.
372    #[must_use]
373    pub fn frame_rate(&self) -> f64 {
374        self.stream_info.fps()
375    }
376
377    /// Returns the total duration of the video.
378    ///
379    /// Returns [`Duration::ZERO`] if duration is unknown.
380    #[must_use]
381    pub fn duration(&self) -> Duration {
382        self.stream_info.duration().unwrap_or(Duration::ZERO)
383    }
384
385    /// Returns the total duration of the video, or `None` for live streams
386    /// or formats that do not carry duration information.
387    #[must_use]
388    pub fn duration_opt(&self) -> Option<Duration> {
389        self.stream_info.duration()
390    }
391
392    /// Returns container-level metadata (format name, bitrate, stream count).
393    #[must_use]
394    pub fn container_info(&self) -> &ContainerInfo {
395        &self.container_info
396    }
397
398    /// Returns the current playback position.
399    #[must_use]
400    pub fn position(&self) -> Duration {
401        self.inner.position()
402    }
403
404    /// Returns `true` if the end of stream has been reached.
405    #[must_use]
406    pub fn is_eof(&self) -> bool {
407        self.inner.is_eof()
408    }
409
410    /// Returns the file path being decoded.
411    #[must_use]
412    pub fn path(&self) -> &Path {
413        &self.path
414    }
415
416    /// Returns a reference to the frame pool, if configured.
417    #[must_use]
418    pub fn frame_pool(&self) -> Option<&Arc<dyn FramePool>> {
419        self.frame_pool.as_ref()
420    }
421
422    /// Returns the currently active hardware acceleration mode.
423    ///
424    /// This method returns the actual hardware acceleration being used,
425    /// which may differ from what was requested:
426    ///
427    /// - If [`HardwareAccel::Auto`] was requested, this returns the specific
428    ///   accelerator that was successfully initialized (e.g., [`HardwareAccel::Nvdec`]),
429    ///   or [`HardwareAccel::None`] if no hardware acceleration is available.
430    /// - If a specific accelerator was requested and initialization failed,
431    ///   the decoder creation would have returned an error.
432    /// - If [`HardwareAccel::None`] was requested, this always returns [`HardwareAccel::None`].
433    ///
434    /// # Examples
435    ///
436    /// ```ignore
437    /// use ff_decode::{VideoDecoder, HardwareAccel};
438    ///
439    /// // Request automatic hardware acceleration
440    /// let decoder = VideoDecoder::open("video.mp4")?
441    ///     .hardware_accel(HardwareAccel::Auto)
442    ///     .build()?;
443    ///
444    /// // Check which accelerator was selected
445    /// match decoder.hardware_accel() {
446    ///     HardwareAccel::None => println!("Using software decoding"),
447    ///     HardwareAccel::Nvdec => println!("Using NVIDIA NVDEC"),
448    ///     HardwareAccel::Qsv => println!("Using Intel Quick Sync"),
449    ///     HardwareAccel::VideoToolbox => println!("Using Apple VideoToolbox"),
450    ///     HardwareAccel::Vaapi => println!("Using VA-API"),
451    ///     HardwareAccel::Amf => println!("Using AMD AMF"),
452    ///     _ => unreachable!(),
453    /// }
454    /// ```
455    #[must_use]
456    pub fn hardware_accel(&self) -> HardwareAccel {
457        self.inner.hardware_accel()
458    }
459}
460
461#[cfg(test)]
462#[allow(clippy::panic, clippy::expect_used)]
463mod tests {
464    use super::*;
465    use std::path::PathBuf;
466
467    #[test]
468    fn builder_default_values_should_have_auto_hw_and_zero_threads() {
469        let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"));
470
471        assert_eq!(builder.path(), Path::new("test.mp4"));
472        assert!(builder.get_output_format().is_none());
473        assert_eq!(builder.get_hardware_accel(), HardwareAccel::Auto);
474        assert_eq!(builder.get_thread_count(), 0);
475    }
476
477    #[test]
478    fn builder_chaining_should_set_all_fields() {
479        let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"))
480            .output_format(PixelFormat::Bgra)
481            .hardware_accel(HardwareAccel::Qsv)
482            .thread_count(4);
483
484        assert_eq!(builder.get_output_format(), Some(PixelFormat::Bgra));
485        assert_eq!(builder.get_hardware_accel(), HardwareAccel::Qsv);
486        assert_eq!(builder.get_thread_count(), 4);
487    }
488
489    #[test]
490    fn decoder_open_should_return_builder_with_path() {
491        let builder = VideoDecoder::open("video.mp4");
492        assert_eq!(builder.path(), Path::new("video.mp4"));
493    }
494
495    #[test]
496    fn decoder_open_pathbuf_should_preserve_path() {
497        let path = PathBuf::from("/path/to/video.mp4");
498        let builder = VideoDecoder::open(&path);
499        assert_eq!(builder.path(), path.as_path());
500    }
501
502    #[test]
503    fn build_nonexistent_file_should_return_file_not_found() {
504        let result = VideoDecoder::open("nonexistent_file_12345.mp4").build();
505
506        assert!(result.is_err());
507        match result {
508            Err(DecodeError::FileNotFound { path }) => {
509                assert!(
510                    path.to_string_lossy()
511                        .contains("nonexistent_file_12345.mp4")
512                );
513            }
514            Err(e) => panic!("Expected FileNotFound error, got: {e:?}"),
515            Ok(_) => panic!("Expected error, got Ok"),
516        }
517    }
518
519    #[test]
520    fn build_invalid_video_file_should_fail() {
521        // Create a temporary test file (not a valid video)
522        let temp_dir = std::env::temp_dir();
523        let test_file = temp_dir.join("ff_decode_test_file.txt");
524        std::fs::write(&test_file, "test").expect("Failed to create test file");
525
526        let result = VideoDecoder::open(&test_file).build();
527
528        // Clean up
529        let _ = std::fs::remove_file(&test_file);
530
531        // The build should fail (not a valid video file)
532        assert!(result.is_err());
533        if let Err(e) = result {
534            // Should get either NoVideoStream or Ffmpeg error
535            assert!(
536                matches!(e, DecodeError::NoVideoStream { .. })
537                    || matches!(e, DecodeError::Ffmpeg { .. })
538            );
539        }
540    }
541}