Skip to main content

ff_stream/
error.rs

1//! Error types for streaming operations.
2//!
3//! This module provides the [`StreamError`] enum which represents all
4//! possible errors that can occur during HLS / DASH output and ABR ladder
5//! generation.
6
7use ff_format::{ErrorSeverity, MediaError};
8
9/// Errors that can occur during streaming output operations.
10///
11/// This enum covers all error conditions that may arise when configuring,
12/// building, or writing HLS / DASH output.
13///
14/// # Error Categories
15///
16/// - **Encoding errors**: [`Encode`](Self::Encode) — wraps errors from `ff-encode`
17/// - **I/O errors**: [`Io`](Self::Io) — file system errors during segment writing
18/// - **Configuration errors**: [`InvalidConfig`](Self::InvalidConfig) — missing or
19///   invalid builder options, or not-yet-implemented stubs
20#[derive(Debug, thiserror::Error)]
21pub enum StreamError {
22    /// An encoding operation in the underlying `ff-encode` crate failed.
23    ///
24    /// This error propagates from [`ff_encode::EncodeError`] when the encoder
25    /// cannot open a codec or write frames.
26    #[error("encode failed: {0}")]
27    Encode(#[from] ff_encode::EncodeError),
28
29    /// An I/O operation failed during segment or playlist writing.
30    ///
31    /// Typical causes include missing output directories, permission errors,
32    /// or a full disk.
33    #[error("io error: {0}")]
34    Io(#[from] std::io::Error),
35
36    /// A configuration value is missing or invalid, or the feature is not yet
37    /// implemented.
38    ///
39    /// This variant is also used as a stub return value for `write()` / `hls()`
40    /// / `dash()` methods that await `FFmpeg` muxing integration.
41    #[error("invalid config: {reason}")]
42    InvalidConfig {
43        /// Human-readable description of the configuration problem.
44        reason: String,
45    },
46
47    /// The requested codec is not supported by the output format.
48    ///
49    /// For example, RTMP/FLV requires H.264 video and AAC audio; requesting
50    /// any other codec returns this error from `build()`.
51    #[error("unsupported codec: {codec} — {reason}")]
52    UnsupportedCodec {
53        /// Name of the codec that was rejected.
54        codec: String,
55        /// Human-readable explanation of the constraint.
56        reason: String,
57    },
58
59    /// One or more [`FanoutOutput`](crate::fanout::FanoutOutput) targets failed to receive a
60    /// frame or to finish cleanly.
61    ///
62    /// All targets still receive every frame even when some fail; errors are
63    /// collected and returned together after the full fan-out pass.
64    #[error("fanout: {failed}/{total} targets failed — {messages:?}")]
65    FanoutFailure {
66        /// Number of targets that returned an error.
67        failed: usize,
68        /// Total number of targets in the fanout.
69        total: usize,
70        /// Per-target error messages in `"target[i]: <error>"` format.
71        messages: Vec<String>,
72    },
73
74    /// The requested network protocol is not compiled into the linked `FFmpeg` build.
75    ///
76    /// Returned by `build()` when a feature-gated output (e.g. `SrtOutput`)
77    /// is opened but the underlying `FFmpeg` library was built without the
78    /// required protocol support (e.g. libsrt).
79    #[error("protocol unavailable: {reason}")]
80    ProtocolUnavailable {
81        /// Human-readable description of why the protocol is unavailable.
82        reason: String,
83    },
84
85    /// An `FFmpeg` runtime error occurred during muxing or transcoding.
86    ///
87    /// `code` is the raw `FFmpeg` negative error value returned by the failing
88    /// function (e.g. `AVERROR(EINVAL)`).  `message` is the human-readable
89    /// string produced by `av_strerror`.  Exposing the numeric code lets
90    /// engineers cross-reference `FFmpeg` documentation and source directly.
91    #[error("ffmpeg error: {message} (code={code})")]
92    Ffmpeg {
93        /// Raw `FFmpeg` error code (negative integer).
94        code: i32,
95        /// Human-readable description of the `FFmpeg` error.
96        message: String,
97    },
98}
99
100impl MediaError for StreamError {
101    fn severity(&self) -> ErrorSeverity {
102        match self {
103            Self::Encode(e) => e.severity(),
104            Self::Ffmpeg { .. } | Self::FanoutFailure { .. } => ErrorSeverity::Other,
105            Self::InvalidConfig { .. }
106            | Self::UnsupportedCodec { .. }
107            | Self::ProtocolUnavailable { .. }
108            | Self::Io(_) => ErrorSeverity::Fatal,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn invalid_config_should_display_reason() {
119        let err = StreamError::InvalidConfig {
120            reason: "missing input path".into(),
121        };
122        let msg = err.to_string();
123        assert!(msg.contains("missing input path"), "got: {msg}");
124    }
125
126    #[test]
127    fn io_error_should_convert_via_from() {
128        let io = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
129        let err: StreamError = io.into();
130        assert!(matches!(err, StreamError::Io(_)));
131    }
132
133    #[test]
134    fn encode_error_should_convert_via_from() {
135        let enc = ff_encode::EncodeError::Cancelled;
136        let err: StreamError = enc.into();
137        assert!(matches!(err, StreamError::Encode(_)));
138    }
139
140    #[test]
141    fn display_io_should_contain_message() {
142        let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
143        let err: StreamError = io.into();
144        assert!(err.to_string().contains("access denied"), "got: {err}");
145    }
146
147    #[test]
148    fn unsupported_codec_should_display_codec_and_reason() {
149        let err = StreamError::UnsupportedCodec {
150            codec: "Vp9".into(),
151            reason: "RTMP/FLV requires H.264 video".into(),
152        };
153        let msg = err.to_string();
154        assert!(msg.contains("Vp9"), "got: {msg}");
155        assert!(msg.contains("H.264"), "got: {msg}");
156    }
157
158    #[test]
159    fn fanout_failure_should_display_failed_and_total() {
160        let err = StreamError::FanoutFailure {
161            failed: 1,
162            total: 2,
163            messages: vec!["target[1]: invalid config: forced failure".into()],
164        };
165        let msg = err.to_string();
166        assert!(msg.contains("1/2"), "got: {msg}");
167    }
168
169    #[test]
170    fn protocol_unavailable_should_display_reason() {
171        let err = StreamError::ProtocolUnavailable {
172            reason: "FFmpeg built without libsrt".into(),
173        };
174        let msg = err.to_string();
175        assert!(msg.contains("libsrt"), "got: {msg}");
176    }
177
178    #[test]
179    fn ffmpeg_error_should_display_code_and_message() {
180        let err = StreamError::Ffmpeg {
181            code: -22,
182            message: "Cannot open codec".into(),
183        };
184        let msg = err.to_string();
185        assert!(msg.contains("Cannot open codec"), "got: {msg}");
186        assert!(msg.contains("code=-22"), "got: {msg}");
187    }
188
189    #[test]
190    fn stream_io_should_be_fatal() {
191        let e: StreamError = std::io::Error::other("x").into();
192        assert!(e.is_fatal() && !e.is_recoverable());
193    }
194
195    #[test]
196    fn stream_ffmpeg_should_be_other() {
197        let e = StreamError::Ffmpeg {
198            code: -22,
199            message: "x".into(),
200        };
201        assert!(!e.is_fatal() && !e.is_recoverable());
202    }
203
204    #[test]
205    fn stream_encode_should_delegate_other() {
206        // A wrapped EncodeError::Cancelled (Other) must classify as Other, not Fatal.
207        let e = StreamError::Encode(ff_encode::EncodeError::Cancelled);
208        assert!(!e.is_fatal() && !e.is_recoverable());
209    }
210}