#![deny(missing_docs)]
use std::net::IpAddr;
use std::sync::Arc;
use tokio::sync::mpsc;
use crate::adapters::{SessionApiCrossCrateEvent, SessionControlEvent};
use crate::api::endpoint::SipAccount;
use crate::api::events::{Event, MediaSecurityState, SipTrace};
use crate::api::handle::{CallId, SessionHandle};
use crate::api::incoming::IncomingCall;
use crate::api::performance::PerformanceConfig;
use crate::api::unified::{
Config, MediaMode, MediaSessionControllerConfig, RegistrationHandle, RegistrationInfo,
RtpSessionBufferConfig, RtpTransportBufferConfig, SdesBase64Mode, SipNatConfig,
SipRuntimeConfig, SymmetricRtpPolicy, UnifiedCoordinator,
};
use crate::auth::SipClientAuth;
use crate::errors::{Result, SessionError};
use crate::session_registry::SessionRegistryHandle;
pub use crate::api::unified::Config as PeerConfig;
pub struct EventReceiver {
rx: mpsc::Receiver<Arc<dyn rvoip_infra_common::events::cross_crate::CrossCrateEvent>>,
control_rx: Option<mpsc::UnboundedReceiver<SessionControlEvent>>,
control_coordinator: Option<Arc<UnifiedCoordinator>>,
owns_control: bool,
observations_open: bool,
filter: Option<CallId>,
exact_filter: Option<SessionRegistryHandle>,
exact_store: Option<Arc<crate::session_store::SessionStore>>,
exact_lifecycle: Option<crate::api::lifecycle::LifecycleIndex>,
exact_watcher: Option<tokio::sync::watch::Receiver<u64>>,
exact_terminal_delivered: bool,
primed: std::collections::VecDeque<Event>,
}
fn attach_incoming_request_authority(
event: &mut Event,
coordinator: &Arc<UnifiedCoordinator>,
lifecycle_handle: Option<&SessionRegistryHandle>,
) {
let captured = lifecycle_handle.cloned();
match event {
Event::ReferReceived {
request: Some(request),
..
}
| Event::NotifyReceived {
request: Some(request),
..
} => {
request.set_coordinator_captured(Arc::clone(coordinator), captured);
}
Event::InfoReceived { request, .. }
| Event::MessageReceived { request, .. }
| Event::OptionsReceived { request, .. }
| Event::UpdateReceived { request, .. } => {
request.set_coordinator_captured(Arc::clone(coordinator), captured);
}
Event::IncomingRegister { register } => {
register.set_coordinator(Arc::clone(coordinator));
}
_ => {}
}
}
async fn receive_optional_control(
receiver: &mut Option<mpsc::UnboundedReceiver<SessionControlEvent>>,
) -> Option<SessionControlEvent> {
match receiver {
Some(receiver) => receiver.recv().await,
None => std::future::pending().await,
}
}
async fn wait_optional_exact_lifecycle(
watcher: &mut Option<tokio::sync::watch::Receiver<u64>>,
) -> bool {
match watcher {
Some(watcher) => watcher.changed().await.is_ok(),
None => std::future::pending().await,
}
}
impl EventReceiver {
pub(crate) fn new(
rx: mpsc::Receiver<Arc<dyn rvoip_infra_common::events::cross_crate::CrossCrateEvent>>,
) -> Self {
Self {
rx,
control_rx: None,
control_coordinator: None,
owns_control: false,
observations_open: true,
filter: None,
exact_filter: None,
exact_store: None,
exact_lifecycle: None,
exact_watcher: None,
exact_terminal_delivered: false,
primed: std::collections::VecDeque::new(),
}
}
pub(crate) fn filtered(
rx: mpsc::Receiver<Arc<dyn rvoip_infra_common::events::cross_crate::CrossCrateEvent>>,
call_id: CallId,
) -> Self {
Self {
rx,
control_rx: None,
control_coordinator: None,
owns_control: false,
observations_open: true,
filter: Some(call_id),
exact_filter: None,
exact_store: None,
exact_lifecycle: None,
exact_watcher: None,
exact_terminal_delivered: false,
primed: std::collections::VecDeque::new(),
}
}
pub(crate) fn filtered_exact(
rx: mpsc::Receiver<Arc<dyn rvoip_infra_common::events::cross_crate::CrossCrateEvent>>,
lifecycle_handle: SessionRegistryHandle,
store: Arc<crate::session_store::SessionStore>,
lifecycle: crate::api::lifecycle::LifecycleIndex,
) -> Self {
let exact_watcher = lifecycle.watcher_exact(&lifecycle_handle);
Self {
rx,
control_rx: None,
control_coordinator: None,
owns_control: false,
observations_open: true,
filter: Some(lifecycle_handle.session_id().clone()),
exact_filter: Some(lifecycle_handle),
exact_store: Some(store),
exact_lifecycle: Some(lifecycle),
exact_watcher: Some(exact_watcher),
exact_terminal_delivered: false,
primed: std::collections::VecDeque::new(),
}
}
pub(crate) fn with_control(
rx: mpsc::Receiver<Arc<dyn rvoip_infra_common::events::cross_crate::CrossCrateEvent>>,
control_rx: mpsc::UnboundedReceiver<SessionControlEvent>,
coordinator: Arc<UnifiedCoordinator>,
) -> Self {
Self {
rx,
control_rx: Some(control_rx),
control_coordinator: Some(coordinator),
owns_control: true,
observations_open: true,
filter: None,
exact_filter: None,
exact_store: None,
exact_lifecycle: None,
exact_watcher: None,
exact_terminal_delivered: false,
primed: std::collections::VecDeque::new(),
}
}
pub(crate) async fn ensure_control(
&mut self,
coordinator: &Arc<UnifiedCoordinator>,
) -> Result<()> {
if self.control_rx.is_none() {
let control = coordinator.claim_session_control_events().await?;
self.control_rx = Some(control);
self.control_coordinator = Some(Arc::clone(coordinator));
self.owns_control = true;
}
Ok(())
}
pub(crate) fn prime(&mut self, event: Event) {
self.primed.push_back(event);
}
fn exact_observation_handle(&self) -> std::result::Result<Option<SessionRegistryHandle>, ()> {
let Some(handle) = self.exact_filter.as_ref() else {
return Ok(None);
};
let Some(store) = self.exact_store.as_ref() else {
return Err(());
};
store.get_session_snapshot_exact(handle).map_err(|_| ())?;
Ok(Some(handle.clone()))
}
fn take_exact_terminal(&mut self) -> Option<(Event, Option<SessionRegistryHandle>)> {
if self.exact_terminal_delivered {
return None;
}
let handle = self.exact_filter.as_ref()?;
let lifecycle = self.exact_lifecycle.as_ref()?;
let terminal = lifecycle.snapshot_exact(handle, None).terminal?;
let event = terminal.to_event(handle.session_id().clone());
let handle = handle.clone();
self.exact_terminal_delivered = true;
Some((event, Some(handle)))
}
pub async fn next(&mut self) -> Option<Event> {
self.next_with_lifecycle().await.map(|(event, _)| event)
}
pub(crate) async fn next_with_lifecycle(
&mut self,
) -> Option<(Event, Option<SessionRegistryHandle>)> {
if let Some(terminal) = self.take_exact_terminal() {
return Some(terminal);
}
if self.exact_filter.is_none() {
if let Some(event) = self.primed.pop_front() {
return Some((event, None));
}
}
loop {
if let Some(terminal) = self.take_exact_terminal() {
return Some(terminal);
}
if self.control_rx.is_none() && !self.observations_open && self.exact_watcher.is_none()
{
return None;
}
#[allow(clippy::large_enum_variant)]
enum Input {
Control(Option<SessionControlEvent>),
Observation(
Option<Arc<dyn rvoip_infra_common::events::cross_crate::CrossCrateEvent>>,
),
ExactLifecycle(bool),
}
let input = tokio::select! {
event = receive_optional_control(&mut self.control_rx) => Input::Control(event),
raw = self.rx.recv(), if self.observations_open => Input::Observation(raw),
changed = wait_optional_exact_lifecycle(&mut self.exact_watcher) => {
Input::ExactLifecycle(changed)
}
};
let (mut event, lifecycle_handle) = match input {
Input::Control(Some(control)) => (control.event, control.lifecycle_handle),
Input::Control(None) => {
self.control_rx = None;
continue;
}
Input::Observation(Some(raw)) => {
let Some(session_event) =
raw.as_any().downcast_ref::<SessionApiCrossCrateEvent>()
else {
continue;
};
if self.owns_control {
continue;
}
let lifecycle_handle = match self.exact_observation_handle() {
Ok(handle) => handle,
Err(()) => continue,
};
(session_event.event.clone(), lifecycle_handle)
}
Input::Observation(None) => {
self.observations_open = false;
continue;
}
Input::ExactLifecycle(changed) => {
if !changed {
self.exact_watcher = None;
}
continue;
}
};
if let Some(coordinator) = &self.control_coordinator {
attach_incoming_request_authority(
&mut event,
coordinator,
lifecycle_handle.as_ref(),
);
}
if let Some(ref filter) = self.filter {
if event.call_id() != Some(filter) {
continue;
}
}
return Some((event, lifecycle_handle));
}
}
pub fn try_next(&mut self) -> Option<Event> {
self.try_next_with_lifecycle().map(|(event, _)| event)
}
pub(crate) fn try_next_with_lifecycle(
&mut self,
) -> Option<(Event, Option<SessionRegistryHandle>)> {
if let Some(terminal) = self.take_exact_terminal() {
return Some(terminal);
}
if self.exact_filter.is_none() {
if let Some(event) = self.primed.pop_front() {
return Some((event, None));
}
}
loop {
if let Some(control_rx) = self.control_rx.as_mut() {
match control_rx.try_recv() {
Ok(control) => {
let mut event = control.event;
let lifecycle_handle = control.lifecycle_handle;
if let Some(coordinator) = &self.control_coordinator {
attach_incoming_request_authority(
&mut event,
coordinator,
lifecycle_handle.as_ref(),
);
}
if self
.filter
.as_ref()
.is_none_or(|filter| event.call_id() == Some(filter))
{
return Some((event, lifecycle_handle));
}
continue;
}
Err(mpsc::error::TryRecvError::Disconnected) => {
self.control_rx = None;
}
Err(mpsc::error::TryRecvError::Empty) => {}
}
}
if !self.observations_open {
return None;
}
let raw = match self.rx.try_recv() {
Ok(raw) => raw,
Err(mpsc::error::TryRecvError::Disconnected) => {
self.observations_open = false;
return None;
}
Err(mpsc::error::TryRecvError::Empty) => return None,
};
let Some(session_event) = raw.as_any().downcast_ref::<SessionApiCrossCrateEvent>()
else {
continue;
};
if self.owns_control {
continue;
}
let lifecycle_handle = match self.exact_observation_handle() {
Ok(handle) => handle,
Err(()) => continue,
};
let mut event = session_event.event.clone();
if let Some(coordinator) = &self.control_coordinator {
attach_incoming_request_authority(
&mut event,
coordinator,
lifecycle_handle.as_ref(),
);
}
if let Some(ref filter) = self.filter {
if event.call_id() != Some(filter) {
continue;
}
}
return Some((event, lifecycle_handle));
}
}
pub async fn next_incoming(&mut self) -> Option<(CallId, String, String, Option<String>)> {
loop {
match self.next().await? {
Event::IncomingCall {
call_id,
from,
to,
sdp,
} => {
return Some((call_id, from, to, sdp));
}
_ => continue,
}
}
}
pub(crate) async fn next_incoming_exact(
&mut self,
) -> Option<(
CallId,
String,
String,
Option<String>,
Option<SessionRegistryHandle>,
)> {
loop {
let (event, lifecycle_handle) = self.next_with_lifecycle().await?;
match event {
Event::IncomingCall {
call_id,
from,
to,
sdp,
} => return Some((call_id, from, to, sdp, lifecycle_handle)),
_ => continue,
}
}
}
pub async fn next_dtmf(&mut self) -> Option<(CallId, char)> {
loop {
match self.next().await? {
Event::DtmfReceived { call_id, digit } => {
return Some((call_id, digit));
}
_ => continue,
}
}
}
pub async fn next_progress(&mut self) -> Option<(CallId, u16, String, Option<String>)> {
loop {
match self.next().await? {
Event::CallProgress {
call_id,
status_code,
reason,
sdp,
} => return Some((call_id, status_code, reason, sdp)),
_ => continue,
}
}
}
pub async fn next_media_security_negotiated(&mut self) -> Option<(CallId, MediaSecurityState)> {
loop {
let event = self.next().await?;
if let Some(state) = media_security_state_from_event(event) {
return Some(state);
}
}
}
pub async fn next_sip_trace(&mut self) -> Option<SipTrace> {
loop {
match self.next().await? {
Event::SipTrace(trace) => return Some(trace),
_ => continue,
}
}
}
pub async fn next_transfer(&mut self) -> Option<Event> {
loop {
let event = self.next().await?;
if event.is_transfer_event() {
return Some(event);
}
}
}
pub async fn next_where<F: FnMut(&Event) -> bool>(
&mut self,
mut predicate: F,
) -> Option<Event> {
loop {
let event = self.next().await?;
if predicate(&event) {
return Some(event);
}
}
}
pub async fn next_for_call(&mut self, call_id: &CallId) -> Option<Event> {
loop {
let event = self.next().await?;
if event.call_id() == Some(call_id) {
return Some(event);
}
}
}
}
#[derive(Clone)]
pub struct PeerControl {
pub(crate) coordinator: Arc<UnifiedCoordinator>,
pub(crate) local_uri: String,
}
impl PeerControl {
pub async fn accept(&self, call_id: &CallId) -> Result<SessionHandle> {
self.coordinator.accept_call(call_id).await?;
Ok(SessionHandle::new(
call_id.clone(),
self.coordinator.clone(),
))
}
pub async fn reject(&self, call_id: &CallId, status: u16, reason: &str) -> Result<()> {
self.coordinator
.reject(call_id)
.with_status(status)
.with_reason(reason.to_string())
.send()
.await
}
pub async fn send_early_media(&self, call_id: &CallId, sdp: Option<String>) -> Result<()> {
self.coordinator.send_early_media(call_id, sdp).await
}
pub async fn subscribe_events(&self) -> Result<EventReceiver> {
let rx = self.coordinator.subscribe_events().await?;
Ok(EventReceiver::new(rx))
}
pub fn subscribe_diagnostics(
&self,
) -> tokio::sync::broadcast::Receiver<crate::api::events::DiagnosticEvent> {
self.coordinator.subscribe_diagnostics()
}
pub fn coordinator(&self) -> &Arc<UnifiedCoordinator> {
&self.coordinator
}
pub fn session(&self, call_id: &CallId) -> SessionHandle {
SessionHandle::new(call_id.clone(), self.coordinator.clone())
}
pub fn invite(&self, target: impl Into<String>) -> crate::api::send::OutboundCallBuilder {
self.coordinator
.invite(Some(self.local_uri.clone()), target)
}
pub fn register(
&self,
registrar: impl Into<String>,
username: impl Into<String>,
password: impl Into<String>,
) -> crate::api::send::RegisterBuilder {
self.coordinator.register(registrar, username, password)
}
pub fn register_account(&self, account: &SipAccount) -> crate::api::send::RegisterBuilder {
let mut builder = self
.register(
account.registrar.clone(),
account.effective_auth_username().to_string(),
account.password.clone(),
)
.with_expires(account.expires);
if let Some(from_uri) = &account.from_uri {
builder = builder.with_from_uri(from_uri.clone());
}
if let Some(contact_uri) = &account.contact_uri {
builder = builder.with_contact_uri(contact_uri.clone());
}
builder
}
pub async fn register_and_wait(
&self,
registrar: impl Into<String>,
username: impl Into<String>,
password: impl Into<String>,
timeout: Option<std::time::Duration>,
) -> Result<RegistrationInfo> {
let mut events = self.subscribe_events().await?;
let handle = self.register(registrar, username, password).send().await?;
wait_for_peer_registration(&self.coordinator, &mut events, &handle, timeout).await
}
}
async fn wait_for_peer_registration(
coordinator: &Arc<UnifiedCoordinator>,
events: &mut EventReceiver,
handle: &RegistrationHandle,
timeout: Option<std::time::Duration>,
) -> Result<RegistrationInfo> {
let registrar = coordinator
.registration_info(handle)
.await?
.registrar
.unwrap_or_default();
let matches = |ev: &str| registrar.is_empty() || ev == registrar;
let fut = async {
loop {
match events.next().await {
Some(Event::RegistrationSuccess { registrar: r, .. }) if matches(&r) => {
return coordinator.registration_info(handle).await;
}
Some(Event::RegistrationFailed {
registrar: r,
status_code,
reason,
}) if matches(&r) => {
return Err(SessionError::Other(format!(
"registration failed for {r}: {status_code} {reason}"
)));
}
Some(_) => {}
None => {
return Err(SessionError::Other(
"event stream closed while waiting for registration".to_string(),
));
}
}
}
};
match timeout {
Some(duration) => tokio::time::timeout(duration, fut)
.await
.map_err(|_| SessionError::Timeout("register_and_wait timed out".to_string()))?,
None => fut.await,
}
}
pub struct StreamPeer {
control: PeerControl,
events: EventReceiver,
}
impl StreamPeer {
pub async fn new(name: &str) -> Result<Self> {
let mut config = Config::default();
config.local_uri = format!("sip:{}@{}:{}", name, config.local_ip, config.sip_port);
Self::with_config(config).await
}
pub async fn with_config(config: Config) -> Result<Self> {
Self::with_config_and_runtime(config, SipRuntimeConfig::default()).await
}
pub async fn with_config_and_nat(config: Config, nat: SipNatConfig) -> Result<Self> {
Self::with_config_and_runtime(config, SipRuntimeConfig::default().with_nat(nat)).await
}
pub async fn with_config_and_runtime(
config: Config,
runtime: SipRuntimeConfig,
) -> Result<Self> {
let local_uri = config.local_uri.clone();
let coordinator = UnifiedCoordinator::new_with_runtime(config, runtime).await?;
let event_rx = coordinator.subscribe_events().await?;
let control_rx = coordinator.claim_session_control_events().await?;
Ok(Self {
control: PeerControl {
coordinator: Arc::clone(&coordinator),
local_uri,
},
events: EventReceiver::with_control(event_rx, control_rx, coordinator),
})
}
pub fn subscribe_diagnostics(
&self,
) -> tokio::sync::broadcast::Receiver<crate::api::events::DiagnosticEvent> {
self.control.subscribe_diagnostics()
}
pub fn split(self) -> (PeerControl, EventReceiver) {
(self.control, self.events)
}
pub fn control(&self) -> &PeerControl {
&self.control
}
pub fn coordinator(&self) -> &Arc<UnifiedCoordinator> {
self.control.coordinator()
}
pub fn session(&self, call_id: &CallId) -> SessionHandle {
self.control.session(call_id)
}
pub fn invite(&self, target: impl Into<String>) -> crate::api::send::OutboundCallBuilder {
self.control.invite(target)
}
pub fn register(
&self,
registrar: impl Into<String>,
username: impl Into<String>,
password: impl Into<String>,
) -> crate::api::send::RegisterBuilder {
self.control.register(registrar, username, password)
}
pub fn register_account(&self, account: &SipAccount) -> crate::api::send::RegisterBuilder {
self.control.register_account(account)
}
pub async fn register_and_wait(
&self,
registrar: impl Into<String>,
username: impl Into<String>,
password: impl Into<String>,
timeout: Option<std::time::Duration>,
) -> Result<RegistrationInfo> {
self.control
.register_and_wait(registrar, username, password, timeout)
.await
}
pub async fn wait_for_incoming(&mut self) -> Result<IncomingCall> {
match self.events.next_incoming_exact().await {
Some((call_id, from, to, sdp, lifecycle_handle)) => {
let coord = self.control.coordinator.clone();
let pending = lifecycle_handle
.as_ref()
.and_then(|handle| coord.pending_incoming_bundle_for_handle_exact(handle));
let parsed = pending.as_ref().and_then(|bundle| bundle.request.clone());
let transport = pending.and_then(|bundle| bundle.transport);
let incoming = match parsed {
Some(req) => IncomingCall::with_request_captured(
call_id,
from,
to,
sdp,
coord,
req,
lifecycle_handle,
),
None => {
IncomingCall::new_captured(call_id, from, to, sdp, coord, lifecycle_handle)
}
}
.with_transport_context(
transport
.as_deref()
.cloned()
.unwrap_or_else(crate::auth::SipTransportSecurityContext::unknown),
);
Ok(incoming)
}
None => Err(SessionError::Other("Event channel closed".to_string())),
}
}
pub async fn wait_for_answered(&mut self, call_id: &CallId) -> Result<SessionHandle> {
loop {
let Some((event, lifecycle_handle)) = self.events.next_with_lifecycle().await else {
return Err(SessionError::Other("Event channel closed".to_string()));
};
match event {
Event::CallAnswered {
call_id: answered_id,
..
} if &answered_id == call_id => {
return Ok(SessionHandle::new_captured(
answered_id,
self.control.coordinator.clone(),
lifecycle_handle,
));
}
Event::CallFailed {
call_id: failed_id,
reason,
status_code,
} if &failed_id == call_id => {
return Err(SessionError::Other(format!(
"Call failed with {}: {}",
status_code, reason
)));
}
_ => {}
}
}
}
pub async fn wait_for_progress<F>(
&mut self,
call_id: &CallId,
mut predicate: F,
) -> Result<Event>
where
F: FnMut(&Event) -> bool,
{
loop {
match self.events.next().await {
Some(event @ Event::CallProgress { .. })
if event.call_id() == Some(call_id) && predicate(&event) =>
{
return Ok(event);
}
Some(Event::CallAnswered {
call_id: answered_id,
..
}) if &answered_id == call_id => {
return Err(SessionError::Other(
"call answered before matching provisional progress".to_string(),
));
}
Some(Event::CallFailed {
call_id: failed_id,
reason,
status_code,
}) if &failed_id == call_id => {
return Err(SessionError::Other(format!(
"Call failed with {}: {}",
status_code, reason
)));
}
Some(Event::CallCancelled { call_id: id }) if &id == call_id => {
return Err(SessionError::Other(
"call cancelled before matching provisional progress".to_string(),
));
}
None => return Err(SessionError::Other("Event channel closed".to_string())),
_ => {}
}
}
}
pub async fn wait_for_media_security(
&mut self,
call_id: &CallId,
) -> Result<MediaSecurityState> {
loop {
match self.events.next().await {
Some(event) if event.call_id() == Some(call_id) => {
if let Some((_, state)) = media_security_state_from_event(event) {
return Ok(state);
}
}
None => return Err(SessionError::Other("Event channel closed".to_string())),
_ => {}
}
}
}
pub async fn wait_for_ended(&mut self, call_id: &CallId) -> Result<String> {
loop {
match self.events.next().await {
Some(Event::CallEnded {
call_id: ended_id,
reason,
}) if &ended_id == call_id => {
return Ok(reason);
}
None => return Err(SessionError::Other("Event channel closed".to_string())),
_ => {}
}
}
}
pub async fn next_event(&mut self) -> Option<Event> {
self.events.next().await
}
pub async fn is_registered(
&self,
handle: &crate::api::unified::RegistrationHandle,
) -> Result<bool> {
self.control.coordinator.is_registered(handle).await
}
pub async fn unregister(&self, handle: &crate::api::unified::RegistrationHandle) -> Result<()> {
self.control.coordinator.unregister(handle).await
}
pub async fn shutdown(self) -> Result<()> {
self.control.coordinator.shutdown_gracefully(None).await?;
drop(self);
Ok(())
}
pub fn shutdown_handle(&self) -> crate::api::callback_peer::ShutdownHandle {
self.control.coordinator.shutdown_handle()
}
pub fn builder() -> StreamPeerBuilder {
StreamPeerBuilder::new()
}
}
fn media_security_state_from_event(event: Event) -> Option<(CallId, MediaSecurityState)> {
match event {
Event::MediaSecurityNegotiated {
call_id,
keying,
suite,
profile,
contexts_installed,
} => Some((
call_id,
MediaSecurityState {
keying,
suite,
profile,
contexts_installed,
},
)),
_ => None,
}
}
pub struct StreamPeerBuilder {
config: Config,
nat: SipNatConfig,
sdes_base64_mode: SdesBase64Mode,
name: Option<String>,
}
impl StreamPeerBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
nat: SipNatConfig::default(),
sdes_base64_mode: SdesBase64Mode::default(),
name: None,
}
}
pub fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
pub fn sip_port(mut self, port: u16) -> Self {
self.config.sip_port = port;
self.config.bind_addr.set_port(port);
self
}
pub fn local_ip(mut self, ip: IpAddr) -> Self {
self.config.local_ip = ip;
self.config.bind_addr.set_ip(ip);
self
}
pub fn media_ports(mut self, start: u16, end: u16) -> Self {
self.config = self.config.with_media_ports(start, end);
self
}
pub fn media_port_capacity(mut self, start: u16, capacity: usize) -> Self {
self.config = self.config.with_media_port_capacity(start, capacity);
self
}
pub fn media_session_capacity(mut self, capacity: usize) -> Self {
self.config = self.config.with_media_session_capacity(capacity);
self
}
pub fn rtp_session_buffer_config(mut self, config: RtpSessionBufferConfig) -> Self {
self.config = self.config.with_rtp_session_buffer_config(config);
self
}
pub fn rtp_transport_buffer_config(mut self, config: RtpTransportBufferConfig) -> Self {
self.config = self.config.with_rtp_transport_buffer_config(config);
self
}
pub fn media_session_controller_config(mut self, config: MediaSessionControllerConfig) -> Self {
self.config = self.config.with_media_session_controller_config(config);
self
}
pub fn nat(mut self, nat: SipNatConfig) -> Self {
self.nat = nat;
self
}
pub fn symmetric_rtp_policy(mut self, policy: SymmetricRtpPolicy) -> Self {
self.nat = self.nat.with_symmetric_rtp_policy(policy);
self
}
pub fn sdes_base64_mode(mut self, mode: SdesBase64Mode) -> Self {
self.sdes_base64_mode = mode;
self
}
pub fn high_cps_udp_auto_answer(mut self, capacity: usize) -> Self {
self.config = self.config.with_high_cps_udp_auto_answer(capacity);
self
}
pub fn performance_config(mut self, performance: PerformanceConfig) -> Result<Self> {
self.config = self.config.try_with_performance_config(performance)?;
Ok(self)
}
pub fn pbx_media_server_performance(mut self, capacity: usize) -> Self {
self.config = self.config.with_pbx_media_server_performance(capacity);
self
}
pub fn signaling_only_server_high_performance(
mut self,
capacity: usize,
sdp_rtp_port: u16,
) -> Self {
self.config = self
.config
.with_signaling_only_server_high_performance(capacity, sdp_rtp_port);
self
}
pub fn app_event_channel_capacity(mut self, capacity: usize) -> Self {
self.config = self.config.with_app_event_channel_capacity(capacity);
self
}
pub fn auto_180_ringing(mut self, enabled: bool) -> Self {
self.config = self.config.with_auto_180_ringing(enabled);
self
}
pub fn auto_100_trying(mut self, enabled: bool) -> Self {
self.config = self.config.with_auto_100_trying(enabled);
self
}
pub fn fast_auto_accept_incoming_calls(mut self, enabled: bool) -> Self {
self.config = self.config.with_fast_auto_accept_incoming_calls(enabled);
self
}
pub fn media_enabled(mut self, enabled: bool) -> Self {
self.config = self.config.with_media_enabled(enabled);
self
}
pub fn signaling_only_media(mut self, sdp_rtp_port: u16) -> Self {
self.config = self
.config
.with_media_mode(MediaMode::SignalingOnly { sdp_rtp_port });
self
}
pub fn sip_udp_parse_workers(mut self, workers: usize) -> Self {
self.config = self.config.with_sip_udp_parse_workers(workers);
self
}
pub fn sip_udp_parse_queue_capacity(mut self, capacity: usize) -> Self {
self.config = self.config.with_sip_udp_parse_queue_capacity(capacity);
self
}
pub fn sip_transaction_command_channel_capacity(mut self, capacity: usize) -> Self {
self.config = self
.config
.with_sip_transaction_command_channel_capacity(capacity);
self
}
pub fn server_call_admission_limit(mut self, limit: usize) -> Self {
self.config = self.config.with_server_call_admission_limit(limit);
self
}
pub fn server_call_admission_soft_limit(mut self, limit: usize) -> Self {
self.config = self.config.with_server_call_admission_soft_limit(limit);
self
}
pub fn server_call_admission_pacing_delay_ms(mut self, delay_ms: u64) -> Self {
self.config = self
.config
.with_server_call_admission_pacing_delay_ms(delay_ms);
self
}
pub fn server_overload_retry_after_secs(mut self, seconds: u32) -> Self {
self.config = self.config.with_server_overload_retry_after_secs(seconds);
self
}
pub fn sip_udp_diagnostics(mut self, enabled: bool) -> Self {
self.config = self.config.with_sip_udp_diagnostics(enabled);
self
}
pub fn media_setup_diagnostics(mut self, enabled: bool) -> Self {
self.config = self.config.with_media_setup_diagnostics(enabled);
self
}
pub fn cleanup_diagnostics(mut self, enabled: bool) -> Self {
self.config = self.config.with_cleanup_diagnostics(enabled);
self
}
pub fn cleanup_diagnostic_events(mut self, enabled: bool) -> Self {
self.config = self.config.with_cleanup_diagnostic_events(enabled);
self
}
#[cfg(feature = "perf-tests")]
pub fn perf_max_rss_growth_mb_per_hr(mut self, limit: f64) -> Self {
self.config = self.config.with_perf_max_rss_growth_mb_per_hr(limit);
self
}
pub fn srtp_diagnostics(mut self, enabled: bool) -> Self {
self.config = self.config.with_srtp_diagnostics(enabled);
self
}
pub fn rtp_diagnostics(mut self, enabled: bool) -> Self {
self.config = self.config.with_rtp_diagnostics(enabled);
self
}
pub fn media_sdp_diagnostics(mut self, enabled: bool) -> Self {
self.config = self.config.with_media_sdp_diagnostics(enabled);
self
}
pub fn config(mut self, config: Config) -> Self {
self.config = config;
self
}
pub fn with_credentials(mut self, username: &str, password: &str) -> Self {
self.config.credentials = Some(crate::types::Credentials::new(username, password));
self
}
pub fn with_auth(mut self, auth: SipClientAuth) -> Self {
self.config.auth = Some(auth);
self
}
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
self.config.auth = Some(SipClientAuth::bearer_token(token));
self
}
pub fn with_basic_credentials(
mut self,
username: impl Into<String>,
password: impl Into<String>,
) -> Self {
self.config.auth = Some(SipClientAuth::basic(username, password));
self
}
pub async fn build(mut self) -> Result<StreamPeer> {
if let Some(name) = self.name {
self.config.local_uri = format!(
"sip:{}@{}:{}",
name, self.config.local_ip, self.config.sip_port
);
}
let runtime = SipRuntimeConfig::default()
.with_nat(self.nat)
.with_sdes_base64_mode(self.sdes_base64_mode);
StreamPeer::with_config_and_runtime(self.config, runtime).await
}
}
impl Default for StreamPeerBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::{EventReceiver, StreamPeerBuilder};
use crate::adapters::SessionApiCrossCrateEvent;
use crate::api::events::{Event, SipTrace, SipTraceDirection};
use crate::api::unified::{
MediaSessionControllerConfig, RtpSessionBufferConfig, RtpTransportBufferConfig,
SipNatConfig, SymmetricRtpPolicy,
};
use crate::state_table::types::SessionId;
use rvoip_infra_common::events::cross_crate::CrossCrateEvent;
use tokio::sync::mpsc;
#[tokio::test]
async fn stream_peer_builder_passes_nat_policy_to_coordinator_validation() {
let invalid_policy = SymmetricRtpPolicy {
probation_packets: 0,
..SymmetricRtpPolicy::default()
};
let result = StreamPeerBuilder::new()
.symmetric_rtp_policy(invalid_policy)
.build()
.await;
assert!(matches!(
result,
Err(crate::errors::SessionError::ConfigError(detail))
if detail.contains("probation_packets")
));
let nat = SipNatConfig::default().with_symmetric_rtp_policy(SymmetricRtpPolicy {
max_rebindings: 9,
..SymmetricRtpPolicy::default()
});
assert_eq!(StreamPeerBuilder::new().nat(nat).nat, nat);
}
#[test]
fn stream_peer_builder_exposes_rtp_media_buffer_tuning() {
let session_buffers = RtpSessionBufferConfig {
sender_channel_capacity: 7,
receiver_channel_capacity: 5,
event_channel_capacity: 11,
};
let transport_buffers = RtpTransportBufferConfig {
event_channel_capacity: 13,
recv_buffer_size: 2048,
rtcp_recv_buffer_size: 1024,
};
let media_config = MediaSessionControllerConfig {
rtp_buffer_size: 960,
rtp_buffer_initial_count: 3,
rtp_buffer_max_count: 9,
..Default::default()
};
let builder = StreamPeerBuilder::new()
.media_session_controller_config(media_config)
.rtp_session_buffer_config(session_buffers)
.rtp_transport_buffer_config(transport_buffers);
assert_eq!(builder.config.rtp_session_buffer_config, session_buffers);
assert_eq!(
builder.config.rtp_transport_buffer_config,
transport_buffers
);
assert_eq!(
builder
.config
.media_session_controller_config
.rtp_buffer_size,
960
);
assert_eq!(
builder
.config
.media_session_controller_config
.rtp_buffer_initial_count,
3
);
assert_eq!(
builder
.config
.media_session_controller_config
.rtp_buffer_max_count,
9
);
}
#[tokio::test]
async fn event_receiver_returns_sip_trace_events() {
let (tx, rx) = mpsc::channel::<Arc<dyn CrossCrateEvent>>(4);
tx.send(SessionApiCrossCrateEvent::new(Event::CallEnded {
call_id: SessionId("other".into()),
reason: "done".into(),
}))
.await
.unwrap();
tx.send(SessionApiCrossCrateEvent::new(Event::SipTrace(
trace_event(),
)))
.await
.unwrap();
drop(tx);
let mut receiver = EventReceiver::new(rx);
let trace = receiver.next_sip_trace().await.unwrap();
assert_eq!(trace.sip_call_id.as_deref(), Some("wire-call"));
assert_eq!(trace.session_id, Some(SessionId("session-1".into())));
}
#[tokio::test]
async fn event_receiver_reuses_held_authority_for_public_observations() {
let store = Arc::new(crate::session_store::SessionStore::new());
let call_id = SessionId("exact-incoming-envelope".into());
store
.create_session(call_id.clone(), crate::state_table::types::Role::UAS, false)
.await
.expect("create exact incoming lifetime");
let lifecycle_handle = store
.lifecycle_handle(&call_id)
.expect("capture exact incoming lifetime");
let event = Event::IncomingCall {
call_id: call_id.clone(),
from: "sip:caller@example.test".into(),
to: "sip:callee@example.test".into(),
sdp: None,
};
let (tx, rx) = mpsc::channel::<Arc<dyn CrossCrateEvent>>(4);
tx.send(SessionApiCrossCrateEvent::new(event))
.await
.expect("send public observation");
let mut receiver = EventReceiver::filtered_exact(
rx,
lifecycle_handle.clone(),
Arc::clone(&store),
crate::api::lifecycle::LifecycleIndex::new(),
);
let (observed, observed_handle) = receiver
.next_with_lifecycle()
.await
.expect("receive incoming observation");
assert!(matches!(observed, Event::IncomingCall { .. }));
assert_eq!(observed_handle, Some(lifecycle_handle.clone()));
}
#[tokio::test]
async fn exact_filtered_receiver_requires_current_generation() {
let store = Arc::new(crate::session_store::SessionStore::new());
let call_id = SessionId("exact-filtered-reuse".into());
store
.create_session(call_id.clone(), crate::state_table::types::Role::UAS, false)
.await
.expect("create exact receiver lifetime");
let generation_a = store
.lifecycle_handle(&call_id)
.expect("capture generation A");
let event = Event::CallAnswered { call_id, sdp: None };
let (tx, rx) = mpsc::channel::<Arc<dyn CrossCrateEvent>>(8);
tx.send(SessionApiCrossCrateEvent::new(event))
.await
.expect("send current generation observation");
let mut receiver = EventReceiver::filtered_exact(
rx,
generation_a.clone(),
Arc::clone(&store),
crate::api::lifecycle::LifecycleIndex::new(),
);
let (_, observed_handle) = receiver
.next_with_lifecycle()
.await
.expect("receive only generation A");
assert_eq!(observed_handle, Some(generation_a));
assert!(receiver.try_next_with_lifecycle().is_none());
}
fn trace_event() -> SipTrace {
SipTrace {
direction: SipTraceDirection::Outbound,
transport: "UDP".into(),
local_addr: "127.0.0.1:5060".into(),
remote_addr: "127.0.0.1:5080".into(),
timestamp_unix_millis: 1,
start_line: "INVITE sip:bob@example.com SIP/2.0".into(),
sip_call_id: Some("wire-call".into()),
session_id: Some(SessionId("session-1".into())),
raw_message: "INVITE sip:bob@example.com SIP/2.0\n\n".into(),
original_len: 40,
truncated: false,
redacted: true,
}
}
}