Skip to main content

av_decoders/
lib.rs

1//! Ready-made decoders for the rust-av ecosystem.
2//!
3//! Each decoder outputs [`v_frame`] structs. Only the y4m decoder is enabled by default;
4//! others require Cargo features for their external dependencies.
5//!
6//! # Backend priority
7//!
8//! 1. **Y4M** — `.y4m`/`.yuv` files (always available, lowest overhead)
9//! 2. **FFMS2** — when the `ffms2` feature is enabled
10//! 3. **FFmpeg** — when the `ffmpeg` feature is enabled
11//! 4. **VapourSynth** — when the `vapoursynth` feature is enabled
12//!
13//! # Example
14//!
15//! ```no_run
16//! use av_decoders::Decoder;
17//!
18//! let mut decoder = Decoder::from_file("video.y4m")?;
19//! let details = decoder.get_video_details();
20//! println!("{}x{} @ {} fps", details.width, details.height, details.frame_rate);
21//!
22//! while let Ok(frame) = decoder.read_video_frame::<u8>() {
23//!     // process frame
24//! }
25//! # Ok::<(), av_decoders::DecoderError>(())
26//! ```
27
28#[cfg(feature = "vapoursynth")]
29use std::collections::HashMap;
30use std::fs::File;
31use std::io::{BufReader, Read, stdin};
32use std::path::Path;
33use v_frame::chroma::ChromaSubsampling;
34use v_frame::frame::Frame;
35use v_frame::pixel::Pixel;
36
37mod error;
38mod helpers {
39    #[cfg(feature = "ffmpeg")]
40    pub(crate) mod ffmpeg;
41    #[cfg(feature = "ffms2")]
42    pub(crate) mod ffms2;
43    mod frame_builder;
44    #[cfg(feature = "vapoursynth")]
45    pub(crate) mod vapoursynth;
46    pub(crate) mod y4m;
47}
48mod util;
49
50#[cfg(feature = "ffmpeg")]
51pub use crate::helpers::ffmpeg::FfmpegDecoder;
52#[cfg(feature = "ffms2")]
53pub use crate::helpers::ffms2::Ffms2Decoder;
54#[cfg(feature = "vapoursynth")]
55pub use crate::helpers::vapoursynth::ModifyNode;
56#[cfg(feature = "vapoursynth")]
57pub use crate::helpers::vapoursynth::VapoursynthDecoder;
58#[cfg(feature = "vapoursynth")]
59use crate::helpers::vapoursynth::{VariableName, VariableValue};
60pub use error::DecoderError;
61pub use num_rational::Rational32;
62pub use v_frame;
63pub use y4m::Decoder as Y4mDecoder;
64
65const Y4M_EXTENSIONS: &[&str] = &["y4m", "yuv"];
66
67// TODO: Get rid of these and make padding an optional parameter
68const SB_SIZE_LOG2: usize = 6;
69const SB_SIZE: usize = 1 << SB_SIZE_LOG2;
70const SUBPEL_FILTER_SIZE: usize = 8;
71const FRAME_MARGIN: usize = 16 + SUBPEL_FILTER_SIZE;
72const LUMA_PADDING: usize = SB_SIZE + FRAME_MARGIN;
73
74/// Video metadata and configuration details, populated by every decoder on init.
75#[derive(Debug, Clone, Copy)]
76pub struct VideoDetails {
77    /// The width of the video frame in pixels.
78    pub width: usize,
79    /// The height of the video frame in pixels.
80    pub height: usize,
81    /// Bits per color component (e.g. 8, 10, 12).
82    pub bit_depth: usize,
83    /// Chroma subsampling format.
84    pub chroma_sampling: ChromaSubsampling,
85    /// Frame rate as a rational number (frames per second).
86    pub frame_rate: Rational32,
87    /// Total number of frames, if known.
88    pub total_frames: Option<usize>,
89}
90
91/// A set of possible configuration flags that are generic across all decoders.
92#[derive(Debug, Clone, Copy, Default)]
93pub struct DecoderConfig {
94    /// If `true`, the decoder will only fetch the luma planes from the video.
95    pub luma_only: bool,
96}
97
98#[cfg(test)]
99impl Default for VideoDetails {
100    #[inline]
101    fn default() -> Self {
102        VideoDetails {
103            width: 640,
104            height: 480,
105            bit_depth: 8,
106            chroma_sampling: ChromaSubsampling::Yuv420,
107            frame_rate: Rational32::new(30, 1),
108            total_frames: None,
109        }
110    }
111}
112
113/// Unified video decoder that auto-selects the best available backend.
114///
115/// See the [crate-level example](self#example) for typical usage.
116pub struct Decoder {
117    decoder: DecoderImpl,
118    video_details: VideoDetails,
119    frames_read: usize,
120    config: DecoderConfig,
121}
122
123impl Decoder {
124    /// Creates a new decoder from a file path, auto-selecting the backend.
125    ///
126    /// Priority: Y4M → FFMS2 → FFmpeg → VapourSynth.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`DecoderError::FileReadError`] if the file cannot be opened,
131    /// [`DecoderError::NoDecoder`] if no backend is available for the format.
132    #[inline]
133    #[expect(clippy::allow_attributes)]
134    #[allow(
135        unreachable_code,
136        reason = "some branches are unreachable with some combinations of features"
137    )]
138    pub fn from_file<P: AsRef<Path>>(input: P) -> Result<Decoder, DecoderError> {
139        // A raw y4m parser is going to be the fastest with the least overhead,
140        // so we should use it if we have a y4m file.
141        let ext = input
142            .as_ref()
143            .extension()
144            .and_then(|ext| ext.to_str())
145            .map(|ext| ext.to_ascii_lowercase());
146        if let Some(ext) = ext.as_deref() {
147            if Y4M_EXTENSIONS.contains(&ext) {
148                let reader =
149                    BufReader::new(File::open(input).map_err(|e| DecoderError::FileReadError {
150                        cause: e.to_string(),
151                    })?);
152                let decoder = DecoderImpl::Y4m(
153                    y4m::decode(Box::new(reader) as Box<dyn Read>).map_err(|e| match e {
154                        y4m::Error::EOF => DecoderError::EndOfFile,
155                        _ => DecoderError::GenericDecodeError {
156                            cause: e.to_string(),
157                        },
158                    })?,
159                );
160                let video_details = decoder.video_details()?;
161                return Ok(Decoder {
162                    decoder,
163                    video_details,
164                    frames_read: 0,
165                    config: DecoderConfig::default(),
166                });
167            }
168
169            #[cfg(feature = "vapoursynth")]
170            if ext == "vpy" {
171                // Decode vapoursynth script file input
172                let decoder = DecoderImpl::Vapoursynth(VapoursynthDecoder::from_file(
173                    input,
174                    HashMap::new(),
175                    None,
176                )?);
177                let video_details = decoder.video_details()?;
178                return Ok(Decoder {
179                    decoder,
180                    video_details,
181                    frames_read: 0,
182                    config: DecoderConfig::default(),
183                });
184            }
185        }
186
187        // Ffms2 is the fastest and most reliable, use it if available.
188        #[cfg(feature = "ffms2")]
189        {
190            let decoder = DecoderImpl::Ffms2(Ffms2Decoder::new(input, None)?);
191            let video_details = decoder.video_details()?;
192            return Ok(Decoder {
193                decoder,
194                video_details,
195                frames_read: 0,
196                config: DecoderConfig::default(),
197            });
198        }
199
200        // Ffmpeg is considerably faster at decoding, so we should prefer it over Vapoursynth
201        // for general use cases.
202        #[cfg(feature = "ffmpeg")]
203        {
204            let decoder = DecoderImpl::Ffmpeg(FfmpegDecoder::new(input)?);
205            let video_details = decoder.video_details()?;
206            return Ok(Decoder {
207                decoder,
208                video_details,
209                frames_read: 0,
210                config: DecoderConfig::default(),
211            });
212        }
213
214        #[cfg(feature = "vapoursynth")]
215        {
216            // Build a vapoursynth script and use that
217            use crate::util::escape_python_string;
218
219            let script = format!(
220                r#"
221import vapoursynth as vs
222core = vs.core
223clip = core.ffms2.Source("{}")
224clip.set_output()
225"#,
226                escape_python_string(
227                    &std::path::absolute(input)
228                        .map_err(|e| DecoderError::FileReadError {
229                            cause: e.to_string()
230                        })?
231                        .to_string_lossy()
232                )
233            );
234            let decoder = DecoderImpl::Vapoursynth(VapoursynthDecoder::from_script(
235                &script,
236                HashMap::new(),
237                None,
238            )?);
239            let video_details = decoder.video_details()?;
240            return Ok(Decoder {
241                decoder,
242                video_details,
243                frames_read: 0,
244                config: DecoderConfig::default(),
245            });
246        }
247
248        Err(DecoderError::NoDecoder)
249    }
250
251    /// Creates a new decoder from a VapourSynth script string.
252    ///
253    /// The script must produce a video clip via `clip.set_output()`.
254    /// Pass `HashMap::new()` for `variables` if none are needed.
255    ///
256    /// # Errors
257    ///
258    /// Returns [`DecoderError`] if the script fails to execute, produces no valid
259    /// output, or required VapourSynth plugins are unavailable.
260    #[inline]
261    #[cfg(feature = "vapoursynth")]
262    pub fn from_script(
263        script: &str,
264        variables: HashMap<VariableName, VariableValue>,
265    ) -> Result<Decoder, DecoderError> {
266        let dec = VapoursynthDecoder::from_script(script, variables, None)?;
267        let decoder = DecoderImpl::Vapoursynth(dec);
268        let video_details = decoder.video_details()?;
269        Ok(Decoder {
270            decoder,
271            video_details,
272            frames_read: 0,
273            config: DecoderConfig::default(),
274        })
275    }
276
277    /// Creates a decoder that reads Y4M data from stdin.
278    ///
279    /// # Errors
280    ///
281    /// Returns [`DecoderError::EndOfFile`] if stdin is empty,
282    /// [`DecoderError::GenericDecodeError`] if the Y4M header is invalid.
283    #[inline]
284    pub fn from_stdin() -> Result<Decoder, DecoderError> {
285        // We can only support y4m for this
286        let reader = BufReader::new(stdin());
287        let decoder = DecoderImpl::Y4m(y4m::decode(Box::new(reader) as Box<dyn Read>).map_err(
288            |e| match e {
289                y4m::Error::EOF => DecoderError::EndOfFile,
290                _ => DecoderError::GenericDecodeError {
291                    cause: e.to_string(),
292                },
293            },
294        )?);
295        let video_details: VideoDetails = decoder.video_details()?;
296        Ok(Decoder {
297            decoder,
298            video_details,
299            frames_read: 0,
300            config: DecoderConfig::default(),
301        })
302    }
303
304    /// Creates a decoder from a specific [`DecoderImpl`] variant, bypassing auto-detection.
305    ///
306    /// Prefer [`from_file`](Self::from_file), `from_script`, or
307    /// [`from_stdin`](Self::from_stdin) unless you need direct backend control.
308    ///
309    /// # Errors
310    ///
311    /// Returns [`DecoderError`] if video metadata cannot be extracted from the implementation.
312    #[inline]
313    pub fn from_decoder_impl(decoder_impl: DecoderImpl) -> Result<Decoder, DecoderError> {
314        let video_details = decoder_impl.video_details()?;
315        Ok(Decoder {
316            decoder: decoder_impl,
317            video_details,
318            frames_read: 0,
319            config: DecoderConfig::default(),
320        })
321    }
322
323    /// Returns the video metadata detected during initialization.
324    #[inline]
325    #[must_use]
326    pub fn get_video_details(&self) -> &VideoDetails {
327        &self.video_details
328    }
329
330    /// Sets the decoder to only fetch the luma planes from the video.
331    /// This may improve performance for applications that do not need chroma data.
332    #[inline]
333    pub fn set_luma_only(&mut self, enabled: bool) {
334        self.config.luma_only = enabled;
335    }
336
337    /// Decodes and returns the next video frame.
338    ///
339    /// `T` must match the video's bit depth: `u8` for 8-bit, `u16` for 10–16 bit.
340    ///
341    /// # Errors
342    ///
343    /// Returns [`DecoderError::EndOfFile`] at end of stream,
344    /// [`DecoderError::GenericDecodeError`] on corrupted data.
345    ///
346    /// Each frame contains uncompressed pixel data; avoid holding frames longer than needed.
347    #[inline]
348    pub fn read_video_frame<T: Pixel>(&mut self) -> Result<Frame<T>, DecoderError> {
349        let result = self.decoder.read_video_frame(
350            &self.video_details,
351            #[cfg(any(feature = "ffmpeg", feature = "vapoursynth", feature = "ffms2"))]
352            self.frames_read,
353            self.config.luma_only,
354        );
355        if result.is_ok() {
356            self.frames_read += 1;
357        }
358        result
359    }
360
361    /// Decodes and returns a specific frame by index.
362    ///
363    /// Not all backends support seeking. `T` must match the video's bit depth.
364    ///
365    /// # Errors
366    ///
367    /// Returns [`DecoderError::UnsupportedDecoder`] if the backend cannot seek,
368    /// [`DecoderError::EndOfFile`] past the last frame.
369    #[inline]
370    #[cfg(feature = "vapoursynth")]
371    pub fn get_video_frame<T: Pixel>(
372        &mut self,
373        frame_index: usize,
374    ) -> Result<Frame<T>, DecoderError> {
375        self.decoder.get_video_frame(
376            #[cfg(feature = "vapoursynth")]
377            &self.video_details,
378            #[cfg(feature = "vapoursynth")]
379            frame_index,
380            self.config.luma_only,
381        )
382    }
383
384    /// Seeks to the given frame index, skipping intermediate frames.
385    ///
386    /// # Errors
387    ///
388    /// Returns [`DecoderError::UnsupportedDecoder`] if the backend does not support seeking,
389    /// [`DecoderError::EndOfFile`] if the index is past the last frame.
390    #[inline]
391    #[cfg(any(feature = "vapoursynth", feature = "ffms2"))]
392    pub fn seek_to_frame(&mut self, frame_index: usize) -> Result<(), DecoderError> {
393        match &self.decoder {
394            #[cfg(feature = "vapoursynth")]
395            DecoderImpl::Vapoursynth(_) => {
396                if self
397                    .video_details
398                    .total_frames
399                    .is_some_and(|total_frames| frame_index >= total_frames)
400                {
401                    return Err(DecoderError::EndOfFile);
402                }
403                self.frames_read = frame_index;
404                Ok(())
405            }
406            #[cfg(feature = "ffms2")]
407            DecoderImpl::Ffms2(_) => {
408                if self
409                    .video_details
410                    .total_frames
411                    .is_some_and(|total_frames| frame_index >= total_frames)
412                {
413                    return Err(DecoderError::EndOfFile);
414                }
415                self.frames_read = frame_index;
416                Ok(())
417            }
418            _ => Err(DecoderError::UnsupportedDecoder),
419        }
420    }
421
422    /// Returns a mutable reference to the underlying FFmpeg decoder, or `None` otherwise.
423    #[inline]
424    #[cfg(feature = "ffmpeg")]
425    pub fn get_ffmpeg_impl(&mut self) -> Option<&mut FfmpegDecoder> {
426        match &mut self.decoder {
427            DecoderImpl::Ffmpeg(dec) => Some(dec),
428            _ => None,
429        }
430    }
431
432    /// Returns a mutable reference to the underlying FFMS2 decoder, or `None` otherwise.
433    #[inline]
434    #[cfg(feature = "ffms2")]
435    pub fn get_ffms2_impl(&mut self) -> Option<&mut Ffms2Decoder> {
436        match &mut self.decoder {
437            DecoderImpl::Ffms2(dec) => Some(dec),
438            _ => None,
439        }
440    }
441
442    /// Returns a mutable reference to the underlying VapourSynth decoder, or `None` otherwise.
443    #[inline]
444    #[cfg(feature = "vapoursynth")]
445    pub fn get_vapoursynth_impl(&mut self) -> Option<&mut VapoursynthDecoder> {
446        match &mut self.decoder {
447            DecoderImpl::Vapoursynth(dec) => Some(dec),
448            _ => None,
449        }
450    }
451}
452
453/// Internal enum representing the active decoder backend.
454///
455/// The variant is selected automatically during [`Decoder`] initialization.
456pub enum DecoderImpl {
457    /// Y4M format parser (always available).
458    Y4m(Y4mDecoder<Box<dyn Read>>),
459
460    /// VapourSynth-based decoder (requires `vapoursynth` feature).
461    #[cfg(feature = "vapoursynth")]
462    Vapoursynth(VapoursynthDecoder),
463
464    /// FFmpeg-based decoder (requires `ffmpeg` feature).
465    #[cfg(feature = "ffmpeg")]
466    Ffmpeg(FfmpegDecoder),
467
468    /// FFMS2-based decoder (requires `ffms2` feature).
469    #[cfg(feature = "ffms2")]
470    Ffms2(Ffms2Decoder),
471}
472
473impl DecoderImpl {
474    pub(crate) fn video_details(&self) -> Result<VideoDetails, DecoderError> {
475        match self {
476            Self::Y4m(dec) => Ok(helpers::y4m::get_video_details(dec)),
477            #[cfg(feature = "vapoursynth")]
478            Self::Vapoursynth(dec) => dec.get_video_details(),
479            #[cfg(feature = "ffmpeg")]
480            Self::Ffmpeg(dec) => Ok(dec.video_details),
481            #[cfg(feature = "ffms2")]
482            Self::Ffms2(dec) => Ok(dec.video_details),
483        }
484    }
485
486    pub(crate) fn read_video_frame<T: Pixel>(
487        &mut self,
488        cfg: &VideoDetails,
489        #[cfg(any(feature = "ffmpeg", feature = "vapoursynth", feature = "ffms2"))]
490        frame_index: usize,
491        luma_only: bool,
492    ) -> Result<Frame<T>, DecoderError> {
493        match self {
494            Self::Y4m(dec) => {
495                helpers::y4m::read_video_frame::<Box<dyn Read>, T>(dec, cfg, luma_only)
496            }
497            #[cfg(feature = "vapoursynth")]
498            Self::Vapoursynth(dec) => dec.read_video_frame::<T>(cfg, frame_index, luma_only),
499            #[cfg(feature = "ffmpeg")]
500            Self::Ffmpeg(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
501            #[cfg(feature = "ffms2")]
502            Self::Ffms2(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
503        }
504    }
505
506    #[cfg(feature = "vapoursynth")]
507    pub(crate) fn get_video_frame<T: Pixel>(
508        &mut self,
509        cfg: &VideoDetails,
510        frame_index: usize,
511        luma_only: bool,
512    ) -> Result<Frame<T>, DecoderError> {
513        match self {
514            #[cfg(feature = "vapoursynth")]
515            Self::Vapoursynth(dec) => dec.read_video_frame::<T>(cfg, frame_index, luma_only),
516            #[cfg(feature = "ffms2")]
517            Self::Ffms2(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
518            _ => Err(DecoderError::UnsupportedDecoder),
519        }
520    }
521}