use rvoip_media_core::codec::transcoding::Transcoder;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tracing::{debug, trace, warn};
use crate::stream::MediaFrame;
pub const DEFAULT_TELEPHONE_EVENT_PT: u8 = 101;
pub struct TranscoderSwap {
pub new_transcoder: Option<Transcoder>,
pub new_from_pt: u8,
pub new_to_pt: u8,
pub ack: Option<tokio::sync::oneshot::Sender<()>>,
}
pub fn spawn_pump(
direction: &'static str,
from: mpsc::Receiver<MediaFrame>,
to: mpsc::Sender<MediaFrame>,
transcoder: Option<Transcoder>,
from_pt: u8,
to_pt: u8,
) -> JoinHandle<()> {
let (_swap_tx, swap_rx) = mpsc::channel::<TranscoderSwap>(1);
spawn_pump_with_swap(direction, from, to, transcoder, from_pt, to_pt, swap_rx)
}
pub fn spawn_pump_with_swap(
direction: &'static str,
mut from: mpsc::Receiver<MediaFrame>,
to: mpsc::Sender<MediaFrame>,
mut transcoder: Option<Transcoder>,
mut from_pt: u8,
mut to_pt: u8,
mut swap_rx: mpsc::Receiver<TranscoderSwap>,
) -> JoinHandle<()> {
tokio::spawn(async move {
let mut need_transcode = transcoder.is_some() && from_pt != to_pt;
debug!(
direction,
from_pt, to_pt, need_transcode, "rvoip-core::frame_pump: started"
);
let mut swap_open = true;
loop {
let frame_opt = if swap_open {
tokio::select! {
biased;
swap = swap_rx.recv() => {
match swap {
Some(s) => {
transcoder = s.new_transcoder;
from_pt = s.new_from_pt;
to_pt = s.new_to_pt;
need_transcode = transcoder.is_some() && from_pt != to_pt;
metrics::counter!(
"uctp_bridge_transcoder_swaps_total",
"direction" => direction,
)
.increment(1);
if let Some(ack) = s.ack {
let _ = ack.send(());
}
debug!(
direction,
from_pt,
to_pt,
need_transcode,
"rvoip-core::frame_pump: hot-swapped transcoder"
);
continue;
}
None => {
swap_open = false;
continue;
}
}
}
f = from.recv() => f,
}
} else {
from.recv().await
};
let Some(mut frame) = frame_opt else {
debug!(direction, "rvoip-core::frame_pump: source closed; exiting");
return;
};
{
let is_telephone_event = frame.payload_type == Some(DEFAULT_TELEPHONE_EVENT_PT);
if is_telephone_event {
metrics::counter!(
"rvoip_bridge_dtmf_passthrough_total",
"direction" => direction,
)
.increment(1);
trace!(
direction,
"rvoip-core::frame_pump: PT={} (RFC 4733 telephone-event) — passing through",
DEFAULT_TELEPHONE_EVENT_PT
);
if to.send(frame).await.is_err() {
debug!(direction, "rvoip-core::frame_pump: peer closed; exiting");
return;
}
continue;
}
if need_transcode {
let t = transcoder.as_mut().expect("checked above");
match t.transcode(&frame.payload, from_pt, to_pt).await {
Ok(bytes) => frame.payload = bytes.into(),
Err(e) => {
if frame.payload.len() == 4 {
metrics::counter!(
"rvoip_bridge_dtmf_passthrough_total",
"direction" => direction,
)
.increment(1);
trace!(
direction,
"rvoip-core::frame_pump: 4-byte transcode failure — likely RFC 4733 DTMF without PT label; passing through"
);
} else {
warn!(
direction,
from_pt,
to_pt,
error = %e,
bytes = frame.payload.len(),
"rvoip-core::frame_pump: transcode failed; dropping frame"
);
metrics::counter!(
"rvoip_bridge_transcode_errors_total",
"direction" => direction,
)
.increment(1);
continue;
}
}
}
}
trace!(direction, bytes = frame.payload.len(), "frame");
if to.send(frame).await.is_err() {
debug!(direction, "rvoip-core::frame_pump: peer closed; exiting");
return;
}
} }
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::StreamId;
use crate::stream::StreamKind;
use bytes::Bytes;
use chrono::Utc;
fn mk_frame(seq: u8) -> MediaFrame {
MediaFrame {
stream_id: StreamId::new(),
kind: StreamKind::Audio,
payload: Bytes::from(vec![seq]),
timestamp_rtp: 0,
captured_at: Utc::now(),
payload_type: None,
}
}
#[tokio::test]
async fn pump_passes_through_when_pts_match() {
let (tx_from, rx_from) = mpsc::channel::<MediaFrame>(8);
let (tx_to, mut rx_to) = mpsc::channel::<MediaFrame>(8);
let pump = spawn_pump("test", rx_from, tx_to, None, 111, 111);
for i in 0u8..5 {
tx_from.send(mk_frame(i)).await.unwrap();
}
drop(tx_from);
let mut received = Vec::new();
while let Some(f) = rx_to.recv().await {
received.push(f.payload[0]);
}
assert_eq!(received, (0u8..5).collect::<Vec<_>>());
pump.await.unwrap();
}
#[tokio::test]
async fn pump_aborts_cleanly() {
let (tx_from, rx_from) = mpsc::channel::<MediaFrame>(8);
let (tx_to, _rx_to) = mpsc::channel::<MediaFrame>(8);
let pump = spawn_pump("test", rx_from, tx_to, None, 111, 111);
let abort = pump.abort_handle();
tx_from.send(mk_frame(0)).await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
abort.abort();
let outcome = pump.await;
assert!(
matches!(&outcome, Err(e) if e.is_cancelled()),
"expected cancelled task; got {:?}",
outcome.as_ref().map(|_| "ok").unwrap_or("err")
);
}
#[tokio::test]
async fn pump_passes_through_4byte_payload_when_transcode_fails() {
use rvoip_media_core::codec::transcoding::Transcoder;
use rvoip_media_core::processing::format::FormatConverter;
use std::sync::Arc;
use tokio::sync::RwLock;
let (tx_from, rx_from) = mpsc::channel::<MediaFrame>(8);
let (tx_to, mut rx_to) = mpsc::channel::<MediaFrame>(8);
let fc = Arc::new(RwLock::new(FormatConverter::new()));
let transcoder = Transcoder::new(fc);
let pump = spawn_pump("test_dtmf", rx_from, tx_to, Some(transcoder), 111, 0);
let dtmf_like = MediaFrame {
stream_id: StreamId::new(),
kind: StreamKind::Audio,
payload: Bytes::from(vec![0x01, 0x0F, 0x00, 0x50]), timestamp_rtp: 0,
captured_at: Utc::now(),
payload_type: None,
};
tx_from.send(dtmf_like.clone()).await.unwrap();
let received = tokio::time::timeout(std::time::Duration::from_millis(500), rx_to.recv())
.await
.expect("dtmf passthrough must deliver the frame")
.expect("channel still open");
assert_eq!(
received.payload.as_ref(),
dtmf_like.payload.as_ref(),
"4-byte DTMF-like payload must pass through unchanged"
);
drop(tx_from);
pump.await.unwrap();
}
#[tokio::test]
async fn pump_passes_through_telephone_event_pt_without_transcode() {
let (tx_from, rx_from) = mpsc::channel::<MediaFrame>(8);
let (tx_to, mut rx_to) = mpsc::channel::<MediaFrame>(8);
let pump = spawn_pump("dtmf_pt_test", rx_from, tx_to, None, 0, 0);
let dtmf = MediaFrame {
stream_id: StreamId::new(),
kind: StreamKind::Audio,
payload: Bytes::from(vec![0x01, 0x0F, 0x00, 0x50]),
timestamp_rtp: 0,
captured_at: Utc::now(),
payload_type: Some(super::DEFAULT_TELEPHONE_EVENT_PT),
};
tx_from.send(dtmf.clone()).await.unwrap();
let received = tokio::time::timeout(std::time::Duration::from_millis(500), rx_to.recv())
.await
.expect("dtmf must pass through")
.expect("channel still open");
assert_eq!(received.payload.as_ref(), dtmf.payload.as_ref());
assert_eq!(
received.payload_type,
Some(super::DEFAULT_TELEPHONE_EVENT_PT)
);
drop(tx_from);
pump.await.unwrap();
}
#[tokio::test]
async fn pump_exits_when_destination_closes() {
let (tx_from, rx_from) = mpsc::channel::<MediaFrame>(8);
let (tx_to, rx_to) = mpsc::channel::<MediaFrame>(8);
drop(rx_to); let pump = spawn_pump("test", rx_from, tx_to, None, 111, 111);
tx_from.send(mk_frame(0)).await.unwrap();
tokio::time::timeout(std::time::Duration::from_secs(2), pump)
.await
.expect("pump should exit on closed dest")
.unwrap();
}
#[tokio::test]
async fn pump_hot_swaps_transcoder_when_swap_channel_receives() {
let (tx_from, rx_from) = mpsc::channel::<MediaFrame>(8);
let (tx_to, mut rx_to) = mpsc::channel::<MediaFrame>(8);
let (swap_tx, swap_rx) = mpsc::channel::<TranscoderSwap>(4);
let pump = spawn_pump_with_swap(
"swap_test",
rx_from,
tx_to,
None,
0, 0, swap_rx,
);
tx_from.send(mk_frame(1)).await.unwrap();
let first = tokio::time::timeout(std::time::Duration::from_millis(500), rx_to.recv())
.await
.expect("first frame")
.expect("channel open");
assert_eq!(first.payload[0], 1);
swap_tx
.send(TranscoderSwap {
new_transcoder: None,
new_from_pt: 0,
new_to_pt: 0,
ack: None,
})
.await
.expect("swap sent");
tokio::task::yield_now().await;
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
tx_from.send(mk_frame(2)).await.unwrap();
let second = tokio::time::timeout(std::time::Duration::from_millis(500), rx_to.recv())
.await
.expect("second frame post-swap")
.expect("channel open");
assert_eq!(second.payload[0], 2);
drop(tx_from);
drop(swap_tx);
let _ = pump.await;
}
}