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 /// A decoded frame exceeds the supported resolution limit.
203 #[error("unsupported resolution {width}x{height}: exceeds 32768 in one or both axes")]
204 UnsupportedResolution {
205 /// Frame width.
206 width: u32,
207 /// Frame height.
208 height: u32,
209 },
210
211 /// Too many consecutive corrupt packets — the stream is unrecoverable.
212 #[error(
213 "stream corrupted: {consecutive_invalid_packets} consecutive invalid packets without recovery"
214 )]
215 StreamCorrupted {
216 /// Number of consecutive invalid packets that triggered the error.
217 consecutive_invalid_packets: u32,
218 },
219}
220
221impl DecodeError {
222 /// Creates a new [`DecodeError::DecodingFailed`] with the given reason.
223 ///
224 /// # Arguments
225 ///
226 /// * `reason` - Description of why decoding failed.
227 ///
228 /// # Examples
229 ///
230 /// ```
231 /// use ff_decode::DecodeError;
232 ///
233 /// let error = DecodeError::decoding_failed("Corrupted frame data");
234 /// assert!(error.to_string().contains("Corrupted frame data"));
235 /// assert!(error.is_recoverable());
236 /// ```
237 #[must_use]
238 pub fn decoding_failed(reason: impl Into<String>) -> Self {
239 Self::DecodingFailed {
240 timestamp: None,
241 reason: reason.into(),
242 }
243 }
244
245 /// Creates a new [`DecodeError::DecodingFailed`] with timestamp and reason.
246 ///
247 /// # Arguments
248 ///
249 /// * `timestamp` - The timestamp where decoding failed.
250 /// * `reason` - Description of why decoding failed.
251 ///
252 /// # Examples
253 ///
254 /// ```
255 /// use ff_decode::DecodeError;
256 /// use std::time::Duration;
257 ///
258 /// let error = DecodeError::decoding_failed_at(
259 /// Duration::from_secs(30),
260 /// "Invalid packet size"
261 /// );
262 /// assert!(error.to_string().contains("30s"));
263 /// assert!(error.is_recoverable());
264 /// ```
265 #[must_use]
266 pub fn decoding_failed_at(timestamp: Duration, reason: impl Into<String>) -> Self {
267 Self::DecodingFailed {
268 timestamp: Some(timestamp),
269 reason: reason.into(),
270 }
271 }
272
273 /// Creates a new [`DecodeError::SeekFailed`].
274 ///
275 /// # Arguments
276 ///
277 /// * `target` - The target position of the failed seek.
278 /// * `reason` - Description of why the seek failed.
279 ///
280 /// # Examples
281 ///
282 /// ```
283 /// use ff_decode::DecodeError;
284 /// use std::time::Duration;
285 ///
286 /// let error = DecodeError::seek_failed(
287 /// Duration::from_secs(60),
288 /// "Index not found"
289 /// );
290 /// assert!(error.to_string().contains("60s"));
291 /// assert!(error.is_recoverable());
292 /// ```
293 #[must_use]
294 pub fn seek_failed(target: Duration, reason: impl Into<String>) -> Self {
295 Self::SeekFailed {
296 target,
297 reason: reason.into(),
298 }
299 }
300
301 /// Creates a new [`DecodeError::DecoderUnavailable`].
302 ///
303 /// # Arguments
304 ///
305 /// * `codec` — Short codec name (e.g. `"exr"`).
306 /// * `hint` — Human-readable suggestion for the user.
307 #[must_use]
308 pub fn decoder_unavailable(codec: impl Into<String>, hint: impl Into<String>) -> Self {
309 Self::DecoderUnavailable {
310 codec: codec.into(),
311 hint: hint.into(),
312 }
313 }
314
315 /// Creates a new [`DecodeError::Ffmpeg`].
316 ///
317 /// # Arguments
318 ///
319 /// * `code` - The raw `FFmpeg` error code (negative integer). Pass `0` when no
320 /// numeric code is available.
321 /// * `message` - Human-readable description of the error.
322 ///
323 /// # Examples
324 ///
325 /// ```
326 /// use ff_decode::DecodeError;
327 ///
328 /// let error = DecodeError::ffmpeg(-22, "Invalid data found when processing input");
329 /// assert!(error.to_string().contains("Invalid data"));
330 /// assert!(error.to_string().contains("code=-22"));
331 /// ```
332 #[must_use]
333 pub fn ffmpeg(code: i32, message: impl Into<String>) -> Self {
334 Self::Ffmpeg {
335 code,
336 message: message.into(),
337 }
338 }
339
340 /// Returns `true` if this error is recoverable.
341 ///
342 /// Recoverable errors are those where the operation that raised the error
343 /// can be retried (or the decoder can transparently reconnect) without
344 /// rebuilding the decoder from scratch.
345 ///
346 /// | Variant | Recoverable |
347 /// |---|---|
348 /// | [`DecodingFailed`](Self::DecodingFailed) | ✓ — corrupt frame; skip and continue |
349 /// | [`SeekFailed`](Self::SeekFailed) | ✓ — retry at a different position |
350 /// | [`NetworkTimeout`](Self::NetworkTimeout) | ✓ — transient; reconnect |
351 /// | [`StreamInterrupted`](Self::StreamInterrupted) | ✓ — transient; reconnect |
352 /// | all others | ✗ |
353 ///
354 /// # Examples
355 ///
356 /// ```
357 /// use ff_decode::DecodeError;
358 /// use std::time::Duration;
359 ///
360 /// // Decoding failures are recoverable
361 /// assert!(DecodeError::decoding_failed("test").is_recoverable());
362 ///
363 /// // Seek failures are recoverable
364 /// assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
365 ///
366 /// ```
367 #[must_use]
368 pub fn is_recoverable(&self) -> bool {
369 match self {
370 Self::DecodingFailed { .. }
371 | Self::SeekFailed { .. }
372 | Self::NetworkTimeout { .. }
373 | Self::StreamInterrupted { .. } => true,
374 Self::FileNotFound { .. }
375 | Self::NoVideoStream { .. }
376 | Self::NoAudioStream { .. }
377 | Self::UnsupportedCodec { .. }
378 | Self::DecoderUnavailable { .. }
379 | Self::HwAccelUnavailable { .. }
380 | Self::InvalidOutputDimensions { .. }
381 | Self::ConnectionFailed { .. }
382 | Self::Io(_)
383 | Self::Ffmpeg { .. }
384 | Self::SeekNotSupported
385 | Self::UnsupportedResolution { .. }
386 | Self::StreamCorrupted { .. } => false,
387 }
388 }
389
390 /// Returns `true` if this error is fatal.
391 ///
392 /// Fatal errors indicate that the decoder cannot continue operating and
393 /// must be discarded; re-opening or reconfiguring is required.
394 ///
395 /// | Variant | Fatal |
396 /// |---|---|
397 /// | [`FileNotFound`](Self::FileNotFound) | ✓ |
398 /// | [`NoVideoStream`](Self::NoVideoStream) | ✓ |
399 /// | [`NoAudioStream`](Self::NoAudioStream) | ✓ |
400 /// | [`UnsupportedCodec`](Self::UnsupportedCodec) | ✓ |
401 /// | [`DecoderUnavailable`](Self::DecoderUnavailable) | ✓ |
402 /// | [`HwAccelUnavailable`](Self::HwAccelUnavailable) | ✓ — must reconfigure without HW |
403 /// | [`InvalidOutputDimensions`](Self::InvalidOutputDimensions) | ✓ — bad config |
404 /// | [`ConnectionFailed`](Self::ConnectionFailed) | ✓ — host unreachable |
405 /// | [`Io`](Self::Io) | ✓ — I/O failure |
406 /// | all others | ✗ |
407 ///
408 /// # Examples
409 ///
410 /// ```
411 /// use ff_decode::DecodeError;
412 /// use std::path::PathBuf;
413 ///
414 /// // File not found is fatal
415 /// assert!(DecodeError::FileNotFound { path: PathBuf::new() }.is_fatal());
416 ///
417 /// // Unsupported codec is fatal
418 /// assert!(DecodeError::UnsupportedCodec { codec: "test".to_string() }.is_fatal());
419 ///
420 /// ```
421 #[must_use]
422 pub fn is_fatal(&self) -> bool {
423 match self {
424 Self::FileNotFound { .. }
425 | Self::NoVideoStream { .. }
426 | Self::NoAudioStream { .. }
427 | Self::UnsupportedCodec { .. }
428 | Self::DecoderUnavailable { .. }
429 | Self::HwAccelUnavailable { .. }
430 | Self::InvalidOutputDimensions { .. }
431 | Self::ConnectionFailed { .. }
432 | Self::Io(_)
433 | Self::StreamCorrupted { .. } => true,
434 Self::DecodingFailed { .. }
435 | Self::SeekFailed { .. }
436 | Self::NetworkTimeout { .. }
437 | Self::StreamInterrupted { .. }
438 | Self::Ffmpeg { .. }
439 | Self::SeekNotSupported
440 | Self::UnsupportedResolution { .. } => false,
441 }
442 }
443}
444
445#[cfg(test)]
446#[allow(clippy::panic)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_decode_error_display() {
452 let error = DecodeError::FileNotFound {
453 path: PathBuf::from("/path/to/video.mp4"),
454 };
455 assert!(error.to_string().contains("File not found"));
456 assert!(error.to_string().contains("/path/to/video.mp4"));
457
458 let error = DecodeError::NoVideoStream {
459 path: PathBuf::from("/path/to/audio.mp3"),
460 };
461 assert!(error.to_string().contains("No video stream"));
462
463 let error = DecodeError::UnsupportedCodec {
464 codec: "unknown_codec".to_string(),
465 };
466 assert!(error.to_string().contains("Codec not supported"));
467 assert!(error.to_string().contains("unknown_codec"));
468 }
469
470 #[test]
471 fn test_decoding_failed_constructor() {
472 let error = DecodeError::decoding_failed("Corrupted frame data");
473 match error {
474 DecodeError::DecodingFailed { timestamp, reason } => {
475 assert!(timestamp.is_none());
476 assert_eq!(reason, "Corrupted frame data");
477 }
478 _ => panic!("Wrong error type"),
479 }
480 }
481
482 #[test]
483 fn test_decoding_failed_at_constructor() {
484 let error = DecodeError::decoding_failed_at(Duration::from_secs(30), "Invalid packet size");
485 match error {
486 DecodeError::DecodingFailed { timestamp, reason } => {
487 assert_eq!(timestamp, Some(Duration::from_secs(30)));
488 assert_eq!(reason, "Invalid packet size");
489 }
490 _ => panic!("Wrong error type"),
491 }
492 }
493
494 #[test]
495 fn test_seek_failed_constructor() {
496 let error = DecodeError::seek_failed(Duration::from_secs(60), "Index not found");
497 match error {
498 DecodeError::SeekFailed { target, reason } => {
499 assert_eq!(target, Duration::from_secs(60));
500 assert_eq!(reason, "Index not found");
501 }
502 _ => panic!("Wrong error type"),
503 }
504 }
505
506 #[test]
507 fn test_ffmpeg_constructor() {
508 let error = DecodeError::ffmpeg(-22, "AVERROR_INVALIDDATA");
509 match error {
510 DecodeError::Ffmpeg { code, message } => {
511 assert_eq!(code, -22);
512 assert_eq!(message, "AVERROR_INVALIDDATA");
513 }
514 _ => panic!("Wrong error type"),
515 }
516 }
517
518 #[test]
519 fn ffmpeg_should_format_with_code_and_message() {
520 let error = DecodeError::ffmpeg(-22, "Invalid data");
521 assert!(error.to_string().contains("code=-22"));
522 assert!(error.to_string().contains("Invalid data"));
523 }
524
525 #[test]
526 fn ffmpeg_with_zero_code_should_be_constructible() {
527 let error = DecodeError::ffmpeg(0, "allocation failed");
528 assert!(matches!(error, DecodeError::Ffmpeg { code: 0, .. }));
529 }
530
531 #[test]
532 fn decoder_unavailable_should_include_codec_and_hint() {
533 let e = DecodeError::decoder_unavailable(
534 "exr",
535 "Requires FFmpeg built with EXR support (--enable-decoder=exr)",
536 );
537 assert!(e.to_string().contains("exr"));
538 assert!(e.to_string().contains("Requires FFmpeg"));
539 }
540
541 #[test]
542 fn decoder_unavailable_should_be_fatal() {
543 let e = DecodeError::decoder_unavailable("exr", "hint");
544 assert!(e.is_fatal());
545 assert!(!e.is_recoverable());
546 }
547
548 #[test]
549 fn test_is_recoverable() {
550 assert!(DecodeError::decoding_failed("test").is_recoverable());
551 assert!(DecodeError::seek_failed(Duration::from_secs(1), "test").is_recoverable());
552 assert!(
553 !DecodeError::FileNotFound {
554 path: PathBuf::new()
555 }
556 .is_recoverable()
557 );
558 }
559
560 #[test]
561 fn test_is_fatal() {
562 assert!(
563 DecodeError::FileNotFound {
564 path: PathBuf::new()
565 }
566 .is_fatal()
567 );
568 assert!(
569 DecodeError::NoVideoStream {
570 path: PathBuf::new()
571 }
572 .is_fatal()
573 );
574 assert!(
575 DecodeError::NoAudioStream {
576 path: PathBuf::new()
577 }
578 .is_fatal()
579 );
580 assert!(
581 DecodeError::UnsupportedCodec {
582 codec: "test".to_string()
583 }
584 .is_fatal()
585 );
586 assert!(!DecodeError::decoding_failed("test").is_fatal());
587 }
588
589 #[test]
590 fn test_io_error_conversion() {
591 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
592 let decode_error: DecodeError = io_error.into();
593 assert!(matches!(decode_error, DecodeError::Io(_)));
594 }
595
596 #[test]
597 fn test_hw_accel_unavailable() {
598 let error = DecodeError::HwAccelUnavailable {
599 accel: HardwareAccel::Nvdec,
600 };
601 assert!(
602 error
603 .to_string()
604 .contains("Hardware acceleration unavailable")
605 );
606 assert!(error.to_string().contains("Nvdec"));
607 }
608
609 // ── is_fatal / is_recoverable exhaustive coverage ────────────────────────
610
611 #[test]
612 fn file_not_found_should_be_fatal_and_not_recoverable() {
613 let e = DecodeError::FileNotFound {
614 path: PathBuf::new(),
615 };
616 assert!(e.is_fatal());
617 assert!(!e.is_recoverable());
618 }
619
620 #[test]
621 fn no_video_stream_should_be_fatal_and_not_recoverable() {
622 let e = DecodeError::NoVideoStream {
623 path: PathBuf::new(),
624 };
625 assert!(e.is_fatal());
626 assert!(!e.is_recoverable());
627 }
628
629 #[test]
630 fn no_audio_stream_should_be_fatal_and_not_recoverable() {
631 let e = DecodeError::NoAudioStream {
632 path: PathBuf::new(),
633 };
634 assert!(e.is_fatal());
635 assert!(!e.is_recoverable());
636 }
637
638 #[test]
639 fn unsupported_codec_should_be_fatal_and_not_recoverable() {
640 let e = DecodeError::UnsupportedCodec {
641 codec: "test".to_string(),
642 };
643 assert!(e.is_fatal());
644 assert!(!e.is_recoverable());
645 }
646
647 #[test]
648 fn decoder_unavailable_should_be_fatal_and_not_recoverable() {
649 let e = DecodeError::decoder_unavailable("exr", "hint");
650 assert!(e.is_fatal());
651 assert!(!e.is_recoverable());
652 }
653
654 #[test]
655 fn decoding_failed_should_be_recoverable_and_not_fatal() {
656 let e = DecodeError::decoding_failed("corrupt frame");
657 assert!(e.is_recoverable());
658 assert!(!e.is_fatal());
659 }
660
661 #[test]
662 fn seek_failed_should_be_recoverable_and_not_fatal() {
663 let e = DecodeError::seek_failed(Duration::from_secs(5), "index not found");
664 assert!(e.is_recoverable());
665 assert!(!e.is_fatal());
666 }
667
668 #[test]
669 fn hw_accel_unavailable_should_be_fatal_and_not_recoverable() {
670 let e = DecodeError::HwAccelUnavailable {
671 accel: HardwareAccel::Nvdec,
672 };
673 assert!(e.is_fatal());
674 assert!(!e.is_recoverable());
675 }
676
677 #[test]
678 fn invalid_output_dimensions_should_be_fatal_and_not_recoverable() {
679 let e = DecodeError::InvalidOutputDimensions {
680 width: 0,
681 height: 0,
682 };
683 assert!(e.is_fatal());
684 assert!(!e.is_recoverable());
685 }
686
687 #[test]
688 fn ffmpeg_error_should_be_neither_fatal_nor_recoverable() {
689 let e = DecodeError::ffmpeg(-22, "AVERROR_INVALIDDATA");
690 assert!(!e.is_fatal());
691 assert!(!e.is_recoverable());
692 }
693
694 #[test]
695 fn io_error_should_be_fatal_and_not_recoverable() {
696 let e: DecodeError =
697 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied").into();
698 assert!(e.is_fatal());
699 assert!(!e.is_recoverable());
700 }
701
702 #[test]
703 fn network_timeout_should_be_recoverable_and_not_fatal() {
704 let e = DecodeError::NetworkTimeout {
705 code: -110,
706 endpoint: "rtmp://example.com/live".to_string(),
707 message: "timed out".to_string(),
708 };
709 assert!(e.is_recoverable());
710 assert!(!e.is_fatal());
711 }
712
713 #[test]
714 fn connection_failed_should_be_fatal_and_not_recoverable() {
715 let e = DecodeError::ConnectionFailed {
716 code: -111,
717 endpoint: "rtmp://example.com/live".to_string(),
718 message: "connection refused".to_string(),
719 };
720 assert!(e.is_fatal());
721 assert!(!e.is_recoverable());
722 }
723
724 #[test]
725 fn stream_interrupted_should_be_recoverable_and_not_fatal() {
726 let e = DecodeError::StreamInterrupted {
727 code: -5,
728 endpoint: "rtmp://example.com/live".to_string(),
729 message: "I/O error".to_string(),
730 };
731 assert!(e.is_recoverable());
732 assert!(!e.is_fatal());
733 }
734
735 #[test]
736 fn seek_not_supported_should_be_neither_fatal_nor_recoverable() {
737 let e = DecodeError::SeekNotSupported;
738 assert!(!e.is_fatal());
739 assert!(!e.is_recoverable());
740 }
741
742 #[test]
743 fn unsupported_resolution_display_should_contain_width_x_height() {
744 let e = DecodeError::UnsupportedResolution {
745 width: 40000,
746 height: 480,
747 };
748 let msg = e.to_string();
749 assert!(msg.contains("40000x480"), "expected '40000x480' in '{msg}'");
750 }
751
752 #[test]
753 fn unsupported_resolution_display_should_contain_axes_hint() {
754 let e = DecodeError::UnsupportedResolution {
755 width: 640,
756 height: 40000,
757 };
758 let msg = e.to_string();
759 assert!(msg.contains("32768"), "expected '32768' limit in '{msg}'");
760 }
761
762 #[test]
763 fn unsupported_resolution_should_be_neither_fatal_nor_recoverable() {
764 let e = DecodeError::UnsupportedResolution {
765 width: 40000,
766 height: 40000,
767 };
768 assert!(!e.is_fatal());
769 assert!(!e.is_recoverable());
770 }
771
772 #[test]
773 fn stream_corrupted_display_should_contain_packet_count() {
774 let e = DecodeError::StreamCorrupted {
775 consecutive_invalid_packets: 32,
776 };
777 let msg = e.to_string();
778 assert!(msg.contains("32"), "expected '32' in '{msg}'");
779 }
780
781 #[test]
782 fn stream_corrupted_display_should_mention_consecutive() {
783 let e = DecodeError::StreamCorrupted {
784 consecutive_invalid_packets: 32,
785 };
786 let msg = e.to_string();
787 assert!(
788 msg.contains("consecutive"),
789 "expected 'consecutive' in '{msg}'"
790 );
791 }
792
793 #[test]
794 fn stream_corrupted_should_be_fatal_and_not_recoverable() {
795 let e = DecodeError::StreamCorrupted {
796 consecutive_invalid_packets: 32,
797 };
798 assert!(e.is_fatal());
799 assert!(!e.is_recoverable());
800 }
801}