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}