1use ff_format::{ErrorSeverity, MediaError};
4use std::path::PathBuf;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
9pub enum EncodeError {
10 #[error("Cannot create output file: {path}")]
12 CannotCreateFile {
13 path: PathBuf,
15 },
16
17 #[error("Unsupported codec: {codec}")]
19 UnsupportedCodec {
20 codec: String,
22 },
23
24 #[error("No suitable encoder found for {codec} (tried: {tried:?})")]
26 NoSuitableEncoder {
27 codec: String,
29 tried: Vec<String>,
31 },
32
33 #[error("Encoding failed at frame {frame}: {reason}")]
35 EncodingFailed {
36 frame: u64,
38 reason: String,
40 },
41
42 #[error("Invalid configuration: {reason}")]
44 InvalidConfig {
45 reason: String,
47 },
48
49 #[error("Hardware encoder unavailable: {encoder}")]
51 HwEncoderUnavailable {
52 encoder: String,
54 },
55
56 #[error("encoder unavailable: codec={codec} hint={hint}")]
58 EncoderUnavailable {
59 codec: String,
61 hint: String,
63 },
64
65 #[error("Muxing failed: {reason}")]
67 MuxingFailed {
68 reason: String,
70 },
71
72 #[error("ffmpeg error: {message} (code={code})")]
74 Ffmpeg {
75 code: i32,
77 message: String,
79 },
80
81 #[error("IO error: {0}")]
83 Io(#[from] std::io::Error),
84
85 #[error("Invalid option: {name} — {reason}")]
87 InvalidOption {
88 name: String,
90 reason: String,
92 },
93
94 #[error("codec {codec} is not supported by container {container} — {hint}")]
96 UnsupportedContainerCodecCombination {
97 container: String,
99 codec: String,
101 hint: String,
103 },
104
105 #[error("dimensions {width}x{height} out of range [2, 32768]")]
107 InvalidDimensions {
108 width: u32,
110 height: u32,
112 },
113
114 #[error("bitrate {bitrate} bps exceeds maximum 800 Mbps (800,000,000 bps)")]
116 InvalidBitrate {
117 bitrate: u64,
119 },
120
121 #[error("channel count {count} exceeds maximum 8")]
123 InvalidChannelCount {
124 count: u32,
126 },
127
128 #[error("sample rate {rate} Hz outside supported range [8000, 384000]")]
130 InvalidSampleRate {
131 rate: u32,
133 },
134
135 #[error("Encoding cancelled by user")]
137 Cancelled,
138
139 #[error("Async encoder worker panicked or disconnected")]
141 WorkerPanicked,
142
143 #[error("preset constraint violated: preset={preset} reason={reason}")]
149 PresetConstraintViolation {
150 preset: String,
152 reason: String,
154 },
155}
156
157impl EncodeError {
158 pub(crate) fn from_ffmpeg_error(errnum: i32) -> Self {
164 EncodeError::Ffmpeg {
165 code: errnum,
166 message: ff_sys::av_error_string(errnum),
167 }
168 }
169}
170
171impl MediaError for EncodeError {
172 fn severity(&self) -> ErrorSeverity {
173 match self {
174 Self::Ffmpeg { .. } | Self::Cancelled => ErrorSeverity::Other,
175 Self::CannotCreateFile { .. }
176 | Self::UnsupportedCodec { .. }
177 | Self::NoSuitableEncoder { .. }
178 | Self::EncodingFailed { .. }
179 | Self::InvalidConfig { .. }
180 | Self::HwEncoderUnavailable { .. }
181 | Self::EncoderUnavailable { .. }
182 | Self::MuxingFailed { .. }
183 | Self::Io(_)
184 | Self::InvalidOption { .. }
185 | Self::UnsupportedContainerCodecCombination { .. }
186 | Self::InvalidDimensions { .. }
187 | Self::InvalidBitrate { .. }
188 | Self::InvalidChannelCount { .. }
189 | Self::InvalidSampleRate { .. }
190 | Self::WorkerPanicked
191 | Self::PresetConstraintViolation { .. } => ErrorSeverity::Fatal,
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use ff_format::MediaError;
199
200 use super::EncodeError;
201
202 #[test]
203 fn from_ffmpeg_error_should_return_ffmpeg_variant() {
204 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
205 assert!(matches!(err, EncodeError::Ffmpeg { .. }));
206 }
207
208 #[test]
209 fn from_ffmpeg_error_should_carry_numeric_code() {
210 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
211 match err {
212 EncodeError::Ffmpeg { code, .. } => assert_eq!(code, ff_sys::error_codes::EINVAL),
213 _ => panic!("expected Ffmpeg variant"),
214 }
215 }
216
217 #[test]
218 fn from_ffmpeg_error_should_format_with_code_in_display() {
219 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
220 let msg = err.to_string();
221 assert!(msg.contains("code=-22"), "expected 'code=-22' in '{msg}'");
222 }
223
224 #[test]
225 fn from_ffmpeg_error_message_should_be_nonempty() {
226 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::ENOMEM);
227 assert!(!err.to_string().is_empty());
228 }
229
230 #[test]
231 fn from_ffmpeg_error_eof_should_be_constructible() {
232 let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EOF);
233 assert!(matches!(err, EncodeError::Ffmpeg { .. }));
234 assert!(!err.to_string().is_empty());
235 }
236
237 #[test]
238 fn invalid_dimensions_display_should_contain_dimension_string() {
239 let err = EncodeError::InvalidDimensions {
240 width: 0,
241 height: 720,
242 };
243 let msg = err.to_string();
244 assert!(msg.contains("0x720"), "expected '0x720' in '{msg}'");
245 }
246
247 #[test]
248 fn invalid_dimensions_display_should_contain_range_hint() {
249 let err = EncodeError::InvalidDimensions {
250 width: 99999,
251 height: 99999,
252 };
253 let msg = err.to_string();
254 assert!(
255 msg.contains("[2, 32768]"),
256 "expected '[2, 32768]' in '{msg}'"
257 );
258 }
259
260 #[test]
261 fn invalid_bitrate_display_should_contain_bitrate_value() {
262 let err = EncodeError::InvalidBitrate {
263 bitrate: 900_000_000,
264 };
265 let msg = err.to_string();
266 assert!(msg.contains("900000000"), "expected '900000000' in '{msg}'");
267 }
268
269 #[test]
270 fn invalid_bitrate_display_should_contain_maximum_hint() {
271 let err = EncodeError::InvalidBitrate {
272 bitrate: 900_000_000,
273 };
274 let msg = err.to_string();
275 assert!(
276 msg.contains("800,000,000"),
277 "expected '800,000,000' in '{msg}'"
278 );
279 }
280
281 #[test]
282 fn invalid_channel_count_display_should_contain_count() {
283 let err = EncodeError::InvalidChannelCount { count: 9 };
284 let msg = err.to_string();
285 assert!(msg.contains('9'), "expected '9' in '{msg}'");
286 }
287
288 #[test]
289 fn invalid_channel_count_display_should_contain_maximum_hint() {
290 let err = EncodeError::InvalidChannelCount { count: 9 };
291 let msg = err.to_string();
292 assert!(msg.contains('8'), "expected '8' in '{msg}'");
293 }
294
295 #[test]
296 fn invalid_sample_rate_display_should_contain_rate() {
297 let err = EncodeError::InvalidSampleRate { rate: 7999 };
298 let msg = err.to_string();
299 assert!(msg.contains("7999"), "expected '7999' in '{msg}'");
300 }
301
302 #[test]
303 fn invalid_sample_rate_display_should_contain_range_hint() {
304 let err = EncodeError::InvalidSampleRate { rate: 7999 };
305 let msg = err.to_string();
306 assert!(
307 msg.contains("[8000, 384000]"),
308 "expected '[8000, 384000]' in '{msg}'"
309 );
310 }
311
312 #[test]
313 fn encode_io_should_be_fatal() {
314 let e: EncodeError = std::io::Error::other("x").into();
315 assert!(e.is_fatal() && !e.is_recoverable());
316 }
317
318 #[test]
319 fn encode_cancelled_should_be_other() {
320 let e = EncodeError::Cancelled;
321 assert!(!e.is_fatal() && !e.is_recoverable());
322 }
323}