Skip to main content

freeswitch_log_parser/session/
media.rs

1//! Per-session codec outcome.
2//!
3//! FreeSWITCH logs the read side of the audio engine and nothing about the
4//! write side at DEBUG, so only the read direction is modelled — a `write_codec`
5//! field would be a guess.
6
7use crate::codec::{CodecMedia, CodecOffer};
8use crate::message::MessageKind;
9use crate::stream::{Block, LogEntry};
10
11/// What one media type's negotiation produced for a session.
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct MediaCodecs {
14    /// Last codec FreeSWITCH saved as a match.
15    pub negotiated: Option<CodecOffer>,
16    /// Distinct codecs the far end offered, in first-seen order.
17    pub offered: Vec<CodecOffer>,
18}
19
20impl MediaCodecs {
21    fn offer(&mut self, codec: &CodecOffer) {
22        if !self.offered.contains(codec) {
23            self.offered.push(codec.clone());
24        }
25    }
26}
27
28/// A codec implementation the engine reports it is running, as distinct from a
29/// [`CodecOffer`] read off the wire.
30///
31/// Every field but the name is optional because each line that produces one
32/// carries a different subset — an engine line naming no payload type must not
33/// be forced to claim one, which is what sharing `CodecOffer` did.
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct CodecImpl {
37    pub name: String,
38    pub payload_type: Option<u8>,
39    pub clock_rate: Option<u32>,
40    pub ptime: Option<u32>,
41    pub bitrate: Option<u32>,
42    pub channels: Option<u8>,
43}
44
45impl CodecImpl {
46    /// A codec known only by name, for a line to fill in what it carries.
47    fn named(name: &str) -> Self {
48        CodecImpl {
49            name: name.to_string(),
50            ..CodecImpl::default()
51        }
52    }
53}
54
55/// Codecs a session negotiated, by media type and by direction.
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct SessionMedia {
58    pub audio: MediaCodecs,
59    pub video: MediaCodecs,
60    /// `Original read codec set to <name>:<pt>` — `switch_core_codec.c:132`.
61    pub read_codec: Option<CodecImpl>,
62    /// `Set Codec <chan> <name>/<rate> <ptime> ms …` — `switch_core_media.c:3739`,
63    /// reporting the audio engine's read implementation.
64    pub active_audio: Option<CodecImpl>,
65}
66
67impl SessionMedia {
68    pub(crate) fn update_from_entry(&mut self, entry: &LogEntry) {
69        if let Some(Block::CodecNegotiation {
70            media,
71            comparisons,
72            matched,
73            ..
74        }) = &entry.block
75        {
76            // Both arms named: a wildcard would file a media type added later
77            // under audio without anyone noticing.
78            let side = match media {
79                CodecMedia::Video => &mut self.video,
80                CodecMedia::Audio => &mut self.audio,
81            };
82            for (offered, _local) in comparisons {
83                side.offer(offered);
84            }
85            if let Some(last) = matched.last() {
86                side.negotiated = Some(last.clone());
87            }
88        }
89
90        // Both live under MessageKind::Media, which keeps the whole message.
91        if let MessageKind::Media { detail } = &entry.message_kind {
92            if let Some(codec) = parse_original_read_codec(detail) {
93                self.read_codec = Some(codec);
94            }
95            if let Some(codec) = parse_set_codec(detail) {
96                self.active_audio = Some(codec);
97            }
98        }
99    }
100}
101
102/// `<chan> Original read codec set to <name>:<payload>`
103fn parse_original_read_codec(msg: &str) -> Option<CodecImpl> {
104    let rest = msg.split_once("Original read codec set to ")?.1;
105    let (name, payload) = rest.trim().split_once(':')?;
106    if name.is_empty() {
107        return None;
108    }
109    Some(CodecImpl {
110        name: name.to_string(),
111        payload_type: Some(payload.parse().ok()?),
112        ..CodecImpl::named(name)
113    })
114}
115
116/// `Set Codec <chan> <name>/<rate> <ptime> ms <samples> samples <bits> bits <channels> channels`
117fn parse_set_codec(msg: &str) -> Option<CodecImpl> {
118    let rest = msg.strip_prefix("Set Codec ")?;
119    let (_channel, rest) = rest.split_once(' ')?;
120    let mut fields = rest.split(' ');
121    let (name, rate) = fields.next()?.split_once('/')?;
122    if name.is_empty() {
123        return None;
124    }
125    let ptime = fields.next()?.parse().ok()?;
126    let mut codec = CodecImpl {
127        clock_rate: rate.parse().ok(),
128        ptime: Some(ptime),
129        ..CodecImpl::named(name)
130    };
131    let tail: Vec<&str> = fields.collect();
132    for pair in tail.windows(2) {
133        match pair {
134            [value, "bits"] => codec.bitrate = value.parse().ok(),
135            [value, "channels"] => codec.channels = value.parse().ok(),
136            _ => {}
137        }
138    }
139    Some(codec)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn original_read_codec_carries_only_name_and_payload() {
148        let c =
149            parse_original_read_codec("sofia/softphone/1213 Original read codec set to opus:116")
150                .expect("parsed");
151        assert_eq!(c.name, "opus");
152        assert_eq!(c.payload_type, Some(116));
153        assert_eq!(c.clock_rate, None);
154        assert_eq!(c.ptime, None);
155    }
156
157    #[test]
158    fn set_codec_carries_rate_ptime_bitrate_and_channels() {
159        let c = parse_set_codec(
160            "Set Codec sofia/softphone/1213 opus/16000 20 ms 320 samples 0 bits 1 channels",
161        )
162        .expect("parsed");
163        assert_eq!(c.name, "opus");
164        assert_eq!(c.clock_rate, Some(16000));
165        assert_eq!(c.ptime, Some(20));
166        assert_eq!(c.bitrate, Some(0));
167        assert_eq!(c.channels, Some(1));
168        assert_eq!(
169            c.payload_type, None,
170            "the line names none, and 0 would claim PCMU"
171        );
172    }
173
174    #[test]
175    fn set_codec_reads_a_bitrate_that_is_not_zero() {
176        let c = parse_set_codec(
177            "Set Codec sofia/softphone/1213 PCMU/8000 20 ms 160 samples 64000 bits 1 channels",
178        )
179        .expect("parsed");
180        assert_eq!(c.bitrate, Some(64000));
181    }
182
183    #[test]
184    fn unrelated_media_lines_are_not_codecs() {
185        assert!(parse_set_codec("Set telephone-event payload to 101@48000").is_none());
186        assert!(parse_original_read_codec("Activating RTCP PORT 4001").is_none());
187    }
188}