arcly-stream 0.1.4

An open-extensible live-media streaming kernel: lock-free zero-copy frame fan-out, instant-start GOP cache, a pluggable multi-protocol ingestion layer (RTMP, RTSP, SRT, WHIP/WHEP shipped), and a feature-gated pure-Rust media plane (MPEG-TS/HLS/fMP4) — runtime, config, and metrics free.
Documentation
//! Minimal SDP parsing for RTSP `DESCRIBE` responses (RFC 4566 / RFC 2326).
//!
//! Only the fields ingest needs are decoded: the media lines (`m=`), their RTP
//! payload types, the `a=rtpmap` codec bindings, and the `a=control` track URLs
//! used to address `SETUP`.

/// A parsed media description (`m=` line plus its attributes).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaDescription {
    /// Media kind: `"video"`, `"audio"`, …
    pub media: String,
    /// The first RTP payload type listed on the `m=` line.
    pub payload_type: u8,
    /// Encoding name from `a=rtpmap` (e.g. `"H264"`), if present.
    pub encoding: Option<String>,
    /// RTP clock rate from `a=rtpmap` (90000 for H.264, the sample rate for AAC).
    pub clock_rate: Option<u32>,
    /// `a=control` value used to build the `SETUP` URL.
    pub control: Option<String>,
}

/// A parsed SDP session description (only the media plane is retained).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Sdp {
    /// One entry per `m=` media line, in document order.
    pub media: Vec<MediaDescription>,
}

impl Sdp {
    /// Parse SDP text. Unknown lines are ignored; malformed media lines are
    /// skipped rather than failing the whole description.
    pub fn parse(text: &str) -> Sdp {
        let mut media: Vec<MediaDescription> = Vec::new();
        for line in text.lines() {
            let line = line.trim_end();
            if let Some(rest) = line.strip_prefix("m=") {
                // "video 0 RTP/AVP 96"
                let mut parts = rest.split(' ');
                let kind = parts.next().unwrap_or("").to_string();
                let pt = parts.nth(2).and_then(|p| p.parse().ok()).unwrap_or(0);
                media.push(MediaDescription {
                    media: kind,
                    payload_type: pt,
                    encoding: None,
                    clock_rate: None,
                    control: None,
                });
            } else if let Some(rest) = line.strip_prefix("a=") {
                let Some(current) = media.last_mut() else {
                    continue;
                };
                if let Some(rtpmap) = rest.strip_prefix("rtpmap:") {
                    // "96 H264/90000" or "97 MPEG4-GENERIC/48000/2"
                    if let Some((_, enc)) = rtpmap.split_once(' ') {
                        let mut fields = enc.split('/');
                        current.encoding = fields.next().map(|s| s.to_string());
                        current.clock_rate = fields.next().and_then(|s| s.parse().ok());
                    }
                } else if let Some(control) = rest.strip_prefix("control:") {
                    current.control = Some(control.to_string());
                }
            }
        }
        Sdp { media }
    }

    /// Resolve the `SETUP` URL for the first track of `kind` (`"video"` /
    /// `"audio"`), joining its `a=control` value against `base_url` (absolute
    /// control URLs win).
    pub fn first_control(&self, kind: &str, base_url: &str) -> Option<String> {
        let media = self.media.iter().find(|m| m.media == kind)?;
        let control = media.control.as_deref().unwrap_or("");
        if control.is_empty() || control == "*" {
            Some(base_url.to_string())
        } else if control.starts_with("rtsp://") {
            Some(control.to_string())
        } else {
            Some(format!("{}/{}", base_url.trim_end_matches('/'), control))
        }
    }

    /// Resolve the `SETUP` URL for the first video track.
    pub fn first_video_control(&self, base_url: &str) -> Option<String> {
        self.first_control("video", base_url)
    }

    /// Resolve the `SETUP` URL for the first audio track, if the session has one.
    pub fn first_audio_control(&self, base_url: &str) -> Option<String> {
        self.first_control("audio", base_url)
    }

    /// Whether the session advertises an AAC audio track (`MPEG4-GENERIC`).
    pub fn has_aac_audio(&self) -> bool {
        self.media.iter().any(|m| {
            m.media == "audio"
                && m.encoding
                    .as_deref()
                    .is_some_and(|e| e.eq_ignore_ascii_case("MPEG4-GENERIC"))
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const SAMPLE: &str = "v=0\r\n\
o=- 0 0 IN IP4 127.0.0.1\r\n\
s=Session\r\n\
m=video 0 RTP/AVP 96\r\n\
a=rtpmap:96 H264/90000\r\n\
a=control:trackID=1\r\n\
m=audio 0 RTP/AVP 97\r\n\
a=rtpmap:97 MPEG4-GENERIC/48000/2\r\n\
a=control:trackID=2\r\n";

    #[test]
    fn parses_both_media_tracks() {
        let sdp = Sdp::parse(SAMPLE);
        assert_eq!(sdp.media.len(), 2);
        let v = &sdp.media[0];
        assert_eq!(v.media, "video");
        assert_eq!(v.payload_type, 96);
        assert_eq!(v.encoding.as_deref(), Some("H264"));
        assert_eq!(v.control.as_deref(), Some("trackID=1"));
        assert_eq!(sdp.media[1].encoding.as_deref(), Some("MPEG4-GENERIC"));
    }

    #[test]
    fn builds_relative_control_url() {
        let sdp = Sdp::parse(SAMPLE);
        assert_eq!(
            sdp.first_video_control("rtsp://cam/stream"),
            Some("rtsp://cam/stream/trackID=1".to_string())
        );
    }

    #[test]
    fn absolute_control_url_overrides_base() {
        let sdp = Sdp::parse("m=video 0 RTP/AVP 96\r\na=control:rtsp://cam/abs\r\n");
        assert_eq!(
            sdp.first_video_control("rtsp://cam/stream"),
            Some("rtsp://cam/abs".to_string())
        );
    }

    #[test]
    fn empty_sdp_yields_no_media() {
        assert!(Sdp::parse("v=0\r\n").media.is_empty());
    }

    #[test]
    fn resolves_audio_control_and_detects_aac() {
        let sdp = Sdp::parse(SAMPLE);
        assert!(sdp.has_aac_audio());
        assert_eq!(
            sdp.first_audio_control("rtsp://cam/stream"),
            Some("rtsp://cam/stream/trackID=2".to_string())
        );
    }
}