sdp_rs/
session_description.rs

1use crate::{lines, Error};
2use std::convert::{TryFrom, TryInto};
3use vec1::Vec1;
4
5/// The Session description high level type tokenizer. It tokenizes an SDP message. This is low
6/// level stuff and you shouldn't interact directly with it, unless you know what you are doing.
7pub use crate::tokenizers::session_description::Tokenizer;
8
9/// The Session description. This is the main `sdp-rs` type that describes the SDP message.
10#[derive(Debug, PartialEq, PartialOrd, Clone)]
11pub struct SessionDescription {
12    pub version: lines::Version,
13    pub origin: lines::Origin,
14    pub session_name: lines::SessionName,
15    pub session_info: Option<lines::SessionInformation>,
16    pub uri: Option<lines::Uri>,
17    pub emails: Vec<lines::Email>,
18    pub phones: Vec<lines::Phone>,
19    pub connection: Option<lines::Connection>,
20    pub bandwidths: Vec<lines::Bandwidth>,
21    pub times: Vec1<crate::Time>,
22    pub key: Option<lines::Key>,
23    pub attributes: Vec<lines::Attribute>,
24    pub media_descriptions: Vec<crate::MediaDescription>,
25}
26
27impl TryFrom<String> for SessionDescription {
28    type Error = Error;
29
30    fn try_from(from: String) -> Result<Self, Self::Error> {
31        let tokenizer = Tokenizer::tokenize(&from)?;
32        Self::try_from(tokenizer.1)
33    }
34}
35
36impl TryFrom<&str> for SessionDescription {
37    type Error = Error;
38
39    fn try_from(from: &str) -> Result<Self, Self::Error> {
40        let tokenizer = Tokenizer::tokenize(from)?;
41        Self::try_from(tokenizer.1)
42    }
43}
44
45impl std::str::FromStr for SessionDescription {
46    type Err = Error;
47    fn from_str(str: &str) -> Result<Self, Self::Err> {
48        Self::try_from(str)
49    }
50}
51
52impl<'a> TryFrom<Tokenizer<'a>> for SessionDescription {
53    type Error = Error;
54
55    fn try_from(tokenizer: Tokenizer<'a>) -> Result<Self, Self::Error> {
56        Ok(Self {
57            version: tokenizer.version.try_into()?,
58            origin: tokenizer.origin.try_into()?,
59            session_name: tokenizer.session_name.into(),
60            session_info: tokenizer.session_info.map(Into::into),
61            uri: tokenizer.uri.map(Into::into),
62            emails: tokenizer
63                .emails
64                .into_iter()
65                .map(Into::into)
66                .collect::<Vec<_>>(),
67            phones: tokenizer
68                .phones
69                .into_iter()
70                .map(Into::into)
71                .collect::<Vec<_>>(),
72            connection: tokenizer.connection.map(TryInto::try_into).transpose()?,
73            bandwidths: tokenizer
74                .bandwidths
75                .into_iter()
76                .map(TryInto::try_into)
77                .collect::<Result<Vec<_>, _>>()?,
78            times: tokenizer
79                .times
80                .into_iter()
81                .map(TryInto::try_into)
82                .collect::<Result<Vec<_>, _>>()?
83                .try_into()
84                .map_err(|_| Error::parser("times", "missing time(s) line(s)"))?,
85            key: tokenizer.key.map(Into::into),
86            attributes: tokenizer
87                .attributes
88                .into_iter()
89                .map(TryInto::try_into)
90                .collect::<Result<Vec<_>, _>>()?,
91            media_descriptions: tokenizer
92                .media_descriptions
93                .into_iter()
94                .map(TryInto::try_into)
95                .collect::<Result<Vec<_>, _>>()?,
96        })
97    }
98}
99
100impl std::fmt::Display for SessionDescription {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        write!(f, "{}\r\n", self.version)?;
103        write!(f, "{}\r\n", self.origin)?;
104        write!(f, "{}\r\n", self.session_name)?;
105        if let Some(info) = &self.session_info {
106            write!(f, "{}\r\n", info)?;
107        }
108        if let Some(uri) = &self.uri {
109            write!(f, "{}\r\n", uri)?;
110        }
111        for email in self.emails.iter() {
112            write!(f, "{}\r\n", email)?;
113        }
114        for phone in self.phones.iter() {
115            write!(f, "{}\r\n", phone)?;
116        }
117        if let Some(connection) = &self.connection {
118            write!(f, "{}\r\n", connection)?;
119        }
120        for bandwidth in self.bandwidths.iter() {
121            write!(f, "{}\r\n", bandwidth)?;
122        }
123        for time in self.times.iter() {
124            write!(f, "{}\r\n", time)?;
125        }
126        if let Some(key) = &self.key {
127            write!(f, "{}\r\n", key)?;
128        }
129        for attribute in self.attributes.iter() {
130            write!(f, "{}\r\n", attribute)?;
131        }
132        for media_description in self.media_descriptions.iter() {
133            write!(f, "{}\r\n", media_description)?;
134        }
135
136        Ok(())
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use chrono::Duration;
144    use std::net::{IpAddr, Ipv4Addr};
145    use vec1::vec1;
146
147    #[test]
148    fn from_tokenizer1() {
149        let tokenizer = Tokenizer {
150            version: "0".into(),
151            origin: crate::tokenizers::origin::Tokenizer {
152                username: "Alice",
153                sess_id: "2890844526",
154                sess_version: "2890842807",
155                nettype: "IN",
156                addrtype: "IP4",
157                unicast_address: "10.47.16.5",
158            },
159            session_name: "-".into(),
160            session_info: Some("A Seminar on the session description protocol".into()),
161            uri: Some("http://www.example.com/seminars/sdp.pdf".into()),
162            emails: vec!["alice@example.com (Alice Smith)".into()],
163            phones: vec!["+1 911-345-1160".into()],
164            connection: Some(crate::tokenizers::connection::Tokenizer {
165                nettype: "IN",
166                addrtype: "IP4",
167                connection_address: ("10.47.16.5").into(),
168            }),
169            bandwidths: vec![("CT", "1024").into()],
170            times: vec1![crate::time::Tokenizer {
171                active: ("2854678930", "2854679000").into(),
172                repeat: vec![("604800", "3600", vec!["0", "90000"]).into(),],
173                zone: Some(vec![("2882844526", "-1h"), ("2898848070", "0h")].into())
174            }],
175            key: Some(("clear", "password").into()),
176            attributes: vec!["recvonly".into()],
177            media_descriptions: vec![crate::tokenizers::media_description::Tokenizer {
178                media: crate::tokenizers::media::Tokenizer {
179                    media: "audio",
180                    port: "49170".into(),
181                    proto: "RTP/AVP",
182                    fmt: "0",
183                },
184                info: Some("audio media".into()),
185                connections: vec![
186                    crate::tokenizers::connection::Tokenizer {
187                        nettype: "IN",
188                        addrtype: "IP4",
189                        connection_address: "10.47.16.5".into(),
190                    },
191                    crate::tokenizers::connection::Tokenizer {
192                        nettype: "IN",
193                        addrtype: "IP4",
194                        connection_address: "10.47.16.6".into(),
195                    },
196                ],
197                bandwidths: vec![("CT", "1000").into(), ("AS", "551").into()],
198                key: Some("prompt".into()),
199                attributes: vec![
200                    ("rtpmap", "99 h232-199/90000").into(),
201                    ("rtpmap", "90 h263-1998/90000").into(),
202                ],
203            }],
204        };
205
206        let expected_times = vec1![crate::Time {
207            active: lines::Active {
208                start: 2854678930,
209                stop: 2854679000,
210            },
211            repeat: vec![lines::Repeat {
212                interval: lines::common::TypedTime::None(Duration::seconds(604800)),
213                duration: lines::common::TypedTime::None(Duration::seconds(3600)),
214                offsets: vec![
215                    lines::common::TypedTime::None(Duration::seconds(0)),
216                    lines::common::TypedTime::None(Duration::seconds(90000))
217                ],
218            },],
219            zone: Some(lines::Zone {
220                parts: vec![
221                    lines::zone::ZonePart {
222                        adjustment_time: 2882844526,
223                        offset: lines::common::TypedTime::Hours(Duration::hours(-1)),
224                    },
225                    lines::zone::ZonePart {
226                        adjustment_time: 2898848070,
227                        offset: lines::common::TypedTime::Hours(Duration::hours(0)),
228                    },
229                ],
230            })
231        }];
232
233        let expected_media_description = crate::MediaDescription {
234            media: lines::Media {
235                media: lines::media::MediaType::Audio,
236                port: 49170,
237                num_of_ports: None,
238                proto: lines::media::ProtoType::RtpAvp,
239                fmt: "0".into(),
240            },
241            info: Some(lines::SessionInformation::new("audio media".into())),
242            connections: vec![
243                lines::Connection {
244                    nettype: lines::common::Nettype::In,
245                    addrtype: lines::common::Addrtype::Ip4,
246                    connection_address: "10.47.16.5".parse::<IpAddr>().unwrap().into(),
247                },
248                lines::Connection {
249                    nettype: lines::common::Nettype::In,
250                    addrtype: lines::common::Addrtype::Ip4,
251                    connection_address: "10.47.16.6".parse::<IpAddr>().unwrap().into(),
252                },
253            ],
254            bandwidths: vec![
255                lines::Bandwidth {
256                    bwtype: lines::bandwidth::Bwtype::Ct,
257                    bandwidth: 1000,
258                },
259                lines::Bandwidth {
260                    bwtype: lines::bandwidth::Bwtype::As,
261                    bandwidth: 551,
262                },
263            ],
264            key: Some(lines::Key {
265                method: lines::key::KeyMethod::Prompt,
266                encryption_key: Default::default(),
267            }),
268            attributes: vec![
269                lines::Attribute::Rtpmap(lines::attribute::Rtpmap {
270                    payload_type: 99,
271                    encoding_name: "h232-199".into(),
272                    clock_rate: 90000,
273                    encoding_params: None,
274                }),
275                lines::Attribute::Rtpmap(lines::attribute::Rtpmap {
276                    payload_type: 90,
277                    encoding_name: "h263-1998".into(),
278                    clock_rate: 90000,
279                    encoding_params: None,
280                }),
281            ],
282        };
283
284        assert_eq!(
285            SessionDescription::try_from(tokenizer),
286            Ok(SessionDescription {
287                version: lines::Version::V0,
288                origin: lines::Origin {
289                    username: "Alice".into(),
290                    sess_id: "2890844526".into(),
291                    sess_version: "2890842807".into(),
292                    nettype: lines::common::Nettype::In,
293                    addrtype: lines::common::Addrtype::Ip4,
294                    unicast_address: IpAddr::V4(Ipv4Addr::new(10, 47, 16, 5)),
295                },
296                session_name: lines::SessionName::new("-".into()),
297                session_info: Some(lines::SessionInformation::new(
298                    "A Seminar on the session description protocol".into()
299                )),
300                uri: Some(lines::Uri::new(
301                    "http://www.example.com/seminars/sdp.pdf".into()
302                )),
303                emails: vec![lines::Email::new("alice@example.com (Alice Smith)".into())],
304                phones: vec![lines::Phone::new("+1 911-345-1160".into())],
305                connection: Some(lines::Connection {
306                    nettype: lines::common::Nettype::In,
307                    addrtype: lines::common::Addrtype::Ip4,
308                    connection_address: lines::connection::ConnectionAddress {
309                        base: "10.47.16.5".parse().unwrap(),
310                        ttl: None,
311                        numaddr: None
312                    }
313                }),
314                bandwidths: vec![lines::Bandwidth {
315                    bwtype: lines::bandwidth::Bwtype::Ct,
316                    bandwidth: 1024,
317                }],
318                times: expected_times,
319                key: Some(lines::Key {
320                    method: lines::key::KeyMethod::Clear,
321                    encryption_key: "password".into()
322                }),
323                attributes: vec![lines::Attribute::Recvonly],
324                media_descriptions: vec![expected_media_description]
325            })
326        );
327    }
328
329    #[test]
330    fn from_tokenizer2() {
331        let tokenizer = Tokenizer {
332            version: "0".into(),
333            origin: crate::tokenizers::origin::Tokenizer {
334                username: "Alice",
335                sess_id: "2890844526",
336                sess_version: "2890842807",
337                nettype: "IN",
338                addrtype: "IP4",
339                unicast_address: "10.47.16.5",
340            },
341            session_name: "-".into(),
342            session_info: None,
343            uri: None,
344            emails: vec![],
345            phones: vec![],
346            connection: None,
347            bandwidths: vec![],
348            times: vec1![crate::tokenizers::time::Tokenizer {
349                active: ("2854678930", "2854679000").into(),
350                repeat: vec![],
351                zone: None
352            }],
353            key: None,
354            attributes: vec![],
355            media_descriptions: vec![crate::media_description::Tokenizer {
356                media: crate::tokenizers::media::Tokenizer {
357                    media: "audio",
358                    port: "49170".into(),
359                    proto: "RTP/AVP",
360                    fmt: "0",
361                },
362                info: None,
363                connections: vec![crate::tokenizers::connection::Tokenizer {
364                    nettype: "IN",
365                    addrtype: "IP4",
366                    connection_address: "10.47.16.6".into(),
367                }],
368                bandwidths: vec![("AS", "551").into()],
369                key: None,
370                attributes: vec![],
371            }],
372        };
373
374        let expected_times = vec1![crate::Time {
375            active: lines::Active {
376                start: 2854678930,
377                stop: 2854679000,
378            },
379            repeat: vec![],
380            zone: None
381        }];
382
383        let expected_media_description = crate::MediaDescription {
384            media: lines::Media {
385                media: lines::media::MediaType::Audio,
386                port: 49170,
387                num_of_ports: None,
388                proto: lines::media::ProtoType::RtpAvp,
389                fmt: "0".into(),
390            },
391            info: None,
392            connections: vec![lines::Connection {
393                nettype: lines::common::Nettype::In,
394                addrtype: lines::common::Addrtype::Ip4,
395                connection_address: "10.47.16.6".parse::<IpAddr>().unwrap().into(),
396            }],
397            bandwidths: vec![lines::Bandwidth {
398                bwtype: lines::bandwidth::Bwtype::As,
399                bandwidth: 551,
400            }],
401            key: None,
402            attributes: vec![],
403        };
404
405        assert_eq!(
406            SessionDescription::try_from(tokenizer),
407            Ok(SessionDescription {
408                version: lines::Version::V0,
409                origin: lines::Origin {
410                    username: "Alice".into(),
411                    sess_id: "2890844526".into(),
412                    sess_version: "2890842807".into(),
413                    nettype: lines::common::Nettype::In,
414                    addrtype: lines::common::Addrtype::Ip4,
415                    unicast_address: IpAddr::V4(Ipv4Addr::new(10, 47, 16, 5)),
416                },
417                session_name: lines::SessionName::new("-".into()),
418                session_info: None,
419                uri: None,
420                emails: vec![],
421                phones: vec![],
422                connection: None,
423                bandwidths: vec![],
424                times: expected_times,
425                key: None,
426                attributes: vec![],
427                media_descriptions: vec![expected_media_description]
428            })
429        );
430    }
431
432    #[test]
433    fn from_tokenizer3() {
434        let tokenizer = Tokenizer {
435            version: "0".into(),
436            origin: crate::tokenizers::origin::Tokenizer {
437                username: "Alice",
438                sess_id: "2890844526",
439                sess_version: "2890842807",
440                nettype: "IN",
441                addrtype: "IP4",
442                unicast_address: "10.47.16.5",
443            },
444            session_name: "-".into(),
445            session_info: None,
446            uri: None,
447            emails: vec![],
448            phones: vec![],
449            connection: None,
450            bandwidths: vec![],
451            times: vec1![crate::tokenizers::time::Tokenizer {
452                active: ("2854678930", "2854679000").into(),
453                repeat: vec![],
454                zone: None
455            }],
456            key: None,
457            attributes: vec![],
458            media_descriptions: vec![],
459        };
460
461        let expected_times = vec1![crate::Time {
462            active: lines::Active {
463                start: 2854678930,
464                stop: 2854679000,
465            },
466            repeat: vec![],
467            zone: None
468        }];
469
470        assert_eq!(
471            SessionDescription::try_from(tokenizer),
472            Ok(SessionDescription {
473                version: lines::Version::V0,
474                origin: lines::Origin {
475                    username: "Alice".into(),
476                    sess_id: "2890844526".into(),
477                    sess_version: "2890842807".into(),
478                    nettype: lines::common::Nettype::In,
479                    addrtype: lines::common::Addrtype::Ip4,
480                    unicast_address: IpAddr::V4(Ipv4Addr::new(10, 47, 16, 5)),
481                },
482                session_name: lines::SessionName::new("-".into()),
483                session_info: None,
484                uri: None,
485                emails: vec![],
486                phones: vec![],
487                connection: None,
488                bandwidths: vec![],
489                times: expected_times,
490                key: None,
491                attributes: vec![],
492                media_descriptions: vec![]
493            })
494        );
495    }
496
497    #[test]
498    fn display1() {
499        let sdp = concat!(
500            "v=0\r\n",
501            "o=Alice 2890844526 2890842807 IN IP4 10.47.16.5\r\n",
502            "s=-\r\n",
503            "i=A Seminar on the session description protocol\r\n",
504            "u=http://www.example.com/seminars/sdp.pdf\r\n",
505            "e=alice@example.com (Alice Smith)\r\n",
506            "p=+1 911-345-1160\r\n",
507            "c=IN IP4 10.47.16.5\r\n",
508            "b=CT:1024\r\n",
509            "t=2854678930 2854679000\r\n",
510            "r=604800 3600 0 90000\r\n",
511            "z=2882844526 -1h 2898848070 0h\r\n",
512            "k=clear:password\r\n",
513            "a=recvonly\r\n",
514            "m=audio 49170 RTP/AVP 0\r\n",
515            "i=audio media\r\n",
516            "c=IN IP4 10.47.16.5\r\n",
517            "c=IN IP4 10.47.16.6\r\n",
518            "b=CT:1000\r\n",
519            "b=AS:551\r\n",
520            "k=prompt\r\n",
521            "a=rtpmap:99 h232-199/90000\r\n",
522            "a=rtpmap:90 h263-1998/90000\r\n"
523        );
524
525        let parsed_sdp = SessionDescription::try_from(Tokenizer::tokenize(sdp).unwrap().1).unwrap();
526        assert_eq!(parsed_sdp.to_string(), sdp);
527    }
528
529    #[test]
530    fn display2() {
531        let sdp = concat!(
532            "v=0\r\n",
533            "o=Alice 2890844526 2890842807 IN IP4 10.47.16.5\r\n",
534            "s=-\r\n",
535            "t=2854678930 2854679000\r\n",
536            "m=audio 49170 RTP/AVP 0\r\n",
537            "c=IN IP4 10.47.16.6\r\n",
538            "b=AS:551\r\n",
539        );
540
541        let parsed_sdp = SessionDescription::try_from(Tokenizer::tokenize(sdp).unwrap().1).unwrap();
542        assert_eq!(parsed_sdp.to_string(), sdp);
543    }
544
545    #[test]
546    fn display3() {
547        let sdp = concat!(
548            "v=0\r\n",
549            "o=Alice 2890844526 2890842807 IN IP4 10.47.16.5\r\n",
550            "s=-\r\n",
551            "t=2854678930 2854679000\r\n",
552        );
553
554        let parsed_sdp = SessionDescription::try_from(Tokenizer::tokenize(sdp).unwrap().1).unwrap();
555        assert_eq!(parsed_sdp.to_string(), sdp);
556    }
557
558    #[test]
559    fn errors1() {
560        let sdp = concat!(
561            "v=0\r\n",
562            "o=Alice 2890844526 2890842807 IN IP4 10.47.16.5\r\n",
563            "s=-\r\n",
564            "i=A Seminar on the session description protocol\r\n",
565            "u=http://www.example.com/seminars/sdp.pdf\r\n",
566            "e=alice@example.com (Alice Smith)\r\n",
567            "p=+1 911-345-1160\r\n",
568            "c=IN IP4 10.47.16\r\n",
569            "b=CT:1024\r\n",
570            "t=2854678930 2854679000\r\n",
571            "r=604800 3600 0 90000\r\n",
572            "z=2882844526 -1h 2898848070 0h\r\n",
573            "k=clear:password\r\n",
574            "a=recvonly\r\n",
575            "m=audio 49170 RTP/AVP 0\r\n",
576            "i=audio media\r\n",
577            "c=IN IP4 10.47.16.5\r\n",
578            "c=IN IP4 10.47.16.6\r\n",
579            "b=CT:1000\r\n",
580            "b=AS:551\r\n",
581            "k=prompt\r\n",
582            "a=rtpmap:99 h232-199/90000\r\n",
583            "a=rtpmap:99 h263-1998/90000\r\n"
584        );
585
586        let parsed_sdp = SessionDescription::try_from(sdp);
587        assert!(parsed_sdp.is_err());
588    }
589}