#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SdpOffer {
pub mid: String,
pub payload_type: u8,
pub send: bool,
}
impl SdpOffer {
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 {
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,
})
}
}
#[derive(Debug, Clone)]
pub struct SdpAnswerParams {
pub fingerprint: String,
pub ice_ufrag: String,
pub ice_pwd: String,
}
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"));
}
}