Skip to main content

rtc_sdp/description/
media.rs

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