1use std::path::PathBuf;
4
5use ff_format::{ErrorSeverity, MediaError};
6use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum PreviewError {
11 #[error("file not found: path={path}")]
13 FileNotFound {
14 path: PathBuf,
16 },
17
18 #[error("no video stream found: path={path}")]
20 NoVideoStream {
21 path: PathBuf,
23 },
24
25 #[error("seek failed: target={target:?} reason={reason}")]
27 SeekFailed {
28 target: std::time::Duration,
30 reason: String,
32 },
33
34 #[error("decode failed: {0}")]
36 Decode(#[from] ff_decode::DecodeError),
37
38 #[error("ffmpeg error: {message} (code={code})")]
44 Ffmpeg {
45 code: i32,
47 message: String,
49 },
50
51 #[error("probe failed: {0}")]
53 Probe(#[from] ff_probe::ProbeError),
54
55 #[cfg(feature = "proxy")]
57 #[error("pipeline failed: {0}")]
58 Pipeline(#[from] ff_pipeline::PipelineError),
59
60 #[error("io error: {0}")]
62 Io(#[from] std::io::Error),
63
64 #[error("seek out of range: pts={pts:?}")]
66 SeekOutOfRange {
67 pts: std::time::Duration,
69 },
70
71 #[error("preview needs the GPU compositor: {reason}")]
78 NeedsGpuCompositor {
79 reason: String,
81 },
82
83 #[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 let e = PreviewError::Decode(ff_decode::DecodeError::decoding_failed("x"));
142 assert!(e.is_recoverable() && !e.is_fatal());
143 }
144}