Skip to main content

ff_filter/
error.rs

1//! Error types for filter graph operations.
2
3use ff_format::{ErrorSeverity, MediaError};
4use thiserror::Error;
5
6/// Errors that can occur during filter graph construction and processing.
7#[derive(Debug, Error)]
8pub enum FilterError {
9    /// Failed to build the filter graph (invalid filter chain or `FFmpeg` error
10    /// during graph creation).
11    #[error("failed to build filter graph")]
12    BuildFailed,
13
14    /// A frame processing operation (push or pull) failed.
15    #[error("failed to process frame")]
16    ProcessFailed,
17
18    /// An invalid configuration was detected during graph construction.
19    #[error("invalid filter configuration: {reason}")]
20    InvalidConfig {
21        /// Human-readable reason for the failure.
22        reason: String,
23    },
24
25    /// A frame was pushed to an invalid input slot.
26    #[error("invalid input: slot={slot} reason={reason}")]
27    InvalidInput {
28        /// The slot index that was out of range or otherwise invalid.
29        slot: usize,
30        /// Human-readable reason for the failure.
31        reason: String,
32    },
33
34    /// An underlying `FFmpeg` function returned an error code.
35    #[error("ffmpeg error: {message} (code={code})")]
36    Ffmpeg {
37        /// The raw `FFmpeg` error code.
38        code: i32,
39        /// Human-readable description of the error.
40        message: String,
41    },
42
43    /// A multi-track composition or mixing operation failed.
44    ///
45    /// Returned by [`MultiTrackComposer::build`](crate::MultiTrackComposer::build) and
46    /// [`MultiTrackAudioMixer::build`](crate::MultiTrackAudioMixer::build) when the
47    /// `FFmpeg` filter graph cannot be constructed.
48    #[error("composition failed: {reason}")]
49    CompositionFailed {
50        /// Human-readable reason for the failure.
51        reason: String,
52    },
53
54    /// A [`CompositeOp`](crate::CompositeOp) the filter path does not implement
55    /// correctly yet.
56    ///
57    /// `In`, `Out`, `Atop` and `Xor` need the backdrop's alpha, which the filter
58    /// chain does not carry (#1784). Rather than compute per-channel arithmetic
59    /// and present it as Porter-Duff, the graph refuses to build; the GPU
60    /// compositor renders these operators.
61    #[error(
62        "composite operator {op:?} is not implemented on the filter path; \
63         it renders on the GPU compositor only (#1784)"
64    )]
65    UnsupportedCompositeOp {
66        /// The operator that was asked for.
67        op: crate::CompositeOp,
68    },
69
70    /// An analysis operation failed for a structural reason.
71    ///
72    /// Returned by [`LoudnessMeter::measure`](crate::analysis::LoudnessMeter::measure)
73    /// when the input file is not found, the format is unsupported, or the
74    /// `FFmpeg` filter graph cannot be constructed.
75    #[error("analysis failed: {reason}")]
76    AnalysisFailed {
77        /// Human-readable reason for the failure.
78        reason: String,
79    },
80
81    /// A function requires a GPL-licensed `FFmpeg` filter but the `gpl` feature
82    /// flag is not enabled.
83    ///
84    /// Enable the `gpl` feature in `Cargo.toml` and ensure your distribution
85    /// complies with the GPL licence before using this function.
86    #[error("GPL-licensed feature required: {feature} (enable the `gpl` feature flag)")]
87    GplRequired {
88        /// Name of the filter or capability that requires GPL.
89        feature: &'static str,
90    },
91}
92
93impl MediaError for FilterError {
94    fn severity(&self) -> ErrorSeverity {
95        match self {
96            Self::Ffmpeg { .. } | Self::ProcessFailed | Self::InvalidInput { .. } => {
97                ErrorSeverity::Other
98            }
99            Self::BuildFailed
100            | Self::InvalidConfig { .. }
101            | Self::CompositionFailed { .. }
102            | Self::UnsupportedCompositeOp { .. }
103            | Self::AnalysisFailed { .. }
104            | Self::GplRequired { .. } => ErrorSeverity::Fatal,
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use ff_format::MediaError;
112
113    use super::FilterError;
114    use std::error::Error;
115
116    #[test]
117    fn build_failed_should_display_correct_message() {
118        let err = FilterError::BuildFailed;
119        assert_eq!(err.to_string(), "failed to build filter graph");
120    }
121
122    #[test]
123    fn process_failed_should_display_correct_message() {
124        let err = FilterError::ProcessFailed;
125        assert_eq!(err.to_string(), "failed to process frame");
126    }
127
128    #[test]
129    fn invalid_input_should_display_slot_and_reason() {
130        let err = FilterError::InvalidInput {
131            slot: 2,
132            reason: "slot out of range".to_string(),
133        };
134        assert_eq!(
135            err.to_string(),
136            "invalid input: slot=2 reason=slot out of range"
137        );
138    }
139
140    #[test]
141    fn ffmpeg_should_display_code_and_message() {
142        let err = FilterError::Ffmpeg {
143            code: -22,
144            message: "Invalid argument".to_string(),
145        };
146        assert_eq!(err.to_string(), "ffmpeg error: Invalid argument (code=-22)");
147    }
148
149    #[test]
150    fn composition_failed_should_display_reason() {
151        let err = FilterError::CompositionFailed {
152            reason: "no layers".to_string(),
153        };
154        assert_eq!(err.to_string(), "composition failed: no layers");
155    }
156
157    #[test]
158    fn analysis_failed_should_display_reason() {
159        let err = FilterError::AnalysisFailed {
160            reason: "file not found".to_string(),
161        };
162        assert_eq!(err.to_string(), "analysis failed: file not found");
163    }
164
165    #[test]
166    fn gpl_required_should_display_feature_name() {
167        let err = FilterError::GplRequired {
168            feature: "rubberband",
169        };
170        assert_eq!(
171            err.to_string(),
172            "GPL-licensed feature required: rubberband (enable the `gpl` feature flag)"
173        );
174    }
175
176    #[test]
177    fn filter_error_should_implement_std_error() {
178        fn assert_error<E: Error>(_: &E) {}
179        assert_error(&FilterError::BuildFailed);
180        assert_error(&FilterError::ProcessFailed);
181        assert_error(&FilterError::InvalidInput {
182            slot: 0,
183            reason: String::new(),
184        });
185        assert_error(&FilterError::Ffmpeg {
186            code: 0,
187            message: String::new(),
188        });
189    }
190
191    #[test]
192    fn filter_build_failed_should_be_fatal() {
193        let e = FilterError::BuildFailed;
194        assert!(e.is_fatal() && !e.is_recoverable());
195    }
196
197    #[test]
198    fn filter_process_failed_should_be_other() {
199        let e = FilterError::ProcessFailed;
200        assert!(!e.is_fatal() && !e.is_recoverable());
201    }
202}