1use std::path::PathBuf;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum EncodeError {
9 #[error("Cannot create output file: {path}")]
11 CannotCreateFile {
12 path: PathBuf,
14 },
15
16 #[error("Unsupported codec: {codec}")]
18 UnsupportedCodec {
19 codec: String,
21 },
22
23 #[error("No suitable encoder found for {codec} (tried: {tried:?})")]
25 NoSuitableEncoder {
26 codec: String,
28 tried: Vec<String>,
30 },
31
32 #[error("Encoding failed at frame {frame}: {reason}")]
34 EncodingFailed {
35 frame: u64,
37 reason: String,
39 },
40
41 #[error("Invalid configuration: {reason}")]
43 InvalidConfig {
44 reason: String,
46 },
47
48 #[error("Hardware encoder unavailable: {encoder}")]
50 HwEncoderUnavailable {
51 encoder: String,
53 },
54
55 #[error("encoder unavailable: codec={codec} hint={hint}")]
57 EncoderUnavailable {
58 codec: String,
60 hint: String,
62 },
63
64 #[error("Muxing failed: {reason}")]
66 MuxingFailed {
67 reason: String,
69 },
70
71 #[error("ffmpeg error: {message} (code={code})")]
73 Ffmpeg {
74 code: i32,
76 message: String,
78 },
79
80 #[error("IO error: {0}")]
82 Io(#[from] std::io::Error),
83
84 #[error("Invalid option: {name} — {reason}")]
86 InvalidOption {
87 name: String,
89 reason: String,
91 },
92
93 #[error("codec {codec} is not supported by container {container} — {hint}")]
95 UnsupportedContainerCodecCombination {
96 container: String,
98 codec: String,
100 hint: String,
102 },
103
104 #[error("dimensions {width}x{height} out of range [2, 32768]")]
106 InvalidDimensions {
107 width: u32,
109 height: u32,
111 },
112
113 #[error("bitrate {bitrate} bps exceeds maximum 800 Mbps (800,000,000 bps)")]
115 InvalidBitrate {
116 bitrate: u64,
118 },
119
120 #[error("channel count {count} exceeds maximum 8")]
122 InvalidChannelCount {
123 count: u32,
125 },
126
127 #[error("sample rate {rate} Hz outside supported range [8000, 384000]")]
129 InvalidSampleRate {
130 rate: u32,
132 },
133
134 #[error("Encoding cancelled by user")]
136 Cancelled,
137
138 #[error("Async encoder worker panicked or disconnected")]
140 WorkerPanicked,
141
142 #[error("media operation failed: {reason}")]
148 MediaOperationFailed {
149 reason: String,
151 },
152
153 #[error("preset constraint violated: preset={preset} reason={reason}")]
159 PresetConstraintViolation {
160 preset: String,
162 reason: String,
164 },
165}
166
167impl EncodeError {
168 pub(crate) fn from_ffmpeg_error(errnum: i32) -> Self {
174 EncodeError::Ffmpeg {
175 code: errnum,
176 message: ff_sys::av_error_string(errnum),
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::EncodeError;
184
185 #[test]
186 fn from_ffmpeg_error_should_return_ffmpeg_variant() {
187 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
188 assert!(matches!(err, EncodeError::Ffmpeg { .. }));
189 }
190
191 #[test]
192 fn from_ffmpeg_error_should_carry_numeric_code() {
193 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
194 match err {
195 EncodeError::Ffmpeg { code, .. } => assert_eq!(code, ff_sys::error_codes::EINVAL),
196 _ => panic!("expected Ffmpeg variant"),
197 }
198 }
199
200 #[test]
201 fn from_ffmpeg_error_should_format_with_code_in_display() {
202 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
203 let msg = err.to_string();
204 assert!(msg.contains("code=-22"), "expected 'code=-22' in '{msg}'");
205 }
206
207 #[test]
208 fn from_ffmpeg_error_message_should_be_nonempty() {
209 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::ENOMEM);
210 assert!(!err.to_string().is_empty());
211 }
212
213 #[test]
214 fn from_ffmpeg_error_eof_should_be_constructible() {
215 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EOF);
216 assert!(matches!(err, EncodeError::Ffmpeg { .. }));
217 assert!(!err.to_string().is_empty());
218 }
219
220 #[test]
221 fn invalid_dimensions_display_should_contain_dimension_string() {
222 let err = EncodeError::InvalidDimensions {
223 width: 0,
224 height: 720,
225 };
226 let msg = err.to_string();
227 assert!(msg.contains("0x720"), "expected '0x720' in '{msg}'");
228 }
229
230 #[test]
231 fn invalid_dimensions_display_should_contain_range_hint() {
232 let err = EncodeError::InvalidDimensions {
233 width: 99999,
234 height: 99999,
235 };
236 let msg = err.to_string();
237 assert!(
238 msg.contains("[2, 32768]"),
239 "expected '[2, 32768]' in '{msg}'"
240 );
241 }
242
243 #[test]
244 fn invalid_bitrate_display_should_contain_bitrate_value() {
245 let err = EncodeError::InvalidBitrate {
246 bitrate: 900_000_000,
247 };
248 let msg = err.to_string();
249 assert!(msg.contains("900000000"), "expected '900000000' in '{msg}'");
250 }
251
252 #[test]
253 fn invalid_bitrate_display_should_contain_maximum_hint() {
254 let err = EncodeError::InvalidBitrate {
255 bitrate: 900_000_000,
256 };
257 let msg = err.to_string();
258 assert!(
259 msg.contains("800,000,000"),
260 "expected '800,000,000' in '{msg}'"
261 );
262 }
263
264 #[test]
265 fn invalid_channel_count_display_should_contain_count() {
266 let err = EncodeError::InvalidChannelCount { count: 9 };
267 let msg = err.to_string();
268 assert!(msg.contains('9'), "expected '9' in '{msg}'");
269 }
270
271 #[test]
272 fn invalid_channel_count_display_should_contain_maximum_hint() {
273 let err = EncodeError::InvalidChannelCount { count: 9 };
274 let msg = err.to_string();
275 assert!(msg.contains('8'), "expected '8' in '{msg}'");
276 }
277
278 #[test]
279 fn invalid_sample_rate_display_should_contain_rate() {
280 let err = EncodeError::InvalidSampleRate { rate: 7999 };
281 let msg = err.to_string();
282 assert!(msg.contains("7999"), "expected '7999' in '{msg}'");
283 }
284
285 #[test]
286 fn invalid_sample_rate_display_should_contain_range_hint() {
287 let err = EncodeError::InvalidSampleRate { rate: 7999 };
288 let msg = err.to_string();
289 assert!(
290 msg.contains("[8000, 384000]"),
291 "expected '[8000, 384000]' in '{msg}'"
292 );
293 }
294
295 #[test]
296 fn encode_error_media_operation_failed_should_display_correctly() {
297 let err = EncodeError::MediaOperationFailed {
298 reason: "input file has no audio stream".to_string(),
299 };
300 let msg = err.to_string();
301 assert!(
302 msg.contains("media operation failed"),
303 "expected 'media operation failed' in '{msg}'"
304 );
305 assert!(
306 msg.contains("input file has no audio stream"),
307 "expected reason in '{msg}'"
308 );
309 assert!(
310 matches!(err, EncodeError::MediaOperationFailed { .. }),
311 "pattern match with struct syntax must work"
312 );
313 }
314}