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}