1mod auth;
44mod egress;
45mod message;
46mod sdp;
47
48pub use egress::RtspServer;
49pub use message::{InterleavedFrame, RtspMethod, RtspRequest, RtspResponse};
50pub use sdp::{MediaDescription, Sdp};
51
52use crate::inbound::{InboundProtocol, IngestContext};
53use crate::protocol::rtp::{AacDepacketizer, H264Depacketizer, RtpHeader};
54use crate::{CodecId, MediaFrame, Result, StreamKey};
55use async_trait::async_trait;
56use std::time::Duration;
57use tokio_util::sync::CancellationToken;
58use tracing::{debug, warn};
59
60#[derive(Debug, Clone)]
62pub struct RtspSource {
63 pub url: String,
65 pub key: StreamKey,
67}
68
69impl RtspSource {
70 pub fn new(url: impl Into<String>, key: StreamKey) -> Self {
72 Self {
73 url: url.into(),
74 key,
75 }
76 }
77}
78
79#[derive(Debug)]
81pub struct RtspHandler {
82 sources: Vec<RtspSource>,
83 retry_backoff: Duration,
84}
85
86impl Default for RtspHandler {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl RtspHandler {
93 pub fn new() -> Self {
95 Self {
96 sources: Vec::new(),
97 retry_backoff: Duration::from_secs(3),
98 }
99 }
100
101 pub fn source(mut self, source: RtspSource) -> Self {
103 self.sources.push(source);
104 self
105 }
106
107 pub fn retry_backoff(mut self, backoff: Duration) -> Self {
109 self.retry_backoff = backoff;
110 self
111 }
112
113 async fn run_source(
116 source: RtspSource,
117 ctx: IngestContext,
118 shutdown: CancellationToken,
119 backoff: Duration,
120 ) {
121 loop {
122 if shutdown.is_cancelled() {
123 return;
124 }
125 tokio::select! {
131 _ = shutdown.cancelled() => return,
132 res = Self::pull_once(&source, &ctx, &shutdown) => {
133 if let Err(e) = res {
134 warn!(url = %source.url, error = %e, "rtsp source dropped; will retry");
135 }
136 }
137 }
138 tokio::select! {
139 _ = shutdown.cancelled() => return,
140 _ = tokio::time::sleep(backoff) => {}
141 }
142 }
143 }
144
145 async fn pull_once(
148 source: &RtspSource,
149 ctx: &IngestContext,
150 shutdown: &CancellationToken,
151 ) -> Result<()> {
152 use tokio::io::{AsyncReadExt, AsyncWriteExt};
153 use tokio::net::TcpStream;
154
155 let (url, creds) = auth::split_userinfo(&source.url);
158 let mut challenge: Option<auth::Challenge> = None;
159
160 let (host, port) = message::host_port(&url)
161 .ok_or_else(|| crate::StreamError::protocol("malformed rtsp url"))?;
162 let mut stream = TcpStream::connect((host.as_str(), port)).await?;
163 let mut cseq = 1u32;
164
165 Self::send_authed(
167 &mut stream,
168 RtspMethod::Options,
169 &url,
170 &mut cseq,
171 &[],
172 &creds,
173 &mut challenge,
174 )
175 .await?;
176
177 let describe = Self::send_authed(
178 &mut stream,
179 RtspMethod::Describe,
180 &url,
181 &mut cseq,
182 &[("Accept", "application/sdp")],
183 &creds,
184 &mut challenge,
185 )
186 .await?;
187 let sdp = Sdp::parse(&describe.body);
188 debug!(url = %url, media = sdp.media.len(), "rtsp DESCRIBE parsed");
189
190 let setup_url = sdp.first_video_control(&url).unwrap_or_else(|| url.clone());
192 let setup = Self::send_authed(
193 &mut stream,
194 RtspMethod::Setup,
195 &setup_url,
196 &mut cseq,
197 &[("Transport", "RTP/AVP/TCP;unicast;interleaved=0-1")],
198 &creds,
199 &mut challenge,
200 )
201 .await?;
202 let session_id = message::session_id(&setup).unwrap_or_default();
203
204 let mut audio_clock = None;
206 if sdp.has_aac_audio() {
207 if let Some(audio_url) = sdp.first_audio_control(&url) {
208 Self::send_authed(
209 &mut stream,
210 RtspMethod::Setup,
211 &audio_url,
212 &mut cseq,
213 &[
214 ("Transport", "RTP/AVP/TCP;unicast;interleaved=2-3"),
215 ("Session", &session_id),
216 ],
217 &creds,
218 &mut challenge,
219 )
220 .await?;
221 audio_clock = sdp
222 .media
223 .iter()
224 .find(|m| m.media == "audio")
225 .and_then(|m| m.clock_rate)
226 .or(Some(48_000));
227 debug!(url = %url, "rtsp AAC audio track set up on ch 2/3");
228 }
229 }
230
231 Self::send_authed(
232 &mut stream,
233 RtspMethod::Play,
234 &url,
235 &mut cseq,
236 &[("Session", &session_id)],
237 &creds,
238 &mut challenge,
239 )
240 .await?;
241
242 let session = ctx.open_publish(source.key.clone()).await?;
244 let mut depack = H264Depacketizer::new();
245 let (size_len, index_len) = sdp.audio_aac_lengths();
246 let aac = AacDepacketizer::with_lengths(size_len, index_len);
247 let mut buf = Vec::with_capacity(64 * 1024);
248 let mut read = [0u8; 16 * 1024];
249
250 loop {
251 tokio::select! {
252 _ = shutdown.cancelled() => break,
253 n = stream.read(&mut read) => {
254 let n = n?;
255 if n == 0 { break; }
256 buf.extend_from_slice(&read[..n]);
257 Self::drain_interleaved(&mut buf, &mut depack, &aac, audio_clock, &session)?;
258 }
259 }
260 }
261
262 let _ = Self::send_authed(
264 &mut stream,
265 RtspMethod::Teardown,
266 &url,
267 &mut cseq,
268 &[("Session", &session_id)],
269 &creds,
270 &mut challenge,
271 )
272 .await;
273 let _ = stream.shutdown().await;
274 session.finish().await
275 }
276
277 #[allow(clippy::too_many_arguments)]
281 async fn send_authed<S>(
282 stream: &mut S,
283 method: RtspMethod,
284 uri: &str,
285 cseq: &mut u32,
286 headers: &[(&str, &str)],
287 creds: &Option<(String, String)>,
288 challenge: &mut Option<auth::Challenge>,
289 ) -> Result<RtspResponse>
290 where
291 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
292 {
293 let auth1 = match (challenge.as_ref(), creds.as_ref()) {
295 (Some(ch), Some((u, p))) => Some(auth::authorization(ch, u, p, method.as_str(), uri)),
296 _ => None,
297 };
298 let mut hdrs: Vec<(&str, &str)> = headers.to_vec();
299 if let Some(a) = &auth1 {
300 hdrs.push(("Authorization", a));
301 }
302 message::write_request(stream, method, uri, *cseq, &hdrs).await?;
303 *cseq += 1;
304 let resp = message::read_response(stream).await?;
305
306 if resp.status != 401 || creds.is_none() {
308 return Ok(resp);
309 }
310 let Some(ch) = resp
311 .header("WWW-Authenticate")
312 .and_then(auth::parse_challenge)
313 else {
314 return Ok(resp);
315 };
316 *challenge = Some(ch);
317
318 let (u, p) = creds.as_ref().unwrap();
320 let auth2 = auth::authorization(challenge.as_ref().unwrap(), u, p, method.as_str(), uri);
321 let mut hdrs: Vec<(&str, &str)> = headers.to_vec();
322 hdrs.push(("Authorization", &auth2));
323 message::write_request(stream, method, uri, *cseq, &hdrs).await?;
324 *cseq += 1;
325 message::read_response(stream).await
326 }
327
328 fn drain_interleaved(
332 buf: &mut Vec<u8>,
333 depack: &mut H264Depacketizer,
334 aac: &AacDepacketizer,
335 audio_clock: Option<u32>,
336 session: &crate::inbound::PublishSession,
337 ) -> Result<()> {
338 let mut consumed = 0;
339 while let Some((frame, len)) = InterleavedFrame::parse(&buf[consumed..]) {
340 consumed += len;
341 let Some(header) = RtpHeader::parse(frame.payload) else {
342 continue;
343 };
344 let payload = &frame.payload[header.payload_offset..];
345 match frame.channel {
348 0 => {
349 match depack.push(payload, header.marker, header.timestamp, header.sequence) {
350 Ok(Some(au)) => {
351 let pts = (au.timestamp / 90) as i64; let mf = MediaFrame::new_video(
353 pts,
354 pts,
355 au.data,
356 CodecId::H264,
357 au.keyframe,
358 );
359 let _ = session.publish_frame(mf)?;
360 }
361 Ok(None) => {}
362 Err(e) => debug!(?e, "rtp depacketize skip"),
363 }
364 }
365 2 => {
366 if let Some(clock) = audio_clock {
367 match aac.push(payload) {
368 Ok(units) => {
369 for au in units {
370 let pts =
371 (header.timestamp as i64 * 1000) / clock.max(1) as i64;
372 let mf = MediaFrame::new_audio(pts, au, CodecId::AAC);
373 let _ = session.publish_frame(mf)?;
374 }
375 }
376 Err(e) => debug!(?e, "aac depacketize skip"),
377 }
378 }
379 }
380 _ => {}
381 }
382 }
383 buf.drain(..consumed);
384 Ok(())
385 }
386}
387
388#[async_trait]
389impl InboundProtocol for RtspHandler {
390 fn name(&self) -> &'static str {
391 "rtsp"
392 }
393
394 async fn serve(&self, ctx: IngestContext, shutdown: CancellationToken) -> Result<()> {
395 let mut tasks = tokio::task::JoinSet::new();
398 for source in &self.sources {
399 tasks.spawn(Self::run_source(
400 source.clone(),
401 ctx.clone(),
402 shutdown.clone(),
403 self.retry_backoff,
404 ));
405 }
406 while tasks.join_next().await.is_some() {}
407 Ok(())
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn source_builder_sets_url_and_key() {
417 let s = RtspSource::new("rtsp://cam/stream", StreamKey::new("live", "cam1"));
418 assert_eq!(s.url, "rtsp://cam/stream");
419 assert_eq!(s.key.stream_id.as_str(), "cam1");
420 }
421
422 #[test]
423 fn handler_collects_sources() {
424 let h = RtspHandler::new()
425 .source(RtspSource::new("rtsp://a/1", StreamKey::new("live", "a")))
426 .source(RtspSource::new("rtsp://b/2", StreamKey::new("live", "b")));
427 assert_eq!(h.sources.len(), 2);
428 assert_eq!(h.name(), "rtsp");
429 }
430}