Skip to main content

ff_preview/
error.rs

1//! Error types for ff-preview.
2
3use std::path::PathBuf;
4
5use ff_format::{ErrorSeverity, MediaError};
6use thiserror::Error;
7
8/// Errors that can occur during preview and proxy operations.
9#[derive(Debug, Error)]
10pub enum PreviewError {
11    /// The media file was not found at the specified path.
12    #[error("file not found: path={path}")]
13    FileNotFound {
14        /// Path that was not found.
15        path: PathBuf,
16    },
17
18    /// The media file has no video stream.
19    #[error("no video stream found: path={path}")]
20    NoVideoStream {
21        /// Path to the media file.
22        path: PathBuf,
23    },
24
25    /// A seek operation failed.
26    #[error("seek failed: target={target:?} reason={reason}")]
27    SeekFailed {
28        /// Target timestamp of the failed seek.
29        target: std::time::Duration,
30        /// Human-readable reason for the failure.
31        reason: String,
32    },
33
34    /// An underlying decode error occurred.
35    #[error("decode failed: {0}")]
36    Decode(#[from] ff_decode::DecodeError),
37
38    /// A raw `FFmpeg` error.
39    ///
40    /// `code` is the negative integer returned by the `FFmpeg` API, or `0` when no
41    /// numeric code is available. `message` is from `av_strerror` or an internal
42    /// description.
43    #[error("ffmpeg error: {message} (code={code})")]
44    Ffmpeg {
45        /// Raw `FFmpeg` error code (negative i32). `0` when no numeric code is available.
46        code: i32,
47        /// Human-readable message from `av_strerror` or an internal description.
48        message: String,
49    },
50
51    /// A probe error while analysing the media file.
52    #[error("probe failed: {0}")]
53    Probe(#[from] ff_probe::ProbeError),
54
55    /// A proxy generation pipeline error.
56    #[cfg(feature = "proxy")]
57    #[error("pipeline failed: {0}")]
58    Pipeline(#[from] ff_pipeline::PipelineError),
59
60    /// An I/O error during file operations.
61    #[error("io error: {0}")]
62    Io(#[from] std::io::Error),
63
64    /// A seek target lies outside the valid range of the timeline.
65    #[error("seek out of range: pts={pts:?}")]
66    SeekOutOfRange {
67        /// The requested presentation timestamp that fell outside all clips.
68        pts: std::time::Duration,
69    },
70
71    /// The scene needs the GPU compositor for something the CPU compositor
72    /// refuses to build.
73    ///
74    /// Raised at open time so a timeline that cannot play correctly on this
75    /// machine fails before decoding starts, instead of showing frames with the
76    /// offending layer silently missing.
77    #[error("preview needs the GPU compositor: {reason}")]
78    NeedsGpuCompositor {
79        /// What the CPU compositor refused, and where in the scene.
80        reason: String,
81    },
82
83    /// The background decode thread panicked and could not be recovered.
84    ///
85    /// The buffer cannot continue decoding; recover at the application level by
86    /// rebuilding it (e.g. reopen the file). This replaces an earlier internal
87    /// panic on the same condition.
88    #[error("decode thread poisoned: the background decoder panicked and cannot be recovered")]
89    DecodeThreadPoisoned,
90}
91
92impl MediaError for PreviewError {
93    fn severity(&self) -> ErrorSeverity {
94        match self {
95            Self::Decode(e) => e.severity(),
96            Self::Probe(e) => e.severity(),
97            #[cfg(feature = "proxy")]
98            Self::Pipeline(e) => e.severity(),
99            Self::SeekFailed { .. } | Self::DecodeThreadPoisoned => ErrorSeverity::Recoverable,
100            Self::Ffmpeg { .. } | Self::SeekOutOfRange { .. } => ErrorSeverity::Other,
101            Self::FileNotFound { .. }
102            | Self::NoVideoStream { .. }
103            | Self::NeedsGpuCompositor { .. }
104            | Self::Io(_) => ErrorSeverity::Fatal,
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn preview_io_should_be_fatal() {
115        let e: PreviewError = std::io::Error::other("x").into();
116        assert!(e.is_fatal() && !e.is_recoverable());
117    }
118
119    #[test]
120    fn preview_seek_failed_should_be_recoverable() {
121        let e = PreviewError::SeekFailed {
122            target: std::time::Duration::from_secs(1),
123            reason: "x".into(),
124        };
125        assert!(e.is_recoverable() && !e.is_fatal());
126    }
127
128    #[test]
129    fn preview_decode_thread_poisoned_should_be_recoverable() {
130        let e = PreviewError::DecodeThreadPoisoned;
131        assert!(e.is_recoverable() && !e.is_fatal());
132        assert!(
133            e.to_string().contains("poisoned"),
134            "message must name the condition: {e}"
135        );
136    }
137
138    #[test]
139    fn preview_decode_should_delegate_recoverable() {
140        // A recoverable inner DecodeError must remain recoverable through the wrapper.
141        let e = PreviewError::Decode(ff_decode::DecodeError::decoding_failed("x"));
142        assert!(e.is_recoverable() && !e.is_fatal());
143    }
144}