Skip to main content

freeswitch_types/sdp/
codec.rs

1//! SDP media types and codec descriptors.
2//!
3//! Line numbers in this module index FreeSWITCH `v1.11.1`
4//! (`c2c59645f6911a76589e5008c4d73349ded44b65`).
5
6use std::fmt;
7use std::str::FromStr;
8
9/// SDP media type from an `m=` line.
10///
11/// Unrecognized types are represented as [`Other`](SdpMediaType::Other) rather
12/// than hard-failing the parse — FreeSWITCH ignores media sections it does not
13/// recognize (BFCP, MSRP, datachannel, etc.), and a converter must not drop
14/// the whole session over an unknown `m=` line.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub enum SdpMediaType {
18    /// `m=audio`
19    Audio,
20    /// `m=video`
21    Video,
22    /// `m=application` — used for BFCP, MSRP, datachannel, etc.
23    Application,
24    /// `m=text` — used for MSRP text streams.
25    Text,
26    /// `m=message` — used for MSRP message streams.
27    Message,
28    /// Any other `m=` media type not recognized above.
29    Other(String),
30}
31
32impl fmt::Display for SdpMediaType {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Audio => f.write_str("audio"),
36            Self::Video => f.write_str("video"),
37            Self::Application => f.write_str("application"),
38            Self::Text => f.write_str("text"),
39            Self::Message => f.write_str("message"),
40            Self::Other(s) => f.write_str(s),
41        }
42    }
43}
44
45impl FromStr for SdpMediaType {
46    type Err = std::convert::Infallible;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        Ok(match s {
50            "audio" => Self::Audio,
51            "video" => Self::Video,
52            "application" => Self::Application,
53            "text" => Self::Text,
54            "message" => Self::Message,
55            other => Self::Other(other.to_string()),
56        })
57    }
58}
59
60/// SDP direction attribute from an `a=` line.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62#[non_exhaustive]
63pub enum SdpDirection {
64    /// `a=sendrecv` — bidirectional (the default when no direction attribute is present).
65    SendRecv,
66    /// `a=sendonly` — the sender transmits but does not receive.
67    SendOnly,
68    /// `a=recvonly` — the sender receives but does not transmit.
69    RecvOnly,
70    /// `a=inactive` — neither side transmits.
71    Inactive,
72}
73
74impl fmt::Display for SdpDirection {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Self::SendRecv => f.write_str("sendrecv"),
78            Self::SendOnly => f.write_str("sendonly"),
79            Self::RecvOnly => f.write_str("recvonly"),
80            Self::Inactive => f.write_str("inactive"),
81        }
82    }
83}
84
85/// Errors returned when an unrecognized direction attribute is encountered.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ParseSdpDirectionError(String);
88
89impl fmt::Display for ParseSdpDirectionError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "unrecognized SDP direction attribute: {:?}", self.0)
92    }
93}
94
95impl std::error::Error for ParseSdpDirectionError {}
96
97impl FromStr for SdpDirection {
98    type Err = ParseSdpDirectionError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        match s {
102            "sendrecv" => Ok(Self::SendRecv),
103            "sendonly" => Ok(Self::SendOnly),
104            "recvonly" => Ok(Self::RecvOnly),
105            "inactive" => Ok(Self::Inactive),
106            other => Err(ParseSdpDirectionError(other.to_string())),
107        }
108    }
109}
110
111/// A codec derived from an SDP `m=` section.
112///
113/// Fields are private and read-only from outside the crate: a codec is what the
114/// offer said. Build one with [`new`](Self::new) plus the `with_*` methods.
115#[derive(Debug, Clone)]
116pub struct SdpCodec {
117    media: SdpMediaType,
118    /// Session-local payload type from the `m=` format list.
119    /// Never emitted into a codec string — it is only meaningful within
120    /// this session and must not be carried across calls.
121    payload_type: u8,
122    name: String,
123    clock_rate: u32,
124    channels: Option<u32>,
125    fmtp: Option<String>,
126    ptime: Option<u32>,
127    maxptime: Option<u32>,
128    bitrate: Option<u32>,
129    direction: SdpDirection,
130    has_rtpmap: bool,
131}
132
133impl SdpCodec {
134    /// Create a codec from the fields an `m=` line and its rtpmap always supply.
135    ///
136    /// The direction defaults to [`SdpDirection::SendRecv`], which is what a
137    /// section carrying no direction attribute means, and `has_rtpmap` to
138    /// `false` — set it with [`with_rtpmap`](Self::with_rtpmap) when the name
139    /// and clock rate came off an `a=rtpmap` line rather than the RFC 3551
140    /// static table.
141    pub fn new(
142        media: SdpMediaType,
143        payload_type: u8,
144        name: impl Into<String>,
145        clock_rate: u32,
146    ) -> Self {
147        Self {
148            media,
149            payload_type,
150            name: name.into(),
151            clock_rate,
152            channels: None,
153            fmtp: None,
154            ptime: None,
155            maxptime: None,
156            bitrate: None,
157            direction: SdpDirection::SendRecv,
158            has_rtpmap: false,
159        }
160    }
161
162    /// Set the channel count from `a=rtpmap`.
163    pub fn with_channels(mut self, channels: u32) -> Self {
164        self.channels = Some(channels);
165        self
166    }
167
168    /// Set the format parameters from `a=fmtp`.
169    pub fn with_fmtp(mut self, fmtp: impl Into<String>) -> Self {
170        self.fmtp = Some(fmtp.into());
171        self
172    }
173
174    /// Set the packetization time in milliseconds.
175    pub fn with_ptime(mut self, ptime: u32) -> Self {
176        self.ptime = Some(ptime);
177        self
178    }
179
180    /// Set the maximum packetization time in milliseconds.
181    pub fn with_maxptime(mut self, maxptime: u32) -> Self {
182        self.maxptime = Some(maxptime);
183        self
184    }
185
186    /// Set the bitrate in bits per second.
187    pub fn with_bitrate(mut self, bitrate: u32) -> Self {
188        self.bitrate = Some(bitrate);
189        self
190    }
191
192    /// Set the media direction of the section this codec came from.
193    pub fn with_direction(mut self, direction: SdpDirection) -> Self {
194        self.direction = direction;
195        self
196    }
197
198    /// Record that the encoding name and clock rate came from an `a=rtpmap` line.
199    pub fn with_rtpmap(mut self) -> Self {
200        self.has_rtpmap = true;
201        self
202    }
203
204    /// The SDP media type this codec belongs to.
205    pub fn media(&self) -> &SdpMediaType {
206        &self.media
207    }
208
209    /// The session-local RTP payload type.
210    ///
211    /// This value is assigned by the offerer for the duration of this session
212    /// only. It is **never** emitted into a FreeSWITCH codec string — codec
213    /// strings identify codecs by name, not by payload type number.
214    pub fn payload_type(&self) -> u8 {
215        self.payload_type
216    }
217
218    /// The codec encoding name (e.g. `"opus"`, `"PCMU"`, `"AMR-WB"`).
219    pub fn name(&self) -> &str {
220        &self.name
221    }
222
223    /// The RTP clock rate in Hz.
224    pub fn clock_rate(&self) -> u32 {
225        self.clock_rate
226    }
227
228    /// The channel count, or `None` when not applicable (video) or stream-defined.
229    pub fn channels(&self) -> Option<u32> {
230        self.channels
231    }
232
233    /// The format parameters from `a=fmtp`, if any.
234    pub fn fmtp(&self) -> Option<&str> {
235        self.fmtp
236            .as_deref()
237    }
238
239    /// The packetization time in milliseconds, or `None` if not specified.
240    pub fn ptime(&self) -> Option<u32> {
241        self.ptime
242    }
243
244    /// The offered `a=maxptime` in milliseconds, or `None` if not specified.
245    ///
246    /// Retained for offer fidelity: nothing in this crate reads it, and the switch
247    /// derives no codec-string qualifier from it.
248    pub fn maxptime(&self) -> Option<u32> {
249        self.maxptime
250    }
251
252    /// The bitrate in bits per second, or `None` if not known.
253    pub fn bitrate(&self) -> Option<u32> {
254        self.bitrate
255    }
256
257    /// The media direction for this codec's stream.
258    pub fn direction(&self) -> SdpDirection {
259        self.direction
260    }
261
262    /// Whether the encoding name and clock rate came from an `a=rtpmap` line.
263    ///
264    /// `false` means they were filled in from the RFC 3551 static table.
265    pub fn has_rtpmap(&self) -> bool {
266        self.has_rtpmap
267    }
268
269    // --- mutable accessors, for this crate's own section walk ---
270
271    /// Mutable access to the format parameters.
272    pub(crate) fn fmtp_mut(&mut self) -> &mut Option<String> {
273        &mut self.fmtp
274    }
275
276    /// Mutable access to ptime.
277    pub(crate) fn ptime_mut(&mut self) -> &mut Option<u32> {
278        &mut self.ptime
279    }
280
281    /// Mutable access to maxptime.
282    pub(crate) fn maxptime_mut(&mut self) -> &mut Option<u32> {
283        &mut self.maxptime
284    }
285
286    /// Mutable access to bitrate.
287    pub(crate) fn bitrate_mut(&mut self) -> &mut Option<u32> {
288        &mut self.bitrate
289    }
290
291    /// Mutable access to the channel count.
292    pub(crate) fn channels_mut(&mut self) -> &mut Option<u32> {
293        &mut self.channels
294    }
295}
296
297/// A payload type FreeSWITCH negotiates outside the codec string.
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
299#[non_exhaustive]
300pub enum NonCodecKind {
301    /// RFC 2833 / RFC 4733 DTMF, carried as `smh->mparams->te`.
302    TelephoneEvent,
303    /// Comfort noise, carried as `smh->mparams->cng_pt`.
304    ComfortNoise,
305}
306
307impl fmt::Display for NonCodecKind {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        match self {
310            Self::TelephoneEvent => f.write_str("telephone-event"),
311            Self::ComfortNoise => f.write_str("CN"),
312        }
313    }
314}
315
316/// A payload offered in an `m=` section that is negotiated outside the codec string.
317///
318/// This is the offer's inventory in `m=` order, undeduplicated — not a negotiation
319/// outcome. FreeSWITCH keeps only one of each per session, picking the entry whose
320/// clock rate matches the negotiated codec's advertised rate
321/// (`switch_core_media.c:5805`, `:5816`) and forcing the retained rate to 8000 Hz
322/// when it does not match (`:5829-5834`). That comparison needs a negotiated codec,
323/// which does not exist at this layer, so a caller holding one applies the rule itself.
324#[derive(Debug, Clone, PartialEq, Eq)]
325#[non_exhaustive]
326pub struct NonCodecPayload {
327    /// Which payload this is, resolved from the canonical encoding name.
328    pub kind: NonCodecKind,
329    /// Session-local payload type from the `m=` format list.
330    pub payload_type: u8,
331    /// The media type of the section where this payload appeared.
332    pub media_type: SdpMediaType,
333    /// Clock rate in Hz, from `a=rtpmap` or the RFC 3551 static table.
334    pub clock_rate: u32,
335    /// Format parameters from `a=fmtp`, verbatim.
336    ///
337    /// FreeSWITCH never reads this from a received offer: both kinds leave the rtpmap
338    /// walk (`switch_core_media.c:5447`, `:5456`) before `switch_core_codec_parse_fmtp`
339    /// at `:5493`, and the DTMF digit range in a generated offer is synthesized from
340    /// `NDLB_line_flash_16` instead (`:10653-10659`). A disagreement between two offers
341    /// is therefore real but is not something the switch acted on.
342    pub fmtp: Option<String>,
343    /// `false` when the payload type was resolved from the RFC 3551 static table
344    /// rather than declared by the peer with an `a=rtpmap` line.
345    pub has_rtpmap: bool,
346}
347
348impl fmt::Display for NonCodecPayload {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        write!(f, "{} {}/{}", self.payload_type, self.kind, self.clock_rate)?;
351        if let Some(fmtp) = &self.fmtp {
352            write!(f, " {fmtp}")?;
353        }
354        Ok(())
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    // --- NonCodecPayload ---
363
364    #[test]
365    fn non_codec_payload_display_reproduces_its_rtpmap() {
366        let te = NonCodecPayload {
367            kind: NonCodecKind::TelephoneEvent,
368            payload_type: 101,
369            media_type: SdpMediaType::Audio,
370            clock_rate: 8000,
371            fmtp: Some("0-16".to_string()),
372            has_rtpmap: true,
373        };
374        assert_eq!(te.to_string(), "101 telephone-event/8000 0-16");
375
376        let cn = NonCodecPayload {
377            kind: NonCodecKind::ComfortNoise,
378            payload_type: 13,
379            media_type: SdpMediaType::Audio,
380            clock_rate: 8000,
381            fmtp: None,
382            has_rtpmap: false,
383        };
384        assert_eq!(cn.to_string(), "13 CN/8000");
385    }
386
387    #[test]
388    fn non_codec_kind_display_is_the_canonical_encoding_name() {
389        assert_eq!(NonCodecKind::TelephoneEvent.to_string(), "telephone-event");
390        assert_eq!(NonCodecKind::ComfortNoise.to_string(), "CN");
391    }
392
393    // --- SdpMediaType ---
394
395    #[test]
396    fn sdp_media_type_display() {
397        assert_eq!(SdpMediaType::Audio.to_string(), "audio");
398        assert_eq!(SdpMediaType::Video.to_string(), "video");
399        assert_eq!(SdpMediaType::Application.to_string(), "application");
400        assert_eq!(SdpMediaType::Text.to_string(), "text");
401        assert_eq!(SdpMediaType::Message.to_string(), "message");
402        assert_eq!(
403            SdpMediaType::Other("image".to_string()).to_string(),
404            "image"
405        );
406    }
407
408    #[test]
409    fn sdp_media_type_from_str_known() {
410        assert_eq!(
411            "audio"
412                .parse::<SdpMediaType>()
413                .unwrap(),
414            SdpMediaType::Audio
415        );
416        assert_eq!(
417            "video"
418                .parse::<SdpMediaType>()
419                .unwrap(),
420            SdpMediaType::Video
421        );
422        assert_eq!(
423            "application"
424                .parse::<SdpMediaType>()
425                .unwrap(),
426            SdpMediaType::Application
427        );
428        assert_eq!(
429            "text"
430                .parse::<SdpMediaType>()
431                .unwrap(),
432            SdpMediaType::Text
433        );
434        assert_eq!(
435            "message"
436                .parse::<SdpMediaType>()
437                .unwrap(),
438            SdpMediaType::Message
439        );
440    }
441
442    #[test]
443    fn sdp_media_type_from_str_unknown_becomes_other() {
444        // Unknown types become Other instead of erroring — FreeSWITCH ignores
445        // media sections it does not recognize.
446        let t = "image"
447            .parse::<SdpMediaType>()
448            .unwrap();
449        assert_eq!(t, SdpMediaType::Other("image".to_string()));
450    }
451
452    #[test]
453    fn sdp_media_type_round_trip() {
454        for s in &[
455            "audio",
456            "video",
457            "application",
458            "text",
459            "message",
460            "datachannel",
461        ] {
462            let parsed: SdpMediaType = s
463                .parse()
464                .unwrap();
465            assert_eq!(&parsed.to_string(), s);
466        }
467    }
468
469    // --- SdpDirection ---
470
471    #[test]
472    fn sdp_direction_display() {
473        assert_eq!(SdpDirection::SendRecv.to_string(), "sendrecv");
474        assert_eq!(SdpDirection::SendOnly.to_string(), "sendonly");
475        assert_eq!(SdpDirection::RecvOnly.to_string(), "recvonly");
476        assert_eq!(SdpDirection::Inactive.to_string(), "inactive");
477    }
478
479    #[test]
480    fn sdp_direction_from_str() {
481        assert_eq!(
482            "sendrecv"
483                .parse::<SdpDirection>()
484                .unwrap(),
485            SdpDirection::SendRecv
486        );
487        assert_eq!(
488            "sendonly"
489                .parse::<SdpDirection>()
490                .unwrap(),
491            SdpDirection::SendOnly
492        );
493        assert_eq!(
494            "recvonly"
495                .parse::<SdpDirection>()
496                .unwrap(),
497            SdpDirection::RecvOnly
498        );
499        assert_eq!(
500            "inactive"
501                .parse::<SdpDirection>()
502                .unwrap(),
503            SdpDirection::Inactive
504        );
505    }
506
507    #[test]
508    fn sdp_direction_from_str_error() {
509        let err = "halfduplex"
510            .parse::<SdpDirection>()
511            .unwrap_err();
512        assert!(err
513            .to_string()
514            .contains("halfduplex"));
515    }
516
517    #[test]
518    fn sdp_direction_round_trip() {
519        for dir in &[
520            SdpDirection::SendRecv,
521            SdpDirection::SendOnly,
522            SdpDirection::RecvOnly,
523            SdpDirection::Inactive,
524        ] {
525            let parsed: SdpDirection = dir
526                .to_string()
527                .parse()
528                .unwrap();
529            assert_eq!(&parsed, dir);
530        }
531    }
532
533    // --- SdpCodec ---
534
535    fn make_audio_codec() -> SdpCodec {
536        SdpCodec::new(SdpMediaType::Audio, 0, "PCMU", 8000)
537            .with_channels(1)
538            .with_ptime(20)
539            .with_bitrate(64000)
540    }
541
542    #[test]
543    fn sdp_codec_new_defaults() {
544        let c = SdpCodec::new(SdpMediaType::Audio, 8, "PCMA", 8000);
545        assert_eq!(c.channels(), None);
546        assert_eq!(c.fmtp(), None);
547        assert_eq!(c.ptime(), None);
548        assert_eq!(c.maxptime(), None);
549        assert_eq!(c.bitrate(), None);
550        assert_eq!(c.direction(), SdpDirection::SendRecv);
551        assert!(!c.has_rtpmap());
552    }
553
554    #[test]
555    fn sdp_codec_builders_set_optional_fields() {
556        let c = SdpCodec::new(SdpMediaType::Audio, 9, "G722", 8000)
557            .with_maxptime(40)
558            .with_direction(SdpDirection::RecvOnly);
559        assert_eq!(c.maxptime(), Some(40));
560        assert_eq!(c.direction(), SdpDirection::RecvOnly);
561    }
562
563    #[test]
564    fn sdp_codec_accessors() {
565        let c = make_audio_codec();
566        assert_eq!(c.media(), &SdpMediaType::Audio);
567        assert_eq!(c.payload_type(), 0);
568        assert_eq!(c.name(), "PCMU");
569        assert_eq!(c.clock_rate(), 8000);
570        assert_eq!(c.channels(), Some(1));
571        assert_eq!(c.fmtp(), None);
572        assert_eq!(c.ptime(), Some(20));
573        assert_eq!(c.maxptime(), None);
574        assert_eq!(c.bitrate(), Some(64000));
575        assert_eq!(c.direction(), SdpDirection::SendRecv);
576        assert!(!c.has_rtpmap());
577    }
578
579    #[test]
580    fn sdp_codec_with_rtpmap() {
581        let c = SdpCodec::new(SdpMediaType::Audio, 111, "opus", 48000)
582            .with_channels(2)
583            .with_fmtp("minptime=10;useinbandfec=1")
584            .with_ptime(20)
585            .with_rtpmap();
586        assert_eq!(c.name(), "opus");
587        assert_eq!(c.clock_rate(), 48000);
588        assert_eq!(c.channels(), Some(2));
589        assert_eq!(c.fmtp(), Some("minptime=10;useinbandfec=1"));
590        assert!(c.has_rtpmap());
591    }
592
593    #[test]
594    fn sdp_codec_video_no_channels() {
595        let c = SdpCodec::new(SdpMediaType::Video, 99, "H264", 90000).with_rtpmap();
596        assert_eq!(c.media(), &SdpMediaType::Video);
597        assert!(c
598            .channels()
599            .is_none());
600    }
601
602    #[test]
603    fn sdp_codec_mutable_accessors() {
604        let mut c = make_audio_codec();
605        *c.ptime_mut() = Some(30);
606        assert_eq!(c.ptime(), Some(30));
607
608        *c.fmtp_mut() = Some("mode=20".into());
609        assert_eq!(c.fmtp(), Some("mode=20"));
610
611        *c.bitrate_mut() = None;
612        assert!(c
613            .bitrate()
614            .is_none());
615
616        *c.maxptime_mut() = Some(40);
617        assert_eq!(c.maxptime(), Some(40));
618
619        *c.channels_mut() = Some(2);
620        assert_eq!(c.channels(), Some(2));
621    }
622}