Skip to main content

freeswitch_types/sdp/
available.rs

1//! Which codec-string entries a loaded FreeSWITCH survives implementation matching.
2//!
3//! Line numbers in this module index FreeSWITCH `v1.11.1`
4//! (`c2c59645f6911a76589e5008c4d73349ded44b65`).
5//!
6//! `switch_loadable_module_get_codecs_sorted` (`switch_loadable_module.c:2796-2929`) drops
7//! a codec-string entry in two unlogged places: no codec interface registered under that
8//! name/modname at all (`:2547-2572`, `:2849`), or an interface exists but no implementation
9//! matches an explicit qualifier (`:2851-2909`). Neither is discoverable from an ESL
10//! connection — no API exposes the loaded implementation table — so the caller supplies
11//! what it knows via [`CodecImplementation`] and [`CodecString::retain_available`] reports
12//! what would be silently dropped.
13
14use crate::sdp::codec::SdpMediaType;
15use crate::sdp::codec_string::{CodecString, CodecStringEntry};
16
17/// One loaded codec implementation, mirroring `switch_codec_implementation_t`.
18///
19/// Every qualifier is `Option`; `None` means "unknown, do not constrain" rather than
20/// "codec has no such value". A caller who only knows codec names (not the loaded
21/// implementation table) can construct one of these per name with all qualifiers
22/// `None` — this still catches an entry naming a codec that is not loaded at all
23/// (the module-lookup failure), it just cannot catch a qualifier mismatch against an
24/// implementation that *is* loaded (the second, ptime/rate-specific failure).
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct CodecImplementation {
27    name: String,
28    modname: Option<String>,
29    media_type: Option<SdpMediaType>,
30    rate: Option<u32>,
31    ptime: Option<u32>,
32    bitrate: Option<u32>,
33    channels: Option<u32>,
34}
35
36impl CodecImplementation {
37    /// Create a new implementation descriptor with only the codec name known.
38    ///
39    /// `None` media type means "unknown, treat as audio" — the qualifier checks
40    /// apply. Set it with [`with_media_type`](Self::with_media_type) for a video
41    /// codec, whose implementations bypass every qualifier check in FreeSWITCH.
42    pub fn new(name: impl Into<String>) -> Self {
43        Self {
44            name: name.into(),
45            modname: None,
46            media_type: None,
47            rate: None,
48            ptime: None,
49            bitrate: None,
50            channels: None,
51        }
52    }
53
54    /// Set the media type (`switch_codec_implementation_t::codec_type`).
55    ///
56    /// A [`SdpMediaType::Video`] implementation matches on name (and modname)
57    /// alone — `switch_loadable_module_get_codecs_sorted` wraps every qualifier
58    /// comparison in `if (imp->codec_type != SWITCH_CODEC_TYPE_VIDEO)`
59    /// (`switch_loadable_module.c:2855, 2886`).
60    pub fn with_media_type(mut self, media_type: SdpMediaType) -> Self {
61        self.media_type = Some(media_type);
62        self
63    }
64
65    /// Set the module name (`switch_codec_implementation_t::modname`).
66    pub fn with_modname(mut self, modname: impl Into<String>) -> Self {
67        self.modname = Some(modname.into());
68        self
69    }
70
71    /// Set the clock rate this implementation matches against (`actual_samples_per_second`,
72    /// or `samples_per_second` for G.722's RFC 3551 quirk).
73    pub fn with_rate(mut self, rate: u32) -> Self {
74        self.rate = Some(rate);
75        self
76    }
77
78    /// Set the packetization interval in milliseconds (`microseconds_per_packet / 1000`).
79    pub fn with_ptime(mut self, ptime: u32) -> Self {
80        self.ptime = Some(ptime);
81        self
82    }
83
84    /// Set the bitrate in bits/s (`bits_per_second`).
85    pub fn with_bitrate(mut self, bitrate: u32) -> Self {
86        self.bitrate = Some(bitrate);
87        self
88    }
89
90    /// Set the channel count (`number_of_channels`).
91    pub fn with_channels(mut self, channels: u32) -> Self {
92        self.channels = Some(channels);
93        self
94    }
95
96    /// The codec name (`iananame`).
97    pub fn name(&self) -> &str {
98        &self.name
99    }
100
101    /// The module name, if known.
102    pub fn modname(&self) -> Option<&str> {
103        self.modname
104            .as_deref()
105    }
106
107    /// The media type, if known.
108    pub fn media_type(&self) -> Option<&SdpMediaType> {
109        self.media_type
110            .as_ref()
111    }
112
113    /// The clock rate, if known.
114    pub fn rate(&self) -> Option<u32> {
115        self.rate
116    }
117
118    /// The packetization interval in milliseconds, if known.
119    pub fn ptime(&self) -> Option<u32> {
120        self.ptime
121    }
122
123    /// The bitrate in bits/s, if known.
124    pub fn bitrate(&self) -> Option<u32> {
125        self.bitrate
126    }
127
128    /// The channel count, if known.
129    pub fn channels(&self) -> Option<u32> {
130        self.channels
131    }
132}
133
134/// `true` if `entry` matches `imp` under the same rules as the second matching pass
135/// in `switch_loadable_module_get_codecs_sorted` (`switch_loadable_module.c:2885-2909`).
136// qual:allow(complexity, max_cyclomatic=16) reason: "mirrors the C matching pass branch for branch"
137fn matches_implementation(entry: &CodecStringEntry, imp: &CodecImplementation) -> bool {
138    if !entry
139        .name()
140        .eq_ignore_ascii_case(imp.name())
141    {
142        return false;
143    }
144
145    if let Some(entry_mod) = entry.modname() {
146        match imp.modname() {
147            Some(imp_mod) if entry_mod.eq_ignore_ascii_case(imp_mod) => {}
148            Some(_) => return false,
149            None => {}
150        }
151    }
152
153    // Video implementations bypass every qualifier comparison
154    // (`switch_loadable_module.c:2855, 2886`); name/modname above is the whole check.
155    if matches!(imp.media_type(), Some(SdpMediaType::Video)) {
156        return true;
157    }
158
159    // G.722 resolves at either advertised rate, through different passes, and one
160    // `rate` field cannot say which — see `docs/codec-string-format.md`.
161    let is_g722 = entry
162        .name()
163        .eq_ignore_ascii_case("g722");
164
165    // Every comparison below is `if (qualifier && …)` in the C, so an explicit `0`
166    // is unconstrained rather than a required zero.
167    if !is_g722 {
168        if let (Some(er), Some(ir)) = (nonzero(entry.rate()), imp.rate()) {
169            if er != ir {
170                return false;
171            }
172        }
173    }
174
175    if let (Some(ep), Some(ip)) = (nonzero(entry.ptime()), imp.ptime()) {
176        if ep != ip {
177            return false;
178        }
179    }
180
181    if let (Some(eb), Some(ib)) = (nonzero(entry.bitrate()), imp.bitrate()) {
182        if eb != ib {
183            return false;
184        }
185    }
186
187    // Channels is seeded at 1, not 0 (`switch_loadable_module.c:2806`), so an absent
188    // `@Nc` constrains to mono and only an explicit `@0c` does not.
189    if let Some(ic) = imp.channels() {
190        let required = entry
191            .channels()
192            .unwrap_or(1);
193        if required != 0 && required != ic {
194            return false;
195        }
196    }
197
198    true
199}
200
201/// Collapse an explicit `0` qualifier to `None` — the matching rule of the three
202/// in `docs/codec-string-format.md`.
203fn nonzero(v: Option<u32>) -> Option<u32> {
204    v.filter(|&n| n != 0)
205}
206
207impl CodecString {
208    /// Remove entries that match no implementation in `implementations`, returning the
209    /// removed entries in their original order.
210    ///
211    /// Ports the drop behaviour of `switch_loadable_module_get_codecs_sorted`
212    /// (`switch_loadable_module.c:2849-2909`): a name/modname miss
213    /// (`switch_loadable_module_get_codec_interface` returning NULL) or a qualifier miss
214    /// against every implementation registered under that name both silently drop the
215    /// entry in FreeSWITCH. A name-only implementation list (all qualifiers `None`) models
216    /// only the first failure; [`CodecString::qualified`] identifies the entries still
217    /// exposed to the second, qualifier-matching failure.
218    pub fn retain_available<'a, I>(&mut self, implementations: I) -> Vec<CodecStringEntry>
219    where
220        I: IntoIterator<Item = &'a CodecImplementation>,
221    {
222        let impls: Vec<&CodecImplementation> = implementations
223            .into_iter()
224            .collect();
225
226        let mut removed = Vec::new();
227        let mut kept = Vec::new();
228        for entry in std::mem::take(self) {
229            if impls
230                .iter()
231                .any(|imp| matches_implementation(&entry, imp))
232            {
233                kept.push(entry);
234            } else {
235                removed.push(entry);
236            }
237        }
238        *self = kept
239            .into_iter()
240            .collect();
241        removed
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    fn parse(s: &str) -> CodecString {
250        s.parse()
251            .unwrap()
252    }
253
254    #[test]
255    fn unavailable_codec_removed_case_insensitive_both_directions() {
256        let mut cs = parse("pcmu@20i,EVS~mode=1,AMR");
257        let impls = vec![
258            CodecImplementation::new("PCMU"),
259            CodecImplementation::new("amr"),
260        ];
261        let removed = cs.retain_available(&impls);
262        assert_eq!(removed.len(), 1);
263        assert_eq!(removed[0].name(), "EVS");
264        assert_eq!(removed[0].fmtp(), Some("mode=1"));
265        assert_eq!(cs.len(), 2);
266        let kept: Vec<&str> = cs
267            .entries()
268            .iter()
269            .map(|e| e.name())
270            .collect();
271        assert_eq!(kept, vec!["pcmu", "AMR"]);
272    }
273
274    #[test]
275    fn amr_ptime_mismatch_is_removed() {
276        // mod_amr.c:690-716 registers both AMR implementations at 20000us (20ms) only.
277        // A peer offering ptime=40 must match nothing and be dropped.
278        let mut cs = parse("AMR@8000h@40i");
279        let impls = vec![CodecImplementation::new("AMR")
280            .with_rate(8000)
281            .with_ptime(20)];
282        let removed = cs.retain_available(&impls);
283        assert_eq!(removed.len(), 1);
284        assert_eq!(removed[0].name(), "AMR");
285        assert!(cs.is_empty());
286    }
287
288    #[test]
289    fn amr_ptime_mismatch_survives_name_only_implementation_list() {
290        // Pins the documented degradation: a name-only list (site (a) coverage only)
291        // cannot catch the qualifier-mismatch failure (site (b)).
292        let mut cs = parse("AMR@8000h@40i");
293        let impls = vec![CodecImplementation::new("AMR")];
294        let removed = cs.retain_available(&impls);
295        assert!(removed.is_empty());
296        assert_eq!(cs.len(), 1);
297    }
298
299    #[test]
300    fn modname_mismatch_is_removed() {
301        let mut cs = CodecString::new();
302        cs.push(
303            CodecStringEntry::new("EVS")
304                .unwrap()
305                .with_module("mod_evs")
306                .unwrap(),
307        )
308        .unwrap();
309        let impls = vec![CodecImplementation::new("EVS").with_modname("mod_other")];
310        let removed = cs.retain_available(&impls);
311        assert_eq!(removed.len(), 1);
312        assert!(cs.is_empty());
313    }
314
315    #[test]
316    fn modname_unknown_on_implementation_does_not_constrain() {
317        let mut cs = CodecString::new();
318        cs.push(
319            CodecStringEntry::new("EVS")
320                .unwrap()
321                .with_module("mod_evs")
322                .unwrap(),
323        )
324        .unwrap();
325        let impls = vec![CodecImplementation::new("EVS")];
326        let removed = cs.retain_available(&impls);
327        assert!(removed.is_empty());
328        assert_eq!(cs.len(), 1);
329    }
330
331    #[test]
332    fn removed_entries_preserve_original_order() {
333        let mut cs = parse("EVS,H264,AMR@8000h@40i");
334        let impls = vec![CodecImplementation::new("PCMU")];
335        let removed = cs.retain_available(&impls);
336        assert_eq!(removed.len(), 3);
337        assert_eq!(removed[0].name(), "EVS");
338        assert_eq!(removed[1].name(), "H264");
339        assert_eq!(removed[2].name(), "AMR");
340        assert!(cs.is_empty());
341    }
342
343    // --- video implementations bypass qualifier checks ---
344
345    #[test]
346    fn video_implementation_matches_regardless_of_rate() {
347        // switch_loadable_module.c:2855/:2886 wrap every qualifier comparison in
348        // `if (imp->codec_type != SWITCH_CODEC_TYPE_VIDEO)`, so name is the whole check.
349        let mut cs = parse("VP8@8000h");
350        let impls = vec![CodecImplementation::new("VP8")
351            .with_media_type(SdpMediaType::Video)
352            .with_rate(90000)];
353        let removed = cs.retain_available(&impls);
354        assert!(removed.is_empty());
355        assert_eq!(cs.len(), 1);
356    }
357
358    // --- G.722's dual rate ---
359
360    #[test]
361    fn g722_matches_at_either_advertised_rate() {
362        // mod_spandsp_codecs.c registers G.722 with samples_per_second = 8000 and
363        // actual_samples_per_second = 16000 -- the opposite of the usual convention.
364        // The first pass compares an explicit rate against actual_samples_per_second
365        // (16000); the second compares against samples_per_second (8000). Both
366        // G722@8000h and G722@16000h therefore resolve, via different passes, and a
367        // single `rate` field on CodecImplementation cannot express that -- so the
368        // rate comparison is skipped entirely for G.722.
369        let mut cs = parse("G722@16000h@20i,G722@8000h@20i");
370        let impls = vec![CodecImplementation::new("G722")
371            .with_rate(8000)
372            .with_ptime(20)];
373        let removed = cs.retain_available(&impls);
374        assert!(removed.is_empty());
375        assert_eq!(cs.len(), 2);
376    }
377
378    // --- explicit zero qualifiers are unconstrained ---
379
380    #[test]
381    fn pcmu_zero_rate_does_not_constrain() {
382        let mut cs = parse("PCMU@0h");
383        let impls = vec![CodecImplementation::new("PCMU").with_rate(8000)];
384        let removed = cs.retain_available(&impls);
385        assert!(removed.is_empty());
386        assert_eq!(cs.len(), 1);
387    }
388
389    #[test]
390    fn pcmu_zero_ptime_does_not_constrain() {
391        let mut cs = parse("PCMU@0i");
392        let impls = vec![CodecImplementation::new("PCMU").with_ptime(20)];
393        let removed = cs.retain_available(&impls);
394        assert!(removed.is_empty());
395        assert_eq!(cs.len(), 1);
396    }
397
398    #[test]
399    fn pcmu_zero_bitrate_does_not_constrain() {
400        let mut cs = parse("PCMU@0b");
401        let impls = vec![CodecImplementation::new("PCMU").with_bitrate(64000)];
402        let removed = cs.retain_available(&impls);
403        assert!(removed.is_empty());
404        assert_eq!(cs.len(), 1);
405    }
406
407    #[test]
408    fn pcmu_zero_channels_does_not_constrain() {
409        let mut cs = parse("PCMU@0c");
410        let impls = vec![CodecImplementation::new("PCMU").with_channels(2)];
411        let removed = cs.retain_available(&impls);
412        assert!(removed.is_empty());
413        assert_eq!(cs.len(), 1);
414    }
415}