Skip to main content

ff_encode/
error.rs

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