use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::num::NonZeroUsize;
use std::time::Duration;
use bytes::Bytes;
use sipx_audio::g711;
use sipx_rtp::{Packet, RtpError};
use thiserror::Error;
use tokio::net::UdpSocket;
pub const MAX_DATAGRAM_BYTES: usize = 2048;
const ECHO_SSRC: u32 = 0x5350_5854;
const PCMU_PAYLOAD_TYPE: u8 = 0;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EchoError {
#[error("invalid {field}: {reason}")]
InvalidConfig {
field: &'static str,
reason: &'static str,
},
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Rtp(#[from] RtpError),
#[error("received RTP from {actual}, expected {expected}")]
UnexpectedPeer {
expected: SocketAddr,
actual: SocketAddr,
},
#[error("RTP payload type {0} is unsupported; the echo fixture accepts PCMU payload type 0")]
UnsupportedPayloadType(u8),
#[error("RTP datagram exceeds the {limit}-byte fixture limit")]
DatagramTooLarge {
limit: usize,
},
#[error("UDP sent {sent} of {expected} echo bytes")]
PartialSend {
sent: usize,
expected: usize,
},
#[error("echoed {received} of {expected} packets before the {within:?} run bound elapsed")]
TimedOut {
received: usize,
expected: usize,
within: Duration,
},
}
#[derive(Debug, Clone, Copy)]
pub struct EchoConfig {
bind: SocketAddr,
peer: SocketAddr,
packets: NonZeroUsize,
within: Duration,
}
impl EchoConfig {
pub fn new(
bind: SocketAddr,
peer: SocketAddr,
packets: NonZeroUsize,
within: Duration,
) -> Result<Self, EchoError> {
let broadcast = peer.ip() == IpAddr::V4(Ipv4Addr::BROADCAST);
if peer.ip().is_unspecified() || peer.ip().is_multicast() || broadcast || peer.port() == 0 {
return Err(EchoError::InvalidConfig {
field: "peer",
reason: "must be a concrete unicast address with a non-zero port",
});
}
if bind.is_ipv4() != peer.is_ipv4() {
return Err(EchoError::InvalidConfig {
field: "peer",
reason: "must use the bind address family",
});
}
if within.is_zero() {
return Err(EchoError::InvalidConfig {
field: "within",
reason: "must be greater than zero",
});
}
Ok(Self {
bind,
peer,
packets,
within,
})
}
#[must_use]
pub const fn bind_addr(self) -> SocketAddr {
self.bind
}
#[must_use]
pub const fn peer(self) -> SocketAddr {
self.peer
}
#[must_use]
pub const fn packets(self) -> NonZeroUsize {
self.packets
}
#[must_use]
pub const fn within(self) -> Duration {
self.within
}
}
#[derive(Debug)]
pub struct RtpEcho {
socket: UdpSocket,
local_addr: SocketAddr,
config: EchoConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EchoReport {
pub packets: usize,
pub samples: usize,
}
impl RtpEcho {
pub async fn bind(config: EchoConfig) -> Result<Self, EchoError> {
let socket = UdpSocket::bind(config.bind).await?;
let local_addr = socket.local_addr()?;
Ok(Self {
socket,
local_addr,
config,
})
}
#[must_use]
pub const fn local_addr(&self) -> SocketAddr {
self.local_addr
}
pub async fn run(self) -> Result<EchoReport, EchoError> {
let expected = self.config.packets.get();
let within = self.config.within;
let deadline = tokio::time::Instant::now() + within;
let mut buffer = [0_u8; MAX_DATAGRAM_BYTES + 1];
let mut packets = 0_usize;
let mut samples = 0_usize;
let mut sequence = 0_u16;
let mut timestamp = 0_u32;
while packets < expected {
let received = tokio::time::timeout_at(
deadline, self.socket.recv_from(&mut buffer),
)
.await;
let (length, source) = match received {
Ok(result) => result?,
Err(_) => {
return Err(EchoError::TimedOut {
received: packets,
expected,
within,
});
}
};
if source != self.config.peer {
return Err(EchoError::UnexpectedPeer {
expected: self.config.peer,
actual: source,
});
}
if length > MAX_DATAGRAM_BYTES {
return Err(EchoError::DatagramTooLarge {
limit: MAX_DATAGRAM_BYTES,
});
}
let input =
Packet::decode(&Bytes::copy_from_slice(buffer.get(..length).unwrap_or(&[])))?;
if input.payload_type != PCMU_PAYLOAD_TYPE {
return Err(EchoError::UnsupportedPayloadType(input.payload_type));
}
let decoded = g711::ulaw_decode_all(&input.payload);
let sample_count = decoded.len();
let output = Packet::new(
PCMU_PAYLOAD_TYPE,
sequence,
timestamp,
ECHO_SSRC,
Bytes::from(g711::ulaw_encode_all(&decoded)),
)
.encode();
let sent = match tokio::time::timeout_at(
deadline, self.socket.send_to(&output, self.config.peer),
)
.await
{
Ok(result) => result?,
Err(_) => {
return Err(EchoError::TimedOut {
received: packets,
expected,
within,
});
}
};
if sent != output.len() {
return Err(EchoError::PartialSend {
sent,
expected: output.len(),
});
}
packets = packets.saturating_add(1);
samples = samples.saturating_add(sample_count);
sequence = sequence.wrapping_add(1);
timestamp = timestamp.wrapping_add(u32::try_from(sample_count).unwrap_or(u32::MAX));
}
Ok(EchoReport { packets, samples })
}
}