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}