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}