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