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`).
136fn matches_implementation(entry: &CodecStringEntry, imp: &CodecImplementation) -> bool {
137    if !entry
138        .name()
139        .eq_ignore_ascii_case(imp.name())
140    {
141        return false;
142    }
143
144    if let Some(entry_mod) = entry.modname() {
145        match imp.modname() {
146            Some(imp_mod) if entry_mod.eq_ignore_ascii_case(imp_mod) => {}
147            Some(_) => return false,
148            None => {}
149        }
150    }
151
152    // Video implementations bypass every qualifier comparison
153    // (`switch_loadable_module.c:2855, 2886`); name/modname above is the whole check.
154    if matches!(imp.media_type(), Some(SdpMediaType::Video)) {
155        return true;
156    }
157
158    // G.722 resolves at either advertised rate, through different passes, and one
159    // `rate` field cannot say which — see `docs/codec-string-format.md`.
160    let is_g722 = entry
161        .name()
162        .eq_ignore_ascii_case("g722");
163
164    // Every comparison below is `if (qualifier && …)` in the C, so an explicit `0`
165    // is unconstrained rather than a required zero.
166    if !is_g722 {
167        if let (Some(er), Some(ir)) = (nonzero(entry.rate()), imp.rate()) {
168            if er != ir {
169                return false;
170            }
171        }
172    }
173
174    if let (Some(ep), Some(ip)) = (nonzero(entry.ptime()), imp.ptime()) {
175        if ep != ip {
176            return false;
177        }
178    }
179
180    if let (Some(eb), Some(ib)) = (nonzero(entry.bitrate()), imp.bitrate()) {
181        if eb != ib {
182            return false;
183        }
184    }
185
186    // Channels is seeded at 1, not 0 (`switch_loadable_module.c:2806`), so an absent
187    // `@Nc` constrains to mono and only an explicit `@0c` does not.
188    if let Some(ic) = imp.channels() {
189        let required = entry
190            .channels()
191            .unwrap_or(1);
192        if required != 0 && required != ic {
193            return false;
194        }
195    }
196
197    true
198}
199
200/// Collapse an explicit `0` qualifier to `None` — the matching rule of the three
201/// in `docs/codec-string-format.md`.
202fn nonzero(v: Option<u32>) -> Option<u32> {
203    v.filter(|&n| n != 0)
204}
205
206impl CodecString {
207    /// Remove entries that match no implementation in `implementations`, returning the
208    /// removed entries in their original order.
209    ///
210    /// Ports the drop behaviour of `switch_loadable_module_get_codecs_sorted`
211    /// (`switch_loadable_module.c:2849-2909`): a name/modname miss
212    /// (`switch_loadable_module_get_codec_interface` returning NULL) or a qualifier miss
213    /// against every implementation registered under that name both silently drop the
214    /// entry in FreeSWITCH. A name-only implementation list (all qualifiers `None`) models
215    /// only the first failure; [`CodecString::qualified`] identifies the entries still
216    /// exposed to the second, qualifier-matching failure.
217    pub fn retain_available<'a, I>(&mut self, implementations: I) -> Vec<CodecStringEntry>
218    where
219        I: IntoIterator<Item = &'a CodecImplementation>,
220    {
221        let impls: Vec<&CodecImplementation> = implementations
222            .into_iter()
223            .collect();
224
225        let mut removed = Vec::new();
226        let mut kept = Vec::new();
227        for entry in std::mem::take(self) {
228            if impls
229                .iter()
230                .any(|imp| matches_implementation(&entry, imp))
231            {
232                kept.push(entry);
233            } else {
234                removed.push(entry);
235            }
236        }
237        *self = kept
238            .into_iter()
239            .collect();
240        removed
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    fn parse(s: &str) -> CodecString {
249        s.parse()
250            .unwrap()
251    }
252
253    #[test]
254    fn unavailable_codec_removed_case_insensitive_both_directions() {
255        let mut cs = parse("pcmu@20i,EVS~mode=1,AMR");
256        let impls = vec![
257            CodecImplementation::new("PCMU"),
258            CodecImplementation::new("amr"),
259        ];
260        let removed = cs.retain_available(&impls);
261        assert_eq!(removed.len(), 1);
262        assert_eq!(removed[0].name(), "EVS");
263        assert_eq!(removed[0].fmtp(), Some("mode=1"));
264        assert_eq!(cs.len(), 2);
265        let kept: Vec<&str> = cs
266            .entries()
267            .iter()
268            .map(|e| e.name())
269            .collect();
270        assert_eq!(kept, vec!["pcmu", "AMR"]);
271    }
272
273    #[test]
274    fn amr_ptime_mismatch_is_removed() {
275        // mod_amr.c:690-716 registers both AMR implementations at 20000us (20ms) only.
276        // A peer offering ptime=40 must match nothing and be dropped.
277        let mut cs = parse("AMR@8000h@40i");
278        let impls = vec![CodecImplementation::new("AMR")
279            .with_rate(8000)
280            .with_ptime(20)];
281        let removed = cs.retain_available(&impls);
282        assert_eq!(removed.len(), 1);
283        assert_eq!(removed[0].name(), "AMR");
284        assert!(cs.is_empty());
285    }
286
287    #[test]
288    fn amr_ptime_mismatch_survives_name_only_implementation_list() {
289        // Pins the documented degradation: a name-only list (site (a) coverage only)
290        // cannot catch the qualifier-mismatch failure (site (b)).
291        let mut cs = parse("AMR@8000h@40i");
292        let impls = vec![CodecImplementation::new("AMR")];
293        let removed = cs.retain_available(&impls);
294        assert!(removed.is_empty());
295        assert_eq!(cs.len(), 1);
296    }
297
298    #[test]
299    fn modname_mismatch_is_removed() {
300        let mut cs = CodecString::new();
301        cs.push(
302            CodecStringEntry::new("EVS")
303                .unwrap()
304                .with_module("mod_evs")
305                .unwrap(),
306        )
307        .unwrap();
308        let impls = vec![CodecImplementation::new("EVS").with_modname("mod_other")];
309        let removed = cs.retain_available(&impls);
310        assert_eq!(removed.len(), 1);
311        assert!(cs.is_empty());
312    }
313
314    #[test]
315    fn modname_unknown_on_implementation_does_not_constrain() {
316        let mut cs = CodecString::new();
317        cs.push(
318            CodecStringEntry::new("EVS")
319                .unwrap()
320                .with_module("mod_evs")
321                .unwrap(),
322        )
323        .unwrap();
324        let impls = vec![CodecImplementation::new("EVS")];
325        let removed = cs.retain_available(&impls);
326        assert!(removed.is_empty());
327        assert_eq!(cs.len(), 1);
328    }
329
330    #[test]
331    fn removed_entries_preserve_original_order() {
332        let mut cs = parse("EVS,H264,AMR@8000h@40i");
333        let impls = vec![CodecImplementation::new("PCMU")];
334        let removed = cs.retain_available(&impls);
335        assert_eq!(removed.len(), 3);
336        assert_eq!(removed[0].name(), "EVS");
337        assert_eq!(removed[1].name(), "H264");
338        assert_eq!(removed[2].name(), "AMR");
339        assert!(cs.is_empty());
340    }
341
342    // --- video implementations bypass qualifier checks ---
343
344    #[test]
345    fn video_implementation_matches_regardless_of_rate() {
346        // switch_loadable_module.c:2855/:2886 wrap every qualifier comparison in
347        // `if (imp->codec_type != SWITCH_CODEC_TYPE_VIDEO)`, so name is the whole check.
348        let mut cs = parse("VP8@8000h");
349        let impls = vec![CodecImplementation::new("VP8")
350            .with_media_type(SdpMediaType::Video)
351            .with_rate(90000)];
352        let removed = cs.retain_available(&impls);
353        assert!(removed.is_empty());
354        assert_eq!(cs.len(), 1);
355    }
356
357    // --- G.722's dual rate ---
358
359    #[test]
360    fn g722_matches_at_either_advertised_rate() {
361        // mod_spandsp_codecs.c registers G.722 with samples_per_second = 8000 and
362        // actual_samples_per_second = 16000 -- the opposite of the usual convention.
363        // The first pass compares an explicit rate against actual_samples_per_second
364        // (16000); the second compares against samples_per_second (8000). Both
365        // G722@8000h and G722@16000h therefore resolve, via different passes, and a
366        // single `rate` field on CodecImplementation cannot express that -- so the
367        // rate comparison is skipped entirely for G.722.
368        let mut cs = parse("G722@16000h@20i,G722@8000h@20i");
369        let impls = vec![CodecImplementation::new("G722")
370            .with_rate(8000)
371            .with_ptime(20)];
372        let removed = cs.retain_available(&impls);
373        assert!(removed.is_empty());
374        assert_eq!(cs.len(), 2);
375    }
376
377    // --- explicit zero qualifiers are unconstrained ---
378
379    #[test]
380    fn pcmu_zero_rate_does_not_constrain() {
381        let mut cs = parse("PCMU@0h");
382        let impls = vec![CodecImplementation::new("PCMU").with_rate(8000)];
383        let removed = cs.retain_available(&impls);
384        assert!(removed.is_empty());
385        assert_eq!(cs.len(), 1);
386    }
387
388    #[test]
389    fn pcmu_zero_ptime_does_not_constrain() {
390        let mut cs = parse("PCMU@0i");
391        let impls = vec![CodecImplementation::new("PCMU").with_ptime(20)];
392        let removed = cs.retain_available(&impls);
393        assert!(removed.is_empty());
394        assert_eq!(cs.len(), 1);
395    }
396
397    #[test]
398    fn pcmu_zero_bitrate_does_not_constrain() {
399        let mut cs = parse("PCMU@0b");
400        let impls = vec![CodecImplementation::new("PCMU").with_bitrate(64000)];
401        let removed = cs.retain_available(&impls);
402        assert!(removed.is_empty());
403        assert_eq!(cs.len(), 1);
404    }
405
406    #[test]
407    fn pcmu_zero_channels_does_not_constrain() {
408        let mut cs = parse("PCMU@0c");
409        let impls = vec![CodecImplementation::new("PCMU").with_channels(2)];
410        let removed = cs.retain_available(&impls);
411        assert!(removed.is_empty());
412        assert_eq!(cs.len(), 1);
413    }
414}