use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::core::random_u32;
use crate::dialog::{DialogId, Role, RouteSet};
use crate::media::{Bitrate, JitterBuffer, JitterBufferConfig, OpusConfig, PlayoutDecision};
use crate::rtp::rtcp::{RtcpCompound, RtcpPacket};
use crate::rtp::session::CongestionController;
use crate::rtp::{RtpPacket, RtpSession};
use crate::sdp::negotiation::{Codec, NegotiatedMedia};
use crate::sdp::parser::SessionDescription;
use crate::sip::SipRequest;
use crate::session::bitrate_bridge::BitrateBridge;
use crate::session::session_codec::SessionCodec;
#[derive(Debug, Clone)]
pub struct Dialog {
id: DialogId,
local_uri: String,
remote_uri: String,
local_cseq: u32,
role: Role,
route_set: RouteSet,
remote_target: String,
local_contact: String,
}
impl Dialog {
pub fn new_uac(
call_id: String,
from_tag: String,
to_tag: String,
local_uri: String,
remote_uri: String,
cseq: u32,
) -> Self {
Self {
id: DialogId::new(&call_id, &from_tag, &to_tag),
local_uri,
remote_uri,
local_cseq: cseq,
role: Role::Uac,
route_set: RouteSet::new(),
remote_target: String::new(),
local_contact: String::new(),
}
}
pub fn new_uas(
call_id: String,
from_tag: String,
to_tag: String,
local_uri: String,
remote_uri: String,
cseq: u32,
) -> Self {
Self {
id: DialogId::new(&call_id, &to_tag, &from_tag),
local_uri,
remote_uri,
local_cseq: cseq,
role: Role::Uas,
route_set: RouteSet::new(),
remote_target: String::new(),
local_contact: String::new(),
}
}
pub fn id(&self) -> &DialogId {
&self.id
}
pub fn local_uri(&self) -> &str {
&self.local_uri
}
pub fn remote_uri(&self) -> &str {
&self.remote_uri
}
pub fn local_cseq(&self) -> u32 {
self.local_cseq
}
pub fn next_cseq(&mut self) -> u32 {
self.local_cseq += 1;
self.local_cseq
}
pub fn role(&self) -> Role {
self.role
}
pub fn route_set(&self) -> &RouteSet {
&self.route_set
}
pub fn remote_target(&self) -> &str {
&self.remote_target
}
pub fn local_contact(&self) -> &str {
&self.local_contact
}
pub fn set_remote_target(&mut self, target: String) {
self.remote_target = target;
}
pub fn set_route_set_from_record_routes(&mut self, record_routes: &[String], reverse: bool) {
self.route_set = RouteSet::from_record_route_values(record_routes, reverse);
}
pub fn set_route_set(&mut self, route_set: RouteSet) {
self.route_set = route_set;
}
pub fn set_local_contact(&mut self, contact: String) {
self.local_contact = contact;
}
pub fn populate_uas_from_invite(&mut self, invite: &SipRequest, local_contact: String) {
if self.role != Role::Uas {
return;
}
let record_routes = invite.record_routes();
if !record_routes.is_empty() {
self.set_route_set_from_record_routes(&record_routes, false);
}
if let Some(contact) = invite.contact_uri() {
self.remote_target = contact.to_string();
}
self.local_contact = local_contact;
}
pub fn populate_uac_from_response(
&mut self,
response: &crate::sip::SipResponse,
local_contact: String,
) {
if self.role != Role::Uac {
return;
}
let record_routes = response.record_routes();
if !record_routes.is_empty() {
self.set_route_set_from_record_routes(&record_routes, true);
}
if let Some(contact) = response.contact_uri() {
self.remote_target = contact.to_string();
}
if !local_contact.is_empty() {
self.local_contact = local_contact;
}
}
pub(crate) fn to_invite_dialog(&self) -> crate::dialog::InviteDialog {
let info = crate::dialog::DialogInfo {
id: self.id.clone(),
state: crate::dialog::DialogState::Confirmed,
local_seq: self.local_cseq,
remote_seq: None,
local_uri: self.local_uri.clone(),
remote_uri: self.remote_uri.clone(),
remote_target: if self.remote_target.is_empty() {
self.remote_uri.clone()
} else {
self.remote_target.clone()
},
local_contact: self.local_contact.clone(),
route_set: self.route_set.clone(),
secure: false,
};
crate::dialog::InviteDialog::from_dialog_info(info, self.role)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallState {
Idle,
Inviting,
Ringing,
EarlyMedia,
Established,
Terminating,
Terminated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallDirection {
Outbound,
Inbound,
}
#[derive(Debug, Clone)]
pub struct CallConfig {
pub local_uri: String,
pub local_name: Option<String>,
pub codecs: Vec<Codec>,
pub rtp_port_start: u16,
pub rtp_port_end: u16,
pub session_expires: Duration,
pub min_se: Duration,
}
impl Default for CallConfig {
fn default() -> Self {
Self {
local_uri: "sip:user@127.0.0.1".to_string(),
local_name: None,
codecs: vec![Codec::pcmu(), Codec::pcma()],
rtp_port_start: 10000,
rtp_port_end: 20000,
session_expires: Duration::from_secs(1800),
min_se: Duration::from_secs(90),
}
}
}
#[derive(Debug, Clone)]
pub enum CallEvent {
StateChanged(CallState),
Ringing,
EarlyMedia,
Answered,
Ended(CallEndReason),
AudioReceived(Vec<i16>),
DtmfReceived(char),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallEndReason {
NormalClearing,
Rejected,
Busy,
NoAnswer,
NetworkError,
Canceled,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CallId(pub String);
impl CallId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
}
impl Default for CallId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for CallId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
struct AdaptiveCongestion {
cc: CongestionController,
bridge: BitrateBridge,
}
impl std::fmt::Debug for AdaptiveCongestion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AdaptiveCongestion")
.field("target_bitrate", &self.cc.target_bitrate())
.finish()
}
}
pub struct MediaSession {
rtp_session: RtpSession,
jitter_buffer: JitterBuffer,
codec: SessionCodec,
adaptive: Option<AdaptiveCongestion>,
remote_addr: Option<SocketAddr>,
local_port: u16,
active: bool,
}
impl std::fmt::Debug for MediaSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MediaSession")
.field("rtp_session", &self.rtp_session)
.field("jitter_buffer", &self.jitter_buffer)
.field("adaptive", &self.adaptive)
.field("remote_addr", &self.remote_addr)
.field("local_port", &self.local_port)
.field("active", &self.active)
.finish()
}
}
impl MediaSession {
pub fn for_negotiated(ssrc: u32, negotiated: &Codec, local_port: u16) -> Result<Self, String> {
let mut codec = SessionCodec::for_negotiated(negotiated)?;
let adaptive = if codec.as_adaptive_mut().is_some() {
let initial_bps: u64 = match OpusConfig::fullband_speech().bitrate {
Bitrate::Bits(b) => u64::from(b),
_ => 32_000,
};
if negotiated.clock_rate != 48_000 || negotiated.channels != 1 {
tracing::warn!(
sdp_clock_rate = negotiated.clock_rate,
sdp_channels = negotiated.channels,
"SDP-negotiated Opus parameters ignored; codec built at 48 kHz / 1 ch (HLD v1 limitation)"
);
}
Some(AdaptiveCongestion {
cc: CongestionController::new(initial_bps, 6_000, 128_000),
bridge: BitrateBridge::new(),
})
} else {
None
};
let samples_per_packet = codec.samples_per_frame() as u32;
let mut jb_config = JitterBufferConfig {
clock_rate: negotiated.clock_rate,
samples_per_packet,
..JitterBufferConfig::default()
};
if negotiated.clock_rate == 8000 && samples_per_packet == 160 {
jb_config = JitterBufferConfig::g711();
}
Ok(Self {
rtp_session: RtpSession::new(ssrc, negotiated.payload_type, negotiated.clock_rate),
jitter_buffer: JitterBuffer::new(jb_config),
codec,
adaptive,
remote_addr: None,
local_port,
active: false,
})
}
pub fn set_remote(&mut self, addr: SocketAddr) {
self.remote_addr = Some(addr);
self.active = true;
}
pub fn encode_audio(&mut self, samples: &[i16], marker: bool) -> Result<RtpPacket, String> {
let encoded = self.codec.encode(samples)?;
Ok(self
.rtp_session
.create_packet(encoded, samples.len() as u32, marker))
}
pub fn receive_rtp(&mut self, packet: &RtpPacket) -> Option<(PlayoutDecision, Vec<i16>)> {
self.rtp_session.receive_packet(packet);
let decoded = match self.codec.decode(&packet.payload) {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "codec decode failed; dropping packet");
return None;
}
};
self.jitter_buffer
.push(packet.sequence_number, packet.timestamp, decoded);
if self.jitter_buffer.is_primed() {
let (decision, samples) = self.jitter_buffer.pop();
Some((decision, samples))
} else {
None
}
}
pub fn handle_rtcp(&mut self, bytes: &[u8]) -> Result<(), String> {
let compound = RtcpCompound::parse(bytes)?;
let Some(adapt) = self.adaptive.as_mut() else {
return Ok(());
};
for packet in &compound.packets {
if let RtcpPacket::Remb(remb) = packet {
adapt.cc.on_remb(remb.bitrate);
}
}
Ok(())
}
pub fn tick(&mut self, now: Instant) {
let Some(adapt) = self.adaptive.as_mut() else {
return;
};
adapt.cc.update();
let target = adapt.cc.target_bitrate();
if let Some(adaptive_codec) = self.codec.as_adaptive_mut() {
if let Err(e) = adapt.bridge.poll(target, adaptive_codec, now) {
tracing::warn!(error = %e, "BitrateBridge::poll rejected by codec");
}
}
}
pub fn get_audio_frame(&mut self) -> (PlayoutDecision, Vec<i16>) {
self.jitter_buffer.pop()
}
pub fn local_port(&self) -> u16 {
self.local_port
}
pub fn remote_addr(&self) -> Option<SocketAddr> {
self.remote_addr
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn rtp_session(&self) -> &RtpSession {
&self.rtp_session
}
pub fn jitter_stats(&self) -> &crate::media::JitterStats {
self.jitter_buffer.stats()
}
}
#[derive(Debug)]
pub(crate) struct PendingAnswer {
pub(crate) offer: SessionDescription,
pub(crate) negotiated: NegotiatedMedia,
}
#[derive(Debug)]
pub struct Call {
id: CallId,
state: CallState,
direction: CallDirection,
config: Arc<CallConfig>,
remote_uri: String,
dialog: Option<Dialog>,
negotiated_media: Option<NegotiatedMedia>,
media: Option<MediaSession>,
pending_answer: Option<PendingAnswer>,
events: Vec<CallEvent>,
pub session_expires: Option<Duration>,
pub min_se: Duration,
pub refresher: Option<crate::sip::headers::Refresher>,
pub refresh_at: Option<Instant>,
pub expiry_at: Option<Instant>,
pub update_unsupported: bool,
pub uac_in_flight: bool,
}
impl Call {
pub fn new_outbound(config: Arc<CallConfig>, remote_uri: String) -> Self {
let min_se = config.min_se;
Self {
id: CallId::new(),
state: CallState::Idle,
direction: CallDirection::Outbound,
config,
remote_uri,
dialog: None,
negotiated_media: None,
media: None,
pending_answer: None,
events: Vec::new(),
session_expires: None,
min_se,
refresher: None,
refresh_at: None,
expiry_at: None,
update_unsupported: false,
uac_in_flight: false,
}
}
pub fn new_inbound(config: Arc<CallConfig>, remote_uri: String, dialog: Dialog) -> Self {
let min_se = config.min_se;
Self {
id: CallId::new(),
state: CallState::Ringing,
direction: CallDirection::Inbound,
config,
remote_uri,
dialog: Some(dialog),
negotiated_media: None,
media: None,
pending_answer: None,
events: vec![CallEvent::StateChanged(CallState::Ringing)],
session_expires: None,
min_se,
refresher: None,
refresh_at: None,
expiry_at: None,
update_unsupported: false,
uac_in_flight: false,
}
}
pub(crate) fn new_inbound_pending(
config: Arc<CallConfig>,
remote_uri: String,
dialog: Dialog,
pending: PendingAnswer,
) -> Self {
let min_se = config.min_se;
Self {
id: CallId::new(),
state: CallState::Ringing,
direction: CallDirection::Inbound,
config,
remote_uri,
dialog: Some(dialog),
negotiated_media: None,
media: None,
pending_answer: Some(pending),
events: vec![CallEvent::StateChanged(CallState::Ringing)],
session_expires: None,
min_se,
refresher: None,
refresh_at: None,
expiry_at: None,
update_unsupported: false,
uac_in_flight: false,
}
}
pub fn id(&self) -> &CallId {
&self.id
}
pub fn state(&self) -> CallState {
self.state
}
pub fn direction(&self) -> CallDirection {
self.direction
}
pub fn remote_uri(&self) -> &str {
&self.remote_uri
}
pub fn config(&self) -> &CallConfig {
&self.config
}
pub fn dialog_id(&self) -> Option<&DialogId> {
self.dialog.as_ref().map(|d| d.id())
}
pub fn set_dialog(&mut self, dialog: Dialog) {
self.dialog = Some(dialog);
}
pub fn set_negotiated_media(
&mut self,
media: NegotiatedMedia,
local_port: u16,
) -> Result<(), String> {
let ssrc = random_u32();
let mut session = MediaSession::for_negotiated(ssrc, &media.codec, local_port)?;
if let Some(ref addr) = media.remote_addr {
if let Ok(ip) = addr.parse() {
session.set_remote(SocketAddr::new(ip, media.remote_port));
}
}
self.negotiated_media = Some(media);
self.media = Some(session);
Ok(())
}
pub fn set_state(&mut self, state: CallState) {
if self.state != state {
self.state = state;
self.events.push(CallEvent::StateChanged(state));
}
}
pub fn handle_provisional(&mut self, has_sdp: bool) {
if has_sdp {
self.set_state(CallState::EarlyMedia);
self.events.push(CallEvent::EarlyMedia);
} else {
self.set_state(CallState::Ringing);
self.events.push(CallEvent::Ringing);
}
}
pub fn handle_answer(&mut self) {
self.set_state(CallState::Established);
self.events.push(CallEvent::Answered);
}
pub fn handle_ended(&mut self, reason: CallEndReason) {
self.set_state(CallState::Terminated);
self.events.push(CallEvent::Ended(reason));
if let Some(ref mut media) = self.media {
media.active = false;
}
}
pub fn drain_events(&mut self) -> Vec<CallEvent> {
std::mem::take(&mut self.events)
}
pub fn media(&self) -> Option<&MediaSession> {
self.media.as_ref()
}
pub fn media_mut(&mut self) -> Option<&mut MediaSession> {
self.media.as_mut()
}
pub fn codec(&self) -> Option<&Codec> {
self.negotiated_media.as_ref().map(|m| &m.codec)
}
pub fn dialog(&self) -> Option<&Dialog> {
self.dialog.as_ref()
}
pub fn dialog_mut(&mut self) -> Option<&mut Dialog> {
self.dialog.as_mut()
}
pub fn is_active(&self) -> bool {
self.state == CallState::Established
}
pub fn can_receive_media(&self) -> bool {
matches!(self.state, CallState::EarlyMedia | CallState::Established)
}
pub(crate) fn has_pending_answer(&self) -> bool {
self.pending_answer.is_some()
}
pub fn slide_deadlines(&mut self, now: Instant) {
let Some(se) = self.session_expires else {
return;
};
if self.refresh_at.is_some() {
self.refresh_at = Some(now + se / 2);
}
if self.expiry_at.is_some() {
self.expiry_at = Some(now + se);
}
}
pub(crate) fn take_pending_answer(&mut self) -> Option<PendingAnswer> {
self.pending_answer.take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_call_id() {
let id1 = CallId::new();
let id2 = CallId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_new_outbound_call() {
let config = Arc::new(CallConfig::default());
let call = Call::new_outbound(config, "sip:bob@example.com".to_string());
assert_eq!(call.state(), CallState::Idle);
assert_eq!(call.direction(), CallDirection::Outbound);
assert_eq!(call.remote_uri(), "sip:bob@example.com");
}
#[test]
fn test_call_state_transitions() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
call.set_state(CallState::Inviting);
assert_eq!(call.state(), CallState::Inviting);
call.handle_provisional(false);
assert_eq!(call.state(), CallState::Ringing);
call.handle_answer();
assert_eq!(call.state(), CallState::Established);
assert!(call.is_active());
call.handle_ended(CallEndReason::NormalClearing);
assert_eq!(call.state(), CallState::Terminated);
assert!(!call.is_active());
}
#[test]
fn test_call_events() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
call.handle_provisional(false);
call.handle_answer();
let events = call.drain_events();
assert!(events.len() >= 2);
let events2 = call.drain_events();
assert!(events2.is_empty());
}
fn pcmu_session(ssrc: u32, local_port: u16) -> MediaSession {
MediaSession::for_negotiated(ssrc, &Codec::pcmu(), local_port).expect("PCMU MediaSession")
}
#[test]
fn test_media_session() {
let mut session = pcmu_session(12345, 5000);
assert_eq!(session.local_port(), 5000);
assert!(!session.is_active());
session.set_remote("10.0.0.1:6000".parse().unwrap());
assert!(session.is_active());
assert_eq!(
session.remote_addr(),
Some("10.0.0.1:6000".parse().unwrap())
);
}
#[test]
fn test_media_encode() {
let mut session = pcmu_session(12345, 5000);
let samples = vec![0i16; 160];
let packet = session.encode_audio(&samples, true).expect("PCMU encode");
assert!(packet.marker);
assert_eq!(packet.payload_type, 0);
assert_eq!(packet.ssrc, 12345);
assert_eq!(packet.payload.len(), 160);
}
#[test]
fn test_set_negotiated_media() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
let media = NegotiatedMedia {
codec: Codec::pcmu(),
remote_port: 6000,
remote_addr: Some("10.0.0.1".to_string()),
direction: crate::sdp::parser::Direction::SendRecv,
};
call.set_negotiated_media(media, 5000)
.expect("PCMU media setup");
assert!(call.media().is_some());
assert_eq!(call.codec().map(|c| c.encoding.as_str()), Some("PCMU"));
}
#[test]
fn test_dialog_new_uac() {
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
assert_eq!(dialog.local_uri(), "sip:alice@example.com");
assert_eq!(dialog.remote_uri(), "sip:bob@example.com");
assert_eq!(dialog.local_cseq(), 1);
}
#[test]
fn test_dialog_new_uas() {
let dialog = Dialog::new_uas(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:bob@example.com".to_string(),
"sip:alice@example.com".to_string(),
1,
);
assert_eq!(dialog.local_uri(), "sip:bob@example.com");
assert_eq!(dialog.remote_uri(), "sip:alice@example.com");
assert_eq!(dialog.local_cseq(), 1);
}
#[test]
fn test_dialog_next_cseq() {
let mut dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
assert_eq!(dialog.local_cseq(), 1);
assert_eq!(dialog.next_cseq(), 2);
assert_eq!(dialog.next_cseq(), 3);
assert_eq!(dialog.local_cseq(), 3);
}
#[test]
fn test_dialog_id() {
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let id = dialog.id();
assert_eq!(id, id);
}
#[test]
fn test_dialog_clone() {
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let cloned = dialog.clone();
assert_eq!(cloned.local_uri(), dialog.local_uri());
assert_eq!(cloned.remote_uri(), dialog.remote_uri());
}
#[test]
fn test_call_state_debug() {
assert!(format!("{:?}", CallState::Idle).contains("Idle"));
assert!(format!("{:?}", CallState::Inviting).contains("Inviting"));
assert!(format!("{:?}", CallState::Ringing).contains("Ringing"));
assert!(format!("{:?}", CallState::EarlyMedia).contains("EarlyMedia"));
assert!(format!("{:?}", CallState::Established).contains("Established"));
assert!(format!("{:?}", CallState::Terminating).contains("Terminating"));
assert!(format!("{:?}", CallState::Terminated).contains("Terminated"));
}
#[test]
fn test_call_state_eq() {
assert_eq!(CallState::Idle, CallState::Idle);
assert_ne!(CallState::Idle, CallState::Inviting);
}
#[test]
fn test_call_state_clone() {
let state = CallState::Established;
let cloned = state;
assert_eq!(state, cloned);
}
#[test]
fn test_call_direction_debug() {
assert!(format!("{:?}", CallDirection::Outbound).contains("Outbound"));
assert!(format!("{:?}", CallDirection::Inbound).contains("Inbound"));
}
#[test]
fn test_call_direction_eq() {
assert_eq!(CallDirection::Outbound, CallDirection::Outbound);
assert_ne!(CallDirection::Outbound, CallDirection::Inbound);
}
#[test]
fn test_call_config_default() {
let config = CallConfig::default();
assert_eq!(config.local_uri, "sip:user@127.0.0.1");
assert!(config.local_name.is_none());
assert!(!config.codecs.is_empty());
assert_eq!(config.rtp_port_start, 10000);
assert_eq!(config.rtp_port_end, 20000);
}
#[test]
fn test_call_config_debug() {
let config = CallConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("CallConfig"));
}
#[test]
fn test_call_config_clone() {
let config = CallConfig::default();
let cloned = config.clone();
assert_eq!(cloned.local_uri, config.local_uri);
}
#[test]
fn test_call_event_debug() {
let event = CallEvent::StateChanged(CallState::Ringing);
let debug = format!("{:?}", event);
assert!(debug.contains("StateChanged"));
}
#[test]
fn test_call_event_ringing() {
let event = CallEvent::Ringing;
let debug = format!("{:?}", event);
assert!(debug.contains("Ringing"));
}
#[test]
fn test_call_event_early_media() {
let event = CallEvent::EarlyMedia;
let debug = format!("{:?}", event);
assert!(debug.contains("EarlyMedia"));
}
#[test]
fn test_call_event_answered() {
let event = CallEvent::Answered;
let debug = format!("{:?}", event);
assert!(debug.contains("Answered"));
}
#[test]
fn test_call_event_ended() {
let event = CallEvent::Ended(CallEndReason::NormalClearing);
let debug = format!("{:?}", event);
assert!(debug.contains("Ended"));
}
#[test]
fn test_call_event_audio_received() {
let event = CallEvent::AudioReceived(vec![0i16; 160]);
let debug = format!("{:?}", event);
assert!(debug.contains("AudioReceived"));
}
#[test]
fn test_call_event_dtmf_received() {
let event = CallEvent::DtmfReceived('5');
let debug = format!("{:?}", event);
assert!(debug.contains("DtmfReceived"));
}
#[test]
fn test_call_event_clone() {
let event = CallEvent::Ringing;
let cloned = event.clone();
assert!(format!("{:?}", cloned).contains("Ringing"));
}
#[test]
fn test_call_end_reason_debug() {
assert!(format!("{:?}", CallEndReason::NormalClearing).contains("NormalClearing"));
assert!(format!("{:?}", CallEndReason::Rejected).contains("Rejected"));
assert!(format!("{:?}", CallEndReason::Busy).contains("Busy"));
assert!(format!("{:?}", CallEndReason::NoAnswer).contains("NoAnswer"));
assert!(format!("{:?}", CallEndReason::NetworkError).contains("NetworkError"));
assert!(format!("{:?}", CallEndReason::Canceled).contains("Canceled"));
assert!(format!("{:?}", CallEndReason::Error).contains("Error"));
}
#[test]
fn test_call_end_reason_eq() {
assert_eq!(CallEndReason::Busy, CallEndReason::Busy);
assert_ne!(CallEndReason::Busy, CallEndReason::Rejected);
}
#[test]
fn test_call_id_default() {
let id = CallId::default();
assert!(!id.0.is_empty());
}
#[test]
fn test_call_id_display() {
let id = CallId::new();
let display = format!("{}", id);
assert!(!display.is_empty());
assert_eq!(display, id.0);
}
#[test]
fn test_call_id_hash() {
use std::collections::HashSet;
let id1 = CallId::new();
let id2 = CallId::new();
let mut set = HashSet::new();
set.insert(id1.clone());
set.insert(id2.clone());
set.insert(id1.clone()); assert_eq!(set.len(), 2);
}
#[test]
fn test_new_inbound_call() {
let config = Arc::new(CallConfig::default());
let dialog = Dialog::new_uas(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:bob@example.com".to_string(),
"sip:alice@example.com".to_string(),
1,
);
let call = Call::new_inbound(config, "sip:alice@example.com".to_string(), dialog);
assert_eq!(call.state(), CallState::Ringing);
assert_eq!(call.direction(), CallDirection::Inbound);
assert!(call.dialog().is_some());
}
#[test]
fn test_call_set_dialog() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
assert!(call.dialog().is_none());
assert!(call.dialog_id().is_none());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
call.set_dialog(dialog);
assert!(call.dialog().is_some());
assert!(call.dialog_id().is_some());
}
#[test]
fn test_call_dialog_mut() {
let config = Arc::new(CallConfig::default());
let dialog = Dialog::new_uas(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:bob@example.com".to_string(),
"sip:alice@example.com".to_string(),
1,
);
let mut call = Call::new_inbound(config, "sip:alice@example.com".to_string(), dialog);
let d = call.dialog_mut().unwrap();
let _ = d.next_cseq();
assert!(call.dialog().is_some());
}
#[test]
fn test_call_can_receive_media() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
assert!(!call.can_receive_media());
call.set_state(CallState::Inviting);
assert!(!call.can_receive_media());
call.set_state(CallState::Ringing);
assert!(!call.can_receive_media());
call.set_state(CallState::EarlyMedia);
assert!(call.can_receive_media());
call.set_state(CallState::Established);
assert!(call.can_receive_media());
call.set_state(CallState::Terminated);
assert!(!call.can_receive_media());
}
#[test]
fn test_call_handle_early_media() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
call.handle_provisional(true);
assert_eq!(call.state(), CallState::EarlyMedia);
let events = call.drain_events();
assert!(events.iter().any(|e| matches!(e, CallEvent::EarlyMedia)));
}
#[test]
fn test_call_handle_ended_with_media() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
let media = NegotiatedMedia {
codec: Codec::pcmu(),
remote_port: 6000,
remote_addr: Some("10.0.0.1".to_string()),
direction: crate::sdp::parser::Direction::SendRecv,
};
call.set_negotiated_media(media, 5000)
.expect("PCMU media setup");
assert!(call.media().unwrap().is_active());
call.handle_ended(CallEndReason::NormalClearing);
assert_eq!(call.state(), CallState::Terminated);
assert!(!call.media().unwrap().is_active());
}
#[test]
fn test_call_media_mut() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
assert!(call.media_mut().is_none());
let media = NegotiatedMedia {
codec: Codec::pcmu(),
remote_port: 6000,
remote_addr: None,
direction: crate::sdp::parser::Direction::SendRecv,
};
call.set_negotiated_media(media, 5000)
.expect("PCMU media setup");
assert!(call.media_mut().is_some());
}
#[test]
fn test_call_config() {
let config = Arc::new(CallConfig {
local_uri: "sip:test@host.com".to_string(),
local_name: Some("Test User".to_string()),
codecs: vec![Codec::pcma()],
rtp_port_start: 20000,
rtp_port_end: 30000,
..CallConfig::default()
});
let call = Call::new_outbound(config, "sip:bob@example.com".to_string());
let cfg = call.config();
assert_eq!(cfg.local_uri, "sip:test@host.com");
assert_eq!(cfg.local_name.as_deref(), Some("Test User"));
}
#[test]
fn test_call_set_state_no_duplicate_events() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
call.set_state(CallState::Established);
let events1 = call.drain_events();
assert_eq!(events1.len(), 1);
call.set_state(CallState::Established);
let events2 = call.drain_events();
assert!(events2.is_empty());
}
#[test]
fn test_media_session_alaw() {
let session =
MediaSession::for_negotiated(12345, &Codec::pcma(), 5000).expect("PCMA MediaSession");
assert_eq!(session.local_port(), 5000);
assert!(!session.is_active());
}
#[test]
fn test_media_session_unknown_payload() {
let unsupported = Codec::new(99, "AMR", 8000);
assert!(MediaSession::for_negotiated(12345, &unsupported, 5000).is_err());
}
#[test]
fn test_media_session_rtp_session() {
let session = pcmu_session(12345, 5000);
let rtp = session.rtp_session();
assert_eq!(rtp.ssrc(), 12345);
}
#[test]
fn test_media_session_jitter_stats() {
let session = pcmu_session(12345, 5000);
let stats = session.jitter_stats();
assert_eq!(stats.packets_received, 0);
}
#[test]
fn test_media_session_get_audio_frame() {
use crate::media::PlayoutDecision;
let mut session = pcmu_session(12345, 5000);
let (decision, samples) = session.get_audio_frame();
assert_eq!(decision, PlayoutDecision::Silence);
assert_eq!(samples.len(), 160);
}
#[test]
fn test_media_session_receive_rtp() {
let mut session = pcmu_session(12345, 5000);
session.set_remote("10.0.0.1:6000".parse().unwrap());
let samples = vec![0i16; 160];
let packet = session.encode_audio(&samples, false).expect("PCMU encode");
let result = session.receive_rtp(&packet);
assert!(result.is_none());
}
#[test]
fn test_media_session_receive_rtp_primes_buffer() {
let mut session = pcmu_session(12345, 5000);
session.set_remote("10.0.0.1:6000".parse().unwrap());
let samples = vec![0i16; 160];
let mut result = None;
for _ in 0..3 {
let packet = session.encode_audio(&samples, false).expect("PCMU encode");
result = session.receive_rtp(&packet);
}
assert!(result.is_some());
}
#[test]
fn test_media_session_debug() {
let session = pcmu_session(12345, 5000);
let debug = format!("{:?}", session);
assert!(debug.contains("MediaSession"));
}
#[test]
fn test_set_negotiated_media_no_remote_addr() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
let media = NegotiatedMedia {
codec: Codec::pcmu(),
remote_port: 6000,
remote_addr: None,
direction: crate::sdp::parser::Direction::SendRecv,
};
call.set_negotiated_media(media, 5000)
.expect("PCMU media setup");
assert!(call.media().is_some());
assert!(!call.media().unwrap().is_active());
}
#[test]
fn test_set_negotiated_media_invalid_addr() {
let config = Arc::new(CallConfig::default());
let mut call = Call::new_outbound(config, "sip:bob@example.com".to_string());
let media = NegotiatedMedia {
codec: Codec::pcmu(),
remote_port: 6000,
remote_addr: Some("not-an-ip".to_string()),
direction: crate::sdp::parser::Direction::SendRecv,
};
call.set_negotiated_media(media, 5000)
.expect("PCMU media setup");
assert!(call.media().is_some());
assert!(!call.media().unwrap().is_active());
}
#[test]
fn test_call_debug() {
let config = Arc::new(CallConfig::default());
let call = Call::new_outbound(config, "sip:bob@example.com".to_string());
let debug = format!("{:?}", call);
assert!(debug.contains("Call"));
}
}