1use std::net::{IpAddr, SocketAddr};
8
9use sipx_media::Codec;
10use sipx_media::ice::Gathering;
11use sipx_sdp::Capabilities;
12use sipx_sdp::ice::Credentials as IceCredentials;
13
14use crate::call::token;
15use crate::error::{Error, Result};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum CodecPreference {
25 Pcmu,
27 Pcma,
29 Opus,
31 L16,
33}
34
35impl CodecPreference {
36 #[must_use]
38 pub const fn name(self) -> &'static str {
39 match self {
40 Self::Pcmu => "pcmu",
41 Self::Pcma => "pcma",
42 Self::Opus => "opus",
43 Self::L16 => "l16",
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
50#[non_exhaustive]
51pub enum CodecSelectionError {
52 #[error("at least one codec must be selected")]
54 Empty,
55 #[error("codec `{0}` was selected more than once")]
57 Duplicate(&'static str),
58 #[error("codec `opus` requires a build with the `opus` feature")]
60 OpusUnavailable,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct Codecs {
70 ordered: [Option<CodecPreference>; 4],
71}
72
73impl Default for Codecs {
74 fn default() -> Self {
75 Self::G711
76 }
77}
78
79impl Codecs {
80 pub const G711: Self = Self {
82 ordered: [
83 Some(CodecPreference::Pcmu),
84 Some(CodecPreference::Pcma),
85 None,
86 None,
87 ],
88 };
89
90 #[cfg(feature = "opus")]
92 #[allow(
93 non_upper_case_globals,
94 reason = "preserves the pre-P-9 public spelling"
95 )]
96 pub const Opus: Self = Self {
97 ordered: [
98 Some(CodecPreference::Opus),
99 Some(CodecPreference::Pcmu),
100 Some(CodecPreference::Pcma),
101 None,
102 ],
103 };
104
105 pub const L16: Self = Self {
107 ordered: [Some(CodecPreference::L16), None, None, None],
108 };
109
110 pub fn ordered(
117 preferences: &[CodecPreference],
118 ) -> std::result::Result<Self, CodecSelectionError> {
119 if preferences.is_empty() {
120 return Err(CodecSelectionError::Empty);
121 }
122 let mut ordered = [None; 4];
123 for (slot, preference) in ordered.iter_mut().zip(preferences.iter().copied()) {
124 if preferences
125 .iter()
126 .filter(|candidate| **candidate == preference)
127 .count()
128 > 1
129 {
130 return Err(CodecSelectionError::Duplicate(preference.name()));
131 }
132 if preference == CodecPreference::Opus && !cfg!(feature = "opus") {
133 return Err(CodecSelectionError::OpusUnavailable);
134 }
135 *slot = Some(preference);
136 }
137 Ok(Self { ordered })
139 }
140
141 pub fn preferences(self) -> impl Iterator<Item = CodecPreference> {
143 self.ordered.into_iter().flatten()
144 }
145
146 pub(crate) fn capabilities(self, address: IpAddr, audio_port: u16) -> Capabilities {
148 let mut capabilities = Capabilities::g711(address, audio_port);
149 capabilities.audio_formats.clear();
150 capabilities.rtpmaps.clear();
151 for preference in self.preferences() {
152 match preference {
153 CodecPreference::Pcmu => {
154 capabilities.audio_formats.push("0".to_owned());
155 capabilities
156 .rtpmaps
157 .push(("0".to_owned(), "PCMU/8000".to_owned()));
158 }
159 CodecPreference::Pcma => {
160 capabilities.audio_formats.push("8".to_owned());
161 capabilities
162 .rtpmaps
163 .push(("8".to_owned(), "PCMA/8000".to_owned()));
164 }
165 CodecPreference::Opus => {
166 capabilities.audio_formats.push("111".to_owned());
168 capabilities
169 .rtpmaps
170 .push(("111".to_owned(), "opus/48000/2".to_owned()));
171 }
172 CodecPreference::L16 => {
173 capabilities.audio_formats.push("11".to_owned());
176 capabilities
177 .rtpmaps
178 .push(("11".to_owned(), "L16/44100/1".to_owned()));
179 capabilities.audio_formats.push("96".to_owned());
180 capabilities
181 .rtpmaps
182 .push(("96".to_owned(), "L16/8000/1".to_owned()));
183 }
184 }
185 }
186 capabilities.audio_formats.push("101".to_owned());
187 capabilities
188 .rtpmaps
189 .push(("101".to_owned(), "telephone-event/8000".to_owned()));
190 capabilities
191 }
192
193 pub(crate) fn carries(self, codec: Codec) -> bool {
195 self.preferences().any(|preference| match preference {
196 CodecPreference::Pcmu => codec == Codec::Pcmu,
197 CodecPreference::Pcma => codec == Codec::Pcma,
198 #[cfg(feature = "opus")]
199 CodecPreference::Opus => codec == Codec::Opus,
200 #[cfg(not(feature = "opus"))]
201 CodecPreference::Opus => false,
202 CodecPreference::L16 => codec == Codec::L16,
203 })
204 }
205
206 pub(crate) fn carries_format(self, codec: Codec, clock_rate: u32) -> bool {
208 self.preferences().any(|preference| match preference {
209 CodecPreference::Pcmu => codec == Codec::Pcmu && clock_rate == 8_000,
210 CodecPreference::Pcma => codec == Codec::Pcma && clock_rate == 8_000,
211 #[cfg(feature = "opus")]
212 CodecPreference::Opus => codec == Codec::Opus && clock_rate == 48_000,
213 #[cfg(not(feature = "opus"))]
214 CodecPreference::Opus => false,
215 CodecPreference::L16 => codec == Codec::L16 && matches!(clock_rate, 8_000 | 44_100),
216 })
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub enum IcePolicy {
223 #[default]
225 Disabled,
226 Host,
228 Stun(SocketAddr),
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
234pub enum Keying {
235 #[default]
237 Auto,
238 Plain,
240 Sdes,
242 DtlsSrtp,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum NegotiatedKeying {
252 Plain,
254 Sdes,
256 DtlsSrtp,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
266pub enum MediaProfile {
267 #[default]
269 Standard,
270 BrowserAudio,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
276pub struct MediaPolicy {
277 pub profile: MediaProfile,
279 pub codecs: Codecs,
281 pub ice: IcePolicy,
283 pub keying: Keying,
285}
286
287impl MediaPolicy {
288 #[must_use]
294 pub const fn browser_audio() -> Self {
295 #[cfg(feature = "opus")]
296 let codecs = Codecs::Opus;
297 #[cfg(not(feature = "opus"))]
298 let codecs = Codecs::G711;
299 Self {
300 profile: MediaProfile::BrowserAudio,
301 codecs,
302 ice: IcePolicy::Host,
303 keying: Keying::DtlsSrtp,
304 }
305 }
306
307 #[must_use]
312 pub const fn with_profile(mut self, profile: MediaProfile) -> Self {
313 self.profile = profile;
314 self
315 }
316
317 #[must_use]
319 pub const fn with_codecs(mut self, codecs: Codecs) -> Self {
320 self.codecs = codecs;
321 self
322 }
323
324 #[must_use]
326 pub const fn with_ice(mut self, ice: IcePolicy) -> Self {
327 self.ice = ice;
328 self
329 }
330
331 #[must_use]
333 pub const fn with_keying(mut self, keying: Keying) -> Self {
334 self.keying = keying;
335 self
336 }
337
338 pub(crate) fn gathering(self, offerer: bool) -> Result<Option<Gathering>> {
340 if self.ice == IcePolicy::Disabled {
341 return Ok(None);
342 }
343 let credentials = IceCredentials::new(token(), format!("{}{}", token(), token()))
344 .ok_or_else(|| Error::Sdp("could not generate valid ICE credentials".to_owned()))?;
345 let mut gathering = Gathering::new(credentials, offerer);
346 if let IcePolicy::Stun(server) = self.ice {
347 gathering.stun_server = Some(server);
348 }
349 Ok(Some(gathering))
350 }
351}
352
353#[cfg(test)]
354#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn the_default_remains_the_g711_pair_in_wire_order() {
360 assert_eq!(Codecs::default(), Codecs::G711);
361 assert_eq!(
362 Codecs::default().preferences().collect::<Vec<_>>(),
363 vec![CodecPreference::Pcmu, CodecPreference::Pcma]
364 );
365 let capabilities = Codecs::default().capabilities("192.0.2.9".parse().unwrap(), 40_000);
366 assert_eq!(capabilities.audio_formats, ["0", "8", "101"]);
367 }
368
369 #[test]
370 fn an_explicit_order_is_the_order_put_in_an_offer() {
371 let codecs = Codecs::ordered(&[CodecPreference::Pcma, CodecPreference::Pcmu]).unwrap();
372 let capabilities = codecs.capabilities("192.0.2.9".parse().unwrap(), 40_000);
373 assert_eq!(capabilities.audio_formats, ["8", "0", "101"]);
374 assert!(codecs.carries(Codec::Pcma));
375 assert!(codecs.carries(Codec::Pcmu));
376 }
377
378 #[test]
381 fn l16_offers_the_static_and_dynamic_mono_formats() {
382 let codecs = Codecs::ordered(&[CodecPreference::L16]).unwrap();
383 let capabilities = codecs.capabilities("192.0.2.9".parse().unwrap(), 40_000);
384 assert_eq!(capabilities.audio_formats, ["11", "96", "101"]);
385 assert!(
386 capabilities
387 .rtpmaps
388 .contains(&("11".to_owned(), "L16/44100/1".to_owned()))
389 );
390 assert!(
391 capabilities
392 .rtpmaps
393 .contains(&("96".to_owned(), "L16/8000/1".to_owned()))
394 );
395 assert!(codecs.carries(Codec::L16));
396 }
397
398 #[test]
399 fn an_empty_or_duplicate_selection_is_refused() {
400 assert_eq!(Codecs::ordered(&[]), Err(CodecSelectionError::Empty));
401 assert_eq!(
402 Codecs::ordered(&[CodecPreference::Pcmu, CodecPreference::Pcmu]),
403 Err(CodecSelectionError::Duplicate("pcmu"))
404 );
405 }
406
407 #[cfg(not(feature = "opus"))]
408 #[test]
409 fn opus_is_a_known_but_unavailable_value_without_the_feature() {
410 assert_eq!(
411 Codecs::ordered(&[CodecPreference::Opus]),
412 Err(CodecSelectionError::OpusUnavailable)
413 );
414 }
415
416 #[cfg(feature = "opus")]
417 #[test]
418 fn opus_can_be_placed_anywhere_in_the_order() {
419 let codecs = Codecs::ordered(&[
420 CodecPreference::Pcma,
421 CodecPreference::Opus,
422 CodecPreference::Pcmu,
423 ])
424 .unwrap();
425 let capabilities = codecs.capabilities("192.0.2.9".parse().unwrap(), 40_000);
426 assert_eq!(capabilities.audio_formats, ["8", "111", "0", "101"]);
427 assert!(codecs.carries(Codec::Opus));
428 }
429
430 #[test]
431 fn keying_default_and_explicit_values_are_distinct() {
432 assert_eq!(Keying::default(), Keying::Auto);
433 assert_ne!(Keying::Plain, Keying::Sdes);
434 assert_ne!(Keying::Auto, Keying::Sdes);
435 assert_ne!(Keying::DtlsSrtp, Keying::Plain);
436 }
437}