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    /// The connection timed out before a response was received.
150    ///
151    /// Maps from `FFmpeg` error code `AVERROR(ETIMEDOUT)`.
152    /// `endpoint` is the sanitized URL (password replaced with `***`,
153    /// query string removed).
154    #[error("network timeout: endpoint={endpoint} — {message} (code={code})")]
155    NetworkTimeout {
156        /// Raw `FFmpeg` error code.
157        code: i32,
158        /// Sanitized endpoint URL (no credentials, no query string).
159        endpoint: String,
160        /// Human-readable error message from `av_strerror`.
161        message: String,
162    },
163
164    /// The connection was refused or the host could not be reached.
165    ///
166    /// Maps from `FFmpeg` error codes `AVERROR(ECONNREFUSED)`,
167    /// `AVERROR(EHOSTUNREACH)`, `AVERROR(ENETUNREACH)`, and DNS failures.
168    /// `endpoint` is the sanitized URL (password replaced with `***`,
169    /// query string removed).
170    #[error("connection failed: endpoint={endpoint} — {message} (code={code})")]
171    ConnectionFailed {
172        /// Raw `FFmpeg` error code.
173        code: i32,
174        /// Sanitized endpoint URL (no credentials, no query string).
175        endpoint: String,
176        /// Human-readable error message from `av_strerror`.
177        message: String,
178    },
179
180    /// The stream was interrupted after a connection was established.
181    ///
182    /// Maps from `AVERROR(EIO)` and `AVERROR_EOF` in a network context.
183    /// `endpoint` is the sanitized URL (password replaced with `***`,
184    /// query string removed).
185    #[error("stream interrupted: endpoint={endpoint} — {message} (code={code})")]
186    StreamInterrupted {
187        /// Raw `FFmpeg` error code.
188        code: i32,
189        /// Sanitized endpoint URL (no credentials, no query string).
190        endpoint: String,
191        /// Human-readable error message from `av_strerror`.
192        message: String,
193    },
194
195    /// Seeking was requested on a live stream where seeking is not supported.
196    ///
197    /// Returned by `VideoDecoder::seek()` and `AudioDecoder::seek()` when
198    /// `is_live()` returns `true`.
199    #[error("seek is not supported on live streams")]
200    SeekNotSupported,
201}
202
203impl DecodeError {
204    /// Creates a new [`DecodeError::DecodingFailed`] with the given reason.
205    ///
206    /// # Arguments
207    ///
208    /// * `reason` - Description of why decoding failed.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use ff_decode::DecodeError;
214    ///
215    /// let error = DecodeError::decoding_failed("Corrupted frame data");
216    /// assert!(error.to_string().contains("Corrupted frame data"));
217    /// assert!(error.is_recoverable());
218    /// ```
219    #[must_use]
220    pub fn decoding_failed(reason: impl Into<String>) -> Self {
221        Self::DecodingFailed {
222            timestamp: None,
223            reason: reason.into(),
224        }
225    }
226
227    /// Creates a new [`DecodeError::DecodingFailed`] with timestamp and reason.
228    ///
229    /// # Arguments
230    ///
231    /// * `timestamp` - The timestamp where decoding failed.
232    /// * `reason` - Description of why decoding failed.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use ff_decode::DecodeError;
238    /// use std::time::Duration;
239    ///
240    /// let error = DecodeError::decoding_failed_at(
241    ///     Duration::from_secs(30),
242    ///     "Invalid packet size"
243    /// );
244    /// assert!(error.to_string().contains("30s"));
245    /// assert!(error.is_recoverable());
246    /// ```
247    #[must_use]
248    pub fn decoding_failed_at(timestamp: Duration, reason: impl Into<String>) -> Self {
249        Self::DecodingFailed {
250            timestamp: Some(timestamp),
251            reason: reason.into(),
252        }
253    }
254
255    /// Creates a new [`DecodeError::SeekFailed`].
256    ///
257    /// # Arguments
258    ///
259    /// * `target` - The target position of the failed seek.
260    /// * `reason` - Description of why the seek failed.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use ff_decode::DecodeError;
266    /// use std::time::Duration;
267    ///
268    /// let error = DecodeError::seek_failed(
269    ///     Duration::from_secs(60),
270    ///     "Index not found"
271    /// );
272    /// assert!(error.to_string().contains("60s"));
273    /// assert!(error.is_recoverable());
274    /// ```
275    #[must_use]
276    pub fn seek_failed(target: Duration, reason: impl Into<String>) -> Self {
277        Self::SeekFailed {
278            target,
279            reason: reason.into(),
280        }
281    }
282
283    /// Creates a new [`DecodeError::DecoderUnavailable`].
284    ///
285    /// # Arguments
286    ///
287    /// * `codec` — Short codec name (e.g. `"exr"`).
288    /// * `hint` — Human-readable suggestion for the user.
289    #[must_use]
290    pub fn decoder_unavailable(codec: impl Into<String>, hint: impl Into<String>) -> Self {
291        Self::DecoderUnavailable {
292            codec: codec.into(),
293            hint: hint.into(),
294        }
295    }
296
297    /// Creates a new [`DecodeError::Ffmpeg`].
298    ///
299    /// # Arguments
300    ///
301    /// * `code` - The raw `FFmpeg` error code (negative integer). Pass `0` when no
302    ///   numeric code is available.
303    /// * `message` - Human-readable description of the error.
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// use ff_decode::DecodeError;
309    ///
310    /// let error = DecodeError::ffmpeg(-22, "Invalid data found when processing input");
311    /// assert!(error.to_string().contains("Invalid data"));
312    /// assert!(error.to_string().contains("code=-22"));
313    /// ```
314    #[must_use]
315    pub fn ffmpeg(code: i32, message: impl Into<String>) -> Self {
316        Self::Ffmpeg {
317            code,
318            message: message.into(),
319        }
320    }
321
322    /// Returns `true` if this error is recoverable.
323    ///
324    /// Recoverable errors are those where the decoder can continue
325    /// operating after the error, such as corrupted frames that can
326    /// be skipped.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use ff_decode::DecodeError;
332    /// use std::time::Duration;
333    ///
334    /// // Decoding failures are recoverable
335    /// assert!(DecodeError::decoding_failed("test").is_recoverable());
336    ///
337    /// // Seek failures are recoverable
338    /// assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
339    ///
340    /// ```
341    #[must_use]
342    pub fn is_recoverable(&self) -> bool {
343        matches!(self, Self::DecodingFailed { .. } | Self::SeekFailed { .. })
344    }
345
346    /// Returns `true` if this error is fatal.
347    ///
348    /// Fatal errors indicate that the decoder cannot continue and
349    /// must be recreated or the file reopened.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use ff_decode::DecodeError;
355    /// use std::path::PathBuf;
356    ///
357    /// // File not found is fatal
358    /// assert!(DecodeError::FileNotFound { path: PathBuf::new() }.is_fatal());
359    ///
360    /// // Unsupported codec is fatal
361    /// assert!(DecodeError::UnsupportedCodec { codec: "test".to_string() }.is_fatal());
362    ///
363    /// ```
364    #[must_use]
365    pub fn is_fatal(&self) -> bool {
366        matches!(
367            self,
368            Self::FileNotFound { .. }
369                | Self::NoVideoStream { .. }
370                | Self::NoAudioStream { .. }
371                | Self::UnsupportedCodec { .. }
372                | Self::DecoderUnavailable { .. }
373        )
374    }
375}
376
377#[cfg(test)]
378#[allow(clippy::panic)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn test_decode_error_display() {
384        let error = DecodeError::FileNotFound {
385            path: PathBuf::from("/path/to/video.mp4"),
386        };
387        assert!(error.to_string().contains("File not found"));
388        assert!(error.to_string().contains("/path/to/video.mp4"));
389
390        let error = DecodeError::NoVideoStream {
391            path: PathBuf::from("/path/to/audio.mp3"),
392        };
393        assert!(error.to_string().contains("No video stream"));
394
395        let error = DecodeError::UnsupportedCodec {
396            codec: "unknown_codec".to_string(),
397        };
398        assert!(error.to_string().contains("Codec not supported"));
399        assert!(error.to_string().contains("unknown_codec"));
400    }
401
402    #[test]
403    fn test_decoding_failed_constructor() {
404        let error = DecodeError::decoding_failed("Corrupted frame data");
405        match error {
406            DecodeError::DecodingFailed { timestamp, reason } => {
407                assert!(timestamp.is_none());
408                assert_eq!(reason, "Corrupted frame data");
409            }
410            _ => panic!("Wrong error type"),
411        }
412    }
413
414    #[test]
415    fn test_decoding_failed_at_constructor() {
416        let error = DecodeError::decoding_failed_at(Duration::from_secs(30), "Invalid packet size");
417        match error {
418            DecodeError::DecodingFailed { timestamp, reason } => {
419                assert_eq!(timestamp, Some(Duration::from_secs(30)));
420                assert_eq!(reason, "Invalid packet size");
421            }
422            _ => panic!("Wrong error type"),
423        }
424    }
425
426    #[test]
427    fn test_seek_failed_constructor() {
428        let error = DecodeError::seek_failed(Duration::from_secs(60), "Index not found");
429        match error {
430            DecodeError::SeekFailed { target, reason } => {
431                assert_eq!(target, Duration::from_secs(60));
432                assert_eq!(reason, "Index not found");
433            }
434            _ => panic!("Wrong error type"),
435        }
436    }
437
438    #[test]
439    fn test_ffmpeg_constructor() {
440        let error = DecodeError::ffmpeg(-22, "AVERROR_INVALIDDATA");
441        match error {
442            DecodeError::Ffmpeg { code, message } => {
443                assert_eq!(code, -22);
444                assert_eq!(message, "AVERROR_INVALIDDATA");
445            }
446            _ => panic!("Wrong error type"),
447        }
448    }
449
450    #[test]
451    fn ffmpeg_should_format_with_code_and_message() {
452        let error = DecodeError::ffmpeg(-22, "Invalid data");
453        assert!(error.to_string().contains("code=-22"));
454        assert!(error.to_string().contains("Invalid data"));
455    }
456
457    #[test]
458    fn ffmpeg_with_zero_code_should_be_constructible() {
459        let error = DecodeError::ffmpeg(0, "allocation failed");
460        assert!(matches!(error, DecodeError::Ffmpeg { code: 0, .. }));
461    }
462
463    #[test]
464    fn decoder_unavailable_should_include_codec_and_hint() {
465        let e = DecodeError::decoder_unavailable(
466            "exr",
467            "Requires FFmpeg built with EXR support (--enable-decoder=exr)",
468        );
469        assert!(e.to_string().contains("exr"));
470        assert!(e.to_string().contains("Requires FFmpeg"));
471    }
472
473    #[test]
474    fn decoder_unavailable_should_be_fatal() {
475        let e = DecodeError::decoder_unavailable("exr", "hint");
476        assert!(e.is_fatal());
477        assert!(!e.is_recoverable());
478    }
479
480    #[test]
481    fn test_is_recoverable() {
482        assert!(DecodeError::decoding_failed("test").is_recoverable());
483        assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
484        assert!(
485            !DecodeError::FileNotFound {
486                path: PathBuf::new()
487            }
488            .is_recoverable()
489        );
490    }
491
492    #[test]
493    fn test_is_fatal() {
494        assert!(
495            DecodeError::FileNotFound {
496                path: PathBuf::new()
497            }
498            .is_fatal()
499        );
500        assert!(
501            DecodeError::NoVideoStream {
502                path: PathBuf::new()
503            }
504            .is_fatal()
505        );
506        assert!(
507            DecodeError::NoAudioStream {
508                path: PathBuf::new()
509            }
510            .is_fatal()
511        );
512        assert!(
513            DecodeError::UnsupportedCodec {
514                codec: "test".to_string()
515            }
516            .is_fatal()
517        );
518        assert!(!DecodeError::decoding_failed("test").is_fatal());
519    }
520
521    #[test]
522    fn test_io_error_conversion() {
523        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
524        let decode_error: DecodeError = io_error.into();
525        assert!(matches!(decode_error, DecodeError::Io(_)));
526    }
527
528    #[test]
529    fn test_hw_accel_unavailable() {
530        let error = DecodeError::HwAccelUnavailable {
531            accel: HardwareAccel::Nvdec,
532        };
533        assert!(
534            error
535                .to_string()
536                .contains("Hardware acceleration unavailable")
537        );
538        assert!(error.to_string().contains("Nvdec"));
539    }
540}