Skip to main content

ff_stream/
srt_output.rs

1//! Frame-push SRT output.
2//!
3//! [`SrtOutput`] receives pre-decoded [`ff_format::VideoFrame`] / [`ff_format::AudioFrame`] values
4//! from the caller, encodes them with H.264/AAC, and pushes the MPEG-TS
5//! stream to an SRT destination using `FFmpeg`'s built-in SRT support.
6//!
7//! This module is only available when the `srt` feature flag is enabled.
8//!
9//! # Example
10//!
11//! ```ignore
12//! use ff_stream::{SrtOutput, StreamOutput};
13//!
14//! let mut out = SrtOutput::new("srt://192.168.1.100:9000")
15//!     .video(1920, 1080, 30.0)
16//!     .audio(44100, 2)
17//!     .video_bitrate(4_000_000)
18//!     .audio_bitrate(128_000)
19//!     .build()?;
20//!
21//! // for each decoded frame:
22//! out.push_video(&video_frame)?;
23//! out.push_audio(&audio_frame)?;
24//!
25//! // when done:
26//! Box::new(out).finish()?;
27//! ```
28
29use ff_format::{AudioCodec, VideoCodec};
30
31use crate::error::StreamError;
32use crate::srt_output_inner::SrtInner;
33
34/// Live SRT output: encodes frames as MPEG-TS and pushes them to an SRT
35/// destination.
36///
37/// Build with [`SrtOutput::new`], chain setter methods, then call
38/// [`build`](Self::build) to open the `FFmpeg` context and establish the SRT
39/// connection. After `build()`:
40///
41/// - `push_video` and `push_audio`
42///   encode and transmit frames in real time.
43/// - [`crate::StreamOutput::finish`] flushes all encoders, writes the MPEG-TS
44///   end-of-stream, and closes the SRT connection.
45///
46/// The SRT transport uses MPEG-TS as the container (H.264 video + AAC audio).
47/// [`build`](Self::build) returns [`StreamError::UnsupportedCodec`] for any
48/// other codec selection.
49///
50/// [`build`](Self::build) also performs a runtime check and returns
51/// [`StreamError::ProtocolUnavailable`] when the linked `FFmpeg` library was
52/// built without libsrt support.
53pub 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    /// Create a new builder that streams to the given SRT URL.
70    ///
71    /// The URL must begin with `srt://`; [`build`](Self::build) returns
72    /// [`StreamError::InvalidConfig`] otherwise.
73    ///
74    /// # Example
75    ///
76    /// ```ignore
77    /// use ff_stream::SrtOutput;
78    ///
79    /// let out = SrtOutput::new("srt://192.168.1.100:9000");
80    /// ```
81    #[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    /// Open the `FFmpeg` MPEG-TS context and establish the SRT connection.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`StreamError::ProtocolUnavailable`] when the linked `FFmpeg`
104    /// was built without libsrt support.
105    ///
106    /// Returns [`StreamError::InvalidConfig`] when:
107    /// - The URL does not start with `srt://`.
108    /// - [`video`](Self::video) was not called before `build`.
109    ///
110    /// Returns [`StreamError::UnsupportedCodec`] when:
111    /// - The video codec is not [`VideoCodec::H264`].
112    /// - The audio codec is not [`AudioCodec::Aac`].
113    ///
114    /// Returns [`StreamError::Ffmpeg`] when any `FFmpeg` operation fails
115    /// (including network connection errors).
116    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        // Skip if libsrt is not available (ProtocolUnavailable would be returned first).
182        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}