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}