use std::collections::HashMap;
use std::time::Duration;
use futures_util::{SinkExt, StreamExt};
use tokio::sync::{mpsc, watch};
use crate::error::{AriError, Result};
use crate::websocket::OwnedTask;
const MEDIA_CLOSE_TIMEOUT: Duration = Duration::from_millis(500);
const MEDIA_WRITE_TIMEOUT: Duration = Duration::from_secs(1);
pub const MAX_MEDIA_PAYLOAD_BYTES: usize = 65_500;
#[derive(Clone, Default)]
pub struct MediaConnectionOptions {
allow_insecure_remote: bool,
tls_trust: crate::config::TlsTrust,
}
impl MediaConnectionOptions {
pub fn new() -> Self {
Self::default()
}
pub fn allow_insecure_remote(mut self, allow: bool) -> Self {
self.allow_insecure_remote = allow;
self
}
pub fn private_ca_pem(mut self, pem: impl AsRef<[u8]>) -> Result<Self> {
self.tls_trust = crate::config::parse_private_ca_pem(pem.as_ref())?;
Ok(self)
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(tag = "event")]
#[non_exhaustive]
pub enum MediaEvent {
#[serde(rename = "MEDIA_START")]
MediaStart {
connection_id: String,
channel: String,
channel_id: String,
format: String,
optimal_frame_size: u32,
ptime: u32,
#[serde(default)]
channel_variables: HashMap<String, String>,
},
#[serde(rename = "DTMF_END")]
DtmfEnd { channel_id: String, digit: String },
#[serde(rename = "MEDIA_XOFF")]
MediaXoff { channel_id: String },
#[serde(rename = "MEDIA_XON")]
MediaXon { channel_id: String },
#[serde(rename = "STATUS")]
Status {
channel_id: String,
queue_length: u32,
xon_level: u32,
xoff_level: u32,
queue_full: bool,
bulk_media: bool,
media_paused: bool,
},
#[serde(rename = "MEDIA_BUFFERING_COMPLETED")]
MediaBufferingCompleted {
channel_id: String,
correlation_id: String,
},
#[serde(rename = "MEDIA_MARK_PROCESSED")]
MediaMarkProcessed {
channel_id: String,
correlation_id: String,
},
#[serde(rename = "QUEUE_DRAINED")]
QueueDrained { channel_id: String },
#[serde(rename = "ERROR")]
Error {
channel_id: String,
error_text: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum MediaDirection {
Both,
In,
Out,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MediaFlowControl {
Unknown,
Flowing,
Paused,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "command")]
#[non_exhaustive]
pub enum MediaCommand {
#[serde(rename = "ANSWER")]
Answer,
#[serde(rename = "HANGUP")]
Hangup,
#[serde(rename = "START_MEDIA_BUFFERING")]
StartMediaBuffering,
#[serde(rename = "STOP_MEDIA_BUFFERING")]
StopMediaBuffering {
#[serde(skip_serializing_if = "Option::is_none")]
correlation_id: Option<String>,
},
#[serde(rename = "FLUSH_MEDIA")]
FlushMedia,
#[serde(rename = "PAUSE_MEDIA")]
PauseMedia,
#[serde(rename = "CONTINUE_MEDIA")]
ContinueMedia,
#[serde(rename = "MARK_MEDIA")]
MarkMedia {
#[serde(skip_serializing_if = "Option::is_none")]
correlation_id: Option<String>,
},
#[serde(rename = "GET_STATUS")]
GetStatus,
#[serde(rename = "REPORT_QUEUE_DRAINED")]
ReportQueueDrained,
#[serde(rename = "SET_MEDIA_DIRECTION")]
SetMediaDirection { direction: MediaDirection },
}
pub struct MediaChannel {
event_rx: mpsc::Receiver<MediaEvent>,
audio_rx: mpsc::Receiver<Vec<u8>>,
control_tx: mpsc::Sender<String>,
outbound_audio_tx: mpsc::Sender<Vec<u8>>,
flow_control_rx: watch::Receiver<MediaFlowControl>,
shutdown_tx: watch::Sender<bool>,
task: OwnedTask,
}
impl std::fmt::Debug for MediaChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MediaChannel")
.field("connected", &!self.control_tx.is_closed())
.finish()
}
}
type OutboundWsStream =
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
type AcceptedWsStream = tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>;
impl MediaChannel {
pub async fn connect(url: &str) -> Result<Self> {
Self::connect_with_options(url, MediaConnectionOptions::default()).await
}
pub async fn connect_with_options(url: &str, options: MediaConnectionOptions) -> Result<Self> {
let parsed =
url::Url::parse(url).map_err(|error| AriError::InvalidUrl(error.to_string()))?;
let host = parsed
.host_str()
.ok_or_else(|| AriError::InvalidUrl("media websocket URL has no host".to_owned()))?;
if parsed.scheme() == "ws"
&& !options.allow_insecure_remote
&& !crate::config::is_loopback_host(host)
{
return Err(AriError::InvalidConfig(format!(
"cleartext media websocket to non-loopback host '{host}' requires allow_insecure_remote(true)"
)));
}
let tls_connector =
crate::websocket::connector_for_url(url, &options.tls_trust.rustls_roots)?;
let (ws_stream, _) = tokio::time::timeout(
std::time::Duration::from_secs(10),
tokio_tungstenite::connect_async_tls_with_config(
url,
Some(crate::websocket::websocket_config(MAX_MEDIA_PAYLOAD_BYTES)),
false,
Some(tls_connector),
),
)
.await
.map_err(|_| AriError::WebSocket("media websocket connection timed out".to_owned()))?
.map_err(|e| AriError::WebSocket(e.to_string()))?;
Ok(Self::spawn_outbound(ws_stream))
}
pub fn from_accepted(ws_stream: AcceptedWsStream) -> Result<Self> {
let config = ws_stream.get_config();
if config
.max_message_size
.is_none_or(|limit| limit > MAX_MEDIA_PAYLOAD_BYTES)
|| config
.max_frame_size
.is_none_or(|limit| limit > MAX_MEDIA_PAYLOAD_BYTES)
{
return Err(AriError::InvalidConfig(format!(
"accepted media websocket must cap messages and frames at {MAX_MEDIA_PAYLOAD_BYTES} bytes"
)));
}
let (event_tx, event_rx) = mpsc::channel(64);
let (audio_tx, audio_rx) = mpsc::channel(256);
let (control_tx, control_rx) = mpsc::channel(64);
let (outbound_audio_tx, outbound_audio_rx) = mpsc::channel(256);
let (flow_control_tx, flow_control_rx) = watch::channel(MediaFlowControl::Unknown);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let task_handle = tokio::spawn(media_loop(
ws_stream,
event_tx,
audio_tx,
control_rx,
outbound_audio_rx,
flow_control_tx,
shutdown_rx,
));
Ok(Self {
event_rx,
audio_rx,
control_tx,
outbound_audio_tx,
flow_control_rx,
shutdown_tx,
task: OwnedTask::new(task_handle),
})
}
pub async fn accept(stream: tokio::net::TcpStream) -> Result<Self> {
let websocket = tokio_tungstenite::accept_async_with_config(
stream,
Some(crate::websocket::websocket_config(MAX_MEDIA_PAYLOAD_BYTES)),
)
.await
.map_err(|error| AriError::WebSocket(error.to_string()))?;
Self::from_accepted(websocket)
}
fn spawn_outbound(ws_stream: OutboundWsStream) -> Self {
let (event_tx, event_rx) = mpsc::channel(64);
let (audio_tx, audio_rx) = mpsc::channel(256);
let (control_tx, control_rx) = mpsc::channel(64);
let (outbound_audio_tx, outbound_audio_rx) = mpsc::channel(256);
let (flow_control_tx, flow_control_rx) = watch::channel(MediaFlowControl::Unknown);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let task_handle = tokio::spawn(media_loop(
ws_stream,
event_tx,
audio_tx,
control_rx,
outbound_audio_rx,
flow_control_tx,
shutdown_rx,
));
Self {
event_rx,
audio_rx,
control_tx,
outbound_audio_tx,
flow_control_rx,
shutdown_tx,
task: OwnedTask::new(task_handle),
}
}
pub async fn recv_event(&mut self) -> Option<MediaEvent> {
self.event_rx.recv().await
}
pub async fn recv_audio(&mut self) -> Option<Vec<u8>> {
self.audio_rx.recv().await
}
pub async fn send_command(&self, cmd: MediaCommand) -> Result<()> {
let json = serde_json::to_string(&cmd).map_err(AriError::Json)?;
self.control_tx
.send(json)
.await
.map_err(|_| AriError::Disconnected)
}
pub async fn send_audio(&self, data: Vec<u8>) -> Result<()> {
if data.len() > MAX_MEDIA_PAYLOAD_BYTES {
return Err(AriError::WebSocket(format!(
"audio frame too large: {} bytes (max 65500)",
data.len()
)));
}
self.outbound_audio_tx
.send(data)
.await
.map_err(|_| AriError::Disconnected)
}
pub fn flow_control(&self) -> MediaFlowControl {
*self.flow_control_rx.borrow()
}
pub async fn flow_control_changed(&mut self) -> Option<MediaFlowControl> {
self.flow_control_rx.changed().await.ok()?;
Some(*self.flow_control_rx.borrow_and_update())
}
pub async fn answer(&self) -> Result<()> {
self.send_command(MediaCommand::Answer).await
}
pub async fn hangup(&self) -> Result<()> {
self.send_command(MediaCommand::Hangup).await
}
pub async fn start_buffering(&self) -> Result<()> {
self.send_command(MediaCommand::StartMediaBuffering).await
}
pub async fn stop_buffering(&self, correlation_id: Option<String>) -> Result<()> {
self.send_command(MediaCommand::StopMediaBuffering { correlation_id })
.await
}
pub async fn flush(&self) -> Result<()> {
self.send_command(MediaCommand::FlushMedia).await
}
pub async fn pause(&self) -> Result<()> {
self.send_command(MediaCommand::PauseMedia).await
}
pub async fn resume(&self) -> Result<()> {
self.send_command(MediaCommand::ContinueMedia).await
}
pub async fn mark(&self, correlation_id: Option<String>) -> Result<()> {
self.send_command(MediaCommand::MarkMedia { correlation_id })
.await
}
pub async fn get_status(&self) -> Result<()> {
self.send_command(MediaCommand::GetStatus).await
}
pub async fn report_queue_drained(&self) -> Result<()> {
self.send_command(MediaCommand::ReportQueueDrained).await
}
pub async fn set_media_direction(&self, direction: MediaDirection) -> Result<()> {
self.send_command(MediaCommand::SetMediaDirection { direction })
.await
}
pub fn disconnect(&self) {
let _ = self.shutdown_tx.send(true);
self.task.abort();
}
pub async fn disconnect_and_wait(&self) {
let _ = self.shutdown_tx.send(true);
self.task.shutdown_and_wait("ARI media websocket").await;
}
}
impl Drop for MediaChannel {
fn drop(&mut self) {
self.disconnect();
}
}
async fn media_loop<S>(
ws_stream: tokio_tungstenite::WebSocketStream<S>,
event_tx: mpsc::Sender<MediaEvent>,
audio_tx: mpsc::Sender<Vec<u8>>,
mut control_rx: mpsc::Receiver<String>,
mut outbound_audio_rx: mpsc::Receiver<Vec<u8>>,
flow_control_tx: watch::Sender<MediaFlowControl>,
mut shutdown_rx: watch::Receiver<bool>,
) where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
use tokio_tungstenite::tungstenite::Message;
let (mut write, mut read) = ws_stream.split();
loop {
tokio::select! {
biased;
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() {
tracing::debug!("media channel shutdown requested");
match tokio::time::timeout(
MEDIA_CLOSE_TIMEOUT,
write.send(Message::Close(None)),
)
.await
{
Ok(Ok(())) => {}
Ok(Err(error)) => {
tracing::debug!(error = %error, "failed to send media close frame");
}
Err(_) => tracing::debug!("timed out sending media close frame"),
}
return;
}
}
cmd = control_rx.recv() => {
match cmd {
Some(json) => {
match tokio::time::timeout(
MEDIA_WRITE_TIMEOUT,
write.send(Message::Text(json.into())),
).await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
tracing::warn!(error = %e, "failed to send media command");
return;
}
Err(_) => {
tracing::warn!("timed out sending media command");
return;
}
}
}
None => return,
}
}
msg = read.next() => {
match msg {
Some(Ok(Message::Text(text))) => {
match serde_json::from_str::<MediaEvent>(&text) {
Ok(event) => {
match &event {
MediaEvent::MediaXoff { .. } => {
flow_control_tx.send_replace(MediaFlowControl::Paused);
}
MediaEvent::MediaXon { .. } => {
flow_control_tx.send_replace(MediaFlowControl::Flowing);
}
_ => {}
}
match event_tx.try_send(event) {
Ok(()) => {}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
tracing::warn!("media event channel full, dropping event");
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
return;
}
}
}
Err(e) => {
tracing::warn!(
error = %e,
"failed to parse media event"
);
}
}
}
Some(Ok(Message::Binary(data))) => {
if data.len() > MAX_MEDIA_PAYLOAD_BYTES {
tracing::warn!(
payload_bytes = data.len(),
limit = MAX_MEDIA_PAYLOAD_BYTES,
"rejecting oversized inbound media frame"
);
return;
}
match audio_tx.try_send(data.to_vec()) {
Ok(()) => {}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
tracing::debug!("audio channel full, dropping frame");
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
return;
}
}
}
Some(Ok(Message::Close(_))) => {
tracing::debug!("media websocket closed by peer");
return;
}
Some(Err(e)) => {
tracing::warn!(error = %e, "media websocket read error");
return;
}
None => return,
_ => {}
}
}
audio = outbound_audio_rx.recv() => {
match audio {
Some(data) => {
match tokio::time::timeout(
MEDIA_WRITE_TIMEOUT,
write.send(Message::Binary(data.into())),
).await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
tracing::warn!(error = %e, "failed to send audio frame");
return;
}
Err(_) => {
tracing::warn!("timed out sending audio frame");
return;
}
}
}
None => return,
}
}
}
}
}