Skip to main content

ff_encode/
error.rs

1//! Error types for encoding operations.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Encoding error type.
7#[derive(Error, Debug)]
8pub enum EncodeError {
9    /// Cannot create output file
10    #[error("Cannot create output file: {path}")]
11    CannotCreateFile {
12        /// File path that failed
13        path: PathBuf,
14    },
15
16    /// Unsupported codec
17    #[error("Unsupported codec: {codec}")]
18    UnsupportedCodec {
19        /// Codec name
20        codec: String,
21    },
22
23    /// No suitable encoder found
24    #[error("No suitable encoder found for {codec} (tried: {tried:?})")]
25    NoSuitableEncoder {
26        /// Requested codec
27        codec: String,
28        /// Attempted encoders
29        tried: Vec<String>,
30    },
31
32    /// Encoding failed at specific frame
33    #[error("Encoding failed at frame {frame}: {reason}")]
34    EncodingFailed {
35        /// Frame number where encoding failed
36        frame: u64,
37        /// Failure reason
38        reason: String,
39    },
40
41    /// Invalid configuration
42    #[error("Invalid configuration: {reason}")]
43    InvalidConfig {
44        /// Configuration issue description
45        reason: String,
46    },
47
48    /// Hardware encoder unavailable
49    #[error("Hardware encoder unavailable: {encoder}")]
50    HwEncoderUnavailable {
51        /// Hardware encoder name
52        encoder: String,
53    },
54
55    /// Specific encoder is unavailable — the hint explains what is needed.
56    #[error("encoder unavailable: codec={codec} hint={hint}")]
57    EncoderUnavailable {
58        /// Requested codec name (e.g. `"h265/hevc"`).
59        codec: String,
60        /// Human-readable guidance (e.g. how to build FFmpeg with this encoder).
61        hint: String,
62    },
63
64    /// Muxing failed
65    #[error("Muxing failed: {reason}")]
66    MuxingFailed {
67        /// Failure reason
68        reason: String,
69    },
70
71    /// `FFmpeg` error
72    #[error("ffmpeg error: {message} (code={code})")]
73    Ffmpeg {
74        /// Raw `FFmpeg` error code (negative integer). `0` when no numeric code is available.
75        code: i32,
76        /// Human-readable error message from `av_strerror` or an internal description.
77        message: String,
78    },
79
80    /// IO error
81    #[error("IO error: {0}")]
82    Io(#[from] std::io::Error),
83
84    /// Invalid option value
85    #[error("Invalid option: {name} — {reason}")]
86    InvalidOption {
87        /// Option name
88        name: String,
89        /// Description of the constraint that was violated
90        reason: String,
91    },
92
93    /// Codec is incompatible with the target container format
94    #[error("codec {codec} is not supported by container {container} — {hint}")]
95    UnsupportedContainerCodecCombination {
96        /// Container format name (e.g. `"webm"`)
97        container: String,
98        /// Codec name that was rejected (e.g. `"h264"`)
99        codec: String,
100        /// Human-readable guidance on compatible codecs
101        hint: String,
102    },
103
104    /// Video dimensions are outside the supported range [2, 32768].
105    #[error("dimensions {width}x{height} out of range [2, 32768]")]
106    InvalidDimensions {
107        /// Requested frame width.
108        width: u32,
109        /// Requested frame height.
110        height: u32,
111    },
112
113    /// Target bitrate exceeds the 800 Mbps maximum.
114    #[error("bitrate {bitrate} bps exceeds maximum 800 Mbps (800,000,000 bps)")]
115    InvalidBitrate {
116        /// Requested bitrate in bits per second.
117        bitrate: u64,
118    },
119
120    /// Audio channel count exceeds the supported maximum of 8.
121    #[error("channel count {count} exceeds maximum 8")]
122    InvalidChannelCount {
123        /// Requested channel count.
124        count: u32,
125    },
126
127    /// Audio sample rate is outside the supported range [8000, 384000] Hz.
128    #[error("sample rate {rate} Hz outside supported range [8000, 384000]")]
129    InvalidSampleRate {
130        /// Requested sample rate in Hz.
131        rate: u32,
132    },
133
134    /// Encoding cancelled by user
135    #[error("Encoding cancelled by user")]
136    Cancelled,
137
138    /// Async encoder worker thread panicked or disconnected unexpectedly
139    #[error("Async encoder worker panicked or disconnected")]
140    WorkerPanicked,
141
142    /// A media operation (trim, extract, replace, …) failed.
143    ///
144    /// Returned by [`StreamCopyTrim`](crate::StreamCopyTrim) and other
145    /// `media_ops` types when a structural precondition is violated or an
146    /// FFmpeg mux/remux call fails.
147    #[error("media operation failed: {reason}")]
148    MediaOperationFailed {
149        /// Human-readable description of the failure.
150        reason: String,
151    },
152
153    /// An export preset violated a platform-specific constraint.
154    ///
155    /// Returned by [`ExportPreset::validate()`](crate::ExportPreset::validate)
156    /// when the preset's configuration conflicts with a platform rule (e.g.
157    /// fps > 60 for a YouTube preset).
158    #[error("preset constraint violated: preset={preset} reason={reason}")]
159    PresetConstraintViolation {
160        /// Name of the preset that failed validation.
161        preset: String,
162        /// Human-readable description of the violated constraint.
163        reason: String,
164    },
165}
166
167impl EncodeError {
168    /// Create an error from an FFmpeg error code.
169    ///
170    /// This is more type-safe than implementing `From<i32>` globally,
171    /// as it makes the conversion explicit and prevents accidental
172    /// conversion of arbitrary i32 values.
173    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}