Skip to main content

freeswitch_types/commands/endpoint/
audio.rs

1use std::fmt;
2
3use super::{
4    after_prefix, check_field, parse_leg, undeliverable, write_module_text, write_variables,
5    EndpointFieldFault,
6};
7use crate::commands::originate::OriginateError;
8use crate::commands::variables::{DialStringCarrier, DialStringTarget, Variables};
9
10/// Audio device endpoint for portaudio, pulseaudio, or ALSA modules.
11///
12/// Wire format: `{module}[/{destination}]` where destination is typically
13/// empty or `auto_answer` (recognized by portaudio and pulseaudio).
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16#[cfg_attr(feature = "serde", serde(try_from = "config::AudioEndpoint"))]
17#[non_exhaustive]
18pub struct AudioEndpoint {
19    /// Destination string (e.g. `auto_answer`). `None` for bare module name.
20    #[cfg_attr(
21        feature = "serde",
22        serde(default, skip_serializing_if = "Option::is_none")
23    )]
24    pub destination: Option<String>,
25    /// Per-channel variables prepended as `{key=value}`.
26    #[cfg_attr(
27        feature = "serde",
28        serde(default, skip_serializing_if = "Option::is_none")
29    )]
30    pub variables: Option<Variables>,
31}
32
33impl AudioEndpoint {
34    /// Create a new audio endpoint with no destination.
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Set the destination string.
40    pub fn with_destination(mut self, destination: impl Into<String>) -> Self {
41        self.destination = Some(destination.into());
42        self
43    }
44
45    /// Format with the given module prefix (`portaudio`, `pulseaudio`, `alsa`),
46    /// which the [`Endpoint`](super::Endpoint) variant supplies because this
47    /// struct does not record which of the three modules it belongs to.
48    ///
49    /// Renders for [`DialStringCarrier::EslApi`]; the [`Endpoint`](super::Endpoint)
50    /// variants render for whichever carrier they were asked for.
51    pub fn fmt_with_prefix(&self, f: &mut fmt::Formatter<'_>, prefix: &str) -> fmt::Result {
52        self.write_with_prefix(f, prefix, DialStringCarrier::EslApi.into())
53    }
54
55    pub(super) fn write_with_prefix(
56        &self,
57        f: &mut fmt::Formatter<'_>,
58        prefix: &str,
59        target: DialStringTarget,
60    ) -> fmt::Result {
61        write_variables(f, &self.variables, target)?;
62        write_module_text(f, &self.module_text(prefix), target)
63    }
64
65    /// The text after the variable block, as the `prefix` module receives it.
66    pub(crate) fn module_text(&self, prefix: &str) -> String {
67        match &self.destination {
68            Some(dest) => format!("{prefix}/{dest}"),
69            None => prefix.to_owned(),
70        }
71    }
72
73    /// Refuse `:_:` and an empty destination, which the module reads as none.
74    pub(crate) fn check_deliverable(&self) -> Result<(), OriginateError> {
75        match self
76            .destination
77            .as_deref()
78        {
79            Some("") => Err(undeliverable(
80                "audio",
81                "destination",
82                EndpointFieldFault::EmptyReadsAsDefault,
83            )),
84            Some(destination) => check_field("audio", "destination", destination, &[]),
85            None => Ok(()),
86        }
87    }
88
89    /// Parse from a dial string with the given module prefix.
90    pub fn parse_with_prefix(s: &str, prefix: &str) -> Result<Self, OriginateError> {
91        let (variables, ep) = parse_leg(s, DialStringCarrier::EslApi.into(), |text| {
92            Self::parse_bare(text, prefix)
93        })?;
94        Ok(Self { variables, ..ep })
95    }
96
97    pub(crate) fn parse_bare(text: &str, prefix: &str) -> Result<Self, OriginateError> {
98        let rest = after_prefix(text, prefix, prefix)?;
99        let ep = Self {
100            destination: rest
101                .strip_prefix('/')
102                .filter(|d| !d.is_empty())
103                .map(str::to_string),
104            variables: None,
105        };
106        ep.check_deliverable()?;
107        Ok(ep)
108    }
109}
110
111impl_dial_string_with_variables!(AudioEndpoint);
112
113impl_endpoint_parse!(config:
114    AudioEndpoint {
115        ;
116        destination: Option<String>,
117        variables: Option<Variables>,
118    }
119);
120
121/// **Warning:** This `Display` impl exists only to satisfy the `DialString: Display`
122/// trait bound. The `"audio"` prefix is not a valid FreeSWITCH endpoint.
123/// Always use `AudioEndpoint` through [`Endpoint::PortAudio`](super::Endpoint::PortAudio),
124/// [`Endpoint::PulseAudio`](super::Endpoint::PulseAudio), or
125/// [`Endpoint::Alsa`](super::Endpoint::Alsa) which call
126/// [`fmt_with_prefix`](AudioEndpoint::fmt_with_prefix) with the correct module name.
127impl fmt::Display for AudioEndpoint {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        self.fmt_with_prefix(f, "audio")
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn parse_with_prefix_portaudio() {
139        let ep = AudioEndpoint::parse_with_prefix("portaudio/auto_answer", "portaudio").unwrap();
140        assert_eq!(
141            ep.destination
142                .as_deref(),
143            Some("auto_answer")
144        );
145        assert!(ep
146            .variables
147            .is_none());
148    }
149
150    #[test]
151    fn parse_with_prefix_bare() {
152        let ep = AudioEndpoint::parse_with_prefix("alsa", "alsa").unwrap();
153        assert!(ep
154            .destination
155            .is_none());
156    }
157
158    #[test]
159    fn parse_with_prefix_trailing_slash() {
160        let ep = AudioEndpoint::parse_with_prefix("pulseaudio/", "pulseaudio").unwrap();
161        assert!(ep
162            .destination
163            .is_none());
164    }
165
166    #[test]
167    fn parse_with_prefix_wrong_module() {
168        let result = AudioEndpoint::parse_with_prefix("portaudio/x", "alsa");
169        assert!(result.is_err());
170    }
171
172    /// A module name is a whole path segment. Accepting a longer one and
173    /// keeping the prefix drops the rest of the dial string without a word.
174    #[test]
175    fn parse_with_prefix_rejects_a_longer_module_name() {
176        assert!(AudioEndpoint::parse_with_prefix("alsafoo/bar", "alsa").is_err());
177        assert!("alsafoo/bar"
178            .parse::<super::super::Endpoint>()
179            .is_err());
180    }
181}