Skip to main content

freeswitch_types/commands/endpoint/
audio.rs

1use std::fmt;
2
3use super::{extract_variables, write_variables};
4use crate::commands::originate::{OriginateError, Variables};
5
6/// Audio device endpoint for portaudio, pulseaudio, or ALSA modules.
7///
8/// Wire format: `{module}[/{destination}]` where destination is typically
9/// empty or `auto_answer` (recognized by portaudio and pulseaudio).
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[non_exhaustive]
13pub struct AudioEndpoint {
14    /// Destination string (e.g. `auto_answer`). `None` for bare module name.
15    #[cfg_attr(
16        feature = "serde",
17        serde(default, skip_serializing_if = "Option::is_none")
18    )]
19    pub destination: Option<String>,
20    /// Per-channel variables prepended as `{key=value}`.
21    #[cfg_attr(
22        feature = "serde",
23        serde(default, skip_serializing_if = "Option::is_none")
24    )]
25    pub variables: Option<Variables>,
26}
27
28impl AudioEndpoint {
29    /// Create a new audio endpoint with no destination.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Set the destination string.
35    pub fn with_destination(mut self, destination: impl Into<String>) -> Self {
36        self.destination = Some(destination.into());
37        self
38    }
39
40    /// Set per-channel variables.
41    pub fn with_variables(mut self, variables: Variables) -> Self {
42        self.variables = Some(variables);
43        self
44    }
45
46    /// Format with the given module prefix (`portaudio`, `pulseaudio`, `alsa`).
47    pub fn fmt_with_prefix(&self, f: &mut fmt::Formatter<'_>, prefix: &str) -> fmt::Result {
48        write_variables(f, &self.variables)?;
49        match &self.destination {
50            Some(dest) => write!(f, "{}/{}", prefix, dest),
51            None => f.write_str(prefix),
52        }
53    }
54
55    /// Parse from a dial string with the given module prefix.
56    pub fn parse_with_prefix(s: &str, prefix: &str) -> Result<Self, OriginateError> {
57        let (variables, uri) = extract_variables(s)?;
58        let rest = uri
59            .strip_prefix(prefix)
60            .ok_or_else(|| OriginateError::ParseError(format!("not a {} endpoint", prefix)))?;
61        let destination = rest
62            .strip_prefix('/')
63            .and_then(|d| {
64                if d.is_empty() {
65                    None
66                } else {
67                    Some(d.to_string())
68                }
69            });
70        Ok(Self {
71            destination,
72            variables,
73        })
74    }
75}
76
77/// **Warning:** This `Display` impl exists only to satisfy the `DialString: Display`
78/// trait bound. The `"audio"` prefix is not a valid FreeSWITCH endpoint.
79/// Always use `AudioEndpoint` through [`Endpoint::PortAudio`](super::Endpoint::PortAudio),
80/// [`Endpoint::PulseAudio`](super::Endpoint::PulseAudio), or
81/// [`Endpoint::Alsa`](super::Endpoint::Alsa) which call
82/// [`fmt_with_prefix`](AudioEndpoint::fmt_with_prefix) with the correct module name.
83impl fmt::Display for AudioEndpoint {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        self.fmt_with_prefix(f, "audio")
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn parse_with_prefix_portaudio() {
95        let ep = AudioEndpoint::parse_with_prefix("portaudio/auto_answer", "portaudio").unwrap();
96        assert_eq!(
97            ep.destination
98                .as_deref(),
99            Some("auto_answer")
100        );
101        assert!(ep
102            .variables
103            .is_none());
104    }
105
106    #[test]
107    fn parse_with_prefix_bare() {
108        let ep = AudioEndpoint::parse_with_prefix("alsa", "alsa").unwrap();
109        assert!(ep
110            .destination
111            .is_none());
112    }
113
114    #[test]
115    fn parse_with_prefix_trailing_slash() {
116        let ep = AudioEndpoint::parse_with_prefix("pulseaudio/", "pulseaudio").unwrap();
117        assert!(ep
118            .destination
119            .is_none());
120    }
121
122    #[test]
123    fn parse_with_prefix_wrong_module() {
124        let result = AudioEndpoint::parse_with_prefix("portaudio/x", "alsa");
125        assert!(result.is_err());
126    }
127}