Skip to main content

ff_decode/
error.rs

1//! Error types for decoding operations.
2//!
3//! This module provides the [`DecodeError`] enum which represents all
4//! possible errors that can occur during video/audio decoding.
5
6use std::path::PathBuf;
7use std::time::Duration;
8
9use thiserror::Error;
10
11use crate::HardwareAccel;
12
13/// Errors that can occur during decoding operations.
14///
15/// This enum covers all error conditions that may arise when opening,
16/// configuring, or decoding media files.
17///
18/// # Error Categories
19///
20/// - **File errors**: [`FileNotFound`](Self::FileNotFound)
21/// - **Stream errors**: [`NoVideoStream`](Self::NoVideoStream), [`NoAudioStream`](Self::NoAudioStream)
22/// - **Codec errors**: [`UnsupportedCodec`](Self::UnsupportedCodec)
23/// - **Runtime errors**: [`DecodingFailed`](Self::DecodingFailed), [`SeekFailed`](Self::SeekFailed)
24/// - **Hardware errors**: [`HwAccelUnavailable`](Self::HwAccelUnavailable)
25/// - **Configuration errors**: [`InvalidOutputDimensions`](Self::InvalidOutputDimensions)
26/// - **Internal errors**: [`Ffmpeg`](Self::Ffmpeg), [`Io`](Self::Io)
27#[derive(Error, Debug)]
28pub enum DecodeError {
29    /// File was not found at the specified path.
30    ///
31    /// This error occurs when attempting to open a file that doesn't exist.
32    #[error("File not found: {path}")]
33    FileNotFound {
34        /// Path that was not found.
35        path: PathBuf,
36    },
37
38    /// No video stream exists in the media file.
39    ///
40    /// This error occurs when trying to decode video from a file that
41    /// only contains audio or other non-video streams.
42    #[error("No video stream found in: {path}")]
43    NoVideoStream {
44        /// Path to the media file.
45        path: PathBuf,
46    },
47
48    /// No audio stream exists in the media file.
49    ///
50    /// This error occurs when trying to decode audio from a file that
51    /// only contains video or other non-audio streams.
52    #[error("No audio stream found in: {path}")]
53    NoAudioStream {
54        /// Path to the media file.
55        path: PathBuf,
56    },
57
58    /// The codec is not supported by this decoder.
59    ///
60    /// This may occur for uncommon or proprietary codecs that are not
61    /// included in the `FFmpeg` build.
62    #[error("Codec not supported: {codec}")]
63    UnsupportedCodec {
64        /// Name of the unsupported codec.
65        codec: String,
66    },
67
68    /// The decoder for a known codec is absent from this `FFmpeg` build.
69    ///
70    /// Unlike [`UnsupportedCodec`](Self::UnsupportedCodec), the codec ID is
71    /// recognised by `FFmpeg` but the decoder was not compiled in (e.g.
72    /// `--enable-decoder=exr` was omitted from the build).
73    #[error("Decoder unavailable: {codec} — {hint}")]
74    DecoderUnavailable {
75        /// Short name of the codec (e.g. `"exr"`).
76        codec: String,
77        /// Human-readable suggestion for the caller.
78        hint: String,
79    },
80
81    /// Decoding operation failed at a specific point.
82    ///
83    /// This can occur due to corrupted data, unexpected stream format,
84    /// or internal decoder errors.
85    #[error("Decoding failed at {timestamp:?}: {reason}")]
86    DecodingFailed {
87        /// Timestamp where decoding failed (if known).
88        timestamp: Option<Duration>,
89        /// Reason for the failure.
90        reason: String,
91    },
92
93    /// Seek operation failed.
94    ///
95    /// Seeking may fail for various reasons including corrupted index,
96    /// seeking beyond file bounds, or stream format limitations.
97    #[error("Seek failed to {target:?}: {reason}")]
98    SeekFailed {
99        /// Target position of the seek.
100        target: Duration,
101        /// Reason for the failure.
102        reason: String,
103    },
104
105    /// Requested hardware acceleration is not available.
106    ///
107    /// This error occurs when a specific hardware accelerator is requested
108    /// but the system doesn't support it. Consider using [`HardwareAccel::Auto`]
109    /// for automatic fallback.
110    #[error("Hardware acceleration unavailable: {accel:?}")]
111    HwAccelUnavailable {
112        /// The unavailable hardware acceleration type.
113        accel: HardwareAccel,
114    },
115
116    /// Output dimensions are invalid.
117    ///
118    /// Width and height passed to [`output_size`](crate::video::builder::VideoDecoderBuilder::output_size),
119    /// [`output_width`](crate::video::builder::VideoDecoderBuilder::output_width), or
120    /// [`output_height`](crate::video::builder::VideoDecoderBuilder::output_height) must be
121    /// greater than zero and even (required by most pixel formats).
122    #[error("Invalid output dimensions: {width}x{height} (must be > 0 and even)")]
123    InvalidOutputDimensions {
124        /// Requested output width.
125        width: u32,
126        /// Requested output height.
127        height: u32,
128    },
129
130    /// `FFmpeg` internal error.
131    ///
132    /// This wraps errors from the underlying `FFmpeg` library that don't
133    /// fit into other categories.
134    #[error("ffmpeg error: {message} (code={code})")]
135    Ffmpeg {
136        /// Raw `FFmpeg` error code (negative integer). `0` when no numeric code is available.
137        code: i32,
138        /// Human-readable error message from `av_strerror` or an internal description.
139        message: String,
140    },
141
142    /// I/O error during file operations.
143    ///
144    /// This wraps standard I/O errors such as permission denied,
145    /// disk full, or network errors for remote files.
146    #[error("IO error: {0}")]
147    Io(#[from] std::io::Error),
148}
149
150impl DecodeError {
151    /// Creates a new [`DecodeError::DecodingFailed`] with the given reason.
152    ///
153    /// # Arguments
154    ///
155    /// * `reason` - Description of why decoding failed.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use ff_decode::DecodeError;
161    ///
162    /// let error = DecodeError::decoding_failed("Corrupted frame data");
163    /// assert!(error.to_string().contains("Corrupted frame data"));
164    /// assert!(error.is_recoverable());
165    /// ```
166    #[must_use]
167    pub fn decoding_failed(reason: impl Into<String>) -> Self {
168        Self::DecodingFailed {
169            timestamp: None,
170            reason: reason.into(),
171        }
172    }
173
174    /// Creates a new [`DecodeError::DecodingFailed`] with timestamp and reason.
175    ///
176    /// # Arguments
177    ///
178    /// * `timestamp` - The timestamp where decoding failed.
179    /// * `reason` - Description of why decoding failed.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use ff_decode::DecodeError;
185    /// use std::time::Duration;
186    ///
187    /// let error = DecodeError::decoding_failed_at(
188    ///     Duration::from_secs(30),
189    ///     "Invalid packet size"
190    /// );
191    /// assert!(error.to_string().contains("30s"));
192    /// assert!(error.is_recoverable());
193    /// ```
194    #[must_use]
195    pub fn decoding_failed_at(timestamp: Duration, reason: impl Into<String>) -> Self {
196        Self::DecodingFailed {
197            timestamp: Some(timestamp),
198            reason: reason.into(),
199        }
200    }
201
202    /// Creates a new [`DecodeError::SeekFailed`].
203    ///
204    /// # Arguments
205    ///
206    /// * `target` - The target position of the failed seek.
207    /// * `reason` - Description of why the seek failed.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use ff_decode::DecodeError;
213    /// use std::time::Duration;
214    ///
215    /// let error = DecodeError::seek_failed(
216    ///     Duration::from_secs(60),
217    ///     "Index not found"
218    /// );
219    /// assert!(error.to_string().contains("60s"));
220    /// assert!(error.is_recoverable());
221    /// ```
222    #[must_use]
223    pub fn seek_failed(target: Duration, reason: impl Into<String>) -> Self {
224        Self::SeekFailed {
225            target,
226            reason: reason.into(),
227        }
228    }
229
230    /// Creates a new [`DecodeError::DecoderUnavailable`].
231    ///
232    /// # Arguments
233    ///
234    /// * `codec` — Short codec name (e.g. `"exr"`).
235    /// * `hint` — Human-readable suggestion for the user.
236    #[must_use]
237    pub fn decoder_unavailable(codec: impl Into<String>, hint: impl Into<String>) -> Self {
238        Self::DecoderUnavailable {
239            codec: codec.into(),
240            hint: hint.into(),
241        }
242    }
243
244    /// Creates a new [`DecodeError::Ffmpeg`].
245    ///
246    /// # Arguments
247    ///
248    /// * `code` - The raw `FFmpeg` error code (negative integer). Pass `0` when no
249    ///   numeric code is available.
250    /// * `message` - Human-readable description of the error.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use ff_decode::DecodeError;
256    ///
257    /// let error = DecodeError::ffmpeg(-22, "Invalid data found when processing input");
258    /// assert!(error.to_string().contains("Invalid data"));
259    /// assert!(error.to_string().contains("code=-22"));
260    /// ```
261    #[must_use]
262    pub fn ffmpeg(code: i32, message: impl Into<String>) -> Self {
263        Self::Ffmpeg {
264            code,
265            message: message.into(),
266        }
267    }
268
269    /// Returns `true` if this error is recoverable.
270    ///
271    /// Recoverable errors are those where the decoder can continue
272    /// operating after the error, such as corrupted frames that can
273    /// be skipped.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use ff_decode::DecodeError;
279    /// use std::time::Duration;
280    ///
281    /// // Decoding failures are recoverable
282    /// assert!(DecodeError::decoding_failed("test").is_recoverable());
283    ///
284    /// // Seek failures are recoverable
285    /// assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
286    ///
287    /// ```
288    #[must_use]
289    pub fn is_recoverable(&self) -> bool {
290        matches!(self, Self::DecodingFailed { .. } | Self::SeekFailed { .. })
291    }
292
293    /// Returns `true` if this error is fatal.
294    ///
295    /// Fatal errors indicate that the decoder cannot continue and
296    /// must be recreated or the file reopened.
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use ff_decode::DecodeError;
302    /// use std::path::PathBuf;
303    ///
304    /// // File not found is fatal
305    /// assert!(DecodeError::FileNotFound { path: PathBuf::new() }.is_fatal());
306    ///
307    /// // Unsupported codec is fatal
308    /// assert!(DecodeError::UnsupportedCodec { codec: "test".to_string() }.is_fatal());
309    ///
310    /// ```
311    #[must_use]
312    pub fn is_fatal(&self) -> bool {
313        matches!(
314            self,
315            Self::FileNotFound { .. }
316                | Self::NoVideoStream { .. }
317                | Self::NoAudioStream { .. }
318                | Self::UnsupportedCodec { .. }
319                | Self::DecoderUnavailable { .. }
320        )
321    }
322}
323
324#[cfg(test)]
325#[allow(clippy::panic)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_decode_error_display() {
331        let error = DecodeError::FileNotFound {
332            path: PathBuf::from("/path/to/video.mp4"),
333        };
334        assert!(error.to_string().contains("File not found"));
335        assert!(error.to_string().contains("/path/to/video.mp4"));
336
337        let error = DecodeError::NoVideoStream {
338            path: PathBuf::from("/path/to/audio.mp3"),
339        };
340        assert!(error.to_string().contains("No video stream"));
341
342        let error = DecodeError::UnsupportedCodec {
343            codec: "unknown_codec".to_string(),
344        };
345        assert!(error.to_string().contains("Codec not supported"));
346        assert!(error.to_string().contains("unknown_codec"));
347    }
348
349    #[test]
350    fn test_decoding_failed_constructor() {
351        let error = DecodeError::decoding_failed("Corrupted frame data");
352        match error {
353            DecodeError::DecodingFailed { timestamp, reason } => {
354                assert!(timestamp.is_none());
355                assert_eq!(reason, "Corrupted frame data");
356            }
357            _ => panic!("Wrong error type"),
358        }
359    }
360
361    #[test]
362    fn test_decoding_failed_at_constructor() {
363        let error = DecodeError::decoding_failed_at(Duration::from_secs(30), "Invalid packet size");
364        match error {
365            DecodeError::DecodingFailed { timestamp, reason } => {
366                assert_eq!(timestamp, Some(Duration::from_secs(30)));
367                assert_eq!(reason, "Invalid packet size");
368            }
369            _ => panic!("Wrong error type"),
370        }
371    }
372
373    #[test]
374    fn test_seek_failed_constructor() {
375        let error = DecodeError::seek_failed(Duration::from_secs(60), "Index not found");
376        match error {
377            DecodeError::SeekFailed { target, reason } => {
378                assert_eq!(target, Duration::from_secs(60));
379                assert_eq!(reason, "Index not found");
380            }
381            _ => panic!("Wrong error type"),
382        }
383    }
384
385    #[test]
386    fn test_ffmpeg_constructor() {
387        let error = DecodeError::ffmpeg(-22, "AVERROR_INVALIDDATA");
388        match error {
389            DecodeError::Ffmpeg { code, message } => {
390                assert_eq!(code, -22);
391                assert_eq!(message, "AVERROR_INVALIDDATA");
392            }
393            _ => panic!("Wrong error type"),
394        }
395    }
396
397    #[test]
398    fn ffmpeg_should_format_with_code_and_message() {
399        let error = DecodeError::ffmpeg(-22, "Invalid data");
400        assert!(error.to_string().contains("code=-22"));
401        assert!(error.to_string().contains("Invalid data"));
402    }
403
404    #[test]
405    fn ffmpeg_with_zero_code_should_be_constructible() {
406        let error = DecodeError::ffmpeg(0, "allocation failed");
407        assert!(matches!(error, DecodeError::Ffmpeg { code: 0, .. }));
408    }
409
410    #[test]
411    fn decoder_unavailable_should_include_codec_and_hint() {
412        let e = DecodeError::decoder_unavailable(
413            "exr",
414            "Requires FFmpeg built with EXR support (--enable-decoder=exr)",
415        );
416        assert!(e.to_string().contains("exr"));
417        assert!(e.to_string().contains("Requires FFmpeg"));
418    }
419
420    #[test]
421    fn decoder_unavailable_should_be_fatal() {
422        let e = DecodeError::decoder_unavailable("exr", "hint");
423        assert!(e.is_fatal());
424        assert!(!e.is_recoverable());
425    }
426
427    #[test]
428    fn test_is_recoverable() {
429        assert!(DecodeError::decoding_failed("test").is_recoverable());
430        assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
431        assert!(
432            !DecodeError::FileNotFound {
433                path: PathBuf::new()
434            }
435            .is_recoverable()
436        );
437    }
438
439    #[test]
440    fn test_is_fatal() {
441        assert!(
442            DecodeError::FileNotFound {
443                path: PathBuf::new()
444            }
445            .is_fatal()
446        );
447        assert!(
448            DecodeError::NoVideoStream {
449                path: PathBuf::new()
450            }
451            .is_fatal()
452        );
453        assert!(
454            DecodeError::NoAudioStream {
455                path: PathBuf::new()
456            }
457            .is_fatal()
458        );
459        assert!(
460            DecodeError::UnsupportedCodec {
461                codec: "test".to_string()
462            }
463            .is_fatal()
464        );
465        assert!(!DecodeError::decoding_failed("test").is_fatal());
466    }
467
468    #[test]
469    fn test_io_error_conversion() {
470        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
471        let decode_error: DecodeError = io_error.into();
472        assert!(matches!(decode_error, DecodeError::Io(_)));
473    }
474
475    #[test]
476    fn test_hw_accel_unavailable() {
477        let error = DecodeError::HwAccelUnavailable {
478            accel: HardwareAccel::Nvdec,
479        };
480        assert!(
481            error
482                .to_string()
483                .contains("Hardware acceleration unavailable")
484        );
485        assert!(error.to_string().contains("Nvdec"));
486    }
487}