Skip to main content

freeswitch_log_parser/
codec.rs

1//! Codec descriptors from `switch_core_media.c`'s negotiation trace.
2//!
3//! The audio and video forms carry different fields, so a token is only
4//! meaningful together with the media type of the line it came from.
5
6use std::fmt;
7
8/// Which negotiation trace a codec token came from.
9#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum CodecMedia {
12    Audio,
13    Video,
14}
15
16impl fmt::Display for CodecMedia {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            CodecMedia::Audio => f.pad("audio"),
20            CodecMedia::Video => f.pad("video"),
21        }
22    }
23}
24
25/// One codec as FreeSWITCH spells it inside a negotiation trace's brackets.
26///
27/// Everything past the payload type is `None` for video, whose trace carries
28/// only `name:payload_type`.
29#[non_exhaustive]
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CodecOffer {
32    pub name: String,
33    pub payload_type: u8,
34    pub clock_rate: Option<u32>,
35    pub ptime: Option<u32>,
36    pub bitrate: Option<u32>,
37    pub channels: Option<u8>,
38}
39
40impl fmt::Display for CodecOffer {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{}:{}", self.name, self.payload_type)?;
43        if let Some(rate) = self.clock_rate {
44            write!(f, " {rate}Hz")?;
45        }
46        if let Some(ptime) = self.ptime {
47            write!(f, " {ptime}ms")?;
48        }
49        if let Some(bitrate) = self.bitrate {
50            write!(f, " {bitrate}b")?;
51        }
52        if let Some(channels) = self.channels {
53            write!(f, " {channels}ch")?;
54        }
55        Ok(())
56    }
57}
58
59/// Why a codec token could not be read.
60#[non_exhaustive]
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum CodecParseError {
63    /// Field count matches no known form for this media type.
64    Arity { media: CodecMedia, fields: usize },
65    /// A numeric field was not a number.
66    NotNumeric { field: &'static str },
67    /// The codec name was empty.
68    EmptyName,
69}
70
71impl fmt::Display for CodecParseError {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            CodecParseError::Arity { media, fields } => {
75                write!(f, "{fields} fields is not a {media} codec token")
76            }
77            CodecParseError::NotNumeric { field } => write!(f, "non-numeric {field}"),
78            CodecParseError::EmptyName => f.write_str("empty codec name"),
79        }
80    }
81}
82
83impl std::error::Error for CodecParseError {}
84
85impl CodecOffer {
86    /// Read a bracket-stripped token, e.g. `opus:116:16000:20:0:1` for audio or
87    /// `H264:109` for video.
88    ///
89    /// The seven-field audio form is `switch_core_media.c:5575`'s only, which
90    /// logs `actual_samples_per_second` after the rate where its siblings do
91    /// not; that extra field is skipped so all audio forms yield the same shape.
92    pub fn parse(media: CodecMedia, token: &str) -> Result<Self, CodecParseError> {
93        let mut fields = token.split(':');
94        let name = fields.next().unwrap_or_default();
95        if name.is_empty() {
96            return Err(CodecParseError::EmptyName);
97        }
98        let rest: Vec<&str> = fields.collect();
99
100        let num = |raw: &str, field: &'static str| -> Result<u32, CodecParseError> {
101            raw.parse()
102                .map_err(|_| CodecParseError::NotNumeric { field })
103        };
104        let payload_type = |raw: &str| -> Result<u8, CodecParseError> {
105            raw.parse().map_err(|_| CodecParseError::NotNumeric {
106                field: "payload type",
107            })
108        };
109
110        let arity = |fields: usize| CodecParseError::Arity { media, fields };
111
112        match (media, rest.as_slice()) {
113            (CodecMedia::Video, [pt]) => Ok(CodecOffer {
114                name: name.to_string(),
115                payload_type: payload_type(pt)?,
116                clock_rate: None,
117                ptime: None,
118                bitrate: None,
119                channels: None,
120            }),
121            (CodecMedia::Audio, [pt, rate, ptime, bitrate, channels])
122            | (CodecMedia::Audio, [pt, rate, _, ptime, bitrate, channels]) => Ok(CodecOffer {
123                name: name.to_string(),
124                payload_type: payload_type(pt)?,
125                clock_rate: Some(num(rate, "clock rate")?),
126                ptime: Some(num(ptime, "ptime")?),
127                bitrate: Some(num(bitrate, "bitrate")?),
128                channels: Some(
129                    num(channels, "channels")?
130                        .try_into()
131                        .map_err(|_| CodecParseError::NotNumeric { field: "channels" })?,
132                ),
133            }),
134            _ => Err(arity(rest.len() + 1)),
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn audio(token: &str) -> CodecOffer {
144        CodecOffer::parse(CodecMedia::Audio, token).expect("parsed")
145    }
146
147    #[test]
148    fn audio_six_field_form() {
149        let c = audio("opus:116:16000:20:0:1");
150        assert_eq!(c.name, "opus");
151        assert_eq!(c.payload_type, 116);
152        assert_eq!(c.clock_rate, Some(16000));
153        assert_eq!(c.ptime, Some(20));
154        assert_eq!(c.bitrate, Some(0));
155        assert_eq!(c.channels, Some(1));
156    }
157
158    #[test]
159    fn audio_seven_field_form_skips_the_extra_rate() {
160        // switch_core_media.c:5575 logs actual_samples_per_second after the
161        // codec rate; every other audio trace omits it.
162        let c = audio("PCMU:0:8000:8000:20:64000:1");
163        assert_eq!(c.clock_rate, Some(8000));
164        assert_eq!(c.ptime, Some(20));
165        assert_eq!(c.bitrate, Some(64000));
166        assert_eq!(c.channels, Some(1));
167    }
168
169    #[test]
170    fn video_two_field_form() {
171        let c = CodecOffer::parse(CodecMedia::Video, "H264:109").expect("parsed");
172        assert_eq!(c.name, "H264");
173        assert_eq!(c.payload_type, 109);
174        assert_eq!(c.clock_rate, None);
175        assert_eq!(c.channels, None);
176    }
177
178    #[test]
179    fn media_type_decides_the_arity() {
180        assert!(CodecOffer::parse(CodecMedia::Audio, "H264:109").is_err());
181        assert!(CodecOffer::parse(CodecMedia::Video, "opus:116:16000:20:0:1").is_err());
182    }
183
184    #[test]
185    fn malformed_tokens_do_not_parse_partially() {
186        assert_eq!(
187            CodecOffer::parse(CodecMedia::Audio, ""),
188            Err(CodecParseError::EmptyName)
189        );
190        assert_eq!(
191            CodecOffer::parse(CodecMedia::Video, "H264:notanumber"),
192            Err(CodecParseError::NotNumeric {
193                field: "payload type"
194            })
195        );
196        assert_eq!(
197            CodecOffer::parse(CodecMedia::Audio, "opus:116:sixteen:20:0:1"),
198            Err(CodecParseError::NotNumeric {
199                field: "clock rate"
200            })
201        );
202        assert!(matches!(
203            CodecOffer::parse(CodecMedia::Audio, "opus:116:16000"),
204            Err(CodecParseError::Arity { fields: 3, .. })
205        ));
206    }
207
208    #[test]
209    fn display_omits_absent_fields() {
210        assert_eq!(
211            audio("opus:116:16000:20:0:1").to_string(),
212            "opus:116 16000Hz 20ms 0b 1ch"
213        );
214        assert_eq!(
215            CodecOffer::parse(CodecMedia::Video, "H264:109")
216                .unwrap()
217                .to_string(),
218            "H264:109"
219        );
220    }
221}