1use std::net::SocketAddr;
15use std::ops::ControlFlow;
16use std::sync::Arc;
17
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::net::{TcpListener, TcpStream, UdpSocket};
20use tokio_util::sync::CancellationToken;
21use tracing::{debug, info, warn};
22
23use super::message::{InterleavedFrame, RtspRequest};
24use super::sdp::Sdp;
25use crate::auth::{Credentials, EgressGate};
26use crate::bus::PlaybackRegistry;
27use crate::inbound::IngestContext;
28use crate::protocol::rtp::{
29 AccessUnit, DepacketizeError, H264Depacketizer, H265Depacketizer, RtpHeader, RtpPacketizer,
30};
31use crate::{CodecId, MediaFrame, Result, StreamKey};
32
33const MAX_CONSECUTIVE_DEPACK_FAILURES: u32 = 64;
37
38fn publish_au(
49 session: &crate::inbound::PublishSession,
50 codec: CodecId,
51 au: AccessUnit,
52 sdp_config: &Option<bytes::Bytes>,
53) -> Result<()> {
54 let pts = (au.timestamp / 90) as i64; if au.keyframe {
56 let cfg =
62 crate::codec::dispatch::parameter_sets(codec, &au.data).or_else(|| sdp_config.clone());
63 if let Some(params) = cfg {
64 let mut frame = MediaFrame::new_video(pts, pts, params, codec, true);
65 frame.flags |= crate::FrameFlags::CONFIG;
66 session.publish_frame(frame)?;
67 }
68 }
69 let mf = MediaFrame::new_video(pts, pts, au.data, codec, au.keyframe);
70 session.publish_frame(mf)?;
71 Ok(())
72}
73
74fn publish_config(
79 session: &crate::inbound::PublishSession,
80 codec: CodecId,
81 annexb: bytes::Bytes,
82) -> Result<()> {
83 let mut cfg = MediaFrame::new_video(0, 0, annexb, codec, true);
84 cfg.flags |= crate::FrameFlags::CONFIG;
85 session.publish_frame(cfg)?;
86 Ok(())
87}
88
89enum VideoDepack {
93 H264(H264Depacketizer),
94 H265(H265Depacketizer),
95}
96
97impl VideoDepack {
98 fn new(codec: CodecId) -> Self {
99 match codec {
100 CodecId::H265 => VideoDepack::H265(H265Depacketizer::new()),
101 _ => VideoDepack::H264(H264Depacketizer::new()),
102 }
103 }
104 fn codec(&self) -> CodecId {
105 match self {
106 VideoDepack::H264(_) => CodecId::H264,
107 VideoDepack::H265(_) => CodecId::H265,
108 }
109 }
110 fn push(
111 &mut self,
112 payload: &[u8],
113 marker: bool,
114 timestamp: u32,
115 sequence: u16,
116 ) -> std::result::Result<Option<AccessUnit>, DepacketizeError> {
117 match self {
118 VideoDepack::H264(d) => d.push(payload, marker, timestamp, sequence),
119 VideoDepack::H265(d) => d.push(payload, marker, timestamp, sequence),
120 }
121 }
122}
123
124fn video_codec_from_sdp(body: &[u8]) -> CodecId {
126 let sdp = Sdp::parse(&String::from_utf8_lossy(body));
127 let enc = sdp
128 .media
129 .iter()
130 .find(|m| m.media == "video")
131 .and_then(|m| m.encoding.as_deref())
132 .map(|e| e.to_ascii_uppercase());
133 match enc.as_deref() {
134 Some("H265") | Some("HEVC") => CodecId::H265,
135 _ => CodecId::H264,
136 }
137}
138
139struct UdpMedia {
142 rtp: Arc<UdpSocket>,
143 client_rtp: SocketAddr,
144 server_rtp_port: u16,
145}
146
147fn parse_client_ports(transport: &str) -> Option<(u16, u16)> {
149 let v = transport
150 .split(';')
151 .find_map(|p| p.trim().strip_prefix("client_port="))?;
152 let (a, b) = v.split_once('-')?;
153 Some((a.trim().parse().ok()?, b.trim().parse().ok()?))
154}
155
156pub struct RtspServer {
161 playback: Arc<dyn PlaybackRegistry>,
162 ingest: IngestContext,
163 bind: SocketAddr,
164 gate: Option<EgressGate>,
166}
167
168impl RtspServer {
169 pub fn new(
172 playback: Arc<dyn PlaybackRegistry>,
173 ingest: IngestContext,
174 bind: SocketAddr,
175 ) -> Self {
176 Self {
177 playback,
178 ingest,
179 bind,
180 gate: None,
181 }
182 }
183
184 pub fn with_gate(mut self, gate: EgressGate) -> Self {
186 self.gate = Some(gate);
187 self
188 }
189
190 pub async fn run(self, shutdown: CancellationToken) -> Result<()> {
192 let listener = TcpListener::bind(self.bind).await?;
193 info!(bind = %self.bind, "rtsp server listening");
194 loop {
195 tokio::select! {
196 _ = shutdown.cancelled() => break,
197 accepted = listener.accept() => {
198 let (sock, peer) = match accepted {
199 Ok(v) => v,
200 Err(e) => { warn!(error = %e, "rtsp accept failed"); continue; }
201 };
202 let playback = Arc::clone(&self.playback);
203 let ingest = self.ingest.clone();
204 let gate = self.gate.clone();
205 let shutdown = shutdown.clone();
206 tokio::spawn(async move {
207 if let Err(e) = serve_connection(sock, playback, ingest, gate, shutdown).await {
208 debug!(%peer, error = %e, "rtsp connection ended");
209 }
210 });
211 }
212 }
213 }
214 Ok(())
215 }
216}
217
218fn uri_token(uri: &str) -> Option<String> {
220 crate::auth::token_from_query(uri)
221}
222
223const PT_H264: u8 = 96;
225const RTP_CHANNEL: u8 = 0;
227
228async fn serve_connection(
229 mut sock: TcpStream,
230 playback: Arc<dyn PlaybackRegistry>,
231 ingest: IngestContext,
232 gate: Option<EgressGate>,
233 shutdown: CancellationToken,
234) -> Result<()> {
235 let mut buf = Vec::with_capacity(2048);
236 let session = new_session_id();
238 let mut announce_key: Option<StreamKey> = None;
240 let mut announce_codec = CodecId::H264;
242 let mut announce_config: Option<bytes::Bytes> = None;
245 let peer = sock.peer_addr().ok();
248 let peer_ip = peer.map(|a| a.ip());
249 let mut udp: Option<UdpMedia> = None;
251 loop {
252 let Some((req, body)) = read_request(&mut sock, &mut buf).await? else {
253 return Ok(()); };
255 let key = stream_key_from_uri(&req.uri);
256 match req.method.as_str() {
257 "OPTIONS" => {
258 sock.write_all(options_response(req.cseq).as_bytes())
259 .await?
260 }
261 "ANNOUNCE" => match &key {
263 Some(k) => {
264 announce_key = Some(k.clone());
265 let sdp = Sdp::parse(&String::from_utf8_lossy(&body));
266 announce_codec = video_codec_from_sdp(&body);
267 announce_config = sdp.video_config_annexb();
268 sock.write_all(simple_ok(req.cseq, &session).as_bytes())
269 .await?;
270 }
271 None => sock.write_all(not_found(req.cseq).as_bytes()).await?,
272 },
273 "RECORD" => match announce_key.take().or_else(|| key.clone()) {
274 Some(k) => {
275 sock.write_all(simple_ok(req.cseq, &session).as_bytes())
276 .await?;
277 let mut creds = Credentials::default();
280 creds.params.push(("proto".into(), "rtsp".into()));
281 creds.token = uri_token(&req.uri);
282 creds.addr = peer;
285 let sess = ingest.open_publish_checked(k.clone(), &creds).await?;
286 let config = announce_config.take();
296 if let Some(cfg) = &config {
297 publish_config(&sess, announce_codec, cfg.clone())?;
298 }
299 match udp.take() {
300 Some(u) => {
301 info!(stream = %k, codec = ?announce_codec, "rtsp ingest (RECORD/UDP) started");
302 return record_udp(sock, sess, u, announce_codec, config, shutdown)
303 .await;
304 }
305 None => {
306 info!(stream = %k, codec = ?announce_codec, "rtsp ingest (RECORD/TCP) started");
307 return record(sock, sess, buf, announce_codec, config, shutdown).await;
308 }
309 }
310 }
311 None => sock.write_all(not_found(req.cseq).as_bytes()).await?,
312 },
313 "DESCRIBE" => match key.as_ref().and_then(|k| playback.get_stream(k).ok()) {
315 Some(handle) => {
316 let codec = stream_video_codec(&handle);
319 let sdp = build_sdp(codec);
320 sock.write_all(describe_response(req.cseq, &req.uri, &sdp).as_bytes())
321 .await?;
322 }
323 None => sock.write_all(not_found(req.cseq).as_bytes()).await?,
324 },
325 "SETUP" => {
326 let transport = req
327 .headers
328 .iter()
329 .find(|(n, _)| n.eq_ignore_ascii_case("transport"))
330 .map(|(_, v)| v.as_str())
331 .unwrap_or("");
332 let is_tcp = transport.is_empty()
333 || transport.contains("TCP")
334 || transport.contains("interleaved");
335 if is_tcp {
336 udp = None;
338 sock.write_all(setup_response(req.cseq, &session, transport).as_bytes())
339 .await?;
340 } else if let Some((cl_rtp, cl_rtcp)) = parse_client_ports(transport) {
341 match (peer_ip, UdpSocket::bind("0.0.0.0:0").await.ok()) {
343 (Some(ip), Some(rtp_sock)) => {
344 let server_rtp_port =
345 rtp_sock.local_addr().map(|a| a.port()).unwrap_or(0);
346 udp = Some(UdpMedia {
347 rtp: Arc::new(rtp_sock),
348 client_rtp: SocketAddr::new(ip, cl_rtp),
349 server_rtp_port,
350 });
351 let record = transport.to_ascii_lowercase().contains("mode=record");
352 sock.write_all(
353 setup_response_udp(
354 req.cseq,
355 &session,
356 cl_rtp,
357 cl_rtcp,
358 server_rtp_port,
359 server_rtp_port.wrapping_add(1),
360 record,
361 )
362 .as_bytes(),
363 )
364 .await?;
365 }
366 _ => {
367 sock.write_all(unsupported_transport(req.cseq).as_bytes())
368 .await?;
369 }
370 }
371 } else {
372 sock.write_all(unsupported_transport(req.cseq).as_bytes())
373 .await?;
374 }
375 }
376 "PLAY" => {
377 let live = key.filter(|k| playback.get_stream(k).is_ok());
378 match live {
379 Some(key) => {
380 let allowed = match gate.as_ref() {
382 Some(g) => g(key.clone(), uri_token(&req.uri), peer).await,
383 None => true,
384 };
385 if !allowed {
386 sock.write_all(unauthorized(req.cseq).as_bytes()).await?;
387 continue;
388 }
389 sock.write_all(play_response(req.cseq, &session).as_bytes())
390 .await?;
391 return match udp.take() {
392 Some(u) => play_udp(sock, &playback, key, u, shutdown).await,
393 None => play(sock, &playback, key, shutdown).await,
394 };
395 }
396 None => sock.write_all(not_found(req.cseq).as_bytes()).await?,
397 }
398 }
399 "TEARDOWN" => {
400 sock.write_all(simple_ok(req.cseq, &session).as_bytes())
401 .await?;
402 return Ok(());
403 }
404 other => {
405 debug!(method = other, "rtsp: unsupported method");
406 sock.write_all(not_implemented(req.cseq).as_bytes()).await?;
407 }
408 }
409 }
410}
411
412async fn record(
416 mut sock: TcpStream,
417 session: crate::inbound::PublishSession,
418 mut carry: Vec<u8>,
419 codec: CodecId,
420 config: Option<bytes::Bytes>,
421 shutdown: CancellationToken,
422) -> Result<()> {
423 let mut depack = VideoDepack::new(codec);
424 let mut fails = 0u32;
425 drain_record(&mut carry, &mut depack, &session, &mut fails, &config)?;
426 let mut read = [0u8; 16 * 1024];
427 loop {
428 tokio::select! {
429 _ = shutdown.cancelled() => break,
430 n = sock.read(&mut read) => {
431 let n = n?;
432 if n == 0 { break; }
433 carry.extend_from_slice(&read[..n]);
434 drain_record(&mut carry, &mut depack, &session, &mut fails, &config)?;
435 }
436 }
437 }
438 session.finish().await
439}
440
441async fn record_udp(
445 mut sock: TcpStream,
446 session: crate::inbound::PublishSession,
447 udp: UdpMedia,
448 codec: CodecId,
449 config: Option<bytes::Bytes>,
450 shutdown: CancellationToken,
451) -> Result<()> {
452 let mut depack = VideoDepack::new(codec);
453 let mut dgram = vec![0u8; 64 * 1024];
454 let mut ctl = [0u8; 4096];
455 let mut fails = 0u32;
456 loop {
457 tokio::select! {
458 _ = shutdown.cancelled() => break,
459 n = sock.read(&mut ctl) => {
461 if n.unwrap_or(0) == 0 { break; }
462 }
463 r = udp.rtp.recv_from(&mut dgram) => {
464 let Ok((n, _from)) = r else { continue };
465 let Some(header) = RtpHeader::parse(&dgram[..n]) else { continue };
466 let payload = &dgram[header.payload_offset..n];
467 match depack.push(payload, header.marker, header.timestamp, header.sequence) {
468 Ok(Some(au)) => {
469 fails = 0;
470 publish_au(&session, codec, au, &config)?;
471 }
472 Ok(None) => fails = 0,
473 Err(_) => {
474 fails += 1;
475 if fails >= MAX_CONSECUTIVE_DEPACK_FAILURES {
476 warn!(?codec, "rtsp udp ingest: repeated depacketize \
477 failures — aborting (codec mismatch?)");
478 break;
479 }
480 }
481 }
482 }
483 }
484 }
485 session.finish().await
486}
487
488async fn play_udp(
492 mut sock: TcpStream,
493 playback: &Arc<dyn PlaybackRegistry>,
494 key: StreamKey,
495 udp: UdpMedia,
496 shutdown: CancellationToken,
497) -> Result<()> {
498 let handle = playback.get_stream(&key)?;
499 let codec = stream_video_codec(&handle);
500 let mut sub = handle.subscribe_resilient();
502 let mut packetizer = packetizer_for(codec);
503 let mut pkts: Vec<Vec<u8>> = Vec::new();
504 let mut ctl = [0u8; 4096];
505 loop {
506 tokio::select! {
507 _ = shutdown.cancelled() => break,
508 n = sock.read(&mut ctl) => {
509 if n.unwrap_or(0) == 0 { break; } }
511 frame = sub.recv() => {
512 let Some(frame) = frame else { break };
513 if !frame.is_video() || frame.codec != codec {
514 continue;
515 }
516 let timestamp = (frame.pts.max(0) as u64).wrapping_mul(90) as u32;
517 packetizer.packetize_into(&frame.data, timestamp, &mut pkts);
518 for pkt in pkts.iter() {
519 if udp.rtp.send_to(pkt, udp.client_rtp).await.is_err() {
520 return Ok(());
521 }
522 }
523 }
524 }
525 }
526 let _ = udp.server_rtp_port; Ok(())
528}
529
530fn drain_record(
533 buf: &mut Vec<u8>,
534 depack: &mut VideoDepack,
535 session: &crate::inbound::PublishSession,
536 fails: &mut u32,
537 config: &Option<bytes::Bytes>,
538) -> Result<()> {
539 if buf.first().is_some_and(|&b| b != b'$') {
544 return Err(crate::StreamError::protocol(
545 "rtsp ingest: non-interleaved data on control connection",
546 ));
547 }
548 let codec = depack.codec();
549 let mut consumed = 0;
550 while let Some((frame, len)) = InterleavedFrame::parse(&buf[consumed..]) {
551 consumed += len;
552 if frame.channel != 0 {
554 continue;
555 }
556 let Some(header) = RtpHeader::parse(frame.payload) else {
557 continue;
558 };
559 let payload = &frame.payload[header.payload_offset..];
560 match depack.push(payload, header.marker, header.timestamp, header.sequence) {
561 Ok(Some(au)) => {
562 *fails = 0;
563 publish_au(session, codec, au, config)?;
564 }
565 Ok(None) => *fails = 0,
566 Err(_) => {
567 *fails += 1;
568 if *fails >= MAX_CONSECUTIVE_DEPACK_FAILURES {
569 warn!(
570 ?codec,
571 "rtsp ingest: repeated depacketize failures — \
572 aborting (codec mismatch or corrupt RTP?)"
573 );
574 return Err(crate::StreamError::protocol(
575 "rtsp ingest: depacketize failure threshold",
576 ));
577 }
578 }
579 }
580 }
581 buf.drain(..consumed);
582 Ok(())
583}
584
585async fn play(
588 mut sock: TcpStream,
589 playback: &Arc<dyn PlaybackRegistry>,
590 key: StreamKey,
591 shutdown: CancellationToken,
592) -> Result<()> {
593 let handle = playback.get_stream(&key)?;
594 let codec = stream_video_codec(&handle);
595 let packetizer = packetizer_for(codec);
596
597 let mut sink = RtspSink {
600 sock: &mut sock,
601 packetizer,
602 codec,
603 pkts: Vec::new(),
604 wbuf: Vec::with_capacity(1500),
605 };
606 handle.drive_to(&shutdown, &mut sink).await
607}
608
609struct RtspSink<'a> {
611 sock: &'a mut TcpStream,
612 packetizer: RtpPacketizer,
613 codec: CodecId,
615 pkts: Vec<Vec<u8>>,
617 wbuf: Vec<u8>,
619}
620
621#[async_trait::async_trait]
622impl crate::bus::FrameSink for RtspSink<'_> {
623 async fn send(&mut self, frame: Arc<MediaFrame>) -> Result<ControlFlow<()>> {
624 match send_frame(
626 self.sock,
627 &mut self.packetizer,
628 self.codec,
629 &mut self.pkts,
630 &mut self.wbuf,
631 &frame,
632 )
633 .await
634 {
635 Ok(()) => Ok(ControlFlow::Continue(())),
636 Err(_) => Ok(ControlFlow::Break(())),
637 }
638 }
639}
640
641async fn send_frame(
642 sock: &mut TcpStream,
643 packetizer: &mut RtpPacketizer,
644 codec: CodecId,
645 pkts: &mut Vec<Vec<u8>>,
646 wbuf: &mut Vec<u8>,
647 frame: &MediaFrame,
648) -> std::io::Result<()> {
649 if !frame.is_video() || frame.codec != codec {
650 return Ok(()); }
652 let timestamp = (frame.pts.max(0) as u64).wrapping_mul(90) as u32; packetizer.packetize_into(&frame.data, timestamp, pkts);
656 wbuf.clear();
659 for pkt in pkts.iter() {
660 frame_interleaved(RTP_CHANNEL, pkt, wbuf);
661 }
662 sock.write_all(wbuf).await
663}
664
665fn frame_interleaved(channel: u8, rtp: &[u8], out: &mut Vec<u8>) {
668 out.push(b'$');
669 out.push(channel);
670 out.extend_from_slice(&(rtp.len() as u16).to_be_bytes());
671 out.extend_from_slice(rtp);
672}
673
674async fn read_request(
678 sock: &mut TcpStream,
679 buf: &mut Vec<u8>,
680) -> Result<Option<(RtspRequest, Vec<u8>)>> {
681 let mut tmp = [0u8; 1024];
682 loop {
683 if let Some(end) = find_double_crlf(buf) {
684 let head = String::from_utf8_lossy(&buf[..end]).into_owned();
685 let Some(req) = RtspRequest::parse(&head) else {
686 buf.drain(..end);
687 return Ok(None);
688 };
689 let want = content_length(&req);
690 while buf.len() < end + want {
692 let n = sock.read(&mut tmp).await?;
693 if n == 0 {
694 return Ok(None);
695 }
696 buf.extend_from_slice(&tmp[..n]);
697 }
698 let body = buf[end..end + want].to_vec();
699 buf.drain(..end + want);
700 return Ok(Some((req, body)));
701 }
702 let n = sock.read(&mut tmp).await?;
703 if n == 0 {
704 return Ok(None);
705 }
706 buf.extend_from_slice(&tmp[..n]);
707 if buf.len() > 64 * 1024 {
708 return Err(crate::StreamError::protocol("rtsp request too large"));
709 }
710 }
711}
712
713fn content_length(req: &RtspRequest) -> usize {
715 req.headers
716 .iter()
717 .find(|(n, _)| n.eq_ignore_ascii_case("content-length"))
718 .and_then(|(_, v)| v.parse().ok())
719 .unwrap_or(0)
720}
721
722fn find_double_crlf(buf: &[u8]) -> Option<usize> {
723 buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
724}
725
726fn stream_key_from_uri(uri: &str) -> Option<StreamKey> {
728 let rest = uri.strip_prefix("rtsp://")?;
729 let path = rest.split_once('/').map(|(_, p)| p)?;
730 let path = path.split(['?', ';']).next().unwrap_or(path);
731 let mut segs = path.split('/').filter(|s| !s.is_empty());
732 let app = segs.next()?;
733 let stream = segs.next()?;
734 Some(StreamKey::new(app, stream))
735}
736
737fn options_response(cseq: u32) -> String {
740 format!(
741 "RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\n\
742 Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN\r\n\r\n"
743 )
744}
745
746fn build_sdp(codec: CodecId) -> String {
747 let enc = if codec == CodecId::H265 {
749 "H265"
750 } else {
751 "H264"
752 };
753 format!(
754 "v=0\r\n\
755 o=- 0 0 IN IP4 0.0.0.0\r\n\
756 s=arcly-stream\r\n\
757 t=0 0\r\n\
758 m=video 0 RTP/AVP 96\r\n\
759 a=rtpmap:96 {enc}/90000\r\n\
760 a=control:streamid=0\r\n"
761 )
762}
763
764fn stream_video_codec(handle: &crate::bus::StreamHandle) -> CodecId {
768 handle
769 .cached_configs()
770 .0
771 .map(|f| f.codec)
772 .unwrap_or(CodecId::H264)
773}
774
775fn packetizer_for(codec: CodecId) -> RtpPacketizer {
777 if codec == CodecId::H265 {
778 RtpPacketizer::new_h265(PT_H264, 0x5254_5350, 1400)
779 } else {
780 RtpPacketizer::new(PT_H264, 0x5254_5350, 1400)
781 }
782}
783
784fn describe_response(cseq: u32, uri: &str, sdp: &str) -> String {
785 format!(
786 "RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\n\
787 Content-Base: {uri}\r\nContent-Type: application/sdp\r\n\
788 Content-Length: {}\r\n\r\n{sdp}",
789 sdp.len()
790 )
791}
792
793fn setup_response(cseq: u32, session: &str, client_transport: &str) -> String {
794 let interleaved = client_transport
797 .split(';')
798 .find_map(|p| p.trim().strip_prefix("interleaved="))
799 .unwrap_or("0-1");
800 let mode =
801 if client_transport.contains("mode=record") || client_transport.contains("mode=RECORD") {
802 ";mode=record"
803 } else {
804 ""
805 };
806 format!(
807 "RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\n\
808 Transport: RTP/AVP/TCP;unicast;interleaved={interleaved}{mode}\r\n\
809 Session: {session}\r\n\r\n"
810 )
811}
812
813#[allow(clippy::too_many_arguments)]
814fn setup_response_udp(
815 cseq: u32,
816 session: &str,
817 client_rtp: u16,
818 client_rtcp: u16,
819 server_rtp: u16,
820 server_rtcp: u16,
821 record: bool,
822) -> String {
823 let mode = if record { ";mode=record" } else { "" };
824 format!(
825 "RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\n\
826 Transport: RTP/AVP;unicast;client_port={client_rtp}-{client_rtcp};\
827 server_port={server_rtp}-{server_rtcp}{mode}\r\n\
828 Session: {session}\r\n\r\n"
829 )
830}
831
832fn unsupported_transport(cseq: u32) -> String {
833 format!("RTSP/1.0 461 Unsupported Transport\r\nCSeq: {cseq}\r\n\r\n")
834}
835
836fn play_response(cseq: u32, session: &str) -> String {
837 format!("RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nSession: {session}\r\nRange: npt=0.000-\r\n\r\n")
838}
839
840fn simple_ok(cseq: u32, session: &str) -> String {
841 format!("RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nSession: {session}\r\n\r\n")
842}
843
844fn not_implemented(cseq: u32) -> String {
845 format!("RTSP/1.0 501 Not Implemented\r\nCSeq: {cseq}\r\n\r\n")
846}
847
848fn not_found(cseq: u32) -> String {
849 format!("RTSP/1.0 404 Not Found\r\nCSeq: {cseq}\r\n\r\n")
850}
851
852fn unauthorized(cseq: u32) -> String {
853 format!("RTSP/1.0 401 Unauthorized\r\nCSeq: {cseq}\r\n\r\n")
854}
855
856fn new_session_id() -> String {
859 use std::collections::hash_map::RandomState;
860 use std::hash::{BuildHasher, Hasher};
861 use std::time::{SystemTime, UNIX_EPOCH};
862 let mut h = RandomState::new().build_hasher();
863 h.write_u128(
864 SystemTime::now()
865 .duration_since(UNIX_EPOCH)
866 .map(|d| d.as_nanos())
867 .unwrap_or(0),
868 );
869 format!("{:016X}", h.finish())
870}
871
872#[cfg(test)]
873mod tests {
874 use super::*;
875 use crate::protocol::rtsp::message::{InterleavedFrame, RtspResponse};
876
877 #[test]
878 fn interleaved_frame_round_trips() {
879 let rtp = [0x80u8, 0xE0, 0x00, 0x01, 0xAA, 0xBB];
880 let mut framed = Vec::new();
881 frame_interleaved(0, &rtp, &mut framed);
882 assert_eq!(framed[0], b'$');
883 let (f, used) = InterleavedFrame::parse(&framed).expect("parse");
884 assert_eq!(used, framed.len());
885 assert_eq!(f.channel, 0);
886 assert_eq!(f.payload, &rtp);
887 }
888
889 #[test]
890 fn stream_key_parsed_from_uri_variants() {
891 let k = stream_key_from_uri("rtsp://host:554/live/cam").unwrap();
892 assert_eq!((k.app.as_str(), k.stream_id.as_str()), ("live", "cam"));
893 let k = stream_key_from_uri("rtsp://h/live/cam/streamid=0?x=1").unwrap();
895 assert_eq!((k.app.as_str(), k.stream_id.as_str()), ("live", "cam"));
896 assert!(stream_key_from_uri("rtsp://host/onlyapp").is_none());
897 }
898
899 #[test]
900 fn uri_token_extracted() {
901 assert_eq!(
902 uri_token("rtsp://h/live/cam?token=abc").as_deref(),
903 Some("abc")
904 );
905 assert_eq!(uri_token("rtsp://h/live/cam").as_deref(), None);
906 }
907
908 #[test]
909 fn announce_request_parses_with_body_len() {
910 let req = RtspRequest::parse(
911 "ANNOUNCE rtsp://h/live/cam RTSP/1.0\r\nCSeq: 2\r\nContent-Length: 5\r\n",
912 )
913 .unwrap();
914 assert_eq!(req.method, "ANNOUNCE");
915 assert_eq!(content_length(&req), 5);
916 assert_eq!(
917 stream_key_from_uri(&req.uri).map(|k| (k.app.to_string(), k.stream_id.to_string())),
918 Some(("live".into(), "cam".into()))
919 );
920 }
921
922 #[test]
923 fn responses_are_well_formed() {
924 assert!(options_response(2).contains("Public: OPTIONS"));
925 let d = describe_response(3, "rtsp://h/live/cam", &build_sdp(CodecId::H264));
926 assert!(build_sdp(CodecId::H265).contains("H265/90000"));
927 let parsed = RtspResponse::parse(
928 d.split("\r\n\r\n").next().unwrap(),
929 d.split("\r\n\r\n").nth(1).unwrap_or("").to_string(),
930 )
931 .expect("response parses");
932 assert_eq!(parsed.header("Content-Type"), Some("application/sdp"));
933 let setup = setup_response(4, "DEADBEEF", "RTP/AVP/TCP;unicast;interleaved=0-1");
934 assert!(setup.contains("interleaved=0-1"));
935 assert!(setup.contains("Session: DEADBEEF"));
936 let rec = setup_response(5, "S", "RTP/AVP/TCP;unicast;interleaved=0-1;mode=record");
938 assert!(rec.contains("mode=record"));
939 assert!(unsupported_transport(6).starts_with("RTSP/1.0 461"));
941 assert!(not_found(7).starts_with("RTSP/1.0 404"));
942 }
943}