1use ff_format::{AudioCodec, VideoCodec};
30
31use crate::error::StreamError;
32use crate::srt_output_inner::SrtInner;
33
34pub struct SrtOutput {
54 url: String,
55 video_width: Option<u32>,
56 video_height: Option<u32>,
57 fps: Option<f64>,
58 sample_rate: u32,
59 channels: u32,
60 video_codec: VideoCodec,
61 audio_codec: AudioCodec,
62 video_bitrate: u64,
63 audio_bitrate: u64,
64 inner: Option<SrtInner>,
65 finished: bool,
66}
67
68impl SrtOutput {
69 #[must_use]
82 pub fn new(url: &str) -> Self {
83 Self {
84 url: url.to_owned(),
85 video_width: None,
86 video_height: None,
87 fps: None,
88 sample_rate: 44100,
89 channels: 2,
90 video_codec: VideoCodec::H264,
91 audio_codec: AudioCodec::Aac,
92 video_bitrate: 4_000_000,
93 audio_bitrate: 128_000,
94 inner: None,
95 finished: false,
96 }
97 }
98
99 pub fn build(mut self) -> Result<Self, StreamError> {
117 if !ff_sys::avformat::srt_available() {
118 return Err(StreamError::ProtocolUnavailable {
119 reason: "FFmpeg was built without libsrt; recompile FFmpeg with --enable-libsrt"
120 .into(),
121 });
122 }
123
124 if !self.url.starts_with("srt://") {
125 return Err(StreamError::InvalidConfig {
126 reason: "SrtOutput URL must start with srt://".into(),
127 });
128 }
129
130 let (Some(width), Some(height), Some(fps)) =
131 (self.video_width, self.video_height, self.fps)
132 else {
133 return Err(StreamError::InvalidConfig {
134 reason: "video parameters not set; call .video(width, height, fps) before .build()"
135 .into(),
136 });
137 };
138
139 if self.video_codec != VideoCodec::H264 {
140 return Err(StreamError::UnsupportedCodec {
141 codec: format!("{:?}", self.video_codec),
142 reason: "SRT/MPEG-TS output requires H.264 video".into(),
143 });
144 }
145
146 if self.audio_codec != AudioCodec::Aac {
147 return Err(StreamError::UnsupportedCodec {
148 codec: format!("{:?}", self.audio_codec),
149 reason: "SRT/MPEG-TS output requires AAC audio".into(),
150 });
151 }
152
153 #[allow(clippy::cast_possible_truncation)]
154 let fps_int = fps.round().max(1.0) as i32;
155
156 let inner = SrtInner::open(
157 &self.url,
158 width.cast_signed(),
159 height.cast_signed(),
160 fps_int,
161 self.video_bitrate,
162 self.sample_rate.cast_signed(),
163 self.channels.cast_signed(),
164 self.audio_bitrate.cast_signed(),
165 )?;
166
167 self.inner = Some(inner);
168 Ok(self)
169 }
170}
171
172impl_live_stream_setters!(SrtOutput, required_audio);
173impl_frame_push_stream_output!(SrtOutput);
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn build_without_srt_scheme_should_return_invalid_config() {
181 if !ff_sys::avformat::srt_available() {
183 println!("Skipping: libsrt not available in linked FFmpeg");
184 return;
185 }
186 let result = SrtOutput::new("rtmp://example.com/live")
187 .video(1280, 720, 30.0)
188 .build();
189 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
190 }
191
192 #[test]
193 fn build_without_video_should_return_invalid_config() {
194 if !ff_sys::avformat::srt_available() {
195 println!("Skipping: libsrt not available in linked FFmpeg");
196 return;
197 }
198 let result = SrtOutput::new("srt://127.0.0.1:9000").build();
199 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
200 }
201
202 #[test]
203 fn build_with_non_h264_video_codec_should_return_unsupported_codec() {
204 if !ff_sys::avformat::srt_available() {
205 println!("Skipping: libsrt not available in linked FFmpeg");
206 return;
207 }
208 let result = SrtOutput::new("srt://127.0.0.1:9000")
209 .video(1280, 720, 30.0)
210 .video_codec(VideoCodec::Vp9)
211 .build();
212 assert!(matches!(result, Err(StreamError::UnsupportedCodec { .. })));
213 }
214
215 #[test]
216 fn build_with_non_aac_audio_codec_should_return_unsupported_codec() {
217 if !ff_sys::avformat::srt_available() {
218 println!("Skipping: libsrt not available in linked FFmpeg");
219 return;
220 }
221 let result = SrtOutput::new("srt://127.0.0.1:9000")
222 .video(1280, 720, 30.0)
223 .audio_codec(AudioCodec::Mp3)
224 .build();
225 assert!(matches!(result, Err(StreamError::UnsupportedCodec { .. })));
226 }
227
228 #[test]
229 fn build_without_libsrt_should_return_protocol_unavailable() {
230 if ff_sys::avformat::srt_available() {
231 println!("Skipping: libsrt is available; cannot test ProtocolUnavailable path");
232 return;
233 }
234 let result = SrtOutput::new("srt://127.0.0.1:9000")
235 .video(1280, 720, 30.0)
236 .build();
237 assert!(matches!(
238 result,
239 Err(StreamError::ProtocolUnavailable { .. })
240 ));
241 }
242
243 #[test]
244 fn video_bitrate_default_should_be_four_megabits() {
245 let out = SrtOutput::new("srt://127.0.0.1:9000");
246 assert_eq!(out.video_bitrate, 4_000_000);
247 }
248
249 #[test]
250 fn audio_defaults_should_be_44100hz_stereo() {
251 let out = SrtOutput::new("srt://127.0.0.1:9000");
252 assert_eq!(out.sample_rate, 44100);
253 assert_eq!(out.channels, 2);
254 }
255}