arcly-stream 0.1.3

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
//! WebRTC SDP offer parsing and answer generation (RFC 8866 + JSEP).
//!
//! Just enough of the offer is parsed to emit a compatible single-video answer:
//! the media line, the chosen H.264 payload type, and the bundle/mid grouping.
//! The crypto-bearing lines in the answer (`a=fingerprint`, `a=ice-ufrag`,
//! `a=ice-pwd`) come from the host's [`DtlsSrtpTransport`](super::DtlsSrtpTransport).

/// A parsed WHIP/WHEP SDP offer (only the fields the answer needs).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SdpOffer {
    /// The video media `mid` (defaults to `"0"` when absent).
    pub mid: String,
    /// The negotiated H.264 dynamic payload type (e.g. 96).
    pub payload_type: u8,
    /// Whether the offer requested `a=sendonly`/`sendrecv` (a publisher).
    pub send: bool,
}

impl SdpOffer {
    /// Parse the offer, returning `None` if it has no video `m=` line.
    pub fn parse(sdp: &str) -> Option<SdpOffer> {
        let mut in_video = false;
        let mut payload_type = None;
        let mut mid = "0".to_string();
        let mut send = false;
        let mut h264_pt = None;

        for line in sdp.lines() {
            let line = line.trim_end();
            if let Some(rest) = line.strip_prefix("m=") {
                in_video = rest.starts_with("video");
                if in_video {
                    // First payload type on the m= line is the default.
                    payload_type = rest.split(' ').nth(3).and_then(|p| p.parse().ok());
                }
            } else if in_video {
                if let Some(rtpmap) = line.strip_prefix("a=rtpmap:") {
                    if rtpmap.contains("H264") {
                        h264_pt = rtpmap.split(' ').next().and_then(|p| p.parse().ok());
                    }
                } else if let Some(m) = line.strip_prefix("a=mid:") {
                    mid = m.to_string();
                } else if line == "a=sendonly" || line == "a=sendrecv" {
                    send = true;
                }
            }
        }

        let payload_type = h264_pt.or(payload_type)?;
        Some(SdpOffer {
            mid,
            payload_type,
            send,
        })
    }
}

/// The crypto/ICE parameters the host injects into the answer.
#[derive(Debug, Clone)]
pub struct SdpAnswerParams {
    /// `a=fingerprint` value (`sha-256 AA:BB:…`).
    pub fingerprint: String,
    /// `a=ice-ufrag` value.
    pub ice_ufrag: String,
    /// `a=ice-pwd` value.
    pub ice_pwd: String,
}

/// Build an SDP answer for `offer` using the host's `params`.
///
/// The answer is a minimal single-video, recvonly description (the server
/// receives the WHIP publisher's media). It echoes the offer's payload type and
/// mid, and advertises the server's DTLS fingerprint and ICE credentials. A
/// real connection's ICE candidates are trickled separately by the transport.
pub fn build_answer(offer: &SdpOffer, params: &SdpAnswerParams) -> String {
    let pt = offer.payload_type;
    format!(
        "v=0\r\n\
o=- 0 0 IN IP4 0.0.0.0\r\n\
s=-\r\n\
t=0 0\r\n\
a=group:BUNDLE {mid}\r\n\
m=video 9 UDP/TLS/RTP/SAVPF {pt}\r\n\
c=IN IP4 0.0.0.0\r\n\
a=rtcp-mux\r\n\
a=mid:{mid}\r\n\
a=recvonly\r\n\
a=ice-ufrag:{ufrag}\r\n\
a=ice-pwd:{pwd}\r\n\
a=fingerprint:{fp}\r\n\
a=setup:passive\r\n\
a=rtpmap:{pt} H264/90000\r\n\
a=rtcp-fb:{pt} nack pli\r\n\
a=rtcp-fb:{pt} ccm fir\r\n",
        mid = offer.mid,
        pt = pt,
        ufrag = params.ice_ufrag,
        pwd = params.ice_pwd,
        fp = params.fingerprint,
    )
}

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

    const OFFER: &str = "v=0\r\n\
o=- 0 0 IN IP4 0.0.0.0\r\n\
m=video 9 UDP/TLS/RTP/SAVPF 96 97\r\n\
a=mid:vid\r\n\
a=sendonly\r\n\
a=rtpmap:96 H264/90000\r\n";

    #[test]
    fn parses_video_offer() {
        let o = SdpOffer::parse(OFFER).unwrap();
        assert_eq!(o.payload_type, 96);
        assert_eq!(o.mid, "vid");
        assert!(o.send);
    }

    #[test]
    fn audio_only_offer_is_rejected() {
        assert!(SdpOffer::parse("m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n").is_none());
    }

    #[test]
    fn answer_echoes_mid_and_payload_and_injects_crypto() {
        let o = SdpOffer::parse(OFFER).unwrap();
        let a = build_answer(
            &o,
            &SdpAnswerParams {
                fingerprint: "sha-256 11:22".into(),
                ice_ufrag: "uf".into(),
                ice_pwd: "pw".into(),
            },
        );
        assert!(a.contains("a=mid:vid"));
        assert!(a.contains("m=video 9 UDP/TLS/RTP/SAVPF 96"));
        assert!(a.contains("a=recvonly"));
        assert!(a.contains("a=ice-ufrag:uf"));
        assert!(a.contains("a=fingerprint:sha-256 11:22"));
        assert!(a.contains("a=rtcp-fb:96 nack pli"));
    }
}