Skip to main content

ff_decode/video/
builder.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, VideoFrame, VideoStreamInfo};
24
25use crate::HardwareAccel;
26use crate::error::DecodeError;
27use crate::video::decoder_inner::VideoDecoderInner;
28use ff_common::FramePool;
29
30/// Requested output scale for decoded frames.
31///
32/// Controls how `libswscale` resizes the frame in the same pass as pixel-format
33/// conversion. The last setter wins — calling `output_width()` after
34/// `output_size()` replaces the earlier setting.
35///
36/// Both width and height are rounded up to the nearest even number if needed,
37/// because most pixel formats (e.g. `yuv420p`) require even dimensions.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub(crate) enum OutputScale {
40    /// Scale to an exact width × height.
41    Exact {
42        /// Target width in pixels.
43        width: u32,
44        /// Target height in pixels.
45        height: u32,
46    },
47    /// Scale to the given width; compute height to preserve aspect ratio.
48    FitWidth(u32),
49    /// Scale to the given height; compute width to preserve aspect ratio.
50    FitHeight(u32),
51}
52
53/// Builder for configuring and constructing a [`VideoDecoder`].
54///
55/// This struct provides a fluent interface for setting up decoder options
56/// before opening a video file. It is created by calling [`VideoDecoder::open()`].
57///
58/// # Examples
59///
60/// ## Basic Usage
61///
62/// ```ignore
63/// use ff_decode::VideoDecoder;
64///
65/// let decoder = VideoDecoder::open("video.mp4")?
66///     .build()?;
67/// ```
68///
69/// ## With Custom Format
70///
71/// ```ignore
72/// use ff_decode::VideoDecoder;
73/// use ff_format::PixelFormat;
74///
75/// let decoder = VideoDecoder::open("video.mp4")?
76///     .output_format(PixelFormat::Rgba)
77///     .build()?;
78/// ```
79///
80/// ## With Hardware Acceleration
81///
82/// ```ignore
83/// use ff_decode::{VideoDecoder, HardwareAccel};
84///
85/// let decoder = VideoDecoder::open("video.mp4")?
86///     .hardware_accel(HardwareAccel::Nvdec)
87///     .build()?;
88/// ```
89///
90/// ## With Frame Pool
91///
92/// ```ignore
93/// use ff_decode::{VideoDecoder, FramePool};
94/// use std::sync::Arc;
95///
96/// let pool: Arc<dyn FramePool> = create_frame_pool();
97/// let decoder = VideoDecoder::open("video.mp4")?
98///     .frame_pool(pool)
99///     .build()?;
100/// ```
101#[derive(Debug)]
102pub struct VideoDecoderBuilder {
103    /// Path to the media file
104    path: PathBuf,
105    /// Output pixel format (None = use source format)
106    output_format: Option<PixelFormat>,
107    /// Output scale (None = use source dimensions)
108    output_scale: Option<OutputScale>,
109    /// Hardware acceleration setting
110    hardware_accel: HardwareAccel,
111    /// Number of decoding threads (0 = auto)
112    thread_count: usize,
113    /// Optional frame pool for memory reuse
114    frame_pool: Option<Arc<dyn FramePool>>,
115    /// Frame rate override for image sequences (default 25 fps when path contains `%`).
116    frame_rate: Option<u32>,
117    /// Network options for URL-based sources (RTMP, RTSP, HTTP, etc.).
118    network_opts: Option<NetworkOptions>,
119}
120
121impl VideoDecoderBuilder {
122    /// Creates a new builder for the specified file path.
123    ///
124    /// This is an internal constructor; use [`VideoDecoder::open()`] instead.
125    pub(crate) fn new(path: PathBuf) -> Self {
126        Self {
127            path,
128            output_format: None,
129            output_scale: None,
130            hardware_accel: HardwareAccel::Auto,
131            thread_count: 0,
132            frame_pool: None,
133            frame_rate: None,
134            network_opts: None,
135        }
136    }
137
138    /// Sets the output pixel format for decoded frames.
139    ///
140    /// If not set, frames are returned in the source format. Setting an
141    /// output format enables automatic conversion during decoding.
142    ///
143    /// # Common Formats
144    ///
145    /// - [`PixelFormat::Rgba`] - Best for UI rendering, includes alpha
146    /// - [`PixelFormat::Rgb24`] - RGB without alpha, smaller memory footprint
147    /// - [`PixelFormat::Yuv420p`] - Source format for most H.264/H.265 videos
148    ///
149    /// # Examples
150    ///
151    /// ```ignore
152    /// use ff_decode::VideoDecoder;
153    /// use ff_format::PixelFormat;
154    ///
155    /// let decoder = VideoDecoder::open("video.mp4")?
156    ///     .output_format(PixelFormat::Rgba)
157    ///     .build()?;
158    /// ```
159    #[must_use]
160    pub fn output_format(mut self, format: PixelFormat) -> Self {
161        self.output_format = Some(format);
162        self
163    }
164
165    /// Scales decoded frames to the given exact dimensions.
166    ///
167    /// The frame is scaled in the same `libswscale` pass as pixel-format
168    /// conversion, so there is no extra copy. If `output_format` is not set,
169    /// the source pixel format is preserved while scaling.
170    ///
171    /// Width and height must be greater than zero. They are rounded up to the
172    /// nearest even number if necessary (required by most pixel formats).
173    ///
174    /// Calling this method overwrites any previous `output_width` or
175    /// `output_height` call. The last setter wins.
176    ///
177    /// # Errors
178    ///
179    /// [`build()`](Self::build) returns [`DecodeError::InvalidOutputDimensions`]
180    /// if either dimension is zero after rounding.
181    ///
182    /// # Examples
183    ///
184    /// ```ignore
185    /// use ff_decode::VideoDecoder;
186    ///
187    /// // Decode every frame at 320×240
188    /// let decoder = VideoDecoder::open("video.mp4")?
189    ///     .output_size(320, 240)
190    ///     .build()?;
191    /// ```
192    #[must_use]
193    pub fn output_size(mut self, width: u32, height: u32) -> Self {
194        self.output_scale = Some(OutputScale::Exact { width, height });
195        self
196    }
197
198    /// Scales decoded frames to the given width, preserving the aspect ratio.
199    ///
200    /// The height is computed from the source aspect ratio and rounded to the
201    /// nearest even number. Calling this method overwrites any previous
202    /// `output_size` or `output_height` call. The last setter wins.
203    ///
204    /// # Errors
205    ///
206    /// [`build()`](Self::build) returns [`DecodeError::InvalidOutputDimensions`]
207    /// if `width` is zero.
208    ///
209    /// # Examples
210    ///
211    /// ```ignore
212    /// use ff_decode::VideoDecoder;
213    ///
214    /// // Decode at 1280 px wide, preserving aspect ratio
215    /// let decoder = VideoDecoder::open("video.mp4")?
216    ///     .output_width(1280)
217    ///     .build()?;
218    /// ```
219    #[must_use]
220    pub fn output_width(mut self, width: u32) -> Self {
221        self.output_scale = Some(OutputScale::FitWidth(width));
222        self
223    }
224
225    /// Scales decoded frames to the given height, preserving the aspect ratio.
226    ///
227    /// The width is computed from the source aspect ratio and rounded to the
228    /// nearest even number. Calling this method overwrites any previous
229    /// `output_size` or `output_width` call. The last setter wins.
230    ///
231    /// # Errors
232    ///
233    /// [`build()`](Self::build) returns [`DecodeError::InvalidOutputDimensions`]
234    /// if `height` is zero.
235    ///
236    /// # Examples
237    ///
238    /// ```ignore
239    /// use ff_decode::VideoDecoder;
240    ///
241    /// // Decode at 720 px tall, preserving aspect ratio
242    /// let decoder = VideoDecoder::open("video.mp4")?
243    ///     .output_height(720)
244    ///     .build()?;
245    /// ```
246    #[must_use]
247    pub fn output_height(mut self, height: u32) -> Self {
248        self.output_scale = Some(OutputScale::FitHeight(height));
249        self
250    }
251
252    /// Sets the hardware acceleration mode.
253    ///
254    /// Hardware acceleration can significantly improve decoding performance,
255    /// especially for high-resolution video (4K and above).
256    ///
257    /// # Available Modes
258    ///
259    /// - [`HardwareAccel::Auto`] - Automatically detect and use available hardware (default)
260    /// - [`HardwareAccel::None`] - Disable hardware acceleration (CPU only)
261    /// - [`HardwareAccel::Nvdec`] - NVIDIA NVDEC (requires NVIDIA GPU)
262    /// - [`HardwareAccel::Qsv`] - Intel Quick Sync Video
263    /// - [`HardwareAccel::Amf`] - AMD Advanced Media Framework
264    /// - [`HardwareAccel::VideoToolbox`] - Apple `VideoToolbox` (macOS/iOS)
265    /// - [`HardwareAccel::Vaapi`] - VA-API (Linux)
266    ///
267    /// # Fallback Behavior
268    ///
269    /// If the requested hardware accelerator is unavailable, the decoder
270    /// will fall back to software decoding unless
271    /// [`DecodeError::HwAccelUnavailable`] is explicitly requested.
272    ///
273    /// # Examples
274    ///
275    /// ```ignore
276    /// use ff_decode::{VideoDecoder, HardwareAccel};
277    ///
278    /// // Use NVIDIA NVDEC if available
279    /// let decoder = VideoDecoder::open("video.mp4")?
280    ///     .hardware_accel(HardwareAccel::Nvdec)
281    ///     .build()?;
282    ///
283    /// // Force CPU decoding
284    /// let cpu_decoder = Decoder::open("video.mp4")?
285    ///     .hardware_accel(HardwareAccel::None)
286    ///     .build()?;
287    /// ```
288    #[must_use]
289    pub fn hardware_accel(mut self, accel: HardwareAccel) -> Self {
290        self.hardware_accel = accel;
291        self
292    }
293
294    /// Sets the number of decoding threads.
295    ///
296    /// More threads can improve decoding throughput, especially for
297    /// high-resolution videos or codecs that support parallel decoding.
298    ///
299    /// # Thread Count Values
300    ///
301    /// - `0` - Auto-detect based on CPU cores (default)
302    /// - `1` - Single-threaded decoding
303    /// - `N` - Use N threads for decoding
304    ///
305    /// # Performance Notes
306    ///
307    /// - H.264/H.265: Benefit significantly from multi-threading
308    /// - VP9: Good parallel decoding support
309    /// - `ProRes`: Limited threading benefit
310    ///
311    /// Setting too many threads may increase memory usage without
312    /// proportional performance gains.
313    ///
314    /// # Examples
315    ///
316    /// ```ignore
317    /// use ff_decode::VideoDecoder;
318    ///
319    /// // Use 4 threads for decoding
320    /// let decoder = VideoDecoder::open("video.mp4")?
321    ///     .thread_count(4)
322    ///     .build()?;
323    ///
324    /// // Single-threaded for minimal memory
325    /// let decoder = VideoDecoder::open("video.mp4")?
326    ///     .thread_count(1)
327    ///     .build()?;
328    /// ```
329    #[must_use]
330    pub fn thread_count(mut self, count: usize) -> Self {
331        self.thread_count = count;
332        self
333    }
334
335    /// Sets the frame rate for image sequence decoding.
336    ///
337    /// Only used when the path contains `%` (e.g. `"frames/frame%04d.png"`).
338    /// Defaults to 25 fps when not set.
339    ///
340    /// # Examples
341    ///
342    /// ```ignore
343    /// use ff_decode::VideoDecoder;
344    ///
345    /// let decoder = VideoDecoder::open("frames/frame%04d.png")?
346    ///     .frame_rate(30)
347    ///     .build()?;
348    /// ```
349    #[must_use]
350    pub fn frame_rate(mut self, fps: u32) -> Self {
351        self.frame_rate = Some(fps);
352        self
353    }
354
355    /// Sets network options for URL-based sources.
356    ///
357    /// When set, the builder skips the file-existence check and passes connect
358    /// and read timeouts to `avformat_open_input` via an `AVDictionary`.
359    /// Call this before `.build()` when opening `rtmp://`, `rtsp://`, `http://`,
360    /// `https://`, `udp://`, `srt://`, or `rtp://` URLs.
361    ///
362    /// # HLS / M3U8 Playlists
363    ///
364    /// HLS playlists (`.m3u8`) are detected automatically by `FFmpeg` — no extra
365    /// configuration is required beyond calling `.network()`. Pass the full
366    /// HTTP(S) URL of the master or media playlist:
367    ///
368    /// ```ignore
369    /// use ff_decode::VideoDecoder;
370    /// use ff_format::NetworkOptions;
371    ///
372    /// let decoder = VideoDecoder::open("https://example.com/live/index.m3u8")
373    ///     .network(NetworkOptions::default())
374    ///     .build()?;
375    /// ```
376    ///
377    /// # DASH / MPD Streams
378    ///
379    /// MPEG-DASH manifests (`.mpd`) are detected automatically by `FFmpeg`'s
380    /// built-in `dash` demuxer. The demuxer downloads the manifest, selects the
381    /// highest-quality representation, and fetches segments automatically:
382    ///
383    /// ```ignore
384    /// use ff_decode::VideoDecoder;
385    /// use ff_format::NetworkOptions;
386    ///
387    /// let decoder = VideoDecoder::open("https://example.com/dash/manifest.mpd")
388    ///     .network(NetworkOptions::default())
389    ///     .build()?;
390    /// ```
391    ///
392    /// **Multi-period caveat**: multi-period DASH streams are supported by
393    /// `FFmpeg` but period boundaries may trigger an internal decoder reset,
394    /// which can cause a brief gap in decoded frames.
395    ///
396    /// **Adaptive bitrate**: representation selection (ABR switching) is handled
397    /// internally by `FFmpeg` and is not exposed through this API.
398    ///
399    /// # UDP / MPEG-TS
400    ///
401    /// `udp://` URLs are always live — `is_live()` returns `true` and seeking
402    /// is not supported. Two extra `AVDictionary` options are set automatically
403    /// to reduce packet loss on high-bitrate streams:
404    ///
405    /// | Option | Value | Reason |
406    /// |---|---|---|
407    /// | `buffer_size` | `65536` | Enlarges the UDP receive buffer |
408    /// | `overrun_nonfatal` | `1` | Discards excess data instead of erroring |
409    ///
410    /// ```ignore
411    /// use ff_decode::VideoDecoder;
412    /// use ff_format::NetworkOptions;
413    ///
414    /// let decoder = VideoDecoder::open("udp://224.0.0.1:1234")
415    ///     .network(NetworkOptions::default())
416    ///     .build()?;
417    /// ```
418    ///
419    /// # SRT (Secure Reliable Transport)
420    ///
421    /// SRT URLs (`srt://host:port`) require the `srt` feature flag **and** a
422    /// libsrt-enabled `FFmpeg` build.  Enable the feature in `Cargo.toml`:
423    ///
424    /// ```toml
425    /// [dependencies]
426    /// ff-decode = { version = "*", features = ["srt"] }
427    /// ```
428    ///
429    /// Without the `srt` feature, opening an `srt://` URL returns
430    /// [`DecodeError::ConnectionFailed`]. If the feature is enabled but the
431    /// linked `FFmpeg` was not built with `--enable-libsrt`, the same error is
432    /// returned with a message directing you to rebuild `FFmpeg`.
433    ///
434    /// ```ignore
435    /// use ff_decode::VideoDecoder;
436    /// use ff_format::NetworkOptions;
437    ///
438    /// let decoder = VideoDecoder::open("srt://ingest.example.com:4200")
439    ///     .network(NetworkOptions::default())
440    ///     .build()?;
441    /// ```
442    ///
443    /// # Credentials
444    ///
445    /// HTTP basic-auth credentials must be embedded directly in the URL:
446    /// `https://user:password@cdn.example.com/live/index.m3u8`.
447    /// The password is redacted in log output.
448    ///
449    /// # DRM Limitation
450    ///
451    /// DRM-protected streams are **not** supported:
452    /// - HLS: `FairPlay`, Widevine, AES-128 with external key servers
453    /// - DASH: CENC, `PlayReady`, Widevine
454    ///
455    /// `FFmpeg` can parse the manifest and fetch segments, but key delivery
456    /// to a DRM license server is outside the scope of this API.
457    ///
458    /// # Examples
459    ///
460    /// ```ignore
461    /// use ff_decode::VideoDecoder;
462    /// use ff_format::NetworkOptions;
463    ///
464    /// let decoder = VideoDecoder::open("rtmp://live.example.com/app/stream_key")
465    ///     .network(NetworkOptions::default())
466    ///     .build()?;
467    /// ```
468    #[must_use]
469    pub fn network(mut self, opts: NetworkOptions) -> Self {
470        self.network_opts = Some(opts);
471        self
472    }
473
474    /// Sets a frame pool for memory reuse.
475    ///
476    /// Using a frame pool can significantly reduce allocation overhead
477    /// during continuous video playback by reusing frame buffers.
478    ///
479    /// # Memory Management
480    ///
481    /// When a frame pool is set:
482    /// - Decoded frames attempt to acquire buffers from the pool
483    /// - When frames are dropped, their buffers are returned to the pool
484    /// - If the pool is exhausted, new buffers are allocated normally
485    ///
486    /// # Examples
487    ///
488    /// ```ignore
489    /// use ff_decode::{VideoDecoder, FramePool, PooledBuffer};
490    /// use std::sync::{Arc, Mutex};
491    ///
492    /// // Create a simple frame pool
493    /// struct SimplePool {
494    ///     buffers: Mutex<Vec<Vec<u8>>>,
495    /// }
496    ///
497    /// impl FramePool for SimplePool {
498    ///     fn acquire(&self, size: usize) -> Option<PooledBuffer> {
499    ///         // Implementation...
500    ///         None
501    ///     }
502    /// }
503    ///
504    /// let pool = Arc::new(SimplePool {
505    ///     buffers: Mutex::new(vec![]),
506    /// });
507    ///
508    /// let decoder = VideoDecoder::open("video.mp4")?
509    ///     .frame_pool(pool)
510    ///     .build()?;
511    /// ```
512    #[must_use]
513    pub fn frame_pool(mut self, pool: Arc<dyn FramePool>) -> Self {
514        self.frame_pool = Some(pool);
515        self
516    }
517
518    /// Returns the configured file path.
519    #[must_use]
520    pub fn path(&self) -> &Path {
521        &self.path
522    }
523
524    /// Returns the configured output format, if any.
525    #[must_use]
526    pub fn get_output_format(&self) -> Option<PixelFormat> {
527        self.output_format
528    }
529
530    /// Returns the configured hardware acceleration mode.
531    #[must_use]
532    pub fn get_hardware_accel(&self) -> HardwareAccel {
533        self.hardware_accel
534    }
535
536    /// Returns the configured thread count.
537    #[must_use]
538    pub fn get_thread_count(&self) -> usize {
539        self.thread_count
540    }
541
542    /// Builds the decoder with the configured options.
543    ///
544    /// This method opens the media file, initializes the decoder context,
545    /// and prepares for frame decoding.
546    ///
547    /// # Errors
548    ///
549    /// Returns an error if:
550    /// - The file cannot be found ([`DecodeError::FileNotFound`])
551    /// - The file contains no video stream ([`DecodeError::NoVideoStream`])
552    /// - The codec is not supported ([`DecodeError::UnsupportedCodec`])
553    /// - Hardware acceleration is unavailable ([`DecodeError::HwAccelUnavailable`])
554    /// - Other `FFmpeg` errors occur ([`DecodeError::Ffmpeg`])
555    ///
556    /// # Examples
557    ///
558    /// ```ignore
559    /// use ff_decode::VideoDecoder;
560    ///
561    /// let decoder = VideoDecoder::open("video.mp4")?
562    ///     .build()?;
563    ///
564    /// // Start decoding
565    /// for result in &mut decoder {
566    ///     let frame = result?;
567    ///     // Process frame...
568    /// }
569    /// ```
570    pub fn build(self) -> Result<VideoDecoder, DecodeError> {
571        // Validate output scale dimensions before opening the file.
572        // FitWidth / FitHeight aspect-ratio dimensions are resolved at decode time
573        // from the actual source dimensions, so we only reject an explicit zero here.
574        if let Some(scale) = self.output_scale {
575            let (w, h) = match scale {
576                OutputScale::Exact { width, height } => (width, height),
577                OutputScale::FitWidth(w) => (w, 1), // height will be derived
578                OutputScale::FitHeight(h) => (1, h), // width will be derived
579            };
580            if w == 0 || h == 0 {
581                return Err(DecodeError::InvalidOutputDimensions {
582                    width: w,
583                    height: h,
584                });
585            }
586        }
587
588        // Image-sequence patterns contain '%' — the literal path does not exist.
589        // Network URLs must also skip the file-existence check.
590        let path_str = self.path.to_str().unwrap_or("");
591        let is_image_sequence = path_str.contains('%');
592        let is_network_url = crate::network::is_url(path_str);
593        if !is_image_sequence && !is_network_url && !self.path.exists() {
594            return Err(DecodeError::FileNotFound {
595                path: self.path.clone(),
596            });
597        }
598
599        // Create the decoder inner
600        let (inner, stream_info, container_info) = VideoDecoderInner::new(
601            &self.path,
602            self.output_format,
603            self.output_scale,
604            self.hardware_accel,
605            self.thread_count,
606            self.frame_rate,
607            self.frame_pool.clone(),
608            self.network_opts,
609        )?;
610
611        Ok(VideoDecoder {
612            path: self.path,
613            frame_pool: self.frame_pool,
614            inner,
615            stream_info,
616            container_info,
617            fused: false,
618        })
619    }
620}
621
622/// A video decoder for extracting frames from media files.
623///
624/// The decoder provides frame-by-frame access to video content with support
625/// for seeking, hardware acceleration, and format conversion.
626///
627/// # Construction
628///
629/// Use [`VideoDecoder::open()`] to create a builder, then call [`VideoDecoderBuilder::build()`]:
630///
631/// ```ignore
632/// use ff_decode::VideoDecoder;
633/// use ff_format::PixelFormat;
634///
635/// let decoder = VideoDecoder::open("video.mp4")?
636///     .output_format(PixelFormat::Rgba)
637///     .build()?;
638/// ```
639///
640/// # Frame Decoding
641///
642/// Frames can be decoded one at a time or using the built-in iterator:
643///
644/// ```ignore
645/// // Decode one frame
646/// if let Some(frame) = decoder.decode_one()? {
647///     println!("Frame at {:?}", frame.timestamp().as_duration());
648/// }
649///
650/// // Iterator form — VideoDecoder implements Iterator directly
651/// for result in &mut decoder {
652///     let frame = result?;
653///     // Process frame...
654/// }
655/// ```
656///
657/// # Seeking
658///
659/// The decoder supports efficient seeking:
660///
661/// ```ignore
662/// use ff_decode::SeekMode;
663/// use std::time::Duration;
664///
665/// // Seek to 30 seconds (keyframe)
666/// decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;
667///
668/// // Seek to exact frame
669/// decoder.seek(Duration::from_secs(30), SeekMode::Exact)?;
670/// ```
671pub struct VideoDecoder {
672    /// Path to the media file
673    path: PathBuf,
674    /// Optional frame pool for memory reuse
675    frame_pool: Option<Arc<dyn FramePool>>,
676    /// Internal decoder state
677    inner: VideoDecoderInner,
678    /// Video stream information
679    stream_info: VideoStreamInfo,
680    /// Container-level metadata
681    container_info: ContainerInfo,
682    /// Set to `true` after a decoding error; causes [`Iterator::next`] to return `None`.
683    fused: bool,
684}
685
686impl VideoDecoder {
687    /// Opens a media file and returns a builder for configuring the decoder.
688    ///
689    /// This is the entry point for creating a decoder. The returned builder
690    /// allows setting options before the decoder is fully initialized.
691    ///
692    /// # Arguments
693    ///
694    /// * `path` - Path to the media file to decode.
695    ///
696    /// # Examples
697    ///
698    /// ```ignore
699    /// use ff_decode::VideoDecoder;
700    ///
701    /// // Simple usage
702    /// let decoder = VideoDecoder::open("video.mp4")?
703    ///     .build()?;
704    ///
705    /// // With options
706    /// let decoder = VideoDecoder::open("video.mp4")?
707    ///     .output_format(PixelFormat::Rgba)
708    ///     .hardware_accel(HardwareAccel::Auto)
709    ///     .build()?;
710    /// ```
711    ///
712    /// # Note
713    ///
714    /// This method does not validate that the file exists or is a valid
715    /// media file. Validation occurs when [`VideoDecoderBuilder::build()`] is called.
716    pub fn open(path: impl AsRef<Path>) -> VideoDecoderBuilder {
717        VideoDecoderBuilder::new(path.as_ref().to_path_buf())
718    }
719
720    // =========================================================================
721    // Information Methods
722    // =========================================================================
723
724    /// Returns the video stream information.
725    ///
726    /// This contains metadata about the video stream including resolution,
727    /// frame rate, codec, and color characteristics.
728    #[must_use]
729    pub fn stream_info(&self) -> &VideoStreamInfo {
730        &self.stream_info
731    }
732
733    /// Returns the video width in pixels.
734    #[must_use]
735    pub fn width(&self) -> u32 {
736        self.stream_info.width()
737    }
738
739    /// Returns the video height in pixels.
740    #[must_use]
741    pub fn height(&self) -> u32 {
742        self.stream_info.height()
743    }
744
745    /// Returns the frame rate in frames per second.
746    #[must_use]
747    pub fn frame_rate(&self) -> f64 {
748        self.stream_info.fps()
749    }
750
751    /// Returns the total duration of the video.
752    ///
753    /// Returns [`Duration::ZERO`] if duration is unknown.
754    #[must_use]
755    pub fn duration(&self) -> Duration {
756        self.stream_info.duration().unwrap_or(Duration::ZERO)
757    }
758
759    /// Returns the total duration of the video, or `None` for live streams
760    /// or formats that do not carry duration information.
761    #[must_use]
762    pub fn duration_opt(&self) -> Option<Duration> {
763        self.stream_info.duration()
764    }
765
766    /// Returns container-level metadata (format name, bitrate, stream count).
767    #[must_use]
768    pub fn container_info(&self) -> &ContainerInfo {
769        &self.container_info
770    }
771
772    /// Returns the current playback position.
773    #[must_use]
774    pub fn position(&self) -> Duration {
775        self.inner.position()
776    }
777
778    /// Returns `true` if the end of stream has been reached.
779    #[must_use]
780    pub fn is_eof(&self) -> bool {
781        self.inner.is_eof()
782    }
783
784    /// Returns the file path being decoded.
785    #[must_use]
786    pub fn path(&self) -> &Path {
787        &self.path
788    }
789
790    /// Returns a reference to the frame pool, if configured.
791    #[must_use]
792    pub fn frame_pool(&self) -> Option<&Arc<dyn FramePool>> {
793        self.frame_pool.as_ref()
794    }
795
796    /// Returns the currently active hardware acceleration mode.
797    ///
798    /// This method returns the actual hardware acceleration being used,
799    /// which may differ from what was requested:
800    ///
801    /// - If [`HardwareAccel::Auto`] was requested, this returns the specific
802    ///   accelerator that was successfully initialized (e.g., [`HardwareAccel::Nvdec`]),
803    ///   or [`HardwareAccel::None`] if no hardware acceleration is available.
804    /// - If a specific accelerator was requested and initialization failed,
805    ///   the decoder creation would have returned an error.
806    /// - If [`HardwareAccel::None`] was requested, this always returns [`HardwareAccel::None`].
807    ///
808    /// # Examples
809    ///
810    /// ```ignore
811    /// use ff_decode::{VideoDecoder, HardwareAccel};
812    ///
813    /// // Request automatic hardware acceleration
814    /// let decoder = VideoDecoder::open("video.mp4")?
815    ///     .hardware_accel(HardwareAccel::Auto)
816    ///     .build()?;
817    ///
818    /// // Check which accelerator was selected
819    /// match decoder.hardware_accel() {
820    ///     HardwareAccel::None => println!("Using software decoding"),
821    ///     HardwareAccel::Nvdec => println!("Using NVIDIA NVDEC"),
822    ///     HardwareAccel::Qsv => println!("Using Intel Quick Sync"),
823    ///     HardwareAccel::VideoToolbox => println!("Using Apple VideoToolbox"),
824    ///     HardwareAccel::Vaapi => println!("Using VA-API"),
825    ///     HardwareAccel::Amf => println!("Using AMD AMF"),
826    ///     _ => unreachable!(),
827    /// }
828    /// ```
829    #[must_use]
830    pub fn hardware_accel(&self) -> HardwareAccel {
831        self.inner.hardware_accel()
832    }
833
834    // =========================================================================
835    // Decoding Methods
836    // =========================================================================
837
838    /// Decodes the next video frame.
839    ///
840    /// This method reads and decodes a single frame from the video stream.
841    /// Frames are returned in presentation order.
842    ///
843    /// # Returns
844    ///
845    /// - `Ok(Some(frame))` - A frame was successfully decoded
846    /// - `Ok(None)` - End of stream reached, no more frames
847    /// - `Err(_)` - An error occurred during decoding
848    ///
849    /// # Errors
850    ///
851    /// Returns [`DecodeError`] if:
852    /// - Reading from the file fails
853    /// - Decoding the frame fails
854    /// - Pixel format conversion fails
855    ///
856    /// # Examples
857    ///
858    /// ```ignore
859    /// use ff_decode::VideoDecoder;
860    ///
861    /// let mut decoder = VideoDecoder::open("video.mp4")?.build()?;
862    ///
863    /// while let Some(frame) = decoder.decode_one()? {
864    ///     println!("Frame at {:?}", frame.timestamp().as_duration());
865    ///     // Process frame...
866    /// }
867    /// ```
868    pub fn decode_one(&mut self) -> Result<Option<VideoFrame>, DecodeError> {
869        self.inner.decode_one()
870    }
871
872    /// Decodes all frames within a specified time range.
873    ///
874    /// This method seeks to the start position and decodes all frames until
875    /// the end position is reached. Frames outside the range are skipped.
876    ///
877    /// # Performance
878    ///
879    /// - The method performs a keyframe seek to the start position
880    /// - Frames before `start` (from nearest keyframe) are decoded but discarded
881    /// - All frames within `[start, end)` are collected and returned
882    /// - The decoder position after this call will be at or past `end`
883    ///
884    /// For large time ranges or high frame rates, this may allocate significant
885    /// memory. Consider iterating manually with [`decode_one()`](Self::decode_one)
886    /// for very large ranges.
887    ///
888    /// # Arguments
889    ///
890    /// * `start` - Start of the time range (inclusive).
891    /// * `end` - End of the time range (exclusive).
892    ///
893    /// # Returns
894    ///
895    /// A vector of frames with timestamps in the range `[start, end)`.
896    /// Frames are returned in presentation order.
897    ///
898    /// # Errors
899    ///
900    /// Returns [`DecodeError`] if:
901    /// - Seeking to the start position fails
902    /// - Decoding frames fails
903    /// - The time range is invalid (start >= end)
904    ///
905    /// # Examples
906    ///
907    /// ```ignore
908    /// use ff_decode::VideoDecoder;
909    /// use std::time::Duration;
910    ///
911    /// let mut decoder = VideoDecoder::open("video.mp4")?.build()?;
912    ///
913    /// // Decode frames from 5s to 10s
914    /// let frames = decoder.decode_range(
915    ///     Duration::from_secs(5),
916    ///     Duration::from_secs(10),
917    /// )?;
918    ///
919    /// println!("Decoded {} frames", frames.len());
920    /// for frame in frames {
921    ///     println!("Frame at {:?}", frame.timestamp().as_duration());
922    /// }
923    /// ```
924    ///
925    /// # Memory Usage
926    ///
927    /// At 30fps, a 5-second range will allocate ~150 frames. For 1080p RGBA:
928    /// - Each frame: ~8.3 MB (1920 × 1080 × 4 bytes)
929    /// - 150 frames: ~1.25 GB
930    ///
931    /// Consider using a frame pool to reduce allocation overhead.
932    pub fn decode_range(
933        &mut self,
934        start: Duration,
935        end: Duration,
936    ) -> Result<Vec<VideoFrame>, DecodeError> {
937        // Validate range
938        if start >= end {
939            return Err(DecodeError::DecodingFailed {
940                timestamp: Some(start),
941                reason: format!(
942                    "Invalid time range: start ({start:?}) must be before end ({end:?})"
943                ),
944            });
945        }
946
947        // Seek to start position (keyframe mode for efficiency)
948        self.seek(start, crate::SeekMode::Keyframe)?;
949
950        // Collect frames in the range
951        let mut frames = Vec::new();
952
953        while let Some(frame) = self.decode_one()? {
954            let frame_time = frame.timestamp().as_duration();
955
956            // Stop if we've passed the end of the range
957            if frame_time >= end {
958                break;
959            }
960
961            // Only collect frames within the range
962            if frame_time >= start {
963                frames.push(frame);
964            }
965            // Frames before start are automatically discarded
966        }
967
968        Ok(frames)
969    }
970
971    // =========================================================================
972    // Seeking Methods
973    // =========================================================================
974
975    /// Seeks to a specified position in the video stream.
976    ///
977    /// This method performs efficient seeking without reopening the file,
978    /// providing significantly better performance than file-reopen-based seeking
979    /// (5-10ms vs 50-100ms).
980    ///
981    /// # Performance
982    ///
983    /// - **Keyframe seeking**: 5-10ms (typical GOP 1-2s)
984    /// - **Exact seeking**: 10-50ms depending on GOP size
985    /// - **Backward seeking**: Similar to keyframe seeking
986    ///
987    /// For videos with large GOP sizes (>5 seconds), exact seeking may take longer
988    /// as it requires decoding all frames from the nearest keyframe to the target.
989    ///
990    /// # Choosing a Seek Mode
991    ///
992    /// - **Use [`crate::SeekMode::Keyframe`]** for:
993    ///   - Video player scrubbing (approximate positioning)
994    ///   - Thumbnail generation
995    ///   - Quick preview navigation
996    ///
997    /// - **Use [`crate::SeekMode::Exact`]** for:
998    ///   - Frame-accurate editing
999    ///   - Precise timestamp extraction
1000    ///   - Quality-critical operations
1001    ///
1002    /// - **Use [`crate::SeekMode::Backward`]** for:
1003    ///   - Guaranteed keyframe positioning
1004    ///   - Preparing for forward playback
1005    ///
1006    /// # Arguments
1007    ///
1008    /// * `position` - Target position to seek to.
1009    /// * `mode` - Seek mode determining accuracy and performance.
1010    ///
1011    /// # Errors
1012    ///
1013    /// Returns [`DecodeError::SeekFailed`] if:
1014    /// - The target position is beyond the video duration
1015    /// - The file format doesn't support seeking
1016    /// - The seek operation fails internally
1017    ///
1018    /// # Examples
1019    ///
1020    /// ```ignore
1021    /// use ff_decode::{VideoDecoder, SeekMode};
1022    /// use std::time::Duration;
1023    ///
1024    /// let mut decoder = VideoDecoder::open("video.mp4")?.build()?;
1025    ///
1026    /// // Fast seek to 30 seconds (keyframe)
1027    /// decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;
1028    ///
1029    /// // Exact seek to 1 minute
1030    /// decoder.seek(Duration::from_secs(60), SeekMode::Exact)?;
1031    ///
1032    /// // Seek and decode next frame
1033    /// decoder.seek(Duration::from_secs(10), SeekMode::Keyframe)?;
1034    /// if let Some(frame) = decoder.decode_one()? {
1035    ///     println!("Frame at {:?}", frame.timestamp().as_duration());
1036    /// }
1037    /// ```
1038    pub fn seek(&mut self, position: Duration, mode: crate::SeekMode) -> Result<(), DecodeError> {
1039        if self.inner.is_live() {
1040            return Err(DecodeError::SeekNotSupported);
1041        }
1042        self.inner.seek(position, mode)
1043    }
1044
1045    /// Returns `true` if the source is a live or streaming input.
1046    ///
1047    /// Live sources (HLS live playlists, RTMP, RTSP, MPEG-TS) have the
1048    /// `AVFMT_TS_DISCONT` flag set on their `AVInputFormat`. Seeking is not
1049    /// supported on live sources — [`VideoDecoder::seek`] will return
1050    /// [`DecodeError::SeekNotSupported`].
1051    #[must_use]
1052    pub fn is_live(&self) -> bool {
1053        self.inner.is_live()
1054    }
1055
1056    /// Flushes the decoder's internal buffers.
1057    ///
1058    /// This method clears any cached frames and resets the decoder state.
1059    /// The decoder is ready to receive new packets after flushing.
1060    ///
1061    /// # When to Use
1062    ///
1063    /// - After seeking to ensure clean state
1064    /// - Before switching between different parts of the video
1065    /// - To clear buffered frames after errors
1066    ///
1067    /// # Examples
1068    ///
1069    /// ```ignore
1070    /// use ff_decode::VideoDecoder;
1071    ///
1072    /// let mut decoder = VideoDecoder::open("video.mp4")?.build()?;
1073    ///
1074    /// // Decode some frames...
1075    /// for _ in 0..10 {
1076    ///     decoder.decode_one()?;
1077    /// }
1078    ///
1079    /// // Flush and start fresh
1080    /// decoder.flush();
1081    /// ```
1082    ///
1083    /// # Note
1084    ///
1085    /// Calling [`seek()`](Self::seek) automatically flushes the decoder,
1086    /// so you don't need to call this method explicitly after seeking.
1087    pub fn flush(&mut self) {
1088        self.inner.flush();
1089    }
1090
1091    // =========================================================================
1092    // Thumbnail Generation Methods
1093    // =========================================================================
1094
1095    /// Generates a thumbnail at a specific timestamp.
1096    ///
1097    /// This method seeks to the specified position, decodes a frame, and scales
1098    /// it to the target dimensions. It's optimized for thumbnail generation by
1099    /// using keyframe seeking for speed.
1100    ///
1101    /// # Performance
1102    ///
1103    /// - Seeking: 5-10ms (keyframe mode)
1104    /// - Decoding: 5-10ms for 1080p H.264
1105    /// - Scaling: 1-3ms for 1080p → 320x180
1106    /// - **Total: ~10-25ms per thumbnail**
1107    ///
1108    /// # Aspect Ratio
1109    ///
1110    /// The thumbnail preserves the video's aspect ratio using a "fit-within"
1111    /// strategy. The output dimensions will be at most the target size, with
1112    /// at least one dimension matching the target. No letterboxing is applied.
1113    ///
1114    /// # Arguments
1115    ///
1116    /// * `position` - Timestamp to extract the thumbnail from.
1117    /// * `width` - Target thumbnail width in pixels.
1118    /// * `height` - Target thumbnail height in pixels.
1119    ///
1120    /// # Returns
1121    ///
1122    /// A scaled `VideoFrame` representing the thumbnail.
1123    ///
1124    /// # Errors
1125    ///
1126    /// Returns [`DecodeError`] if:
1127    /// - Seeking to the position fails
1128    /// - No frame can be decoded at that position (returns `Ok(None)`)
1129    /// - Scaling fails
1130    ///
1131    /// # Examples
1132    ///
1133    /// ```ignore
1134    /// use ff_decode::VideoDecoder;
1135    /// use std::time::Duration;
1136    ///
1137    /// let mut decoder = VideoDecoder::open("video.mp4")?.build()?;
1138    ///
1139    /// // Generate a 320x180 thumbnail at 5 seconds
1140    /// let thumbnail = decoder.thumbnail_at(
1141    ///     Duration::from_secs(5),
1142    ///     320,
1143    ///     180,
1144    /// )?;
1145    ///
1146    /// assert_eq!(thumbnail.width(), 320);
1147    /// assert_eq!(thumbnail.height(), 180);
1148    /// ```
1149    ///
1150    /// # Use Cases
1151    ///
1152    /// - Video player scrubbing preview
1153    /// - Timeline thumbnail strips
1154    /// - Gallery view thumbnails
1155    /// - Social media preview images
1156    pub fn thumbnail_at(
1157        &mut self,
1158        position: Duration,
1159        width: u32,
1160        height: u32,
1161    ) -> Result<Option<VideoFrame>, DecodeError> {
1162        // 1. Seek to the specified position (keyframe mode for speed)
1163        self.seek(position, crate::SeekMode::Keyframe)?;
1164
1165        // 2. Decode one frame — Ok(None) means no frame at this position
1166        match self.decode_one()? {
1167            Some(frame) => self.inner.scale_frame(&frame, width, height).map(Some),
1168            None => Ok(None),
1169        }
1170    }
1171
1172    /// Generates multiple thumbnails evenly distributed across the video.
1173    ///
1174    /// This method creates a series of thumbnails by dividing the video duration
1175    /// into equal intervals and extracting a frame at each position. This is
1176    /// commonly used for timeline preview strips or video galleries.
1177    ///
1178    /// # Performance
1179    ///
1180    /// For a 2-minute video generating 10 thumbnails:
1181    /// - Per thumbnail: ~10-25ms (see [`thumbnail_at()`](Self::thumbnail_at))
1182    /// - **Total: ~100-250ms**
1183    ///
1184    /// Performance scales linearly with the number of thumbnails.
1185    ///
1186    /// # Thumbnail Positions
1187    ///
1188    /// Thumbnails are extracted at evenly spaced intervals:
1189    /// - Position 0: `0s`
1190    /// - Position 1: `duration / count`
1191    /// - Position 2: `2 * (duration / count)`
1192    /// - ...
1193    /// - Position N-1: `(N-1) * (duration / count)`
1194    ///
1195    /// # Arguments
1196    ///
1197    /// * `count` - Number of thumbnails to generate.
1198    /// * `width` - Target thumbnail width in pixels.
1199    /// * `height` - Target thumbnail height in pixels.
1200    ///
1201    /// # Returns
1202    ///
1203    /// A vector of `VideoFrame` thumbnails in temporal order.
1204    ///
1205    /// # Errors
1206    ///
1207    /// Returns [`DecodeError`] if:
1208    /// - Any individual thumbnail generation fails (see [`thumbnail_at()`](Self::thumbnail_at))
1209    /// - The video duration is unknown ([`Duration::ZERO`])
1210    /// - Count is zero
1211    ///
1212    /// # Examples
1213    ///
1214    /// ```ignore
1215    /// use ff_decode::VideoDecoder;
1216    ///
1217    /// let mut decoder = VideoDecoder::open("video.mp4")?.build()?;
1218    ///
1219    /// // Generate 10 thumbnails at 160x90 resolution
1220    /// let thumbnails = decoder.thumbnails(10, 160, 90)?;
1221    ///
1222    /// assert_eq!(thumbnails.len(), 10);
1223    /// for thumb in thumbnails {
1224    ///     assert_eq!(thumb.width(), 160);
1225    ///     assert_eq!(thumb.height(), 90);
1226    /// }
1227    /// ```
1228    ///
1229    /// # Use Cases
1230    ///
1231    /// - Timeline preview strips (like `YouTube`'s timeline hover)
1232    /// - Video gallery grid views
1233    /// - Storyboard generation for editing
1234    /// - Video summary/preview pages
1235    ///
1236    /// # Memory Usage
1237    ///
1238    /// For 10 thumbnails at 160x90 RGBA:
1239    /// - Per thumbnail: ~56 KB (160 × 90 × 4 bytes)
1240    /// - Total: ~560 KB
1241    ///
1242    /// This is typically acceptable, but consider using a smaller resolution
1243    /// or generating thumbnails on-demand for very large thumbnail counts.
1244    pub fn thumbnails(
1245        &mut self,
1246        count: usize,
1247        width: u32,
1248        height: u32,
1249    ) -> Result<Vec<VideoFrame>, DecodeError> {
1250        // Validate count
1251        if count == 0 {
1252            return Err(DecodeError::DecodingFailed {
1253                timestamp: None,
1254                reason: "Thumbnail count must be greater than zero".to_string(),
1255            });
1256        }
1257
1258        let duration = self.duration();
1259
1260        // Check if duration is valid
1261        if duration.is_zero() {
1262            return Err(DecodeError::DecodingFailed {
1263                timestamp: None,
1264                reason: "Cannot generate thumbnails: video duration is unknown".to_string(),
1265            });
1266        }
1267
1268        // Calculate interval between thumbnails
1269        let interval_nanos = duration.as_nanos() / count as u128;
1270
1271        // Generate thumbnails
1272        let mut thumbnails = Vec::with_capacity(count);
1273
1274        for i in 0..count {
1275            // Use saturating_mul to prevent u128 overflow
1276            let position_nanos = interval_nanos.saturating_mul(i as u128);
1277            // Clamp to u64::MAX to prevent overflow when converting to Duration
1278            #[allow(clippy::cast_possible_truncation)]
1279            let position_nanos_u64 = position_nanos.min(u128::from(u64::MAX)) as u64;
1280            let position = Duration::from_nanos(position_nanos_u64);
1281
1282            if let Some(thumbnail) = self.thumbnail_at(position, width, height)? {
1283                thumbnails.push(thumbnail);
1284            }
1285        }
1286
1287        Ok(thumbnails)
1288    }
1289}
1290
1291impl Iterator for VideoDecoder {
1292    type Item = Result<VideoFrame, DecodeError>;
1293
1294    fn next(&mut self) -> Option<Self::Item> {
1295        if self.fused {
1296            return None;
1297        }
1298        match self.decode_one() {
1299            Ok(Some(frame)) => Some(Ok(frame)),
1300            Ok(None) => None,
1301            Err(e) => {
1302                self.fused = true;
1303                Some(Err(e))
1304            }
1305        }
1306    }
1307}
1308
1309impl std::iter::FusedIterator for VideoDecoder {}
1310
1311#[cfg(test)]
1312#[allow(clippy::panic, clippy::expect_used, clippy::float_cmp)]
1313mod tests {
1314    use super::*;
1315    use std::path::PathBuf;
1316
1317    #[test]
1318    fn test_builder_default_values() {
1319        let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"));
1320
1321        assert_eq!(builder.path(), Path::new("test.mp4"));
1322        assert!(builder.get_output_format().is_none());
1323        assert_eq!(builder.get_hardware_accel(), HardwareAccel::Auto);
1324        assert_eq!(builder.get_thread_count(), 0);
1325    }
1326
1327    #[test]
1328    fn test_builder_output_format() {
1329        let builder =
1330            VideoDecoderBuilder::new(PathBuf::from("test.mp4")).output_format(PixelFormat::Rgba);
1331
1332        assert_eq!(builder.get_output_format(), Some(PixelFormat::Rgba));
1333    }
1334
1335    #[test]
1336    fn test_builder_hardware_accel() {
1337        let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"))
1338            .hardware_accel(HardwareAccel::Nvdec);
1339
1340        assert_eq!(builder.get_hardware_accel(), HardwareAccel::Nvdec);
1341    }
1342
1343    #[test]
1344    fn test_builder_thread_count() {
1345        let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4")).thread_count(8);
1346
1347        assert_eq!(builder.get_thread_count(), 8);
1348    }
1349
1350    #[test]
1351    fn test_builder_chaining() {
1352        let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"))
1353            .output_format(PixelFormat::Bgra)
1354            .hardware_accel(HardwareAccel::Qsv)
1355            .thread_count(4);
1356
1357        assert_eq!(builder.get_output_format(), Some(PixelFormat::Bgra));
1358        assert_eq!(builder.get_hardware_accel(), HardwareAccel::Qsv);
1359        assert_eq!(builder.get_thread_count(), 4);
1360    }
1361
1362    #[test]
1363    fn test_decoder_open() {
1364        let builder = VideoDecoder::open("video.mp4");
1365        assert_eq!(builder.path(), Path::new("video.mp4"));
1366    }
1367
1368    #[test]
1369    fn test_decoder_open_pathbuf() {
1370        let path = PathBuf::from("/path/to/video.mp4");
1371        let builder = VideoDecoder::open(&path);
1372        assert_eq!(builder.path(), path.as_path());
1373    }
1374
1375    #[test]
1376    fn test_build_file_not_found() {
1377        let result = VideoDecoder::open("nonexistent_file_12345.mp4").build();
1378
1379        assert!(result.is_err());
1380        match result {
1381            Err(DecodeError::FileNotFound { path }) => {
1382                assert!(
1383                    path.to_string_lossy()
1384                        .contains("nonexistent_file_12345.mp4")
1385                );
1386            }
1387            Err(e) => panic!("Expected FileNotFound error, got: {e:?}"),
1388            Ok(_) => panic!("Expected error, got Ok"),
1389        }
1390    }
1391
1392    #[test]
1393    fn test_decoder_initial_state_with_invalid_file() {
1394        // Create a temporary test file (not a valid video)
1395        let temp_dir = std::env::temp_dir();
1396        let test_file = temp_dir.join("ff_decode_test_file.txt");
1397        std::fs::write(&test_file, "test").expect("Failed to create test file");
1398
1399        let result = VideoDecoder::open(&test_file).build();
1400
1401        // Clean up
1402        let _ = std::fs::remove_file(&test_file);
1403
1404        // The build should fail (not a valid video file)
1405        assert!(result.is_err());
1406        if let Err(e) = result {
1407            // Should get either NoVideoStream or Ffmpeg error
1408            assert!(
1409                matches!(e, DecodeError::NoVideoStream { .. })
1410                    || matches!(e, DecodeError::Ffmpeg { .. })
1411            );
1412        }
1413    }
1414
1415    #[test]
1416    fn test_seek_mode_variants() {
1417        // Test that all SeekMode variants exist and are accessible
1418        use crate::SeekMode;
1419
1420        let keyframe = SeekMode::Keyframe;
1421        let exact = SeekMode::Exact;
1422        let backward = SeekMode::Backward;
1423
1424        // Verify they can be compared
1425        assert_eq!(keyframe, SeekMode::Keyframe);
1426        assert_eq!(exact, SeekMode::Exact);
1427        assert_eq!(backward, SeekMode::Backward);
1428        assert_ne!(keyframe, exact);
1429        assert_ne!(exact, backward);
1430    }
1431
1432    #[test]
1433    fn test_seek_mode_default() {
1434        use crate::SeekMode;
1435
1436        let default_mode = SeekMode::default();
1437        assert_eq!(default_mode, SeekMode::Keyframe);
1438    }
1439
1440    #[test]
1441    fn test_decode_range_invalid_range() {
1442        use std::time::Duration;
1443
1444        // Create a temporary test file
1445        let temp_dir = std::env::temp_dir();
1446        let test_file = temp_dir.join("ff_decode_range_test.txt");
1447        std::fs::write(&test_file, "test").expect("Failed to create test file");
1448
1449        // Try to build decoder (will fail, but that's ok for this test)
1450        let result = VideoDecoder::open(&test_file).build();
1451
1452        // Clean up
1453        let _ = std::fs::remove_file(&test_file);
1454
1455        // If we somehow got a decoder (shouldn't happen with text file),
1456        // test that invalid range returns error
1457        if let Ok(mut decoder) = result {
1458            let start = Duration::from_secs(10);
1459            let end = Duration::from_secs(5); // end < start
1460
1461            let range_result = decoder.decode_range(start, end);
1462            assert!(range_result.is_err());
1463
1464            if let Err(DecodeError::DecodingFailed { reason, .. }) = range_result {
1465                assert!(reason.contains("Invalid time range"));
1466            }
1467        }
1468    }
1469
1470    #[test]
1471    fn test_decode_range_equal_start_end() {
1472        use std::time::Duration;
1473
1474        // Test that start == end is treated as invalid range
1475        let temp_dir = std::env::temp_dir();
1476        let test_file = temp_dir.join("ff_decode_range_equal_test.txt");
1477        std::fs::write(&test_file, "test").expect("Failed to create test file");
1478
1479        let result = VideoDecoder::open(&test_file).build();
1480
1481        // Clean up
1482        let _ = std::fs::remove_file(&test_file);
1483
1484        if let Ok(mut decoder) = result {
1485            let time = Duration::from_secs(5);
1486            let range_result = decoder.decode_range(time, time);
1487            assert!(range_result.is_err());
1488
1489            if let Err(DecodeError::DecodingFailed { reason, .. }) = range_result {
1490                assert!(reason.contains("Invalid time range"));
1491            }
1492        }
1493    }
1494
1495    #[test]
1496    fn test_thumbnails_zero_count() {
1497        // Create a temporary test file
1498        let temp_dir = std::env::temp_dir();
1499        let test_file = temp_dir.join("ff_decode_thumbnails_zero_test.txt");
1500        std::fs::write(&test_file, "test").expect("Failed to create test file");
1501
1502        let result = VideoDecoder::open(&test_file).build();
1503
1504        // Clean up
1505        let _ = std::fs::remove_file(&test_file);
1506
1507        // If we somehow got a decoder (shouldn't happen with text file),
1508        // test that zero count returns error
1509        if let Ok(mut decoder) = result {
1510            let thumbnails_result = decoder.thumbnails(0, 160, 90);
1511            assert!(thumbnails_result.is_err());
1512
1513            if let Err(DecodeError::DecodingFailed { reason, .. }) = thumbnails_result {
1514                assert!(reason.contains("Thumbnail count must be greater than zero"));
1515            }
1516        }
1517    }
1518
1519    #[test]
1520    fn test_thumbnail_api_exists() {
1521        // Compile-time test to verify thumbnail methods exist on Decoder
1522        // This ensures the API surface is correct even without real video files
1523
1524        // Create a builder (won't actually build successfully with a nonexistent file)
1525        let builder = VideoDecoder::open("nonexistent.mp4");
1526
1527        // Verify the builder exists
1528        let _ = builder;
1529
1530        // The actual thumbnail generation tests require real video files
1531        // and should be in integration tests. This test just verifies
1532        // that the methods are accessible at compile time.
1533    }
1534
1535    #[test]
1536    fn test_thumbnail_dimensions_calculation() {
1537        // Test aspect ratio preservation logic (indirectly through DecoderInner)
1538        // This is a compile-time test to ensure the code structure is correct
1539
1540        // Source: 1920x1080 (16:9)
1541        // Target: 320x180 (16:9)
1542        // Expected: 320x180 (exact fit)
1543
1544        let src_width = 1920.0_f64;
1545        let src_height = 1080.0_f64;
1546        let target_width = 320.0_f64;
1547        let target_height = 180.0_f64;
1548
1549        let src_aspect = src_width / src_height;
1550        let target_aspect = target_width / target_height;
1551
1552        let (scaled_width, scaled_height) = if src_aspect > target_aspect {
1553            let height = (target_width / src_aspect).round();
1554            (target_width, height)
1555        } else {
1556            let width = (target_height * src_aspect).round();
1557            (width, target_height)
1558        };
1559
1560        assert_eq!(scaled_width, 320.0);
1561        assert_eq!(scaled_height, 180.0);
1562    }
1563
1564    #[test]
1565    fn test_thumbnail_aspect_ratio_wide_source() {
1566        // Test aspect ratio preservation for wide source
1567        // Source: 1920x1080 (16:9)
1568        // Target: 180x180 (1:1)
1569        // Expected: 180x101 (fits width, height adjusted)
1570
1571        let src_width = 1920.0_f64;
1572        let src_height = 1080.0_f64;
1573        let target_width = 180.0_f64;
1574        let target_height = 180.0_f64;
1575
1576        let src_aspect = src_width / src_height;
1577        let target_aspect = target_width / target_height;
1578
1579        let (scaled_width, scaled_height) = if src_aspect > target_aspect {
1580            let height = (target_width / src_aspect).round();
1581            (target_width, height)
1582        } else {
1583            let width = (target_height * src_aspect).round();
1584            (width, target_height)
1585        };
1586
1587        assert_eq!(scaled_width, 180.0);
1588        // 180 / (16/9) = 101.25 → 101
1589        assert!((scaled_height - 101.0).abs() < 1.0);
1590    }
1591
1592    #[test]
1593    fn test_thumbnail_aspect_ratio_tall_source() {
1594        // Test aspect ratio preservation for tall source
1595        // Source: 1080x1920 (9:16 - portrait)
1596        // Target: 180x180 (1:1)
1597        // Expected: 101x180 (fits height, width adjusted)
1598
1599        let src_width = 1080.0_f64;
1600        let src_height = 1920.0_f64;
1601        let target_width = 180.0_f64;
1602        let target_height = 180.0_f64;
1603
1604        let src_aspect = src_width / src_height;
1605        let target_aspect = target_width / target_height;
1606
1607        let (scaled_width, scaled_height) = if src_aspect > target_aspect {
1608            let height = (target_width / src_aspect).round();
1609            (target_width, height)
1610        } else {
1611            let width = (target_height * src_aspect).round();
1612            (width, target_height)
1613        };
1614
1615        // 180 * (9/16) = 101.25 → 101
1616        assert!((scaled_width - 101.0).abs() < 1.0);
1617        assert_eq!(scaled_height, 180.0);
1618    }
1619}