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: {0}")]
115    Ffmpeg(String),
116
117    /// I/O error during file operations.
118    ///
119    /// This wraps standard I/O errors such as permission denied,
120    /// disk full, or network errors for remote files.
121    #[error("IO error: {0}")]
122    Io(#[from] std::io::Error),
123}
124
125impl DecodeError {
126    /// Creates a new [`DecodeError::DecodingFailed`] with the given reason.
127    ///
128    /// # Arguments
129    ///
130    /// * `reason` - Description of why decoding failed.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use ff_decode::DecodeError;
136    ///
137    /// let error = DecodeError::decoding_failed("Corrupted frame data");
138    /// assert!(error.to_string().contains("Corrupted frame data"));
139    /// assert!(error.is_recoverable());
140    /// ```
141    #[must_use]
142    pub fn decoding_failed(reason: impl Into<String>) -> Self {
143        Self::DecodingFailed {
144            timestamp: None,
145            reason: reason.into(),
146        }
147    }
148
149    /// Creates a new [`DecodeError::DecodingFailed`] with timestamp and reason.
150    ///
151    /// # Arguments
152    ///
153    /// * `timestamp` - The timestamp where decoding failed.
154    /// * `reason` - Description of why decoding failed.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use ff_decode::DecodeError;
160    /// use std::time::Duration;
161    ///
162    /// let error = DecodeError::decoding_failed_at(
163    ///     Duration::from_secs(30),
164    ///     "Invalid packet size"
165    /// );
166    /// assert!(error.to_string().contains("30s"));
167    /// assert!(error.is_recoverable());
168    /// ```
169    #[must_use]
170    pub fn decoding_failed_at(timestamp: Duration, reason: impl Into<String>) -> Self {
171        Self::DecodingFailed {
172            timestamp: Some(timestamp),
173            reason: reason.into(),
174        }
175    }
176
177    /// Creates a new [`DecodeError::SeekFailed`].
178    ///
179    /// # Arguments
180    ///
181    /// * `target` - The target position of the failed seek.
182    /// * `reason` - Description of why the seek failed.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use ff_decode::DecodeError;
188    /// use std::time::Duration;
189    ///
190    /// let error = DecodeError::seek_failed(
191    ///     Duration::from_secs(60),
192    ///     "Index not found"
193    /// );
194    /// assert!(error.to_string().contains("60s"));
195    /// assert!(error.is_recoverable());
196    /// ```
197    #[must_use]
198    pub fn seek_failed(target: Duration, reason: impl Into<String>) -> Self {
199        Self::SeekFailed {
200            target,
201            reason: reason.into(),
202        }
203    }
204
205    /// Creates a new [`DecodeError::Ffmpeg`].
206    ///
207    /// # Arguments
208    ///
209    /// * `message` - The `FFmpeg` error message.
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use ff_decode::DecodeError;
215    ///
216    /// let error = DecodeError::ffmpeg("AVERROR_INVALIDDATA");
217    /// assert!(error.to_string().contains("AVERROR_INVALIDDATA"));
218    /// ```
219    #[must_use]
220    pub fn ffmpeg(message: impl Into<String>) -> Self {
221        Self::Ffmpeg(message.into())
222    }
223
224    /// Returns `true` if this error indicates end of stream.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use ff_decode::DecodeError;
230    ///
231    /// assert!(DecodeError::EndOfStream.is_eof());
232    /// assert!(!DecodeError::decoding_failed("test").is_eof());
233    /// ```
234    #[must_use]
235    pub fn is_eof(&self) -> bool {
236        matches!(self, Self::EndOfStream)
237    }
238
239    /// Returns `true` if this error is recoverable.
240    ///
241    /// Recoverable errors are those where the decoder can continue
242    /// operating after the error, such as corrupted frames that can
243    /// be skipped.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use ff_decode::DecodeError;
249    /// use std::time::Duration;
250    ///
251    /// // Decoding failures are recoverable
252    /// assert!(DecodeError::decoding_failed("test").is_recoverable());
253    ///
254    /// // Seek failures are recoverable
255    /// assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
256    ///
257    /// // End of stream is not recoverable
258    /// assert!(!DecodeError::EndOfStream.is_recoverable());
259    /// ```
260    #[must_use]
261    pub fn is_recoverable(&self) -> bool {
262        matches!(self, Self::DecodingFailed { .. } | Self::SeekFailed { .. })
263    }
264
265    /// Returns `true` if this error is fatal.
266    ///
267    /// Fatal errors indicate that the decoder cannot continue and
268    /// must be recreated or the file reopened.
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use ff_decode::DecodeError;
274    /// use std::path::PathBuf;
275    ///
276    /// // File not found is fatal
277    /// assert!(DecodeError::FileNotFound { path: PathBuf::new() }.is_fatal());
278    ///
279    /// // Unsupported codec is fatal
280    /// assert!(DecodeError::UnsupportedCodec { codec: "test".to_string() }.is_fatal());
281    ///
282    /// // End of stream is not fatal
283    /// assert!(!DecodeError::EndOfStream.is_fatal());
284    /// ```
285    #[must_use]
286    pub fn is_fatal(&self) -> bool {
287        matches!(
288            self,
289            Self::FileNotFound { .. }
290                | Self::NoVideoStream { .. }
291                | Self::NoAudioStream { .. }
292                | Self::UnsupportedCodec { .. }
293        )
294    }
295}
296
297#[cfg(test)]
298#[allow(clippy::panic)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_decode_error_display() {
304        let error = DecodeError::FileNotFound {
305            path: PathBuf::from("/path/to/video.mp4"),
306        };
307        assert!(error.to_string().contains("File not found"));
308        assert!(error.to_string().contains("/path/to/video.mp4"));
309
310        let error = DecodeError::NoVideoStream {
311            path: PathBuf::from("/path/to/audio.mp3"),
312        };
313        assert!(error.to_string().contains("No video stream"));
314
315        let error = DecodeError::UnsupportedCodec {
316            codec: "unknown_codec".to_string(),
317        };
318        assert!(error.to_string().contains("Codec not supported"));
319        assert!(error.to_string().contains("unknown_codec"));
320
321        let error = DecodeError::EndOfStream;
322        assert_eq!(error.to_string(), "End of stream");
323    }
324
325    #[test]
326    fn test_decoding_failed_constructor() {
327        let error = DecodeError::decoding_failed("Corrupted frame data");
328        match error {
329            DecodeError::DecodingFailed { timestamp, reason } => {
330                assert!(timestamp.is_none());
331                assert_eq!(reason, "Corrupted frame data");
332            }
333            _ => panic!("Wrong error type"),
334        }
335    }
336
337    #[test]
338    fn test_decoding_failed_at_constructor() {
339        let error = DecodeError::decoding_failed_at(Duration::from_secs(30), "Invalid packet size");
340        match error {
341            DecodeError::DecodingFailed { timestamp, reason } => {
342                assert_eq!(timestamp, Some(Duration::from_secs(30)));
343                assert_eq!(reason, "Invalid packet size");
344            }
345            _ => panic!("Wrong error type"),
346        }
347    }
348
349    #[test]
350    fn test_seek_failed_constructor() {
351        let error = DecodeError::seek_failed(Duration::from_secs(60), "Index not found");
352        match error {
353            DecodeError::SeekFailed { target, reason } => {
354                assert_eq!(target, Duration::from_secs(60));
355                assert_eq!(reason, "Index not found");
356            }
357            _ => panic!("Wrong error type"),
358        }
359    }
360
361    #[test]
362    fn test_ffmpeg_constructor() {
363        let error = DecodeError::ffmpeg("AVERROR_INVALIDDATA");
364        match error {
365            DecodeError::Ffmpeg(msg) => {
366                assert_eq!(msg, "AVERROR_INVALIDDATA");
367            }
368            _ => panic!("Wrong error type"),
369        }
370    }
371
372    #[test]
373    fn test_is_eof() {
374        assert!(DecodeError::EndOfStream.is_eof());
375        assert!(!DecodeError::decoding_failed("test").is_eof());
376        assert!(
377            !DecodeError::FileNotFound {
378                path: PathBuf::new()
379            }
380            .is_eof()
381        );
382    }
383
384    #[test]
385    fn test_is_recoverable() {
386        assert!(DecodeError::decoding_failed("test").is_recoverable());
387        assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
388        assert!(!DecodeError::EndOfStream.is_recoverable());
389        assert!(
390            !DecodeError::FileNotFound {
391                path: PathBuf::new()
392            }
393            .is_recoverable()
394        );
395    }
396
397    #[test]
398    fn test_is_fatal() {
399        assert!(
400            DecodeError::FileNotFound {
401                path: PathBuf::new()
402            }
403            .is_fatal()
404        );
405        assert!(
406            DecodeError::NoVideoStream {
407                path: PathBuf::new()
408            }
409            .is_fatal()
410        );
411        assert!(
412            DecodeError::NoAudioStream {
413                path: PathBuf::new()
414            }
415            .is_fatal()
416        );
417        assert!(
418            DecodeError::UnsupportedCodec {
419                codec: "test".to_string()
420            }
421            .is_fatal()
422        );
423        assert!(!DecodeError::EndOfStream.is_fatal());
424        assert!(!DecodeError::decoding_failed("test").is_fatal());
425    }
426
427    #[test]
428    fn test_io_error_conversion() {
429        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
430        let decode_error: DecodeError = io_error.into();
431        assert!(matches!(decode_error, DecodeError::Io(_)));
432    }
433
434    #[test]
435    fn test_hw_accel_unavailable() {
436        let error = DecodeError::HwAccelUnavailable {
437            accel: HardwareAccel::Nvdec,
438        };
439        assert!(
440            error
441                .to_string()
442                .contains("Hardware acceleration unavailable")
443        );
444        assert!(error.to_string().contains("Nvdec"));
445    }
446}