use std::net::SocketAddr;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::error::FlowcatError;
use crate::sip::rtp::{depacketize, JitterBuffer, RtpSender};
use crate::sip::sdp::G711Codec;
use crate::transport::media::{MediaIn, MediaTransport};
use crate::types::AudioChunk;
const CARRIER_RATE: u32 = 8000;
const FRAME_SAMPLES: usize = 160;
const FRAME_INTERVAL: std::time::Duration = std::time::Duration::from_millis(20);
const PREROLL_FRAMES: usize = 10;
const RTP_RECV_BUF: usize = 2048;
const INBOUND_CHAN_DEPTH: usize = 50;
const OUTBOUND_CHAN_DEPTH: usize = 64;
pub struct SipTransport {
call_id: String,
started: bool,
inbound_rx: mpsc::Receiver<AudioChunk>,
outbound_tx: Option<mpsc::Sender<Vec<i16>>>,
hangup: CancellationToken,
rx_task: tokio::task::JoinHandle<()>,
tx_task: tokio::task::JoinHandle<()>,
}
impl SipTransport {
pub fn start(
socket: UdpSocket,
peer: SocketAddr,
codec: G711Codec,
call_id: String,
hangup: CancellationToken,
) -> Self {
let socket = Arc::new(socket);
let (inbound_tx, inbound_rx) = mpsc::channel::<AudioChunk>(INBOUND_CHAN_DEPTH);
let (outbound_tx, outbound_rx) = mpsc::channel::<Vec<i16>>(OUTBOUND_CHAN_DEPTH);
let rx_task = tokio::spawn(rx_loop(
socket.clone(),
peer,
codec,
inbound_tx,
hangup.clone(),
));
let tx_task = tokio::spawn(tx_loop(socket, peer, codec, outbound_rx, hangup.clone()));
Self {
call_id,
started: false,
inbound_rx,
outbound_tx: Some(outbound_tx),
hangup,
rx_task,
tx_task,
}
}
}
impl Drop for SipTransport {
fn drop(&mut self) {
self.hangup.cancel();
self.rx_task.abort();
self.tx_task.abort();
}
}
#[async_trait]
impl MediaTransport for SipTransport {
async fn recv(&mut self) -> Option<MediaIn> {
if !self.started {
self.started = true;
return Some(MediaIn::StreamStart {
call_id: self.call_id.clone(),
});
}
tokio::select! {
chunk = self.inbound_rx.recv() => match chunk {
Some(c) => Some(MediaIn::Audio(c)),
None => Some(MediaIn::Stop),
},
_ = self.hangup.cancelled() => Some(MediaIn::Stop),
}
}
async fn send_audio(&mut self, chunk: AudioChunk) -> Result<(), FlowcatError> {
let Some(tx) = self.outbound_tx.as_ref() else {
return Err(FlowcatError::Transport("SIP transport closed".into()));
};
tx.send(chunk.pcm).await.map_err(|_| {
FlowcatError::Transport("SIP RTP send pump stopped (peer hung up?)".into())
})
}
async fn send_clear(&mut self) -> Result<(), FlowcatError> {
Ok(())
}
fn carrier_rate(&self) -> u32 {
CARRIER_RATE
}
}
fn decode_g711(codec: G711Codec, payload: &[u8]) -> Vec<i16> {
match codec {
G711Codec::Pcmu => crate::codec::ulaw_to_pcm16(payload),
G711Codec::Pcma => crate::codec::alaw_to_pcm16(payload),
}
}
fn encode_g711(codec: G711Codec, pcm: &[i16]) -> Vec<u8> {
match codec {
G711Codec::Pcmu => crate::codec::pcm16_to_ulaw(pcm),
G711Codec::Pcma => crate::codec::pcm16_to_alaw(pcm),
}
}
async fn rx_loop(
socket: Arc<UdpSocket>,
initial_peer: SocketAddr,
codec: G711Codec,
inbound_tx: mpsc::Sender<AudioChunk>,
hangup: CancellationToken,
) {
let mut jitter = JitterBuffer::new();
let mut learned_peer: Option<SocketAddr> = None;
let mut current_ssrc: Option<u32> = None;
let mut buf = vec![0u8; RTP_RECV_BUF];
loop {
let (len, src) = tokio::select! {
_ = hangup.cancelled() => break,
r = socket.recv_from(&mut buf) => match r {
Ok(v) => v,
Err(e) => {
tracing::debug!(error = %e, "SIP RTP recv error; ending RX loop");
break;
}
},
};
match learned_peer {
None => {
if src != initial_peer {
tracing::debug!(
sdp_peer = %initial_peer, learned = %src,
"symmetric RTP: learned peer source differs from SDP"
);
}
learned_peer = Some(src);
}
Some(p) if src != p => {
tracing::trace!(expected = %p, got = %src, "dropping RTP from foreign source");
continue;
}
_ => {}
}
let pkt = match depacketize(&buf[..len]) {
Ok(p) => p,
Err(e) => {
tracing::trace!(error = %e, "dropping malformed RTP packet");
continue;
}
};
match current_ssrc {
Some(s) if s != pkt.ssrc => {
tracing::debug!(
old = s,
new = pkt.ssrc,
"RTP SSRC changed; resetting jitter buffer"
);
for released in jitter.drain() {
let pcm = decode_g711(codec, &released.payload);
if !pcm.is_empty()
&& inbound_tx
.send(AudioChunk::new(pcm, CARRIER_RATE))
.await
.is_err()
{
return;
}
}
jitter.reset();
current_ssrc = Some(pkt.ssrc);
}
None => current_ssrc = Some(pkt.ssrc),
_ => {}
}
for released in jitter.push(pkt) {
let pcm = decode_g711(codec, &released.payload);
if pcm.is_empty() {
continue;
}
if inbound_tx
.send(AudioChunk::new(pcm, CARRIER_RATE))
.await
.is_err()
{
return;
}
}
}
for released in jitter.drain() {
let pcm = decode_g711(codec, &released.payload);
if !pcm.is_empty() {
let _ = inbound_tx.send(AudioChunk::new(pcm, CARRIER_RATE)).await;
}
}
}
async fn tx_loop(
socket: Arc<UdpSocket>,
peer: SocketAddr,
codec: G711Codec,
mut outbound_rx: mpsc::Receiver<Vec<i16>>,
hangup: CancellationToken,
) {
let mut sender = RtpSender::new(codec.payload_type());
let mut pending: Vec<i16> = Vec::with_capacity(FRAME_SAMPLES * 2);
let mut ticker = tokio::time::interval(FRAME_INTERVAL);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let silence = encode_g711(codec, &[0i16; FRAME_SAMPLES]);
for _ in 0..PREROLL_FRAMES {
tokio::select! {
_ = hangup.cancelled() => return,
_ = ticker.tick() => {
let dgram = sender.packetize(&silence);
if let Err(e) = socket.send_to(&dgram, peer).await {
tracing::debug!(error = %e, "SIP RTP preroll send error; ending TX loop");
return;
}
}
}
}
let mut channel_open = true;
loop {
tokio::select! {
_ = hangup.cancelled() => break,
maybe = outbound_rx.recv(), if channel_open => match maybe {
Some(mut pcm) => pending.append(&mut pcm),
None => channel_open = false,
},
_ = ticker.tick() => {
if pending.len() >= FRAME_SAMPLES {
let frame: Vec<i16> = pending.drain(..FRAME_SAMPLES).collect();
let payload = encode_g711(codec, &frame);
let dgram = sender.packetize(&payload);
if let Err(e) = socket.send_to(&dgram, peer).await {
tracing::debug!(error = %e, "SIP RTP send error; ending TX loop");
break;
}
} else if !channel_open {
break;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sip::rtp::{RtpSender, PT_PCMU};
async fn socket_pair() -> (UdpSocket, SocketAddr, UdpSocket, SocketAddr) {
let a = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let b = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let a_addr = a.local_addr().unwrap();
let b_addr = b.local_addr().unwrap();
(a, a_addr, b, b_addr)
}
#[tokio::test]
async fn fed_rtp_packets_surface_as_audio_after_streamstart() {
let (transport_sock, transport_addr, peer_sock, peer_addr) = socket_pair().await;
let hangup = CancellationToken::new();
let mut tr = SipTransport::start(
transport_sock,
peer_addr,
G711Codec::Pcmu,
"call-sip-1".to_string(),
hangup.clone(),
);
match tr.recv().await {
Some(MediaIn::StreamStart { call_id }) => assert_eq!(call_id, "call-sip-1"),
other => panic!("expected StreamStart, got {other:?}"),
}
let mut sender = RtpSender::with_seeds(PT_PCMU, 0x1234, 1000, 0);
let payload: Vec<u8> = (0..FRAME_SAMPLES).map(|i| (i % 200) as u8 + 1).collect();
for _ in 0..6 {
let dgram = sender.packetize(&payload);
peer_sock.send_to(&dgram, transport_addr).await.unwrap();
}
let got = tokio::time::timeout(std::time::Duration::from_secs(2), tr.recv())
.await
.expect("recv timed out");
match got {
Some(MediaIn::Audio(chunk)) => {
assert_eq!(chunk.sample_rate, 8000);
assert_eq!(chunk.pcm.len(), FRAME_SAMPLES);
assert_eq!(chunk.pcm, crate::codec::ulaw_to_pcm16(&payload));
}
other => panic!("expected Audio, got {other:?}"),
}
}
#[tokio::test]
async fn hangup_signal_surfaces_as_stop() {
let (transport_sock, _t_addr, _peer_sock, peer_addr) = socket_pair().await;
let hangup = CancellationToken::new();
let mut tr = SipTransport::start(
transport_sock,
peer_addr,
G711Codec::Pcmu,
"call-sip-2".to_string(),
hangup.clone(),
);
assert!(matches!(tr.recv().await, Some(MediaIn::StreamStart { .. })));
hangup.cancel();
assert_eq!(tr.recv().await, Some(MediaIn::Stop));
}
#[tokio::test]
async fn rtp_from_foreign_source_is_dropped() {
let (transport_sock, transport_addr, peer_sock, peer_addr) = socket_pair().await;
let foreign = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let hangup = CancellationToken::new();
let mut tr = SipTransport::start(
transport_sock,
peer_addr,
G711Codec::Pcmu,
"call-sip-3".to_string(),
hangup.clone(),
);
assert!(matches!(tr.recv().await, Some(MediaIn::StreamStart { .. })));
let mut good = RtpSender::with_seeds(PT_PCMU, 1, 10, 0);
let payload: Vec<u8> = vec![0x55u8; FRAME_SAMPLES];
peer_sock
.send_to(&good.packetize(&payload), transport_addr)
.await
.unwrap();
let mut bad = RtpSender::with_seeds(PT_PCMU, 999, 5000, 0);
let bad_payload: Vec<u8> = vec![0x01u8; FRAME_SAMPLES];
foreign
.send_to(&bad.packetize(&bad_payload), transport_addr)
.await
.unwrap();
for _ in 0..6 {
peer_sock
.send_to(&good.packetize(&payload), transport_addr)
.await
.unwrap();
}
let good_pcm = crate::codec::ulaw_to_pcm16(&payload);
let bad_pcm = crate::codec::ulaw_to_pcm16(&bad_payload);
for _ in 0..3 {
match tokio::time::timeout(std::time::Duration::from_secs(2), tr.recv())
.await
.expect("recv timed out")
{
Some(MediaIn::Audio(chunk)) => {
assert_eq!(chunk.pcm, good_pcm, "got foreign audio!");
assert_ne!(chunk.pcm, bad_pcm);
}
Some(MediaIn::Stop) => break,
other => panic!("unexpected {other:?}"),
}
}
}
#[tokio::test]
async fn send_audio_emits_paced_rtp_frames_to_peer() {
let (transport_sock, _t_addr, peer_sock, peer_addr) = socket_pair().await;
let hangup = CancellationToken::new();
let mut tr = SipTransport::start(
transport_sock,
peer_addr,
G711Codec::Pcmu,
"call-sip-4".to_string(),
hangup.clone(),
);
let _ = tr.recv().await;
let pcm: Vec<i16> = (0..(FRAME_SAMPLES * 2))
.map(|i| ((i % 100) as i16 - 50) * 100)
.collect();
tr.send_audio(AudioChunk::new(pcm.clone(), 8000))
.await
.unwrap();
let silence_payload = crate::codec::pcm16_to_ulaw(&[0i16; FRAME_SAMPLES]);
let mut buf = vec![0u8; RTP_RECV_BUF];
let mut pkts = Vec::new();
for _ in 0..(PREROLL_FRAMES + 2) {
let (len, from) = tokio::time::timeout(
std::time::Duration::from_secs(2),
peer_sock.recv_from(&mut buf),
)
.await
.expect("no RTP received")
.unwrap();
assert_eq!(from.ip(), std::net::Ipv4Addr::LOCALHOST);
pkts.push(depacketize(&buf[..len]).unwrap());
}
for i in 0..PREROLL_FRAMES {
assert_eq!(pkts[i].payload_type, PT_PCMU);
assert_eq!(
pkts[i].payload, silence_payload,
"preroll frame {i} not silence"
);
if i > 0 {
assert_eq!(pkts[i].seq, pkts[i - 1].seq.wrapping_add(1));
}
}
let audio0 = &pkts[PREROLL_FRAMES];
assert_eq!(audio0.payload_type, PT_PCMU);
assert_eq!(audio0.payload.len(), FRAME_SAMPLES);
assert_eq!(
audio0.payload,
crate::codec::pcm16_to_ulaw(&pcm[..FRAME_SAMPLES])
);
assert_eq!(audio0.seq, pkts[PREROLL_FRAMES - 1].seq.wrapping_add(1));
let audio1 = &pkts[PREROLL_FRAMES + 1];
assert_eq!(audio1.seq, audio0.seq.wrapping_add(1));
assert_eq!(
audio1.timestamp,
audio0.timestamp.wrapping_add(FRAME_SAMPLES as u32)
);
}
}