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/// Codecs a session negotiated, by media type and by direction.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30pub struct SessionMedia {
31    pub audio: MediaCodecs,
32    pub video: MediaCodecs,
33    /// `Original read codec set to <name>:<pt>` — `switch_core_codec.c:132`.
34    /// Carries no rate or ptime, so those stay `None`.
35    pub read_codec: Option<CodecOffer>,
36    /// `Set Codec <chan> <name>/<rate> <ptime> ms …` — `switch_core_media.c:3739`,
37    /// reporting the audio engine's read implementation. No payload type.
38    pub active_audio: Option<CodecOffer>,
39}
40
41impl SessionMedia {
42    pub(crate) fn update_from_entry(&mut self, entry: &LogEntry) {
43        if let Some(Block::CodecNegotiation {
44            media,
45            comparisons,
46            matched,
47            ..
48        }) = &entry.block
49        {
50            let side = match media {
51                CodecMedia::Video => &mut self.video,
52                _ => &mut self.audio,
53            };
54            for (offered, _local) in comparisons {
55                side.offer(offered);
56            }
57            if let Some(last) = matched.last() {
58                side.negotiated = Some(last.clone());
59            }
60        }
61
62        // Both live under MessageKind::Media, which keeps the whole message.
63        if let MessageKind::Media { detail } = &entry.message_kind {
64            if let Some(codec) = parse_original_read_codec(detail) {
65                self.read_codec = Some(codec);
66            }
67            if let Some(codec) = parse_set_codec(detail) {
68                self.active_audio = Some(codec);
69            }
70        }
71    }
72}
73
74/// `<chan> Original read codec set to <name>:<payload>`
75fn parse_original_read_codec(msg: &str) -> Option<CodecOffer> {
76    let rest = msg.split_once("Original read codec set to ")?.1;
77    let (name, payload) = rest.trim().split_once(':')?;
78    if name.is_empty() {
79        return None;
80    }
81    Some(CodecOffer {
82        name: name.to_string(),
83        payload_type: payload.parse().ok()?,
84        clock_rate: None,
85        ptime: None,
86        bitrate: None,
87        channels: None,
88    })
89}
90
91/// `Set Codec <chan> <name>/<rate> <ptime> ms <samples> samples <bits> bits <channels> channels`
92fn parse_set_codec(msg: &str) -> Option<CodecOffer> {
93    let rest = msg.strip_prefix("Set Codec ")?;
94    let (_channel, rest) = rest.split_once(' ')?;
95    let mut fields = rest.split(' ');
96    let (name, rate) = fields.next()?.split_once('/')?;
97    if name.is_empty() {
98        return None;
99    }
100    let ptime = fields.next()?.parse().ok()?;
101    // The payload type is absent from this line; 0 would claim PCMU.
102    let mut codec = CodecOffer {
103        name: name.to_string(),
104        payload_type: 0,
105        clock_rate: rate.parse().ok(),
106        ptime: Some(ptime),
107        bitrate: None,
108        channels: None,
109    };
110    let tail: Vec<&str> = fields.collect();
111    for pair in tail.windows(2) {
112        match pair {
113            [value, "bits"] => codec.bitrate = value.parse().ok(),
114            [value, "channels"] => codec.channels = value.parse().ok(),
115            _ => {}
116        }
117    }
118    Some(codec)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn original_read_codec_carries_only_name_and_payload() {
127        let c =
128            parse_original_read_codec("sofia/softphone/1213 Original read codec set to opus:116")
129                .expect("parsed");
130        assert_eq!(c.name, "opus");
131        assert_eq!(c.payload_type, 116);
132        assert_eq!(c.clock_rate, None);
133        assert_eq!(c.ptime, None);
134    }
135
136    #[test]
137    fn set_codec_carries_rate_ptime_bitrate_and_channels() {
138        let c = parse_set_codec(
139            "Set Codec sofia/softphone/1213 opus/16000 20 ms 320 samples 0 bits 1 channels",
140        )
141        .expect("parsed");
142        assert_eq!(c.name, "opus");
143        assert_eq!(c.clock_rate, Some(16000));
144        assert_eq!(c.ptime, Some(20));
145        assert_eq!(c.bitrate, Some(0));
146        assert_eq!(c.channels, Some(1));
147    }
148
149    #[test]
150    fn set_codec_reads_a_bitrate_that_is_not_zero() {
151        let c = parse_set_codec(
152            "Set Codec sofia/softphone/1213 PCMU/8000 20 ms 160 samples 64000 bits 1 channels",
153        )
154        .expect("parsed");
155        assert_eq!(c.bitrate, Some(64000));
156    }
157
158    #[test]
159    fn unrelated_media_lines_are_not_codecs() {
160        assert!(parse_set_codec("Set telephone-event payload to 101@48000").is_none());
161        assert!(parse_original_read_codec("Activating RTCP PORT 4001").is_none());
162    }
163}