arcly_stream/protocol/webrtc/
sdp.rs1#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct SdpOffer {
11 pub mid: String,
13 pub payload_type: u8,
15 pub send: bool,
17}
18
19impl SdpOffer {
20 pub fn parse(sdp: &str) -> Option<SdpOffer> {
22 let mut in_video = false;
23 let mut payload_type = None;
24 let mut mid = "0".to_string();
25 let mut send = false;
26 let mut h264_pt = None;
27
28 for line in sdp.lines() {
29 let line = line.trim_end();
30 if let Some(rest) = line.strip_prefix("m=") {
31 in_video = rest.starts_with("video");
32 if in_video {
33 payload_type = rest.split(' ').nth(3).and_then(|p| p.parse().ok());
35 }
36 } else if in_video {
37 if let Some(rtpmap) = line.strip_prefix("a=rtpmap:") {
38 if rtpmap.contains("H264") {
39 h264_pt = rtpmap.split(' ').next().and_then(|p| p.parse().ok());
40 }
41 } else if let Some(m) = line.strip_prefix("a=mid:") {
42 mid = m.to_string();
43 } else if line == "a=sendonly" || line == "a=sendrecv" {
44 send = true;
45 }
46 }
47 }
48
49 let payload_type = h264_pt.or(payload_type)?;
50 Some(SdpOffer {
51 mid,
52 payload_type,
53 send,
54 })
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct SdpAnswerParams {
61 pub fingerprint: String,
63 pub ice_ufrag: String,
65 pub ice_pwd: String,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum MediaDirection {
73 RecvOnly,
75 SendOnly,
77}
78
79impl MediaDirection {
80 fn attr(self) -> &'static str {
81 match self {
82 MediaDirection::RecvOnly => "recvonly",
83 MediaDirection::SendOnly => "sendonly",
84 }
85 }
86}
87
88pub fn build_answer(offer: &SdpOffer, params: &SdpAnswerParams) -> String {
90 build_answer_directed(offer, params, MediaDirection::RecvOnly)
91}
92
93pub fn build_answer_directed(
99 offer: &SdpOffer,
100 params: &SdpAnswerParams,
101 direction: MediaDirection,
102) -> String {
103 let pt = offer.payload_type;
104 format!(
105 "v=0\r\n\
106o=- 0 0 IN IP4 0.0.0.0\r\n\
107s=-\r\n\
108t=0 0\r\n\
109a=group:BUNDLE {mid}\r\n\
110m=video 9 UDP/TLS/RTP/SAVPF {pt}\r\n\
111c=IN IP4 0.0.0.0\r\n\
112a=rtcp-mux\r\n\
113a=mid:{mid}\r\n\
114a={dir}\r\n\
115a=ice-ufrag:{ufrag}\r\n\
116a=ice-pwd:{pwd}\r\n\
117a=fingerprint:{fp}\r\n\
118a=setup:passive\r\n\
119a=rtpmap:{pt} H264/90000\r\n\
120a=rtcp-fb:{pt} nack pli\r\n\
121a=rtcp-fb:{pt} ccm fir\r\n",
122 mid = offer.mid,
123 pt = pt,
124 dir = direction.attr(),
125 ufrag = params.ice_ufrag,
126 pwd = params.ice_pwd,
127 fp = params.fingerprint,
128 )
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 const OFFER: &str = "v=0\r\n\
136o=- 0 0 IN IP4 0.0.0.0\r\n\
137m=video 9 UDP/TLS/RTP/SAVPF 96 97\r\n\
138a=mid:vid\r\n\
139a=sendonly\r\n\
140a=rtpmap:96 H264/90000\r\n";
141
142 #[test]
143 fn parses_video_offer() {
144 let o = SdpOffer::parse(OFFER).unwrap();
145 assert_eq!(o.payload_type, 96);
146 assert_eq!(o.mid, "vid");
147 assert!(o.send);
148 }
149
150 #[test]
151 fn audio_only_offer_is_rejected() {
152 assert!(SdpOffer::parse("m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n").is_none());
153 }
154
155 #[test]
156 fn answer_echoes_mid_and_payload_and_injects_crypto() {
157 let o = SdpOffer::parse(OFFER).unwrap();
158 let a = build_answer(
159 &o,
160 &SdpAnswerParams {
161 fingerprint: "sha-256 11:22".into(),
162 ice_ufrag: "uf".into(),
163 ice_pwd: "pw".into(),
164 },
165 );
166 assert!(a.contains("a=mid:vid"));
167 assert!(a.contains("m=video 9 UDP/TLS/RTP/SAVPF 96"));
168 assert!(a.contains("a=recvonly"));
169 assert!(a.contains("a=ice-ufrag:uf"));
170 assert!(a.contains("a=fingerprint:sha-256 11:22"));
171 assert!(a.contains("a=rtcp-fb:96 nack pli"));
172 }
173}