Skip to main content

freeswitch_types/sdp/
options.rs

1//! Options controlling which qualifiers are emitted when converting an [`SdpCodec`]
2//! to a [`CodecStringEntry`].
3//!
4//! Line numbers in this module index FreeSWITCH `v1.11.1`
5//! (`c2c59645f6911a76589e5008c4d73349ded44b65`).
6
7use crate::sdp::{codec::SdpCodec, codec_string::CodecStringEntry, error::CodecStringError};
8
9/// Controls which qualifiers appear in a [`CodecStringEntry`] built from an [`SdpCodec`].
10///
11/// Audio defaults emit rate, ptime, bitrate, and channels but **not** fmtp (see
12/// `docs/codec-string-format.md` — audio fmtp in the codec string does not reach a
13/// generated offer unless the leg has a bridged partner). Video entries emit only name
14/// and fmtp, no numeric qualifiers.
15///
16/// This deliberately deviates from FreeSWITCH's own `add_audio_codec`, which emits
17/// `@<n>c` only when channels > 1 and never emits `@<n>b` and `@<n>c` together — the two
18/// share one format buffer, so one overwrites the other (`switch_core_media.c:13530-13536`,
19/// see `docs/codec-string-format.md`). Audio defaults here emit `@1c` on every mono codec
20/// and can emit both qualifiers at once. This is behaviourally neutral for matching and
21/// dedup — an absent channel count normalizes to `1` either way — but it is not a
22/// byte-for-byte port of upstream's emission quirk.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct CodecStringOptions {
25    emit_rate: bool,
26    /// Off with a rate on, G.722 selects whichever implementation is first rather
27    /// than the 20 ms one.
28    emit_ptime: bool,
29    emit_bitrate: bool,
30    emit_channels: bool,
31    /// Off for audio: without a bridged partner at INVITE time it never reaches the
32    /// generated offer.
33    emit_fmtp: bool,
34}
35
36impl Default for CodecStringOptions {
37    /// Audio-mode defaults: rate, ptime, bitrate, channels on; fmtp off.
38    fn default() -> Self {
39        Self {
40            emit_rate: true,
41            emit_ptime: true,
42            emit_bitrate: true,
43            emit_channels: true,
44            emit_fmtp: false,
45        }
46    }
47}
48
49impl CodecStringOptions {
50    /// Default options for audio: rate, ptime, bitrate, channels on; fmtp off.
51    pub fn audio() -> Self {
52        Self::default()
53    }
54
55    /// Options for video: only name and fmtp; no numeric qualifiers.
56    ///
57    /// Matches FreeSWITCH's video path in `add_audio_codec`.
58    pub fn video() -> Self {
59        Self {
60            emit_rate: false,
61            emit_ptime: false,
62            emit_bitrate: false,
63            emit_channels: false,
64            emit_fmtp: true,
65        }
66    }
67
68    /// Emit or suppress the rate qualifier (`@<n>h`).
69    pub fn with_rate(mut self, emit: bool) -> Self {
70        self.emit_rate = emit;
71        self
72    }
73
74    /// Emit or suppress the ptime qualifier (`@<n>i`).
75    ///
76    /// # Warning
77    ///
78    /// For G.722, suppressing ptime while keeping rate causes FreeSWITCH to select
79    /// whichever implementation is first in its list rather than the 20 ms default,
80    /// potentially producing non-standard packetization. Leave this on.
81    pub fn with_ptime(mut self, emit: bool) -> Self {
82        self.emit_ptime = emit;
83        self
84    }
85
86    /// Emit or suppress the bitrate qualifier (`@<n>b`).
87    pub fn with_bitrate(mut self, emit: bool) -> Self {
88        self.emit_bitrate = emit;
89        self
90    }
91
92    /// Emit or suppress the channel count qualifier (`@<n>c`).
93    pub fn with_channels(mut self, emit: bool) -> Self {
94        self.emit_channels = emit;
95        self
96    }
97
98    /// Emit or suppress the fmtp (`~fmtp` segment).
99    pub fn with_fmtp(mut self, emit: bool) -> Self {
100        self.emit_fmtp = emit;
101        self
102    }
103
104    /// Whether rate will be emitted.
105    pub fn emits_rate(&self) -> bool {
106        self.emit_rate
107    }
108
109    /// Whether ptime will be emitted.
110    pub fn emits_ptime(&self) -> bool {
111        self.emit_ptime
112    }
113
114    /// Whether bitrate will be emitted.
115    pub fn emits_bitrate(&self) -> bool {
116        self.emit_bitrate
117    }
118
119    /// Whether channels will be emitted.
120    pub fn emits_channels(&self) -> bool {
121        self.emit_channels
122    }
123
124    /// Whether fmtp will be emitted.
125    pub fn emits_fmtp(&self) -> bool {
126        self.emit_fmtp
127    }
128
129    /// Convert an [`SdpCodec`] into a [`CodecStringEntry`] honouring these options.
130    ///
131    /// Returns `Err` if the codec name or fmtp value would produce an invalid entry
132    /// (wire-injection, `@` in fmtp, dotted fmtp without a module prefix).
133    pub fn sdp_codec_to_entry(
134        &self,
135        codec: &SdpCodec,
136    ) -> Result<CodecStringEntry, CodecStringError> {
137        let mut entry = CodecStringEntry::new(codec.name())?;
138
139        if self.emit_fmtp {
140            if let Some(fmtp) = codec.fmtp() {
141                // A dotted fmtp needs a module prefix, so it needs the builder.
142                entry = entry.with_fmtp(fmtp)?;
143            }
144        }
145
146        Ok(self.apply_qualifiers(entry, codec))
147    }
148
149    /// Apply the infallible numeric qualifiers (rate/ptime/bitrate/channels) per these
150    /// options.
151    ///
152    /// Split out from [`sdp_codec_to_entry`](Self::sdp_codec_to_entry) so
153    /// `SdpCodecs::audio_codec_string`/`video_codec_string` can recover from a failed
154    /// fmtp embedding (clear it, keep the entry, warn) without re-deriving this logic.
155    pub(crate) fn apply_qualifiers(
156        &self,
157        mut entry: CodecStringEntry,
158        codec: &SdpCodec,
159    ) -> CodecStringEntry {
160        if self.emit_rate {
161            entry = entry.with_rate(codec.clock_rate());
162        }
163
164        if self.emit_ptime {
165            if let Some(p) = codec.ptime() {
166                entry = entry.with_ptime(p);
167            }
168        }
169
170        if self.emit_bitrate {
171            if let Some(b) = codec.bitrate() {
172                entry = entry.with_bitrate(b);
173            }
174        }
175
176        if self.emit_channels {
177            if let Some(c) = codec.channels() {
178                entry = entry.with_channels(c);
179            }
180        }
181
182        entry
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::sdp::codec::SdpMediaType;
190
191    fn make_audio_codec(name: &str, rate: u32, ptime: u32) -> SdpCodec {
192        SdpCodec::new(SdpMediaType::Audio, 0, name, rate)
193            .with_channels(1)
194            .with_ptime(ptime)
195            .with_bitrate(64000)
196    }
197
198    fn make_video_codec(name: &str) -> SdpCodec {
199        SdpCodec::new(SdpMediaType::Video, 99, name, 90000)
200            .with_fmtp("profile-level-id=42e01f")
201            .with_rtpmap()
202    }
203
204    // --- defaults ---
205
206    #[test]
207    fn audio_defaults_emit_rate_ptime_bitrate_channels_not_fmtp() {
208        let opts = CodecStringOptions::audio();
209        assert!(opts.emits_rate());
210        assert!(opts.emits_ptime());
211        assert!(opts.emits_bitrate());
212        assert!(opts.emits_channels());
213        assert!(!opts.emits_fmtp(), "audio fmtp must be off by default");
214    }
215
216    #[test]
217    fn video_defaults_emit_only_fmtp() {
218        let opts = CodecStringOptions::video();
219        assert!(!opts.emits_rate());
220        assert!(!opts.emits_ptime());
221        assert!(!opts.emits_bitrate());
222        assert!(!opts.emits_channels());
223        assert!(opts.emits_fmtp());
224    }
225
226    // --- sdp_codec_to_entry ---
227
228    #[test]
229    fn audio_entry_has_rate_ptime_bitrate_no_fmtp() {
230        let codec = make_audio_codec("PCMU", 8000, 20);
231        let entry = CodecStringOptions::audio()
232            .sdp_codec_to_entry(&codec)
233            .unwrap();
234        assert_eq!(entry.name(), "PCMU");
235        assert_eq!(entry.rate(), Some(8000));
236        assert_eq!(entry.ptime(), Some(20));
237        assert_eq!(entry.bitrate(), Some(64000));
238        assert_eq!(entry.channels(), Some(1));
239        assert!(
240            entry
241                .fmtp()
242                .is_none(),
243            "audio fmtp must not be emitted by default"
244        );
245    }
246
247    #[test]
248    fn video_entry_emits_no_qualifiers() {
249        let codec = make_video_codec("H264");
250        let entry = CodecStringOptions::video()
251            .sdp_codec_to_entry(&codec)
252            .unwrap();
253        assert_eq!(entry.name(), "H264");
254        assert!(
255            entry
256                .rate()
257                .is_none(),
258            "video must not emit rate"
259        );
260        assert!(
261            entry
262                .ptime()
263                .is_none(),
264            "video must not emit ptime"
265        );
266        assert!(
267            entry
268                .bitrate()
269                .is_none(),
270            "video must not emit bitrate"
271        );
272        assert!(
273            entry
274                .channels()
275                .is_none(),
276            "video must not emit channels"
277        );
278        assert_eq!(
279            entry.fmtp(),
280            Some("profile-level-id=42e01f"),
281            "video must emit fmtp"
282        );
283    }
284
285    // --- pure-data: Clone, Debug, PartialEq ---
286
287    #[test]
288    fn codec_string_options_clone_and_eq() {
289        let a = CodecStringOptions::audio();
290        let b = a.clone();
291        assert_eq!(a, b);
292        let c = CodecStringOptions::video();
293        assert_ne!(a, c);
294    }
295}