Skip to main content

rtc_sdp/util/
mod.rs

1#[cfg(test)]
2mod util_test;
3
4use shared::error::{Error, Result};
5
6use std::collections::HashMap;
7use std::fmt;
8
9/// The `a=` line prefix.
10pub const ATTRIBUTE_KEY: &str = "a=";
11
12/// ConnectionRole indicates which of the end points should initiate the connection establishment
13#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
14pub enum ConnectionRole {
15    #[default]
16    /// No `a=setup` attribute was present.
17    Unspecified,
18
19    /// ConnectionRoleActive indicates the endpoint will initiate an outgoing connection.
20    Active,
21
22    /// ConnectionRolePassive indicates the endpoint will accept an incoming connection.
23    Passive,
24
25    /// ConnectionRoleActpass indicates the endpoint is willing to accept an incoming connection or to initiate an outgoing connection.
26    Actpass,
27
28    /// ConnectionRoleHoldconn indicates the endpoint does not want the connection to be established for the time being.
29    Holdconn,
30}
31
32const CONNECTION_ROLE_ACTIVE_STR: &str = "active";
33const CONNECTION_ROLE_PASSIVE_STR: &str = "passive";
34const CONNECTION_ROLE_ACTPASS_STR: &str = "actpass";
35const CONNECTION_ROLE_HOLDCONN_STR: &str = "holdconn";
36
37impl fmt::Display for ConnectionRole {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        let s = match self {
40            ConnectionRole::Active => CONNECTION_ROLE_ACTIVE_STR,
41            ConnectionRole::Passive => CONNECTION_ROLE_PASSIVE_STR,
42            ConnectionRole::Actpass => CONNECTION_ROLE_ACTPASS_STR,
43            ConnectionRole::Holdconn => CONNECTION_ROLE_HOLDCONN_STR,
44            _ => "Unspecified",
45        };
46        write!(f, "{s}")
47    }
48}
49
50impl From<u8> for ConnectionRole {
51    fn from(v: u8) -> Self {
52        match v {
53            1 => ConnectionRole::Active,
54            2 => ConnectionRole::Passive,
55            3 => ConnectionRole::Actpass,
56            4 => ConnectionRole::Holdconn,
57            _ => ConnectionRole::Unspecified,
58        }
59    }
60}
61
62impl From<&str> for ConnectionRole {
63    fn from(raw: &str) -> Self {
64        match raw {
65            CONNECTION_ROLE_ACTIVE_STR => ConnectionRole::Active,
66            CONNECTION_ROLE_PASSIVE_STR => ConnectionRole::Passive,
67            CONNECTION_ROLE_ACTPASS_STR => ConnectionRole::Actpass,
68            CONNECTION_ROLE_HOLDCONN_STR => ConnectionRole::Holdconn,
69            _ => ConnectionRole::Unspecified,
70        }
71    }
72}
73
74/// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-26#section-5.2.1
75/// Session ID is recommended to be constructed by generating a 64-bit
76/// quantity with the highest bit set to zero and the remaining 63-bits
77/// being cryptographically random.
78pub(crate) fn new_session_id() -> u64 {
79    let c = u64::MAX ^ (1u64 << 63);
80    rand::random::<u64>() & c
81}
82
83// Codec represents a codec
84#[derive(Debug, Clone, Default, PartialEq, Eq)]
85/// One codec offered by a media section, assembled from its `a=rtpmap`, `a=fmtp` and
86/// `a=rtcp-fb` attributes.
87pub struct Codec {
88    /// The RTP payload type that identifies this codec in the stream.
89    pub payload_type: u8,
90    /// The encoding name, such as `VP8` or `opus`.
91    pub name: String,
92    /// The RTP clock rate in Hz.
93    pub clock_rate: u32,
94    /// Codec-specific encoding parameters — the channel count, for audio.
95    pub encoding_parameters: String,
96    /// The `a=fmtp` format parameters, verbatim.
97    pub fmtp: String,
98    /// The `a=rtcp-fb` feedback types negotiated for this codec, such as `nack` or `goog-remb`.
99    pub rtcp_feedback: Vec<String>,
100}
101
102impl fmt::Display for Codec {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(
105            f,
106            "{} {}/{}/{} ({}) [",
107            self.payload_type, self.name, self.clock_rate, self.encoding_parameters, self.fmtp,
108        )?;
109
110        let mut first = true;
111        for part in &self.rtcp_feedback {
112            if first {
113                first = false;
114                write!(f, "{part}")?;
115            } else {
116                write!(f, ", {part}")?;
117            }
118        }
119
120        write!(f, "]")
121    }
122}
123
124pub(crate) fn parse_rtpmap(rtpmap: &str) -> Result<Codec> {
125    // a=rtpmap:<payload type> <encoding name>/<clock rate>[/<encoding parameters>]
126    let split: Vec<&str> = rtpmap.split_whitespace().collect();
127    if split.len() != 2 {
128        return Err(Error::MissingWhitespace);
129    }
130
131    let pt_split: Vec<&str> = split[0].split(':').collect();
132    if pt_split.len() != 2 {
133        return Err(Error::MissingColon);
134    }
135    let payload_type = pt_split[1].parse::<u8>()?;
136
137    let split: Vec<&str> = split[1].split('/').collect();
138    let name = split[0].to_string();
139    let parts = split.len();
140    let clock_rate = if parts > 1 {
141        split[1].parse::<u32>()?
142    } else {
143        0
144    };
145    let encoding_parameters = if parts > 2 {
146        split[2].to_string()
147    } else {
148        "".to_string()
149    };
150
151    Ok(Codec {
152        payload_type,
153        name,
154        clock_rate,
155        encoding_parameters,
156        ..Default::default()
157    })
158}
159
160pub(crate) fn parse_fmtp(fmtp: &str) -> Result<Codec> {
161    // a=fmtp:<format> <format specific parameters>
162    let split: Vec<&str> = fmtp.split_whitespace().collect();
163    if split.len() != 2 {
164        return Err(Error::MissingWhitespace);
165    }
166
167    let fmtp = split[1].to_string();
168
169    let split: Vec<&str> = split[0].split(':').collect();
170    if split.len() != 2 {
171        return Err(Error::MissingColon);
172    }
173    let payload_type = split[1].parse::<u8>()?;
174
175    Ok(Codec {
176        payload_type,
177        fmtp,
178        ..Default::default()
179    })
180}
181
182pub(crate) fn parse_rtcp_fb(rtcp_fb: &str) -> Result<Codec> {
183    // a=ftcp-fb:<payload type> <RTCP feedback type> [<RTCP feedback parameter>]
184    let split: Vec<&str> = rtcp_fb.splitn(2, ' ').collect();
185    if split.len() != 2 {
186        return Err(Error::MissingWhitespace);
187    }
188
189    let pt_split: Vec<&str> = split[0].split(':').collect();
190    if pt_split.len() != 2 {
191        return Err(Error::MissingColon);
192    }
193
194    Ok(Codec {
195        payload_type: pt_split[1].parse::<u8>()?,
196        rtcp_feedback: vec![split[1].to_string()],
197        ..Default::default()
198    })
199}
200
201pub(crate) fn merge_codecs(mut codec: Codec, codecs: &mut HashMap<u8, Codec>) {
202    if let Some(saved_codec) = codecs.get_mut(&codec.payload_type) {
203        if saved_codec.payload_type == 0 {
204            saved_codec.payload_type = codec.payload_type
205        }
206        if saved_codec.name.is_empty() {
207            saved_codec.name = codec.name
208        }
209        if saved_codec.clock_rate == 0 {
210            saved_codec.clock_rate = codec.clock_rate
211        }
212        if saved_codec.encoding_parameters.is_empty() {
213            saved_codec.encoding_parameters = codec.encoding_parameters
214        }
215        if saved_codec.fmtp.is_empty() {
216            saved_codec.fmtp = codec.fmtp
217        }
218        saved_codec.rtcp_feedback.append(&mut codec.rtcp_feedback);
219    } else {
220        codecs.insert(codec.payload_type, codec);
221    }
222}
223
224fn equivalent_fmtp(want: &str, got: &str) -> bool {
225    let mut want_split: Vec<&str> = want.split(';').collect();
226    let mut got_split: Vec<&str> = got.split(';').collect();
227
228    if want_split.len() != got_split.len() {
229        return false;
230    }
231
232    want_split.sort_unstable();
233    got_split.sort_unstable();
234
235    for (i, &want_part) in want_split.iter().enumerate() {
236        let want_part = want_part.trim();
237        let got_part = got_split[i].trim();
238        if got_part != want_part {
239            return false;
240        }
241    }
242
243    true
244}
245
246pub(crate) fn codecs_match(wanted: &Codec, got: &Codec) -> bool {
247    if !wanted.name.is_empty() && wanted.name.to_lowercase() != got.name.to_lowercase() {
248        return false;
249    }
250    if wanted.clock_rate != 0 && wanted.clock_rate != got.clock_rate {
251        return false;
252    }
253    if !wanted.encoding_parameters.is_empty()
254        && wanted.encoding_parameters != got.encoding_parameters
255    {
256        return false;
257    }
258    if !wanted.fmtp.is_empty() && !equivalent_fmtp(&wanted.fmtp, &got.fmtp) {
259        return false;
260    }
261
262    true
263}