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/// - **Stream state**: [`EndOfStream`](Self::EndOfStream)
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    /// Decoding operation failed at a specific point.
69    ///
70    /// This can occur due to corrupted data, unexpected stream format,
71    /// or internal decoder errors.
72    #[error("Decoding failed at {timestamp:?}: {reason}")]
73    DecodingFailed {
74        /// Timestamp where decoding failed (if known).
75        timestamp: Option<Duration>,
76        /// Reason for the failure.
77        reason: String,
78    },
79
80    /// Seek operation failed.
81    ///
82    /// Seeking may fail for various reasons including corrupted index,
83    /// seeking beyond file bounds, or stream format limitations.
84    #[error("Seek failed to {target:?}: {reason}")]
85    SeekFailed {
86        /// Target position of the seek.
87        target: Duration,
88        /// Reason for the failure.
89        reason: String,
90    },
91
92    /// Requested hardware acceleration is not available.
93    ///
94    /// This error occurs when a specific hardware accelerator is requested
95    /// but the system doesn't support it. Consider using [`HardwareAccel::Auto`]
96    /// for automatic fallback.
97    #[error("Hardware acceleration unavailable: {accel:?}")]
98    HwAccelUnavailable {
99        /// The unavailable hardware acceleration type.
100        accel: HardwareAccel,
101    },
102
103    /// End of stream has been reached.
104    ///
105    /// This is returned when attempting to decode past the end of the file.
106    /// It's a normal condition that indicates all frames have been decoded.
107    #[error("End of stream")]
108    EndOfStream,
109
110    /// `FFmpeg` internal error.
111    ///
112    /// This wraps errors from the underlying `FFmpeg` library that don't
113    /// fit into other categories.
114    #[error("ffmpeg error: {message} (code={code})")]
115    Ffmpeg {
116        /// Raw `FFmpeg` error code (negative integer). `0` when no numeric code is available.
117        code: i32,
118        /// Human-readable error message from `av_strerror` or an internal description.
119        message: String,
120    },
121
122    /// I/O error during file operations.
123    ///
124    /// This wraps standard I/O errors such as permission denied,
125    /// disk full, or network errors for remote files.
126    #[error("IO error: {0}")]
127    Io(#[from] std::io::Error),
128}
129
130impl DecodeError {
131    /// Creates a new [`DecodeError::DecodingFailed`] with the given reason.
132    ///
133    /// # Arguments
134    ///
135    /// * `reason` - Description of why decoding failed.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use ff_decode::DecodeError;
141    ///
142    /// let error = DecodeError::decoding_failed("Corrupted frame data");
143    /// assert!(error.to_string().contains("Corrupted frame data"));
144    /// assert!(error.is_recoverable());
145    /// ```
146    #[must_use]
147    pub fn decoding_failed(reason: impl Into<String>) -> Self {
148        Self::DecodingFailed {
149            timestamp: None,
150            reason: reason.into(),
151        }
152    }
153
154    /// Creates a new [`DecodeError::DecodingFailed`] with timestamp and reason.
155    ///
156    /// # Arguments
157    ///
158    /// * `timestamp` - The timestamp where decoding failed.
159    /// * `reason` - Description of why decoding failed.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use ff_decode::DecodeError;
165    /// use std::time::Duration;
166    ///
167    /// let error = DecodeError::decoding_failed_at(
168    ///     Duration::from_secs(30),
169    ///     "Invalid packet size"
170    /// );
171    /// assert!(error.to_string().contains("30s"));
172    /// assert!(error.is_recoverable());
173    /// ```
174    #[must_use]
175    pub fn decoding_failed_at(timestamp: Duration, reason: impl Into<String>) -> Self {
176        Self::DecodingFailed {
177            timestamp: Some(timestamp),
178            reason: reason.into(),
179        }
180    }
181
182    /// Creates a new [`DecodeError::SeekFailed`].
183    ///
184    /// # Arguments
185    ///
186    /// * `target` - The target position of the failed seek.
187    /// * `reason` - Description of why the seek failed.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use ff_decode::DecodeError;
193    /// use std::time::Duration;
194    ///
195    /// let error = DecodeError::seek_failed(
196    ///     Duration::from_secs(60),
197    ///     "Index not found"
198    /// );
199    /// assert!(error.to_string().contains("60s"));
200    /// assert!(error.is_recoverable());
201    /// ```
202    #[must_use]
203    pub fn seek_failed(target: Duration, reason: impl Into<String>) -> Self {
204        Self::SeekFailed {
205            target,
206            reason: reason.into(),
207        }
208    }
209
210    /// Creates a new [`DecodeError::Ffmpeg`].
211    ///
212    /// # Arguments
213    ///
214    /// * `code` - The raw `FFmpeg` error code (negative integer). Pass `0` when no
215    ///   numeric code is available.
216    /// * `message` - Human-readable description of the error.
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use ff_decode::DecodeError;
222    ///
223    /// let error = DecodeError::ffmpeg(-22, "Invalid data found when processing input");
224    /// assert!(error.to_string().contains("Invalid data"));
225    /// assert!(error.to_string().contains("code=-22"));
226    /// ```
227    #[must_use]
228    pub fn ffmpeg(code: i32, message: impl Into<String>) -> Self {
229        Self::Ffmpeg {
230            code,
231            message: message.into(),
232        }
233    }
234
235    /// Returns `true` if this error indicates end of stream.
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// use ff_decode::DecodeError;
241    ///
242    /// assert!(DecodeError::EndOfStream.is_eof());
243    /// assert!(!DecodeError::decoding_failed("test").is_eof());
244    /// ```
245    #[must_use]
246    pub fn is_eof(&self) -> bool {
247        matches!(self, Self::EndOfStream)
248    }
249
250    /// Returns `true` if this error is recoverable.
251    ///
252    /// Recoverable errors are those where the decoder can continue
253    /// operating after the error, such as corrupted frames that can
254    /// be skipped.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use ff_decode::DecodeError;
260    /// use std::time::Duration;
261    ///
262    /// // Decoding failures are recoverable
263    /// assert!(DecodeError::decoding_failed("test").is_recoverable());
264    ///
265    /// // Seek failures are recoverable
266    /// assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
267    ///
268    /// // End of stream is not recoverable
269    /// assert!(!DecodeError::EndOfStream.is_recoverable());
270    /// ```
271    #[must_use]
272    pub fn is_recoverable(&self) -> bool {
273        matches!(self, Self::DecodingFailed { .. } | Self::SeekFailed { .. })
274    }
275
276    /// Returns `true` if this error is fatal.
277    ///
278    /// Fatal errors indicate that the decoder cannot continue and
279    /// must be recreated or the file reopened.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use ff_decode::DecodeError;
285    /// use std::path::PathBuf;
286    ///
287    /// // File not found is fatal
288    /// assert!(DecodeError::FileNotFound { path: PathBuf::new() }.is_fatal());
289    ///
290    /// // Unsupported codec is fatal
291    /// assert!(DecodeError::UnsupportedCodec { codec: "test".to_string() }.is_fatal());
292    ///
293    /// // End of stream is not fatal
294    /// assert!(!DecodeError::EndOfStream.is_fatal());
295    /// ```
296    #[must_use]
297    pub fn is_fatal(&self) -> bool {
298        matches!(
299            self,
300            Self::FileNotFound { .. }
301                | Self::NoVideoStream { .. }
302                | Self::NoAudioStream { .. }
303                | Self::UnsupportedCodec { .. }
304        )
305    }
306}
307
308#[cfg(test)]
309#[allow(clippy::panic)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_decode_error_display() {
315        let error = DecodeError::FileNotFound {
316            path: PathBuf::from("/path/to/video.mp4"),
317        };
318        assert!(error.to_string().contains("File not found"));
319        assert!(error.to_string().contains("/path/to/video.mp4"));
320
321        let error = DecodeError::NoVideoStream {
322            path: PathBuf::from("/path/to/audio.mp3"),
323        };
324        assert!(error.to_string().contains("No video stream"));
325
326        let error = DecodeError::UnsupportedCodec {
327            codec: "unknown_codec".to_string(),
328        };
329        assert!(error.to_string().contains("Codec not supported"));
330        assert!(error.to_string().contains("unknown_codec"));
331
332        let error = DecodeError::EndOfStream;
333        assert_eq!(error.to_string(), "End of stream");
334    }
335
336    #[test]
337    fn test_decoding_failed_constructor() {
338        let error = DecodeError::decoding_failed("Corrupted frame data");
339        match error {
340            DecodeError::DecodingFailed { timestamp, reason } => {
341                assert!(timestamp.is_none());
342                assert_eq!(reason, "Corrupted frame data");
343            }
344            _ => panic!("Wrong error type"),
345        }
346    }
347
348    #[test]
349    fn test_decoding_failed_at_constructor() {
350        let error = DecodeError::decoding_failed_at(Duration::from_secs(30), "Invalid packet size");
351        match error {
352            DecodeError::DecodingFailed { timestamp, reason } => {
353                assert_eq!(timestamp, Some(Duration::from_secs(30)));
354                assert_eq!(reason, "Invalid packet size");
355            }
356            _ => panic!("Wrong error type"),
357        }
358    }
359
360    #[test]
361    fn test_seek_failed_constructor() {
362        let error = DecodeError::seek_failed(Duration::from_secs(60), "Index not found");
363        match error {
364            DecodeError::SeekFailed { target, reason } => {
365                assert_eq!(target, Duration::from_secs(60));
366                assert_eq!(reason, "Index not found");
367            }
368            _ => panic!("Wrong error type"),
369        }
370    }
371
372    #[test]
373    fn test_ffmpeg_constructor() {
374        let error = DecodeError::ffmpeg(-22, "AVERROR_INVALIDDATA");
375        match error {
376            DecodeError::Ffmpeg { code, message } => {
377                assert_eq!(code, -22);
378                assert_eq!(message, "AVERROR_INVALIDDATA");
379            }
380            _ => panic!("Wrong error type"),
381        }
382    }
383
384    #[test]
385    fn ffmpeg_should_format_with_code_and_message() {
386        let error = DecodeError::ffmpeg(-22, "Invalid data");
387        assert!(error.to_string().contains("code=-22"));
388        assert!(error.to_string().contains("Invalid data"));
389    }
390
391    #[test]
392    fn ffmpeg_with_zero_code_should_be_constructible() {
393        let error = DecodeError::ffmpeg(0, "allocation failed");
394        assert!(matches!(error, DecodeError::Ffmpeg { code: 0, .. }));
395    }
396
397    #[test]
398    fn test_is_eof() {
399        assert!(DecodeError::EndOfStream.is_eof());
400        assert!(!DecodeError::decoding_failed("test").is_eof());
401        assert!(
402            !DecodeError::FileNotFound {
403                path: PathBuf::new()
404            }
405            .is_eof()
406        );
407    }
408
409    #[test]
410    fn test_is_recoverable() {
411        assert!(DecodeError::decoding_failed("test").is_recoverable());
412        assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
413        assert!(!DecodeError::EndOfStream.is_recoverable());
414        assert!(
415            !DecodeError::FileNotFound {
416                path: PathBuf::new()
417            }
418            .is_recoverable()
419        );
420    }
421
422    #[test]
423    fn test_is_fatal() {
424        assert!(
425            DecodeError::FileNotFound {
426                path: PathBuf::new()
427            }
428            .is_fatal()
429        );
430        assert!(
431            DecodeError::NoVideoStream {
432                path: PathBuf::new()
433            }
434            .is_fatal()
435        );
436        assert!(
437            DecodeError::NoAudioStream {
438                path: PathBuf::new()
439            }
440            .is_fatal()
441        );
442        assert!(
443            DecodeError::UnsupportedCodec {
444                codec: "test".to_string()
445            }
446            .is_fatal()
447        );
448        assert!(!DecodeError::EndOfStream.is_fatal());
449        assert!(!DecodeError::decoding_failed("test").is_fatal());
450    }
451
452    #[test]
453    fn test_io_error_conversion() {
454        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
455        let decode_error: DecodeError = io_error.into();
456        assert!(matches!(decode_error, DecodeError::Io(_)));
457    }
458
459    #[test]
460    fn test_hw_accel_unavailable() {
461        let error = DecodeError::HwAccelUnavailable {
462            accel: HardwareAccel::Nvdec,
463        };
464        assert!(
465            error
466                .to_string()
467                .contains("Hardware acceleration unavailable")
468        );
469        assert!(error.to_string().contains("Nvdec"));
470    }
471}