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
143impl EncodeError {
144    /// Create an error from an FFmpeg error code.
145    ///
146    /// This is more type-safe than implementing `From<i32>` globally,
147    /// as it makes the conversion explicit and prevents accidental
148    /// conversion of arbitrary i32 values.
149    pub(crate) fn from_ffmpeg_error(errnum: i32) -> Self {
150        EncodeError::Ffmpeg {
151            code: errnum,
152            message: ff_sys::av_error_string(errnum),
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::EncodeError;
160
161    #[test]
162    fn from_ffmpeg_error_should_return_ffmpeg_variant() {
163        let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
164        assert!(matches!(err, EncodeError::Ffmpeg { .. }));
165    }
166
167    #[test]
168    fn from_ffmpeg_error_should_carry_numeric_code() {
169        let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
170        match err {
171            EncodeError::Ffmpeg { code, .. } => assert_eq!(code, ff_sys::error_codes::EINVAL),
172            _ => panic!("expected Ffmpeg variant"),
173        }
174    }
175
176    #[test]
177    fn from_ffmpeg_error_should_format_with_code_in_display() {
178        let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
179        let msg = err.to_string();
180        assert!(msg.contains("code=-22"), "expected 'code=-22' in '{msg}'");
181    }
182
183    #[test]
184    fn from_ffmpeg_error_message_should_be_nonempty() {
185        let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::ENOMEM);
186        assert!(!err.to_string().is_empty());
187    }
188
189    #[test]
190    fn from_ffmpeg_error_eof_should_be_constructible() {
191        let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EOF);
192        assert!(matches!(err, EncodeError::Ffmpeg { .. }));
193        assert!(!err.to_string().is_empty());
194    }
195
196    #[test]
197    fn invalid_dimensions_display_should_contain_dimension_string() {
198        let err = EncodeError::InvalidDimensions {
199            width: 0,
200            height: 720,
201        };
202        let msg = err.to_string();
203        assert!(msg.contains("0x720"), "expected '0x720' in '{msg}'");
204    }
205
206    #[test]
207    fn invalid_dimensions_display_should_contain_range_hint() {
208        let err = EncodeError::InvalidDimensions {
209            width: 99999,
210            height: 99999,
211        };
212        let msg = err.to_string();
213        assert!(
214            msg.contains("[2, 32768]"),
215            "expected '[2, 32768]' in '{msg}'"
216        );
217    }
218
219    #[test]
220    fn invalid_bitrate_display_should_contain_bitrate_value() {
221        let err = EncodeError::InvalidBitrate {
222            bitrate: 900_000_000,
223        };
224        let msg = err.to_string();
225        assert!(msg.contains("900000000"), "expected '900000000' in '{msg}'");
226    }
227
228    #[test]
229    fn invalid_bitrate_display_should_contain_maximum_hint() {
230        let err = EncodeError::InvalidBitrate {
231            bitrate: 900_000_000,
232        };
233        let msg = err.to_string();
234        assert!(
235            msg.contains("800,000,000"),
236            "expected '800,000,000' in '{msg}'"
237        );
238    }
239
240    #[test]
241    fn invalid_channel_count_display_should_contain_count() {
242        let err = EncodeError::InvalidChannelCount { count: 9 };
243        let msg = err.to_string();
244        assert!(msg.contains('9'), "expected '9' in '{msg}'");
245    }
246
247    #[test]
248    fn invalid_channel_count_display_should_contain_maximum_hint() {
249        let err = EncodeError::InvalidChannelCount { count: 9 };
250        let msg = err.to_string();
251        assert!(msg.contains('8'), "expected '8' in '{msg}'");
252    }
253
254    #[test]
255    fn invalid_sample_rate_display_should_contain_rate() {
256        let err = EncodeError::InvalidSampleRate { rate: 7999 };
257        let msg = err.to_string();
258        assert!(msg.contains("7999"), "expected '7999' in '{msg}'");
259    }
260
261    #[test]
262    fn invalid_sample_rate_display_should_contain_range_hint() {
263        let err = EncodeError::InvalidSampleRate { rate: 7999 };
264        let msg = err.to_string();
265        assert!(
266            msg.contains("[8000, 384000]"),
267            "expected '[8000, 384000]' in '{msg}'"
268        );
269    }
270}