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