rtc_sdp/description/
media.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use url::Url;
5
6use crate::description::common::*;
7use crate::extmap::*;
8
9/// Constants for extmap key
10pub const EXT_MAP_VALUE_TRANSPORT_CC_KEY: u16 = 3;
11pub const EXT_MAP_VALUE_TRANSPORT_CC_URI: &str =
12    "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01";
13
14fn ext_map_uri() -> HashMap<u16, &'static str> {
15    let mut m = HashMap::new();
16    m.insert(
17        EXT_MAP_VALUE_TRANSPORT_CC_KEY,
18        EXT_MAP_VALUE_TRANSPORT_CC_URI,
19    );
20    m
21}
22
23/// MediaDescription represents a media type.
24///
25/// ## Specifications
26///
27/// * [RFC 4566 §5.14]
28///
29/// [RFC 4566 §5.14]: https://tools.ietf.org/html/rfc4566#section-5.14
30#[derive(Debug, Default, Clone)]
31pub struct MediaDescription {
32    /// `m=<media> <port>/<number of ports> <proto> <fmt> ...`
33    ///
34    /// <https://tools.ietf.org/html/rfc4566#section-5.14>
35    pub media_name: MediaName,
36
37    /// `i=<session description>`
38    ///
39    /// <https://tools.ietf.org/html/rfc4566#section-5.4>
40    pub media_title: Option<Information>,
41
42    /// `c=<nettype> <addrtype> <connection-address>`
43    ///
44    /// <https://tools.ietf.org/html/rfc4566#section-5.7>
45    pub connection_information: Option<ConnectionInformation>,
46
47    /// `b=<bwtype>:<bandwidth>`
48    ///
49    /// <https://tools.ietf.org/html/rfc4566#section-5.8>
50    pub bandwidth: Vec<Bandwidth>,
51
52    /// `k=<method>`
53    ///
54    /// `k=<method>:<encryption key>`
55    ///
56    /// <https://tools.ietf.org/html/rfc4566#section-5.12>
57    pub encryption_key: Option<EncryptionKey>,
58
59    /// Attributes are the primary means for extending SDP.  Attributes may
60    /// be defined to be used as "session-level" attributes, "media-level"
61    /// attributes, or both.
62    ///
63    /// <https://tools.ietf.org/html/rfc4566#section-5.12>
64    pub attributes: Vec<Attribute>,
65}
66
67impl MediaDescription {
68    /// Returns whether an attribute exists
69    pub fn has_attribute(&self, key: &str) -> bool {
70        self.attributes.iter().any(|a| a.key == key)
71    }
72
73    /// attribute returns the value of an attribute and if it exists
74    pub fn attribute(&self, key: &str) -> Option<Option<&str>> {
75        for a in &self.attributes {
76            if a.key == key {
77                return Some(a.value.as_ref().map(|s| s.as_ref()));
78            }
79        }
80        None
81    }
82
83    /// new_jsep_media_description creates a new MediaName with
84    /// some settings that are required by the JSEP spec.
85    pub fn new_jsep_media_description(codec_type: String, _codec_prefs: Vec<&str>) -> Self {
86        MediaDescription {
87            media_name: MediaName {
88                media: codec_type,
89                port: RangedPort {
90                    value: 9,
91                    range: None,
92                },
93                protos: vec![
94                    "UDP".to_string(),
95                    "TLS".to_string(),
96                    "RTP".to_string(),
97                    "SAVPF".to_string(),
98                ],
99                formats: vec![],
100            },
101            media_title: None,
102            connection_information: Some(ConnectionInformation {
103                network_type: "IN".to_string(),
104                address_type: "IP4".to_string(),
105                address: Some(Address {
106                    address: "0.0.0.0".to_string(),
107                    ttl: None,
108                    range: None,
109                }),
110            }),
111            bandwidth: vec![],
112            encryption_key: None,
113            attributes: vec![],
114        }
115    }
116
117    /// with_property_attribute adds a property attribute 'a=key' to the media description
118    pub fn with_property_attribute(mut self, key: String) -> Self {
119        self.attributes.push(Attribute::new(key, None));
120        self
121    }
122
123    /// with_value_attribute adds a value attribute 'a=key:value' to the media description
124    pub fn with_value_attribute(mut self, key: String, value: String) -> Self {
125        self.attributes.push(Attribute::new(key, Some(value)));
126        self
127    }
128
129    /// with_fingerprint adds a fingerprint to the media description
130    pub fn with_fingerprint(self, algorithm: String, value: String) -> Self {
131        self.with_value_attribute("fingerprint".to_owned(), algorithm + " " + &value)
132    }
133
134    /// with_ice_credentials adds ICE credentials to the media description
135    pub fn with_ice_credentials(self, username: String, password: String) -> Self {
136        self.with_value_attribute("ice-ufrag".to_string(), username)
137            .with_value_attribute("ice-pwd".to_string(), password)
138    }
139
140    /// with_codec adds codec information to the media description
141    pub fn with_codec(
142        mut self,
143        payload_type: u8,
144        name: String,
145        clockrate: u32,
146        channels: u16,
147        fmtp: String,
148    ) -> Self {
149        self.media_name.formats.push(payload_type.to_string());
150        let rtpmap = if channels > 0 {
151            format!("{payload_type} {name}/{clockrate}/{channels}")
152        } else {
153            format!("{payload_type} {name}/{clockrate}")
154        };
155
156        if !fmtp.is_empty() {
157            self.with_value_attribute("rtpmap".to_string(), rtpmap)
158                .with_value_attribute("fmtp".to_string(), format!("{payload_type} {fmtp}"))
159        } else {
160            self.with_value_attribute("rtpmap".to_string(), rtpmap)
161        }
162    }
163
164    /// with_media_source adds media source information to the media description
165    pub fn with_media_source(
166        self,
167        ssrc: u32,
168        cname: String,
169        stream_label: String,
170        label: String,
171    ) -> Self {
172        self.
173            with_value_attribute("ssrc".to_string(), format!("{ssrc} cname:{cname}")). // Deprecated but not phased out?
174            with_value_attribute("ssrc".to_string(), format!("{ssrc} msid:{stream_label} {label}")).
175            with_value_attribute("ssrc".to_string(), format!("{ssrc} mslabel:{stream_label}")). // Deprecated but not phased out?
176            with_value_attribute("ssrc".to_string(), format!("{ssrc} label:{label}"))
177        // Deprecated but not phased out?
178    }
179
180    /// with_candidate adds an ICE candidate to the media description
181    /// Deprecated: use WithICECandidate instead
182    pub fn with_candidate(self, value: String) -> Self {
183        self.with_value_attribute("candidate".to_string(), value)
184    }
185
186    pub fn with_extmap(self, e: ExtMap) -> Self {
187        self.with_property_attribute(e.marshal())
188    }
189
190    /// with_transport_cc_extmap adds an extmap to the media description
191    pub fn with_transport_cc_extmap(self) -> Self {
192        let uri = {
193            let m = ext_map_uri();
194            if let Some(uri_str) = m.get(&EXT_MAP_VALUE_TRANSPORT_CC_KEY) {
195                Url::parse(uri_str).ok()
196            } else {
197                None
198            }
199        };
200
201        let e = ExtMap {
202            value: EXT_MAP_VALUE_TRANSPORT_CC_KEY,
203            uri,
204            ..Default::default()
205        };
206
207        self.with_extmap(e)
208    }
209}
210
211/// RangedPort supports special format for the media field "m=" port value. If
212/// it may be necessary to specify multiple transport ports, the protocol allows
213/// to write it as: `<port>/<number of ports>` where number of ports is a an
214/// offsetting range.
215#[derive(Debug, Default, Clone)]
216pub struct RangedPort {
217    pub value: isize,
218    pub range: Option<isize>,
219}
220
221impl fmt::Display for RangedPort {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        if let Some(range) = self.range {
224            write!(f, "{}/{}", self.value, range)
225        } else {
226            write!(f, "{}", self.value)
227        }
228    }
229}
230
231/// MediaName describes the "m=" field storage structure.
232#[derive(Debug, Default, Clone)]
233pub struct MediaName {
234    pub media: String,
235    pub port: RangedPort,
236    pub protos: Vec<String>,
237    pub formats: Vec<String>,
238}
239
240impl fmt::Display for MediaName {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        write!(f, "{} {}", self.media, self.port)?;
243
244        let mut first = true;
245        for part in &self.protos {
246            if first {
247                first = false;
248                write!(f, " {part}")?;
249            } else {
250                write!(f, "/{part}")?;
251            }
252        }
253
254        for part in &self.formats {
255            write!(f, " {part}")?;
256        }
257
258        Ok(())
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::MediaDescription;
265
266    #[test]
267    fn test_attribute_missing() {
268        let media_description = MediaDescription::default();
269
270        assert_eq!(media_description.attribute("recvonly"), None);
271    }
272
273    #[test]
274    fn test_attribute_present_with_no_value() {
275        let media_description =
276            MediaDescription::default().with_property_attribute("recvonly".to_owned());
277
278        assert_eq!(media_description.attribute("recvonly"), Some(None));
279    }
280
281    #[test]
282    fn test_attribute_present_with_value() {
283        let media_description =
284            MediaDescription::default().with_value_attribute("ptime".to_owned(), "1".to_owned());
285
286        assert_eq!(media_description.attribute("ptime"), Some(Some("1")));
287    }
288}