Skip to main content

sip_core/
sdp.rs

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        // Add standard codecs
135        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; // Skip malformed lines
165            }
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                    // c=IN IP4 224.2.17.12
188                    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                            // Media-level connection; we'll set it on finalization
193                        }
194                        connection_address = Some(addr);
195                    }
196                }
197                'm' => {
198                    // Finalize previous media
199                    if let Some(m) = current_media.take() {
200                        media_descriptions.push(m);
201                    }
202
203                    // m=audio 49170 RTP/AVP 0 8 111
204                    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                _ => {} // Ignore other line types
258            }
259        }
260
261        // Finalize last media
262        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    /// Create an audio media section with a specific direction attribute (for hold/resume).
291    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    /// Get the media direction attribute (sendrecv, sendonly, recvonly, inactive).
304    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    // Format: "111 opus/48000/2" or "0 PCMU/8000"
330    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; // Already handled above
393                }
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        // Standard add_audio_media uses sendrecv
578        let mut sdp = SdpSession::new("10.0.0.1");
579        sdp.add_audio_media(5000);
580        // sendrecv is added by add_audio_media
581        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}