1use std::fmt;
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum MediaType {
6 Audio,
7 Video,
8 Other(String),
9}
10
11impl fmt::Display for MediaType {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 match self {
14 MediaType::Audio => write!(f, "audio"),
15 MediaType::Video => write!(f, "video"),
16 MediaType::Other(s) => write!(f, "{}", s),
17 }
18 }
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum TransportProtocol {
23 RtpAvp,
24 RtpSavp,
25 Other(String),
26}
27
28impl fmt::Display for TransportProtocol {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 TransportProtocol::RtpAvp => write!(f, "RTP/AVP"),
32 TransportProtocol::RtpSavp => write!(f, "RTP/SAVP"),
33 TransportProtocol::Other(s) => write!(f, "{}", s),
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct RtpMap {
40 pub payload_type: u8,
41 pub encoding_name: String,
42 pub clock_rate: u32,
43 pub channels: Option<u32>,
44}
45
46impl fmt::Display for RtpMap {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{} {}/{}", self.payload_type, self.encoding_name, self.clock_rate)?;
49 if let Some(ch) = self.channels {
50 write!(f, "/{}", ch)?;
51 }
52 Ok(())
53 }
54}
55
56#[derive(Debug, Clone)]
57pub struct MediaDescription {
58 pub media_type: MediaType,
59 pub port: u16,
60 pub protocol: TransportProtocol,
61 pub formats: Vec<u8>,
62 pub rtpmaps: Vec<RtpMap>,
63 pub attributes: Vec<(String, Option<String>)>,
64}
65
66impl MediaDescription {
67 pub fn new_audio(port: u16) -> Self {
68 Self {
69 media_type: MediaType::Audio,
70 port,
71 protocol: TransportProtocol::RtpAvp,
72 formats: Vec::new(),
73 rtpmaps: Vec::new(),
74 attributes: Vec::new(),
75 }
76 }
77
78 pub fn add_codec(&mut self, payload_type: u8, name: &str, clock_rate: u32, channels: Option<u32>) {
79 self.formats.push(payload_type);
80 self.rtpmaps.push(RtpMap {
81 payload_type,
82 encoding_name: name.to_string(),
83 clock_rate,
84 channels,
85 });
86 }
87
88 pub fn add_attribute(&mut self, name: &str, value: Option<&str>) {
89 self.attributes.push((name.to_string(), value.map(|s| s.to_string())));
90 }
91}
92
93#[derive(Debug, Clone)]
94pub struct SdpSession {
95 pub version: u32,
96 pub origin_username: String,
97 pub session_id: String,
98 pub session_version: String,
99 pub origin_address: String,
100 pub session_name: String,
101 pub connection_address: Option<String>,
102 pub media_descriptions: Vec<MediaDescription>,
103 pub attributes: Vec<(String, Option<String>)>,
104}
105
106#[derive(Debug, Error)]
107pub enum SdpError {
108 #[error("missing required field: {0}")]
109 MissingField(String),
110 #[error("invalid SDP line: {0}")]
111 InvalidLine(String),
112 #[error("invalid media line: {0}")]
113 InvalidMedia(String),
114}
115
116impl SdpSession {
117 pub fn new(address: &str) -> Self {
118 let session_id = format!("{}", rand::random::<u32>());
119 Self {
120 version: 0,
121 origin_username: "-".to_string(),
122 session_id: session_id.clone(),
123 session_version: session_id,
124 origin_address: address.to_string(),
125 session_name: "sip-rs".to_string(),
126 connection_address: Some(address.to_string()),
127 media_descriptions: Vec::new(),
128 attributes: Vec::new(),
129 }
130 }
131
132 pub fn add_audio_media(&mut self, port: u16) -> &mut MediaDescription {
133 let mut media = MediaDescription::new_audio(port);
134 media.add_codec(0, "PCMU", 8000, None);
136 media.add_codec(8, "PCMA", 8000, None);
137 media.add_codec(101, "telephone-event", 8000, None);
138 media.add_attribute("fmtp", Some("101 0-15"));
139 media.add_codec(111, "opus", 48000, Some(2));
140 media.add_attribute("sendrecv", None);
141 self.media_descriptions.push(media);
142 self.media_descriptions.last_mut().unwrap()
143 }
144
145 pub fn parse(input: &str) -> Result<Self, SdpError> {
146 let mut version = 0u32;
147 let mut origin_username = "-".to_string();
148 let mut session_id = String::new();
149 let mut session_version = String::new();
150 let mut origin_address = String::new();
151 let mut session_name = String::new();
152 let mut connection_address = None;
153 let mut media_descriptions: Vec<MediaDescription> = Vec::new();
154 let mut session_attributes: Vec<(String, Option<String>)> = Vec::new();
155 let mut current_media: Option<MediaDescription> = None;
156
157 for line in input.lines() {
158 let line = line.trim();
159 if line.is_empty() {
160 continue;
161 }
162
163 if line.len() < 2 || line.as_bytes()[1] != b'=' {
164 continue; }
166
167 let line_type = line.as_bytes()[0] as char;
168 let value = &line[2..];
169
170 match line_type {
171 'v' => {
172 version = value.parse().unwrap_or(0);
173 }
174 'o' => {
175 let parts: Vec<&str> = value.splitn(6, ' ').collect();
176 if parts.len() >= 6 {
177 origin_username = parts[0].to_string();
178 session_id = parts[1].to_string();
179 session_version = parts[2].to_string();
180 origin_address = parts[5].to_string();
181 }
182 }
183 's' => {
184 session_name = value.to_string();
185 }
186 'c' => {
187 let parts: Vec<&str> = value.split(' ').collect();
189 if parts.len() >= 3 {
190 let addr = parts[2].to_string();
191 if current_media.is_some() {
192 }
194 connection_address = Some(addr);
195 }
196 }
197 'm' => {
198 if let Some(m) = current_media.take() {
200 media_descriptions.push(m);
201 }
202
203 let parts: Vec<&str> = value.split(' ').collect();
205 if parts.len() < 3 {
206 return Err(SdpError::InvalidMedia(value.to_string()));
207 }
208
209 let media_type = match parts[0] {
210 "audio" => MediaType::Audio,
211 "video" => MediaType::Video,
212 other => MediaType::Other(other.to_string()),
213 };
214
215 let port: u16 = parts[1].parse().unwrap_or(0);
216
217 let protocol = match parts[2] {
218 "RTP/AVP" => TransportProtocol::RtpAvp,
219 "RTP/SAVP" => TransportProtocol::RtpSavp,
220 other => TransportProtocol::Other(other.to_string()),
221 };
222
223 let formats: Vec<u8> = parts[3..]
224 .iter()
225 .filter_map(|s| s.parse().ok())
226 .collect();
227
228 current_media = Some(MediaDescription {
229 media_type,
230 port,
231 protocol,
232 formats,
233 rtpmaps: Vec::new(),
234 attributes: Vec::new(),
235 });
236 }
237 'a' => {
238 let (attr_name, attr_value) = if let Some((name, val)) = value.split_once(':') {
239 (name.to_string(), Some(val.to_string()))
240 } else {
241 (value.to_string(), None)
242 };
243
244 if let Some(ref mut media) = current_media {
245 if attr_name == "rtpmap" {
246 if let Some(val) = &attr_value {
247 if let Some(rtpmap) = parse_rtpmap(val) {
248 media.rtpmaps.push(rtpmap);
249 }
250 }
251 }
252 media.attributes.push((attr_name, attr_value));
253 } else {
254 session_attributes.push((attr_name, attr_value));
255 }
256 }
257 _ => {} }
259 }
260
261 if let Some(m) = current_media.take() {
263 media_descriptions.push(m);
264 }
265
266 Ok(SdpSession {
267 version,
268 origin_username,
269 session_id,
270 session_version,
271 origin_address,
272 session_name,
273 connection_address,
274 media_descriptions,
275 attributes: session_attributes,
276 })
277 }
278
279 pub fn get_audio_port(&self) -> Option<u16> {
280 self.media_descriptions
281 .iter()
282 .find(|m| m.media_type == MediaType::Audio)
283 .map(|m| m.port)
284 }
285
286 pub fn get_connection_address(&self) -> Option<&str> {
287 self.connection_address.as_deref()
288 }
289
290 pub fn add_audio_media_directed(&mut self, port: u16, direction: &str) -> &mut MediaDescription {
292 let mut media = MediaDescription::new_audio(port);
293 media.add_codec(0, "PCMU", 8000, None);
294 media.add_codec(8, "PCMA", 8000, None);
295 media.add_codec(101, "telephone-event", 8000, None);
296 media.add_attribute("fmtp", Some("101 0-15"));
297 media.add_codec(111, "opus", 48000, Some(2));
298 media.add_attribute(direction, None);
299 self.media_descriptions.push(media);
300 self.media_descriptions.last_mut().unwrap()
301 }
302
303 pub fn get_audio_direction(&self) -> Option<&str> {
305 let audio = self.media_descriptions.iter().find(|m| m.media_type == MediaType::Audio)?;
306 for (name, _) in &audio.attributes {
307 match name.as_str() {
308 "sendrecv" | "sendonly" | "recvonly" | "inactive" => return Some(name.as_str()),
309 _ => {}
310 }
311 }
312 None
313 }
314
315 pub fn get_audio_dtmf_payload_type(&self) -> Option<u8> {
316 let audio = self
317 .media_descriptions
318 .iter()
319 .find(|m| m.media_type == MediaType::Audio)?;
320 audio
321 .rtpmaps
322 .iter()
323 .find(|rtpmap| rtpmap.encoding_name.eq_ignore_ascii_case("telephone-event"))
324 .map(|rtpmap| rtpmap.payload_type)
325 }
326}
327
328fn parse_rtpmap(value: &str) -> Option<RtpMap> {
329 let parts: Vec<&str> = value.splitn(2, ' ').collect();
331 if parts.len() != 2 {
332 return None;
333 }
334
335 let payload_type: u8 = parts[0].parse().ok()?;
336 let codec_parts: Vec<&str> = parts[1].split('/').collect();
337 if codec_parts.len() < 2 {
338 return None;
339 }
340
341 let encoding_name = codec_parts[0].to_string();
342 let clock_rate: u32 = codec_parts[1].parse().ok()?;
343 let channels = codec_parts.get(2).and_then(|s| s.parse().ok());
344
345 Some(RtpMap {
346 payload_type,
347 encoding_name,
348 clock_rate,
349 channels,
350 })
351}
352
353impl fmt::Display for SdpSession {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 writeln!(f, "v={}", self.version)?;
356 writeln!(
357 f,
358 "o={} {} {} IN IP4 {}",
359 self.origin_username, self.session_id, self.session_version, self.origin_address
360 )?;
361 writeln!(f, "s={}", self.session_name)?;
362 if let Some(addr) = &self.connection_address {
363 writeln!(f, "c=IN IP4 {}", addr)?;
364 }
365 writeln!(f, "t=0 0")?;
366
367 for (name, value) in &self.attributes {
368 if let Some(val) = value {
369 writeln!(f, "a={}:{}", name, val)?;
370 } else {
371 writeln!(f, "a={}", name)?;
372 }
373 }
374
375 for media in &self.media_descriptions {
376 let formats: Vec<String> = media.formats.iter().map(|f| f.to_string()).collect();
377 writeln!(
378 f,
379 "m={} {} {} {}",
380 media.media_type,
381 media.port,
382 media.protocol,
383 formats.join(" ")
384 )?;
385
386 for rtpmap in &media.rtpmaps {
387 writeln!(f, "a=rtpmap:{}", rtpmap)?;
388 }
389
390 for (name, value) in &media.attributes {
391 if name == "rtpmap" {
392 continue; }
394 if let Some(val) = value {
395 writeln!(f, "a={}:{}", name, val)?;
396 } else {
397 writeln!(f, "a={}", name)?;
398 }
399 }
400 }
401
402 Ok(())
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 const SAMPLE_SDP: &str = "v=0\r\n\
411 o=- 123456 654321 IN IP4 192.168.1.100\r\n\
412 s=sip-rs\r\n\
413 c=IN IP4 192.168.1.100\r\n\
414 t=0 0\r\n\
415 m=audio 49170 RTP/AVP 0 8 101 111\r\n\
416 a=rtpmap:0 PCMU/8000\r\n\
417 a=rtpmap:8 PCMA/8000\r\n\
418 a=rtpmap:101 telephone-event/8000\r\n\
419 a=fmtp:101 0-15\r\n\
420 a=rtpmap:111 opus/48000/2\r\n\
421 a=sendrecv\r\n";
422
423 #[test]
424 fn test_parse_sdp() {
425 let sdp = SdpSession::parse(SAMPLE_SDP).unwrap();
426 assert_eq!(sdp.version, 0);
427 assert_eq!(sdp.origin_username, "-");
428 assert_eq!(sdp.session_id, "123456");
429 assert_eq!(sdp.connection_address, Some("192.168.1.100".to_string()));
430 assert_eq!(sdp.media_descriptions.len(), 1);
431
432 let audio = &sdp.media_descriptions[0];
433 assert_eq!(audio.media_type, MediaType::Audio);
434 assert_eq!(audio.port, 49170);
435 assert_eq!(audio.protocol, TransportProtocol::RtpAvp);
436 assert_eq!(audio.formats, vec![0, 8, 101, 111]);
437 assert_eq!(audio.rtpmaps.len(), 4);
438
439 assert_eq!(audio.rtpmaps[0].encoding_name, "PCMU");
440 assert_eq!(audio.rtpmaps[0].clock_rate, 8000);
441 assert_eq!(audio.rtpmaps[1].encoding_name, "PCMA");
442 assert_eq!(audio.rtpmaps[2].encoding_name, "telephone-event");
443 assert_eq!(audio.rtpmaps[2].clock_rate, 8000);
444 assert_eq!(audio.rtpmaps[3].encoding_name, "opus");
445 assert_eq!(audio.rtpmaps[3].clock_rate, 48000);
446 assert_eq!(audio.rtpmaps[3].channels, Some(2));
447 }
448
449 #[test]
450 fn test_sdp_get_audio_port() {
451 let sdp = SdpSession::parse(SAMPLE_SDP).unwrap();
452 assert_eq!(sdp.get_audio_port(), Some(49170));
453 }
454
455 #[test]
456 fn test_sdp_get_connection_address() {
457 let sdp = SdpSession::parse(SAMPLE_SDP).unwrap();
458 assert_eq!(sdp.get_connection_address(), Some("192.168.1.100"));
459 }
460
461 #[test]
462 fn test_create_sdp() {
463 let mut sdp = SdpSession::new("10.0.0.1");
464 sdp.add_audio_media(5004);
465
466 let output = sdp.to_string();
467 assert!(output.contains("v=0"));
468 assert!(output.contains("c=IN IP4 10.0.0.1"));
469 assert!(output.contains("m=audio 5004 RTP/AVP 0 8 101 111"));
470 assert!(output.contains("a=rtpmap:0 PCMU/8000"));
471 assert!(output.contains("a=rtpmap:8 PCMA/8000"));
472 assert!(output.contains("a=rtpmap:101 telephone-event/8000"));
473 assert!(output.contains("a=fmtp:101 0-15"));
474 assert!(output.contains("a=rtpmap:111 opus/48000/2"));
475 assert!(output.contains("a=sendrecv"));
476 }
477
478 #[test]
479 fn test_sdp_roundtrip() {
480 let mut sdp = SdpSession::new("192.168.1.50");
481 sdp.add_audio_media(8000);
482
483 let serialized = sdp.to_string();
484 let parsed = SdpSession::parse(&serialized).unwrap();
485
486 assert_eq!(parsed.version, 0);
487 assert_eq!(parsed.connection_address, Some("192.168.1.50".to_string()));
488 assert_eq!(parsed.get_audio_port(), Some(8000));
489 assert_eq!(parsed.media_descriptions[0].rtpmaps.len(), 4);
490 assert_eq!(parsed.get_audio_dtmf_payload_type(), Some(101));
491 }
492
493 #[test]
494 fn test_parse_rtpmap() {
495 let rtpmap = parse_rtpmap("111 opus/48000/2").unwrap();
496 assert_eq!(rtpmap.payload_type, 111);
497 assert_eq!(rtpmap.encoding_name, "opus");
498 assert_eq!(rtpmap.clock_rate, 48000);
499 assert_eq!(rtpmap.channels, Some(2));
500
501 let rtpmap = parse_rtpmap("0 PCMU/8000").unwrap();
502 assert_eq!(rtpmap.payload_type, 0);
503 assert_eq!(rtpmap.encoding_name, "PCMU");
504 assert_eq!(rtpmap.clock_rate, 8000);
505 assert_eq!(rtpmap.channels, None);
506 }
507
508 #[test]
509 fn test_media_description_add_codec() {
510 let mut media = MediaDescription::new_audio(5000);
511 media.add_codec(96, "telephone-event", 8000, None);
512 assert_eq!(media.formats, vec![96]);
513 assert_eq!(media.rtpmaps[0].encoding_name, "telephone-event");
514 }
515
516 #[test]
517 fn test_sdp_no_media() {
518 let sdp_str = "v=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=test\r\nc=IN IP4 127.0.0.1\r\nt=0 0\r\n";
519 let sdp = SdpSession::parse(sdp_str).unwrap();
520 assert!(sdp.media_descriptions.is_empty());
521 assert_eq!(sdp.get_audio_port(), None);
522 }
523
524 #[test]
525 fn test_rtpmap_display() {
526 let rtpmap = RtpMap {
527 payload_type: 111,
528 encoding_name: "opus".to_string(),
529 clock_rate: 48000,
530 channels: Some(2),
531 };
532 assert_eq!(rtpmap.to_string(), "111 opus/48000/2");
533
534 let rtpmap = RtpMap {
535 payload_type: 0,
536 encoding_name: "PCMU".to_string(),
537 clock_rate: 8000,
538 channels: None,
539 };
540 assert_eq!(rtpmap.to_string(), "0 PCMU/8000");
541 }
542
543 #[test]
544 fn test_add_audio_media_directed_sendonly() {
545 let mut sdp = SdpSession::new("192.168.1.1");
546 sdp.add_audio_media_directed(4000, "sendonly");
547 let direction = sdp.get_audio_direction();
548 assert_eq!(direction, Some("sendonly"));
549 let s = sdp.to_string();
550 assert!(s.contains("a=sendonly"));
551 assert!(!s.contains("a=sendrecv"));
552 }
553
554 #[test]
555 fn test_add_audio_media_directed_recvonly() {
556 let mut sdp = SdpSession::new("10.0.0.1");
557 sdp.add_audio_media_directed(5000, "recvonly");
558 assert_eq!(sdp.get_audio_direction(), Some("recvonly"));
559 }
560
561 #[test]
562 fn test_add_audio_media_directed_inactive() {
563 let mut sdp = SdpSession::new("10.0.0.1");
564 sdp.add_audio_media_directed(5000, "inactive");
565 assert_eq!(sdp.get_audio_direction(), Some("inactive"));
566 }
567
568 #[test]
569 fn test_add_audio_media_directed_sendrecv() {
570 let mut sdp = SdpSession::new("10.0.0.1");
571 sdp.add_audio_media_directed(5000, "sendrecv");
572 assert_eq!(sdp.get_audio_direction(), Some("sendrecv"));
573 }
574
575 #[test]
576 fn test_get_audio_direction_default_is_none() {
577 let mut sdp = SdpSession::new("10.0.0.1");
579 sdp.add_audio_media(5000);
580 assert_eq!(sdp.get_audio_direction(), Some("sendrecv"));
582 }
583
584 #[test]
585 fn test_parse_sdp_with_direction() {
586 let sdp_text = "v=0\r\n\
587o=- 0 0 IN IP4 10.0.0.1\r\n\
588s=-\r\n\
589c=IN IP4 10.0.0.1\r\n\
590t=0 0\r\n\
591m=audio 4000 RTP/AVP 0\r\n\
592a=rtpmap:0 PCMU/8000\r\n\
593a=sendonly\r\n";
594 let sdp = SdpSession::parse(sdp_text).unwrap();
595 assert_eq!(sdp.get_audio_direction(), Some("sendonly"));
596 assert_eq!(sdp.get_audio_port(), Some(4000));
597 }
598}