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 /// A caller-supplied byte source to demux from instead of `path`.
126 ///
127 /// Set by [`VideoDecoder::from_reader`]; when present it wins over `path`,
128 /// which is then only a label for diagnostics.
129 source: Option<SourceHandle>,
130}
131
132/// A boxed byte source that can sit in a `#[derive(Debug)]` struct.
133///
134/// A caller's reader carries no `Debug` bound -- requiring one would rule out
135/// most of what this feature exists to accept -- so the handle prints a label
136/// instead of its contents.
137struct SourceHandle(Box<dyn ff_sys::IoSource>);
138
139impl std::fmt::Debug for SourceHandle {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 f.write_str("<reader>")
142 }
143}
144
145impl VideoDecoderBuilder {
146 /// Creates a new builder for the specified file path.
147 ///
148 /// This is an internal constructor; use [`VideoDecoder::open()`] instead.
149 pub(crate) fn new(path: PathBuf) -> Self {
150 Self {
151 path,
152 output_format: None,
153 output_scale: None,
154 hardware_accel: HardwareAccel::Auto,
155 thread_count: 0,
156 frame_pool: None,
157 frame_rate: None,
158 network_opts: None,
159 source: None,
160 }
161 }
162
163 /// Creates a builder that demuxes `source` instead of a file.
164 ///
165 /// This is an internal constructor; use [`VideoDecoder::from_reader()`].
166 pub(crate) fn from_source(source: Box<dyn ff_sys::IoSource>) -> Self {
167 let mut builder = Self::new(PathBuf::from("<reader>"));
168 builder.source = Some(SourceHandle(source));
169 builder
170 }
171
172 /// Returns the configured file path.
173 #[must_use]
174 pub fn path(&self) -> &Path {
175 &self.path
176 }
177
178 /// Returns the configured output format, if any.
179 #[must_use]
180 pub fn get_output_format(&self) -> Option<PixelFormat> {
181 self.output_format
182 }
183
184 /// Returns the configured hardware acceleration mode.
185 #[must_use]
186 pub fn get_hardware_accel(&self) -> HardwareAccel {
187 self.hardware_accel
188 }
189
190 /// Returns the configured thread count.
191 #[must_use]
192 pub fn get_thread_count(&self) -> usize {
193 self.thread_count
194 }
195
196 /// Builds the decoder with the configured options.
197 ///
198 /// This method opens the media file, initializes the decoder context,
199 /// and prepares for frame decoding.
200 ///
201 /// # Errors
202 ///
203 /// Returns an error if:
204 /// - The file cannot be found ([`DecodeError::FileNotFound`])
205 /// - The file contains no video stream ([`DecodeError::NoVideoStream`])
206 /// - The codec is not supported ([`DecodeError::UnsupportedCodec`])
207 /// - Hardware acceleration is unavailable ([`DecodeError::HwAccelUnavailable`])
208 /// - Other `FFmpeg` errors occur ([`DecodeError::Ffmpeg`])
209 ///
210 /// # Examples
211 ///
212 /// ```ignore
213 /// use ff_decode::VideoDecoder;
214 ///
215 /// let decoder = VideoDecoder::open("video.mp4")?
216 /// .build()?;
217 ///
218 /// // Start decoding
219 /// for result in &mut decoder {
220 /// let frame = result?;
221 /// // Process frame...
222 /// }
223 /// ```
224 pub fn build(self) -> Result<VideoDecoder, DecodeError> {
225 // Validate output scale dimensions before opening the file.
226 // FitWidth / FitHeight aspect-ratio dimensions are resolved at decode time
227 // from the actual source dimensions, so we only reject an explicit zero here.
228 if let Some(scale) = self.output_scale {
229 let (w, h) = match scale {
230 OutputScale::Exact { width, height } => (width, height),
231 OutputScale::FitWidth(w) => (w, 1), // height will be derived
232 OutputScale::FitHeight(h) => (1, h), // width will be derived
233 };
234 if w == 0 || h == 0 {
235 return Err(DecodeError::InvalidOutputDimensions {
236 width: w,
237 height: h,
238 });
239 }
240 }
241
242 // Image-sequence patterns contain '%' — the literal path does not exist.
243 // Network URLs must also skip the file-existence check, and so must a
244 // caller-supplied source, whose `path` is a label rather than a location.
245 let path_str = self.path.to_str().unwrap_or("");
246 let is_image_sequence = path_str.contains('%');
247 let is_network_url = crate::network::is_url(path_str);
248 if self.source.is_none() && !is_image_sequence && !is_network_url && !self.path.exists() {
249 return Err(DecodeError::FileNotFound {
250 path: self.path.clone(),
251 });
252 }
253
254 // Create the decoder inner
255 let (inner, stream_info, container_info) = VideoDecoderInner::new(
256 &self.path,
257 self.output_format,
258 self.output_scale,
259 self.hardware_accel,
260 self.thread_count,
261 self.frame_rate,
262 self.frame_pool.clone(),
263 self.network_opts,
264 self.source.map(|s| s.0),
265 )?;
266
267 Ok(VideoDecoder {
268 path: self.path,
269 frame_pool: self.frame_pool,
270 inner,
271 stream_info,
272 container_info,
273 fused: false,
274 })
275 }
276}
277
278/// A video decoder for extracting frames from media files.
279///
280/// The decoder provides frame-by-frame access to video content with support
281/// for seeking, hardware acceleration, and format conversion.
282///
283/// # Construction
284///
285/// Use [`VideoDecoder::open()`] to create a builder, then call [`VideoDecoderBuilder::build()`]:
286///
287/// ```ignore
288/// use ff_decode::VideoDecoder;
289/// use ff_format::PixelFormat;
290///
291/// let decoder = VideoDecoder::open("video.mp4")?
292/// .output_format(PixelFormat::Rgba)
293/// .build()?;
294/// ```
295///
296/// # Frame Decoding
297///
298/// Frames can be decoded one at a time or using the built-in iterator:
299///
300/// ```ignore
301/// // Decode one frame
302/// if let Some(frame) = decoder.decode_one()? {
303/// println!("Frame at {:?}", frame.timestamp().as_duration());
304/// }
305///
306/// // Iterator form — VideoDecoder implements Iterator directly
307/// for result in &mut decoder {
308/// let frame = result?;
309/// // Process frame...
310/// }
311/// ```
312///
313/// # Seeking
314///
315/// The decoder supports efficient seeking:
316///
317/// ```ignore
318/// use ff_decode::SeekMode;
319/// use std::time::Duration;
320///
321/// // Seek to 30 seconds (keyframe)
322/// decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;
323///
324/// // Seek to exact frame
325/// decoder.seek(Duration::from_secs(30), SeekMode::Exact)?;
326/// ```
327pub struct VideoDecoder {
328 /// Path to the media file
329 path: PathBuf,
330 /// Optional frame pool for memory reuse
331 frame_pool: Option<Arc<dyn FramePool>>,
332 /// Internal decoder state
333 inner: VideoDecoderInner,
334 /// Video stream information
335 stream_info: VideoStreamInfo,
336 /// Container-level metadata
337 container_info: ContainerInfo,
338 /// Set to `true` after a decoding error; causes [`Iterator::next`] to return `None`.
339 fused: bool,
340}
341
342impl VideoDecoder {
343 /// Opens a media file and returns a builder for configuring the decoder.
344 ///
345 /// This is the entry point for creating a decoder. The returned builder
346 /// allows setting options before the decoder is fully initialized.
347 ///
348 /// # Arguments
349 ///
350 /// * `path` - Path to the media file to decode.
351 ///
352 /// # Examples
353 ///
354 /// ```ignore
355 /// use ff_decode::VideoDecoder;
356 ///
357 /// // Simple usage
358 /// let decoder = VideoDecoder::open("video.mp4")?
359 /// .build()?;
360 ///
361 /// // With options
362 /// let decoder = VideoDecoder::open("video.mp4")?
363 /// .output_format(PixelFormat::Rgba)
364 /// .hardware_accel(HardwareAccel::Auto)
365 /// .build()?;
366 /// ```
367 ///
368 /// # Note
369 ///
370 /// This method does not validate that the file exists or is a valid
371 /// media file. Validation occurs when [`VideoDecoderBuilder::build()`] is called.
372 pub fn open(path: impl AsRef<Path>) -> VideoDecoderBuilder {
373 VideoDecoderBuilder::new(path.as_ref().to_path_buf())
374 }
375
376 /// Creates a builder that decodes from `source` instead of a file.
377 ///
378 /// `source` is anything that reads and seeks and can move to the decoder's
379 /// thread -- an in-memory `Cursor<Vec<u8>>`, a `File`, a custom byte store.
380 /// The container format is probed from the bytes, so nothing has to be named.
381 ///
382 /// `Seek` is required: `FFmpeg` can demux without it, but the containers that
383 /// survive a non-seekable input are a narrower set with different failure
384 /// modes, and supporting it is separate work.
385 ///
386 /// # Examples
387 ///
388 /// ```ignore
389 /// use std::io::Cursor;
390 /// use ff_decode::VideoDecoder;
391 ///
392 /// let bytes: Vec<u8> = std::fs::read("input.mp4")?;
393 /// let mut decoder = VideoDecoder::from_reader(Cursor::new(bytes)).build()?;
394 /// let frame = decoder.decode_one()?;
395 /// # Ok::<(), ff_decode::DecodeError>(())
396 /// ```
397 ///
398 /// # Note
399 ///
400 /// As with [`open`](Self::open), nothing is read until
401 /// [`VideoDecoderBuilder::build()`] is called.
402 pub fn from_reader(source: impl ff_sys::IoSource + 'static) -> VideoDecoderBuilder {
403 VideoDecoderBuilder::from_source(Box::new(source))
404 }
405
406 // =========================================================================
407 // Information Methods
408 // =========================================================================
409
410 /// Returns the video stream information.
411 ///
412 /// This contains metadata about the video stream including resolution,
413 /// frame rate, codec, and color characteristics.
414 #[must_use]
415 pub fn stream_info(&self) -> &VideoStreamInfo {
416 &self.stream_info
417 }
418
419 /// Returns the video width in pixels.
420 #[must_use]
421 pub fn width(&self) -> u32 {
422 self.stream_info.width()
423 }
424
425 /// Returns the video height in pixels.
426 #[must_use]
427 pub fn height(&self) -> u32 {
428 self.stream_info.height()
429 }
430
431 /// Returns the frame rate in frames per second.
432 #[must_use]
433 pub fn frame_rate(&self) -> f64 {
434 self.stream_info.fps()
435 }
436
437 /// Returns the total duration of the video.
438 ///
439 /// Returns [`Duration::ZERO`] if duration is unknown.
440 #[must_use]
441 pub fn duration(&self) -> Duration {
442 self.stream_info.duration().unwrap_or(Duration::ZERO)
443 }
444
445 /// Returns the total duration of the video, or `None` for live streams
446 /// or formats that do not carry duration information.
447 #[must_use]
448 pub fn duration_opt(&self) -> Option<Duration> {
449 self.stream_info.duration()
450 }
451
452 /// Returns container-level metadata (format name, bitrate, stream count).
453 #[must_use]
454 pub fn container_info(&self) -> &ContainerInfo {
455 &self.container_info
456 }
457
458 /// Returns the current playback position.
459 #[must_use]
460 pub fn position(&self) -> Duration {
461 self.inner.position()
462 }
463
464 /// Returns `true` if the end of stream has been reached.
465 #[must_use]
466 pub fn is_eof(&self) -> bool {
467 self.inner.is_eof()
468 }
469
470 /// Returns the file path being decoded.
471 #[must_use]
472 pub fn path(&self) -> &Path {
473 &self.path
474 }
475
476 /// Returns a reference to the frame pool, if configured.
477 #[must_use]
478 pub fn frame_pool(&self) -> Option<&Arc<dyn FramePool>> {
479 self.frame_pool.as_ref()
480 }
481
482 /// Returns the currently active hardware acceleration mode.
483 ///
484 /// This method returns the actual hardware acceleration being used,
485 /// which may differ from what was requested:
486 ///
487 /// - If [`HardwareAccel::Auto`] was requested, this returns the specific
488 /// accelerator that was successfully initialized (e.g., [`HardwareAccel::Nvdec`]),
489 /// or [`HardwareAccel::None`] if no hardware acceleration is available.
490 /// - If a specific accelerator was requested and initialization failed,
491 /// the decoder creation would have returned an error.
492 /// - If [`HardwareAccel::None`] was requested, this always returns [`HardwareAccel::None`].
493 ///
494 /// # Examples
495 ///
496 /// ```ignore
497 /// use ff_decode::{VideoDecoder, HardwareAccel};
498 ///
499 /// // Request automatic hardware acceleration
500 /// let decoder = VideoDecoder::open("video.mp4")?
501 /// .hardware_accel(HardwareAccel::Auto)
502 /// .build()?;
503 ///
504 /// // Check which accelerator was selected
505 /// match decoder.hardware_accel() {
506 /// HardwareAccel::None => println!("Using software decoding"),
507 /// HardwareAccel::Nvdec => println!("Using NVIDIA NVDEC"),
508 /// HardwareAccel::Qsv => println!("Using Intel Quick Sync"),
509 /// HardwareAccel::VideoToolbox => println!("Using Apple VideoToolbox"),
510 /// HardwareAccel::Vaapi => println!("Using VA-API"),
511 /// HardwareAccel::Amf => println!("Using AMD AMF"),
512 /// _ => unreachable!(),
513 /// }
514 /// ```
515 #[must_use]
516 pub fn hardware_accel(&self) -> HardwareAccel {
517 self.inner.hardware_accel()
518 }
519}
520
521#[cfg(test)]
522#[allow(clippy::panic, clippy::expect_used)]
523mod tests {
524 use super::*;
525 use std::path::PathBuf;
526
527 #[test]
528 fn builder_default_values_should_have_auto_hw_and_zero_threads() {
529 let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"));
530
531 assert_eq!(builder.path(), Path::new("test.mp4"));
532 assert!(builder.get_output_format().is_none());
533 assert_eq!(builder.get_hardware_accel(), HardwareAccel::Auto);
534 assert_eq!(builder.get_thread_count(), 0);
535 }
536
537 #[test]
538 fn builder_chaining_should_set_all_fields() {
539 let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"))
540 .output_format(PixelFormat::Bgra)
541 .hardware_accel(HardwareAccel::Qsv)
542 .thread_count(4);
543
544 assert_eq!(builder.get_output_format(), Some(PixelFormat::Bgra));
545 assert_eq!(builder.get_hardware_accel(), HardwareAccel::Qsv);
546 assert_eq!(builder.get_thread_count(), 4);
547 }
548
549 #[test]
550 fn decoder_open_should_return_builder_with_path() {
551 let builder = VideoDecoder::open("video.mp4");
552 assert_eq!(builder.path(), Path::new("video.mp4"));
553 }
554
555 #[test]
556 fn decoder_open_pathbuf_should_preserve_path() {
557 let path = PathBuf::from("/path/to/video.mp4");
558 let builder = VideoDecoder::open(&path);
559 assert_eq!(builder.path(), path.as_path());
560 }
561
562 #[test]
563 fn build_nonexistent_file_should_return_file_not_found() {
564 let result = VideoDecoder::open("nonexistent_file_12345.mp4").build();
565
566 assert!(result.is_err());
567 match result {
568 Err(DecodeError::FileNotFound { path }) => {
569 assert!(
570 path.to_string_lossy()
571 .contains("nonexistent_file_12345.mp4")
572 );
573 }
574 Err(e) => panic!("Expected FileNotFound error, got: {e:?}"),
575 Ok(_) => panic!("Expected error, got Ok"),
576 }
577 }
578
579 #[test]
580 fn build_invalid_video_file_should_fail() {
581 // Create a temporary test file (not a valid video)
582 let temp_dir = std::env::temp_dir();
583 let test_file = temp_dir.join("ff_decode_test_file.txt");
584 std::fs::write(&test_file, "test").expect("Failed to create test file");
585
586 let result = VideoDecoder::open(&test_file).build();
587
588 // Clean up
589 let _ = std::fs::remove_file(&test_file);
590
591 // The build should fail (not a valid video file)
592 assert!(result.is_err());
593 if let Err(e) = result {
594 // Should get either NoVideoStream or Ffmpeg error
595 assert!(
596 matches!(e, DecodeError::NoVideoStream { .. })
597 || matches!(e, DecodeError::Ffmpeg { .. })
598 );
599 }
600 }
601}