1use std::net::IpAddr;
5
6pub const DTMF_DEFAULT_PT: u8 = 101;
12
13pub fn build_sdp(local_ip: IpAddr, rtp_port: u16) -> Vec<u8> {
22 format!(
23 "v=0\r\n\
24 o=wavekat 0 0 IN IP4 {local_ip}\r\n\
25 s=wavekat-sip\r\n\
26 c=IN IP4 {local_ip}\r\n\
27 t=0 0\r\n\
28 m=audio {rtp_port} RTP/AVP 0 8 {DTMF_DEFAULT_PT}\r\n\
29 a=rtpmap:0 PCMU/8000\r\n\
30 a=rtpmap:8 PCMA/8000\r\n\
31 a=rtpmap:{DTMF_DEFAULT_PT} telephone-event/8000\r\n\
32 a=fmtp:{DTMF_DEFAULT_PT} 0-15\r\n\
33 a=sendrecv\r\n"
34 )
35 .into_bytes()
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct RemoteMedia {
41 pub addr: IpAddr,
43 pub port: u16,
45 pub payload_type: u8,
47 pub dtmf_payload_type: Option<u8>,
52}
53
54pub fn parse_sdp(sdp_bytes: &[u8]) -> Result<RemoteMedia, String> {
57 let sdp = std::str::from_utf8(sdp_bytes).map_err(|e| format!("SDP not UTF-8: {e}"))?;
58
59 let mut addr: Option<IpAddr> = None;
60 let mut port: Option<u16> = None;
61 let mut audio_pts: Vec<u8> = Vec::new();
62 let mut dtmf_candidate_pts: Vec<u8> = Vec::new();
64
65 for line in sdp.lines() {
66 let line = line.trim();
67
68 if line.starts_with("c=IN IP4 ") || line.starts_with("c=IN IP6 ") {
70 if let Some(ip_str) = line.split_whitespace().nth(2) {
71 addr = ip_str.parse().ok();
72 }
73 }
74
75 if line.starts_with("m=audio ") {
79 let parts: Vec<&str> = line.split_whitespace().collect();
80 if parts.len() >= 2 {
81 port = parts[1].parse().ok();
82 }
83 for pt_str in parts.iter().skip(3) {
85 if let Ok(pt) = pt_str.parse::<u8>() {
86 audio_pts.push(pt);
87 }
88 }
89 }
90
91 if let Some(rest) = line.strip_prefix("a=rtpmap:") {
93 let mut sp = rest.splitn(2, char::is_whitespace);
95 let pt_str = sp.next().unwrap_or("");
96 let mapping = sp.next().unwrap_or("");
97 if let Ok(pt) = pt_str.parse::<u8>() {
98 let name_lc = mapping.split('/').next().unwrap_or("").to_ascii_lowercase();
99 let rate = mapping.split('/').nth(1).unwrap_or("");
100 if name_lc == "telephone-event" && rate == "8000" {
101 dtmf_candidate_pts.push(pt);
102 }
103 }
104 }
105 }
106
107 let dtmf_payload_type = dtmf_candidate_pts
111 .into_iter()
112 .find(|pt| audio_pts.contains(pt));
113
114 let payload_type = audio_pts.first().copied();
115
116 match (addr, port, payload_type) {
117 (Some(addr), Some(port), Some(pt)) => Ok(RemoteMedia {
118 addr,
119 port,
120 payload_type: pt,
121 dtmf_payload_type,
122 }),
123 (None, _, _) => Err("No connection address (c=) in SDP".to_string()),
124 (_, None, _) => Err("No audio media line (m=audio) in SDP".to_string()),
125 (_, _, None) => Err("No payload type in m=audio line".to_string()),
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::net::Ipv4Addr;
133
134 #[test]
135 fn build_sdp_contains_required_fields() {
136 let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
137 let sdp = build_sdp(ip, 5004);
138 let text = String::from_utf8(sdp).unwrap();
139
140 assert!(text.contains("v=0\r\n"));
141 assert!(text.contains("c=IN IP4 10.0.0.1\r\n"));
142 assert!(text.contains("m=audio 5004 RTP/AVP 0 8 101\r\n"));
143 assert!(text.contains("a=sendrecv\r\n"));
144 assert!(text.contains("a=rtpmap:0 PCMU/8000\r\n"));
145 assert!(text.contains("a=rtpmap:8 PCMA/8000\r\n"));
146 assert!(text.contains("a=rtpmap:101 telephone-event/8000\r\n"));
147 assert!(text.contains("a=fmtp:101 0-15\r\n"));
148 }
149
150 #[test]
151 fn parse_sdp_extracts_addr_port_and_codec() {
152 let sdp = b"v=0\r\nc=IN IP4 192.168.1.100\r\nm=audio 20000 RTP/AVP 0\r\n";
153 let media = parse_sdp(sdp).unwrap();
154 assert_eq!(media.addr, Ipv4Addr::new(192, 168, 1, 100));
155 assert_eq!(media.port, 20000);
156 assert_eq!(media.payload_type, 0);
157 assert_eq!(media.dtmf_payload_type, None);
159 }
160
161 #[test]
162 fn parse_sdp_extracts_first_codec_when_multiple() {
163 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 8 0 101\r\n\
164 a=rtpmap:101 telephone-event/8000\r\n";
165 let media = parse_sdp(sdp).unwrap();
166 assert_eq!(media.payload_type, 8); assert_eq!(media.dtmf_payload_type, Some(101));
171 }
172
173 #[test]
174 fn round_trip_build_then_parse() {
175 let ip = IpAddr::V4(Ipv4Addr::new(172, 16, 0, 5));
176 let sdp = build_sdp(ip, 8000);
177 let media = parse_sdp(&sdp).unwrap();
178 assert_eq!(media.addr, ip);
179 assert_eq!(media.port, 8000);
180 assert_eq!(media.payload_type, 0); assert_eq!(media.dtmf_payload_type, Some(DTMF_DEFAULT_PT));
182 }
183
184 #[test]
185 fn parse_sdp_picks_dtmf_pt_from_rtpmap_not_constant() {
186 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 96\r\n\
189 a=rtpmap:0 PCMU/8000\r\n\
190 a=rtpmap:96 telephone-event/8000\r\n";
191 let media = parse_sdp(sdp).unwrap();
192 assert_eq!(media.dtmf_payload_type, Some(96));
193 }
194
195 #[test]
196 fn parse_sdp_ignores_telephone_event_at_wrong_rate() {
197 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
201 a=rtpmap:0 PCMU/8000\r\n\
202 a=rtpmap:101 telephone-event/16000\r\n";
203 let media = parse_sdp(sdp).unwrap();
204 assert_eq!(media.dtmf_payload_type, None);
205 }
206
207 #[test]
208 fn parse_sdp_rejects_telephone_event_not_listed_on_m_line() {
209 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0\r\n\
212 a=rtpmap:0 PCMU/8000\r\n\
213 a=rtpmap:101 telephone-event/8000\r\n";
214 let media = parse_sdp(sdp).unwrap();
215 assert_eq!(media.dtmf_payload_type, None);
216 }
217
218 #[test]
219 fn parse_sdp_handles_mixed_case_telephone_event() {
220 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
223 a=rtpmap:0 PCMU/8000\r\n\
224 a=rtpmap:101 Telephone-Event/8000\r\n";
225 let media = parse_sdp(sdp).unwrap();
226 assert_eq!(media.dtmf_payload_type, Some(101));
227 }
228
229 #[test]
230 fn parse_sdp_handles_telephone_event_without_fmtp() {
231 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
234 a=rtpmap:0 PCMU/8000\r\n\
235 a=rtpmap:101 telephone-event/8000\r\n";
236 let media = parse_sdp(sdp).unwrap();
237 assert_eq!(media.dtmf_payload_type, Some(101));
238 }
239
240 #[test]
241 fn parse_sdp_missing_connection_line() {
242 let sdp = b"v=0\r\nm=audio 20000 RTP/AVP 0\r\n";
243 let err = parse_sdp(sdp).unwrap_err();
244 assert!(err.contains("connection address"));
245 }
246
247 #[test]
248 fn parse_sdp_missing_media_line() {
249 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\n";
250 let err = parse_sdp(sdp).unwrap_err();
251 assert!(err.contains("audio media"));
252 }
253
254 #[test]
255 fn parse_sdp_invalid_utf8() {
256 let sdp = &[0xFF, 0xFE, 0xFD];
257 let err = parse_sdp(sdp).unwrap_err();
258 assert!(err.contains("UTF-8"));
259 }
260}