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