2026-04-10 09:57:27 +00:00
|
|
|
//! Call manager — central registry and orchestration for all calls.
|
|
|
|
|
//!
|
2026-04-10 12:52:48 +00:00
|
|
|
//! Unified model: every call owns N legs and a mixer task.
|
|
|
|
|
//! Legs can be SIP (provider/device), WebRTC (browser), or Media (voicemail/IVR).
|
|
|
|
|
//! The mixer provides mix-minus audio to all participants.
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
use crate::call::{Call, CallDirection, CallState, LegId, LegInfo, LegKind, LegState};
|
2026-04-10 09:57:27 +00:00
|
|
|
use crate::config::{AppConfig, ProviderConfig};
|
|
|
|
|
use crate::ipc::{emit_event, OutTx};
|
2026-04-10 12:52:48 +00:00
|
|
|
use crate::leg_io::{create_leg_channels, spawn_sip_inbound, spawn_sip_outbound};
|
|
|
|
|
use crate::mixer::spawn_mixer;
|
2026-04-10 09:57:27 +00:00
|
|
|
use crate::registrar::Registrar;
|
|
|
|
|
use crate::rtp::RtpPortPool;
|
2026-04-10 12:52:48 +00:00
|
|
|
use crate::sip_leg::{SipLeg, SipLegAction, SipLegConfig};
|
2026-04-10 14:54:21 +00:00
|
|
|
use sip_proto::helpers::{build_sdp, generate_call_id, generate_tag, parse_sdp_endpoint, SdpOptions};
|
|
|
|
|
use sip_proto::message::{ResponseOptions, SipMessage};
|
2026-04-10 09:57:27 +00:00
|
|
|
use sip_proto::rewrite::{rewrite_sdp, rewrite_sip_uri};
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::net::UdpSocket;
|
|
|
|
|
|
|
|
|
|
pub struct CallManager {
|
2026-04-10 12:52:48 +00:00
|
|
|
/// All active calls, keyed by internal call ID.
|
|
|
|
|
pub calls: HashMap<String, Call>,
|
|
|
|
|
/// Index: SIP Call-ID → (internal call_id, leg_id).
|
|
|
|
|
/// Each SIP leg in a call has its own SIP Call-ID.
|
|
|
|
|
sip_index: HashMap<String, (String, LegId)>,
|
2026-04-10 09:57:27 +00:00
|
|
|
/// Call ID counter.
|
|
|
|
|
next_call_num: u64,
|
|
|
|
|
/// Output channel for events.
|
|
|
|
|
out_tx: OutTx,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CallManager {
|
|
|
|
|
pub fn new(out_tx: OutTx) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
calls: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
sip_index: HashMap::new(),
|
2026-04-10 09:57:27 +00:00
|
|
|
next_call_num: 0,
|
|
|
|
|
out_tx,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn next_call_id(&mut self) -> String {
|
|
|
|
|
let id = format!(
|
|
|
|
|
"call-{}-{}",
|
|
|
|
|
std::time::SystemTime::now()
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_millis(),
|
|
|
|
|
self.next_call_num,
|
|
|
|
|
);
|
|
|
|
|
self.next_call_num += 1;
|
|
|
|
|
id
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
fn next_leg_id(&mut self) -> String {
|
|
|
|
|
self.next_call_num += 1;
|
|
|
|
|
format!("leg-{}", self.next_call_num)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check if a SIP Call-ID belongs to any active call.
|
|
|
|
|
pub fn has_call(&self, sip_call_id: &str) -> bool {
|
|
|
|
|
self.sip_index.contains_key(sip_call_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get an RTP socket for a call's provider leg (used by webrtc_link).
|
|
|
|
|
pub fn get_call_provider_rtp_socket(&self, call_id: &str) -> Option<Arc<UdpSocket>> {
|
|
|
|
|
let call = self.calls.get(call_id)?;
|
|
|
|
|
for leg in call.legs.values() {
|
|
|
|
|
if leg.kind == LegKind::SipProvider {
|
|
|
|
|
return leg.rtp_socket.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all active call statuses for the dashboard.
|
|
|
|
|
pub fn get_all_statuses(&self) -> Vec<serde_json::Value> {
|
|
|
|
|
self.calls
|
|
|
|
|
.values()
|
|
|
|
|
.filter(|c| c.state != CallState::Terminated)
|
|
|
|
|
.map(|c| c.to_status_json())
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// SIP message routing
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Route a SIP message to the correct call and leg.
|
|
|
|
|
/// Returns true if the message was handled.
|
2026-04-10 09:57:27 +00:00
|
|
|
pub async fn route_sip_message(
|
|
|
|
|
&mut self,
|
|
|
|
|
msg: &SipMessage,
|
|
|
|
|
from_addr: SocketAddr,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
) -> bool {
|
|
|
|
|
let sip_call_id = msg.call_id().to_string();
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let (call_id, leg_id) = match self.sip_index.get(&sip_call_id) {
|
|
|
|
|
Some((cid, lid)) => (cid.clone(), lid.clone()),
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Check if this is a B2BUA leg (has a SipLeg with dialog management).
|
|
|
|
|
let is_b2bua_leg = self
|
|
|
|
|
.calls
|
|
|
|
|
.get(&call_id)
|
|
|
|
|
.and_then(|c| c.legs.get(&leg_id))
|
|
|
|
|
.map(|l| l.sip_leg.is_some())
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
if is_b2bua_leg {
|
|
|
|
|
return self
|
|
|
|
|
.route_b2bua_message(&call_id, &leg_id, msg, from_addr, socket)
|
|
|
|
|
.await;
|
2026-04-10 12:19:20 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Passthrough-style routing for inbound/outbound device↔provider calls.
|
|
|
|
|
self.route_passthrough_message(&call_id, &leg_id, msg, from_addr, socket, config)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Route a message to a B2BUA leg (has SipLeg dialog management).
|
|
|
|
|
async fn route_b2bua_message(
|
|
|
|
|
&mut self,
|
|
|
|
|
call_id: &str,
|
|
|
|
|
leg_id: &str,
|
|
|
|
|
msg: &SipMessage,
|
|
|
|
|
from_addr: SocketAddr,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
) -> bool {
|
|
|
|
|
// Process the SipLeg action first, extracting all needed data.
|
|
|
|
|
let (action, target, codecs, rtp_socket_clone) = {
|
|
|
|
|
let call = match self.calls.get_mut(call_id) {
|
|
|
|
|
Some(c) => c,
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
let leg = match call.legs.get_mut(leg_id) {
|
|
|
|
|
Some(l) => l,
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
let sip_leg = match &mut leg.sip_leg {
|
|
|
|
|
Some(sl) => sl,
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
let action = sip_leg.handle_message(msg);
|
|
|
|
|
let target = sip_leg.config.sip_target;
|
|
|
|
|
let codecs = sip_leg.config.codecs.clone();
|
|
|
|
|
let rtp_socket_clone = leg.rtp_socket.clone();
|
|
|
|
|
(action, target, codecs, rtp_socket_clone)
|
|
|
|
|
};
|
|
|
|
|
// Mutable borrow on call/leg is now released.
|
|
|
|
|
|
|
|
|
|
let sip_pt = codecs.first().copied().unwrap_or(9);
|
|
|
|
|
|
|
|
|
|
match action {
|
|
|
|
|
SipLegAction::None => {}
|
|
|
|
|
SipLegAction::Send(buf) => {
|
|
|
|
|
let _ = socket.send_to(&buf, target).await;
|
|
|
|
|
}
|
|
|
|
|
SipLegAction::StateChange(crate::sip_leg::LegState::Ringing) => {
|
|
|
|
|
if let Some(call) = self.calls.get_mut(call_id) {
|
|
|
|
|
if let Some(leg) = call.legs.get_mut(leg_id) {
|
|
|
|
|
leg.state = LegState::Ringing;
|
|
|
|
|
}
|
2026-04-10 14:54:21 +00:00
|
|
|
// Forward 180 Ringing to device if this is a device-originated call.
|
|
|
|
|
if let Some(device_invite) = &call.device_invite {
|
|
|
|
|
let device_leg = call.legs.values().find(|l| l.kind == LegKind::SipDevice);
|
|
|
|
|
if let Some(dev) = device_leg {
|
|
|
|
|
if let Some(dev_addr) = dev.signaling_addr {
|
|
|
|
|
let ringing = SipMessage::create_response(180, "Ringing", device_invite, None);
|
|
|
|
|
let _ = socket.send_to(&ringing.serialize(), dev_addr).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 12:52:48 +00:00
|
|
|
}
|
|
|
|
|
emit_event(&self.out_tx, "call_ringing", serde_json::json!({ "call_id": call_id }));
|
|
|
|
|
emit_event(&self.out_tx, "leg_state_changed",
|
|
|
|
|
serde_json::json!({ "call_id": call_id, "leg_id": leg_id, "state": "ringing" }));
|
|
|
|
|
}
|
|
|
|
|
SipLegAction::ConnectedWithAck(ack_buf) => {
|
|
|
|
|
let _ = socket.send_to(&ack_buf, target).await;
|
|
|
|
|
|
|
|
|
|
// Update leg state and get remote media.
|
|
|
|
|
let remote = {
|
|
|
|
|
let call = self.calls.get_mut(call_id).unwrap();
|
|
|
|
|
let leg = call.legs.get_mut(leg_id).unwrap();
|
|
|
|
|
let sip_leg = leg.sip_leg.as_ref().unwrap();
|
|
|
|
|
let remote = sip_leg.remote_media;
|
|
|
|
|
leg.state = LegState::Connected;
|
|
|
|
|
leg.remote_media = remote;
|
|
|
|
|
call.state = CallState::Connected;
|
|
|
|
|
remote
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// Wire the provider leg to the mixer if remote media is known.
|
2026-04-10 12:52:48 +00:00
|
|
|
if let (Some(remote_addr), Some(rtp_socket)) = (remote, rtp_socket_clone) {
|
|
|
|
|
let channels = create_leg_channels();
|
|
|
|
|
spawn_sip_inbound(rtp_socket.clone(), channels.inbound_tx);
|
|
|
|
|
spawn_sip_outbound(rtp_socket, remote_addr, channels.outbound_rx);
|
|
|
|
|
if let Some(call) = self.calls.get(call_id) {
|
|
|
|
|
call.add_leg_to_mixer(leg_id, sip_pt, channels.inbound_rx, channels.outbound_tx)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// For device-originated calls: send 200 OK to device and wire device leg.
|
|
|
|
|
if let Some(call) = self.calls.get(call_id) {
|
|
|
|
|
if let Some(device_invite) = call.device_invite.clone() {
|
|
|
|
|
let device_leg_info: Option<(SocketAddr, u16, Arc<UdpSocket>, Option<SocketAddr>, String)> =
|
|
|
|
|
call.legs.values().find(|l| l.kind == LegKind::SipDevice).and_then(|dev| {
|
|
|
|
|
Some((
|
|
|
|
|
dev.signaling_addr?,
|
|
|
|
|
dev.rtp_port,
|
|
|
|
|
dev.rtp_socket.clone()?,
|
|
|
|
|
dev.remote_media,
|
|
|
|
|
dev.id.clone(),
|
|
|
|
|
))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if let Some((dev_addr, dev_rtp_port, dev_rtp_socket, dev_remote, dev_leg_id)) = device_leg_info {
|
|
|
|
|
// Build SDP pointing device to our device_rtp port.
|
|
|
|
|
// Use LAN IP for the device (it's on the local network).
|
|
|
|
|
let call_ref = self.calls.get(call_id).unwrap();
|
|
|
|
|
let prov_leg = call_ref.legs.values().find(|l| l.kind == LegKind::SipProvider);
|
|
|
|
|
let lan_ip_str = prov_leg
|
|
|
|
|
.and_then(|l| l.sip_leg.as_ref())
|
|
|
|
|
.map(|sl| sl.config.lan_ip.clone())
|
|
|
|
|
.unwrap_or_else(|| "0.0.0.0".to_string());
|
|
|
|
|
|
|
|
|
|
let sdp = build_sdp(&SdpOptions {
|
|
|
|
|
ip: &lan_ip_str,
|
|
|
|
|
port: dev_rtp_port,
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let ok = SipMessage::create_response(200, "OK", &device_invite, Some(ResponseOptions {
|
|
|
|
|
to_tag: Some(generate_tag()),
|
|
|
|
|
contact: Some(format!("<sip:{}:{}>", lan_ip_str, 5060)),
|
|
|
|
|
body: Some(sdp),
|
|
|
|
|
content_type: Some("application/sdp".to_string()),
|
|
|
|
|
extra_headers: None,
|
|
|
|
|
}));
|
|
|
|
|
let _ = socket.send_to(&ok.serialize(), dev_addr).await;
|
|
|
|
|
|
|
|
|
|
// Update device leg state.
|
|
|
|
|
if let Some(call) = self.calls.get_mut(call_id) {
|
|
|
|
|
if let Some(dev_leg) = call.legs.get_mut(&dev_leg_id) {
|
|
|
|
|
dev_leg.state = LegState::Connected;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wire device leg to mixer.
|
|
|
|
|
if let Some(dev_remote_addr) = dev_remote {
|
|
|
|
|
let dev_channels = create_leg_channels();
|
|
|
|
|
spawn_sip_inbound(dev_rtp_socket.clone(), dev_channels.inbound_tx);
|
|
|
|
|
spawn_sip_outbound(dev_rtp_socket, dev_remote_addr, dev_channels.outbound_rx);
|
|
|
|
|
if let Some(call) = self.calls.get(call_id) {
|
|
|
|
|
call.add_leg_to_mixer(&dev_leg_id, sip_pt, dev_channels.inbound_rx, dev_channels.outbound_tx)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
emit_event(&self.out_tx, "call_answered", serde_json::json!({
|
|
|
|
|
"call_id": call_id,
|
|
|
|
|
"provider_media_addr": remote.map(|a| a.ip().to_string()),
|
|
|
|
|
"provider_media_port": remote.map(|a| a.port()),
|
|
|
|
|
"sip_pt": sip_pt,
|
|
|
|
|
}));
|
|
|
|
|
emit_event(&self.out_tx, "leg_state_changed",
|
|
|
|
|
serde_json::json!({ "call_id": call_id, "leg_id": leg_id, "state": "connected" }));
|
|
|
|
|
}
|
|
|
|
|
SipLegAction::Terminated(reason) => {
|
|
|
|
|
let duration = self.calls.get(call_id).map(|c| c.duration_secs()).unwrap_or(0);
|
2026-04-10 14:54:21 +00:00
|
|
|
|
|
|
|
|
// Notify device if this is a device-originated outbound call.
|
|
|
|
|
if let Some(call) = self.calls.get(call_id) {
|
|
|
|
|
if let Some(device_invite) = &call.device_invite {
|
|
|
|
|
let device_leg = call.legs.values().find(|l| l.kind == LegKind::SipDevice);
|
|
|
|
|
if let Some(dev) = device_leg {
|
|
|
|
|
if let Some(dev_addr) = dev.signaling_addr {
|
|
|
|
|
// Map reason to SIP response code.
|
|
|
|
|
let code: u16 = if reason.starts_with("rejected_") {
|
|
|
|
|
reason.strip_prefix("rejected_")
|
|
|
|
|
.and_then(|s| s.parse().ok())
|
|
|
|
|
.unwrap_or(503)
|
|
|
|
|
} else if reason == "bye" {
|
|
|
|
|
// Provider sent BYE — send BYE to device too.
|
|
|
|
|
// (200 OK already connected; just let terminate_call handle it)
|
|
|
|
|
0
|
|
|
|
|
} else {
|
|
|
|
|
503
|
|
|
|
|
};
|
|
|
|
|
if code > 0 && dev.state != LegState::Connected {
|
|
|
|
|
let resp = SipMessage::create_response(code, "Service Unavailable", device_invite, None);
|
|
|
|
|
let _ = socket.send_to(&resp.serialize(), dev_addr).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
if let Some(call) = self.calls.get_mut(call_id) {
|
|
|
|
|
if let Some(leg) = call.legs.get_mut(leg_id) {
|
|
|
|
|
leg.state = LegState::Terminated;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
emit_event(&self.out_tx, "call_ended",
|
|
|
|
|
serde_json::json!({ "call_id": call_id, "reason": reason, "duration": duration }));
|
|
|
|
|
self.terminate_call(call_id).await;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
SipLegAction::SendAndTerminate(buf, reason) => {
|
|
|
|
|
let _ = socket.send_to(&buf, from_addr).await;
|
|
|
|
|
let duration = self.calls.get(call_id).map(|c| c.duration_secs()).unwrap_or(0);
|
|
|
|
|
emit_event(&self.out_tx, "call_ended",
|
|
|
|
|
serde_json::json!({ "call_id": call_id, "reason": reason, "duration": duration }));
|
|
|
|
|
self.terminate_call(call_id).await;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
SipLegAction::AuthRetry { ack_407, invite_with_auth } => {
|
|
|
|
|
if let Some(ack) = ack_407 {
|
|
|
|
|
let _ = socket.send_to(&ack, target).await;
|
|
|
|
|
}
|
|
|
|
|
let _ = socket.send_to(&invite_with_auth, target).await;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Route a passthrough-style message (inbound/outbound device↔provider).
|
|
|
|
|
/// In the new model, both sides still go through the mixer, but SIP signaling
|
|
|
|
|
/// is forwarded between the two endpoints with SDP rewriting.
|
|
|
|
|
async fn route_passthrough_message(
|
|
|
|
|
&mut self,
|
|
|
|
|
call_id: &str,
|
|
|
|
|
this_leg_id: &str,
|
|
|
|
|
msg: &SipMessage,
|
|
|
|
|
from_addr: SocketAddr,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
) -> bool {
|
|
|
|
|
let call = match self.calls.get_mut(call_id) {
|
|
|
|
|
Some(c) => c,
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Find the "other" leg — the one we forward to.
|
|
|
|
|
let this_leg = call.legs.get(this_leg_id);
|
|
|
|
|
let this_kind = this_leg.map(|l| l.kind).unwrap_or(LegKind::SipProvider);
|
|
|
|
|
|
|
|
|
|
// Find the counterpart leg.
|
|
|
|
|
let other_leg = call.legs.values().find(|l| l.id != this_leg_id && l.state != LegState::Terminated);
|
|
|
|
|
let (other_addr, other_rtp_port, other_leg_id) = match other_leg {
|
|
|
|
|
Some(l) => (l.signaling_addr, l.rtp_port, l.id.clone()),
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
let forward_to = match other_addr {
|
|
|
|
|
Some(a) => a,
|
|
|
|
|
None => return false,
|
2026-04-10 09:57:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let lan_ip = config.proxy.lan_ip.clone();
|
|
|
|
|
let lan_port = config.proxy.lan_port;
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Get this leg's RTP port (for SDP rewriting — tell the other side to send RTP here).
|
|
|
|
|
let this_rtp_port = call.legs.get(this_leg_id).map(|l| l.rtp_port).unwrap_or(0);
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// Check if the other leg is a B2BUA leg (has SipLeg for proper dialog mgmt).
|
|
|
|
|
let other_has_sip_leg = call.legs.get(&other_leg_id)
|
|
|
|
|
.map(|l| l.sip_leg.is_some())
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
2026-04-10 09:57:27 +00:00
|
|
|
if msg.is_request() {
|
|
|
|
|
let method = msg.method().unwrap_or("");
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// ACK: In hybrid B2BUA mode, the device's ACK for our 200 OK
|
|
|
|
|
// is absorbed silently (provider's 200 was already ACKed by SipLeg).
|
|
|
|
|
if method == "ACK" {
|
|
|
|
|
if other_has_sip_leg {
|
|
|
|
|
return true; // Absorb — provider ACK handled by SipLeg.
|
|
|
|
|
}
|
|
|
|
|
// Pure passthrough: forward ACK normally.
|
|
|
|
|
let _ = socket.send_to(&msg.serialize(), forward_to).await;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// INVITE retransmit: the call already exists, re-send 100 Trying.
|
|
|
|
|
if method == "INVITE" {
|
|
|
|
|
let trying = SipMessage::create_response(100, "Trying", msg, None);
|
|
|
|
|
let _ = socket.send_to(&trying.serialize(), from_addr).await;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 09:57:27 +00:00
|
|
|
if method == "BYE" {
|
|
|
|
|
let ok = SipMessage::create_response(200, "OK", msg, None);
|
|
|
|
|
let _ = socket.send_to(&ok.serialize(), from_addr).await;
|
2026-04-10 14:54:21 +00:00
|
|
|
|
|
|
|
|
// If other leg has SipLeg, use build_hangup for proper dialog teardown.
|
|
|
|
|
if other_has_sip_leg {
|
|
|
|
|
if let Some(other) = call.legs.get_mut(&other_leg_id) {
|
|
|
|
|
if let Some(sip_leg) = &mut other.sip_leg {
|
|
|
|
|
if let Some(hangup_buf) = sip_leg.build_hangup() {
|
|
|
|
|
let _ = socket.send_to(&hangup_buf, sip_leg.config.sip_target).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let _ = socket.send_to(&msg.serialize(), forward_to).await;
|
|
|
|
|
}
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let duration = call.duration_secs();
|
2026-04-10 09:57:27 +00:00
|
|
|
emit_event(
|
|
|
|
|
&self.out_tx,
|
|
|
|
|
"call_ended",
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"call_id": call_id,
|
|
|
|
|
"reason": "bye",
|
|
|
|
|
"duration": duration,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-04-10 12:52:48 +00:00
|
|
|
self.terminate_call(call_id).await;
|
2026-04-10 09:57:27 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if method == "CANCEL" {
|
|
|
|
|
let ok = SipMessage::create_response(200, "OK", msg, None);
|
|
|
|
|
let _ = socket.send_to(&ok.serialize(), from_addr).await;
|
2026-04-10 14:54:21 +00:00
|
|
|
|
|
|
|
|
// If other leg has SipLeg, use build_hangup (produces CANCEL for early dialog).
|
|
|
|
|
if other_has_sip_leg {
|
|
|
|
|
if let Some(other) = call.legs.get_mut(&other_leg_id) {
|
|
|
|
|
if let Some(sip_leg) = &mut other.sip_leg {
|
|
|
|
|
if let Some(hangup_buf) = sip_leg.build_hangup() {
|
|
|
|
|
let _ = socket.send_to(&hangup_buf, sip_leg.config.sip_target).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let _ = socket.send_to(&msg.serialize(), forward_to).await;
|
|
|
|
|
}
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let duration = call.duration_secs();
|
2026-04-10 09:57:27 +00:00
|
|
|
emit_event(
|
|
|
|
|
&self.out_tx,
|
|
|
|
|
"call_ended",
|
2026-04-10 12:52:48 +00:00
|
|
|
serde_json::json!({ "call_id": call_id, "reason": "cancel", "duration": duration }),
|
2026-04-10 09:57:27 +00:00
|
|
|
);
|
2026-04-10 12:52:48 +00:00
|
|
|
self.terminate_call(call_id).await;
|
2026-04-10 09:57:27 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if method == "INFO" {
|
|
|
|
|
let ok = SipMessage::create_response(200, "OK", msg, None);
|
|
|
|
|
let _ = socket.send_to(&ok.serialize(), from_addr).await;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forward other requests with SDP rewriting.
|
|
|
|
|
let mut fwd = msg.clone();
|
2026-04-10 12:52:48 +00:00
|
|
|
// Rewrite SDP to point the other side to this leg's RTP port
|
|
|
|
|
// (so we receive their audio on our socket).
|
|
|
|
|
if fwd.has_sdp_body() {
|
|
|
|
|
let (new_body, _) = rewrite_sdp(&fwd.body, &lan_ip, other_rtp_port);
|
|
|
|
|
fwd.body = new_body;
|
|
|
|
|
fwd.update_content_length();
|
|
|
|
|
}
|
|
|
|
|
if this_kind == LegKind::SipProvider {
|
|
|
|
|
// From provider → forward to device: rewrite request URI.
|
2026-04-10 09:57:27 +00:00
|
|
|
if let Some(ruri) = fwd.request_uri().map(|s| s.to_string()) {
|
2026-04-10 12:52:48 +00:00
|
|
|
let new_ruri = rewrite_sip_uri(&ruri, &forward_to.ip().to_string(), forward_to.port());
|
2026-04-10 09:57:27 +00:00
|
|
|
fwd.set_request_uri(&new_ruri);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if fwd.is_dialog_establishing() {
|
|
|
|
|
fwd.prepend_header("Record-Route", &format!("<sip:{lan_ip}:{lan_port};lr>"));
|
|
|
|
|
}
|
|
|
|
|
let _ = socket.send_to(&fwd.serialize(), forward_to).await;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Responses ---
|
|
|
|
|
if msg.is_response() {
|
|
|
|
|
let code = msg.status_code().unwrap_or(0);
|
|
|
|
|
let cseq_method = msg.cseq_method().unwrap_or("").to_uppercase();
|
|
|
|
|
|
|
|
|
|
let mut fwd = msg.clone();
|
2026-04-10 12:52:48 +00:00
|
|
|
// Rewrite SDP so the forward-to side sends RTP to the correct leg port.
|
|
|
|
|
if fwd.has_sdp_body() {
|
|
|
|
|
let rewrite_ip = if this_kind == LegKind::SipDevice {
|
|
|
|
|
// Response from device → send to provider: use LAN/public IP.
|
|
|
|
|
&lan_ip
|
|
|
|
|
} else {
|
|
|
|
|
&lan_ip
|
|
|
|
|
};
|
|
|
|
|
let (new_body, _) = rewrite_sdp(&fwd.body, rewrite_ip, other_rtp_port);
|
|
|
|
|
fwd.body = new_body;
|
|
|
|
|
fwd.update_content_length();
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// State transitions on INVITE responses.
|
2026-04-10 09:57:27 +00:00
|
|
|
if cseq_method == "INVITE" {
|
2026-04-10 12:52:48 +00:00
|
|
|
if code == 180 || code == 183 {
|
|
|
|
|
if call.state == CallState::SettingUp {
|
|
|
|
|
call.state = CallState::Ringing;
|
|
|
|
|
emit_event(&self.out_tx, "call_ringing", serde_json::json!({ "call_id": call_id }));
|
|
|
|
|
}
|
|
|
|
|
if let Some(leg) = call.legs.get_mut(this_leg_id) {
|
|
|
|
|
leg.state = LegState::Ringing;
|
|
|
|
|
}
|
2026-04-10 09:57:27 +00:00
|
|
|
} else if code >= 200 && code < 300 {
|
2026-04-10 12:52:48 +00:00
|
|
|
let mut needs_wiring = false;
|
|
|
|
|
if let Some(leg) = call.legs.get_mut(this_leg_id) {
|
|
|
|
|
leg.state = LegState::Connected;
|
|
|
|
|
// Learn remote media from SDP.
|
|
|
|
|
if msg.has_sdp_body() {
|
|
|
|
|
if let Some(ep) = parse_sdp_endpoint(&msg.body) {
|
|
|
|
|
if let Ok(addr) = format!("{}:{}", ep.address, ep.port).parse() {
|
|
|
|
|
leg.remote_media = Some(addr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
needs_wiring = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if call.state != CallState::Connected {
|
|
|
|
|
call.state = CallState::Connected;
|
|
|
|
|
emit_event(&self.out_tx, "call_answered", serde_json::json!({ "call_id": call_id }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forward the response before wiring (drop call borrow).
|
|
|
|
|
let _ = socket.send_to(&fwd.serialize(), forward_to).await;
|
|
|
|
|
|
|
|
|
|
// Wire legs to mixer (needs &mut self, so call borrow must be released).
|
|
|
|
|
if needs_wiring {
|
|
|
|
|
self.maybe_wire_passthrough_legs(call_id).await;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2026-04-10 09:57:27 +00:00
|
|
|
} else if code >= 300 {
|
|
|
|
|
let duration = call.duration_secs();
|
|
|
|
|
emit_event(
|
|
|
|
|
&self.out_tx,
|
|
|
|
|
"call_ended",
|
2026-04-10 12:52:48 +00:00
|
|
|
serde_json::json!({ "call_id": call_id, "reason": format!("rejected_{code}"), "duration": duration }),
|
2026-04-10 09:57:27 +00:00
|
|
|
);
|
2026-04-10 12:52:48 +00:00
|
|
|
// Don't terminate yet — let the forward happen first.
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let _ = socket.send_to(&fwd.serialize(), forward_to).await;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
/// Wire passthrough legs to the mixer once both have remote media addresses.
|
|
|
|
|
async fn maybe_wire_passthrough_legs(&mut self, call_id: &str) {
|
|
|
|
|
let call = match self.calls.get(call_id) {
|
|
|
|
|
Some(c) => c,
|
|
|
|
|
None => return,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Collect legs that need wiring (have remote_media + rtp_socket but aren't yet in mixer).
|
|
|
|
|
let mut to_wire: Vec<(String, u8, Arc<UdpSocket>, SocketAddr)> = Vec::new();
|
|
|
|
|
for leg in call.legs.values() {
|
|
|
|
|
if leg.state == LegState::Connected || leg.state == LegState::Ringing {
|
|
|
|
|
if let (Some(rtp_socket), Some(remote)) = (&leg.rtp_socket, leg.remote_media) {
|
|
|
|
|
to_wire.push((leg.id.clone(), leg.codec_pt, rtp_socket.clone(), remote));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only wire if we have at least 2 legs ready.
|
|
|
|
|
if to_wire.len() < 2 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let call = match self.calls.get(call_id) {
|
|
|
|
|
Some(c) => c,
|
|
|
|
|
None => return,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (leg_id, codec_pt, rtp_socket, remote) in to_wire {
|
|
|
|
|
let channels = create_leg_channels();
|
|
|
|
|
spawn_sip_inbound(rtp_socket.clone(), channels.inbound_tx);
|
|
|
|
|
spawn_sip_outbound(rtp_socket, remote, channels.outbound_rx);
|
|
|
|
|
call.add_leg_to_mixer(&leg_id, codec_pt, channels.inbound_rx, channels.outbound_tx)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Call creation
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Create an inbound call (provider → device).
|
2026-04-10 09:57:27 +00:00
|
|
|
pub async fn create_inbound_call(
|
|
|
|
|
&mut self,
|
|
|
|
|
invite: &SipMessage,
|
|
|
|
|
from_addr: SocketAddr,
|
|
|
|
|
provider_id: &str,
|
|
|
|
|
provider_config: &ProviderConfig,
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
registrar: &Registrar,
|
|
|
|
|
rtp_pool: &mut RtpPortPool,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
public_ip: Option<&str>,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let call_id = self.next_call_id();
|
|
|
|
|
let lan_ip = &config.proxy.lan_ip;
|
|
|
|
|
let lan_port = config.proxy.lan_port;
|
2026-04-10 12:52:48 +00:00
|
|
|
let sip_call_id = invite.call_id().to_string();
|
2026-04-10 09:57:27 +00:00
|
|
|
|
|
|
|
|
// Extract caller/callee info.
|
|
|
|
|
let from_header = invite.get_header("From").unwrap_or("");
|
|
|
|
|
let caller_number = SipMessage::extract_uri(from_header)
|
|
|
|
|
.unwrap_or("Unknown")
|
|
|
|
|
.to_string();
|
|
|
|
|
let called_number = invite
|
|
|
|
|
.request_uri()
|
|
|
|
|
.and_then(|uri| SipMessage::extract_uri(uri))
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.to_string();
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Resolve target device.
|
2026-04-10 09:57:27 +00:00
|
|
|
let device_addr = match self.resolve_first_device(config, registrar) {
|
|
|
|
|
Some(addr) => addr,
|
|
|
|
|
None => {
|
2026-04-10 12:52:48 +00:00
|
|
|
// No device registered → voicemail.
|
2026-04-10 11:36:18 +00:00
|
|
|
return self
|
|
|
|
|
.route_to_voicemail(
|
2026-04-10 12:52:48 +00:00
|
|
|
&call_id, invite, from_addr, &caller_number,
|
|
|
|
|
provider_id, provider_config, config, rtp_pool, socket, public_ip,
|
2026-04-10 11:36:18 +00:00
|
|
|
)
|
|
|
|
|
.await;
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Allocate RTP ports for both legs.
|
|
|
|
|
let provider_rtp = match rtp_pool.allocate().await {
|
2026-04-10 09:57:27 +00:00
|
|
|
Some(a) => a,
|
|
|
|
|
None => {
|
|
|
|
|
let resp = SipMessage::create_response(503, "Service Unavailable", invite, None);
|
|
|
|
|
let _ = socket.send_to(&resp.serialize(), from_addr).await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-10 12:52:48 +00:00
|
|
|
let device_rtp = match rtp_pool.allocate().await {
|
|
|
|
|
Some(a) => a,
|
|
|
|
|
None => {
|
|
|
|
|
let resp = SipMessage::create_response(503, "Service Unavailable", invite, None);
|
|
|
|
|
let _ = socket.send_to(&resp.serialize(), from_addr).await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2026-04-10 09:57:27 +00:00
|
|
|
};
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Create the call with a mixer.
|
|
|
|
|
let (mixer_cmd_tx, mixer_task) = spawn_mixer(call_id.clone(), self.out_tx.clone());
|
|
|
|
|
let mut call = Call::new(
|
|
|
|
|
call_id.clone(),
|
|
|
|
|
CallDirection::Inbound,
|
|
|
|
|
provider_id.to_string(),
|
|
|
|
|
mixer_cmd_tx,
|
|
|
|
|
mixer_task,
|
|
|
|
|
);
|
|
|
|
|
call.caller_number = Some(caller_number);
|
|
|
|
|
call.callee_number = Some(called_number);
|
|
|
|
|
call.state = CallState::Ringing;
|
|
|
|
|
|
|
|
|
|
let codec_pt = provider_config.codecs.first().copied().unwrap_or(9);
|
|
|
|
|
|
|
|
|
|
// Provider leg — extract media from SDP.
|
|
|
|
|
let mut provider_media: Option<SocketAddr> = None;
|
2026-04-10 09:57:27 +00:00
|
|
|
if invite.has_sdp_body() {
|
|
|
|
|
if let Some(ep) = parse_sdp_endpoint(&invite.body) {
|
|
|
|
|
if let Ok(addr) = format!("{}:{}", ep.address, ep.port).parse() {
|
2026-04-10 12:52:48 +00:00
|
|
|
provider_media = Some(addr);
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let provider_leg_id = format!("{call_id}-prov");
|
|
|
|
|
call.legs.insert(
|
|
|
|
|
provider_leg_id.clone(),
|
|
|
|
|
LegInfo {
|
|
|
|
|
id: provider_leg_id.clone(),
|
|
|
|
|
kind: LegKind::SipProvider,
|
|
|
|
|
state: LegState::Connected, // Provider already connected (sent us the INVITE).
|
|
|
|
|
codec_pt,
|
|
|
|
|
sip_leg: None,
|
|
|
|
|
sip_call_id: Some(sip_call_id.clone()),
|
|
|
|
|
webrtc_session_id: None,
|
|
|
|
|
rtp_socket: Some(provider_rtp.socket.clone()),
|
|
|
|
|
rtp_port: provider_rtp.port,
|
|
|
|
|
remote_media: provider_media,
|
|
|
|
|
signaling_addr: Some(from_addr),
|
2026-04-10 14:54:21 +00:00
|
|
|
metadata: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Device leg.
|
|
|
|
|
let device_leg_id = format!("{call_id}-dev");
|
|
|
|
|
call.legs.insert(
|
|
|
|
|
device_leg_id.clone(),
|
|
|
|
|
LegInfo {
|
|
|
|
|
id: device_leg_id.clone(),
|
|
|
|
|
kind: LegKind::SipDevice,
|
|
|
|
|
state: LegState::Inviting,
|
|
|
|
|
codec_pt,
|
|
|
|
|
sip_leg: None,
|
|
|
|
|
sip_call_id: Some(sip_call_id.clone()), // Same SIP Call-ID for passthrough.
|
|
|
|
|
webrtc_session_id: None,
|
|
|
|
|
rtp_socket: Some(device_rtp.socket.clone()),
|
|
|
|
|
rtp_port: device_rtp.port,
|
|
|
|
|
remote_media: None, // Learned from device's 200 OK.
|
|
|
|
|
signaling_addr: Some(device_addr),
|
2026-04-10 14:54:21 +00:00
|
|
|
metadata: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Register SIP Call-ID → both legs (provider leg handles provider messages).
|
|
|
|
|
// For passthrough, both legs share the same SIP Call-ID.
|
|
|
|
|
// We route based on source address in route_passthrough_message.
|
|
|
|
|
self.sip_index
|
|
|
|
|
.insert(sip_call_id.clone(), (call_id.clone(), provider_leg_id.clone()));
|
2026-04-10 09:57:27 +00:00
|
|
|
|
|
|
|
|
// Rewrite and forward INVITE to device.
|
|
|
|
|
let mut fwd_invite = invite.clone();
|
|
|
|
|
fwd_invite.set_request_uri(&rewrite_sip_uri(
|
|
|
|
|
fwd_invite.request_uri().unwrap_or(""),
|
|
|
|
|
&device_addr.ip().to_string(),
|
|
|
|
|
device_addr.port(),
|
|
|
|
|
));
|
|
|
|
|
fwd_invite.prepend_header("Record-Route", &format!("<sip:{lan_ip}:{lan_port};lr>"));
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Rewrite SDP: tell the device to send RTP to the device leg's port.
|
2026-04-10 09:57:27 +00:00
|
|
|
if fwd_invite.has_sdp_body() {
|
2026-04-10 12:52:48 +00:00
|
|
|
let (new_body, _) = rewrite_sdp(&fwd_invite.body, lan_ip, device_rtp.port);
|
2026-04-10 09:57:27 +00:00
|
|
|
fwd_invite.body = new_body;
|
|
|
|
|
fwd_invite.update_content_length();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let _ = socket.send_to(&fwd_invite.serialize(), device_addr).await;
|
|
|
|
|
|
|
|
|
|
// Store the call.
|
2026-04-10 12:52:48 +00:00
|
|
|
self.calls.insert(call_id.clone(), call);
|
2026-04-10 09:57:27 +00:00
|
|
|
|
|
|
|
|
Some(call_id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
/// Initiate an outbound B2BUA call from the dashboard.
|
|
|
|
|
/// Creates a Call with a single SipLeg (provider). WebRTC leg added later via webrtc_link.
|
|
|
|
|
pub async fn make_outbound_call(
|
|
|
|
|
&mut self,
|
|
|
|
|
number: &str,
|
|
|
|
|
provider_config: &ProviderConfig,
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
rtp_pool: &mut RtpPortPool,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
public_ip: Option<&str>,
|
|
|
|
|
registered_aor: &str,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let call_id = self.next_call_id();
|
|
|
|
|
let lan_ip = &config.proxy.lan_ip;
|
|
|
|
|
let lan_port = config.proxy.lan_port;
|
|
|
|
|
|
|
|
|
|
let provider_dest: SocketAddr = match provider_config.outbound_proxy.to_socket_addr() {
|
|
|
|
|
Some(a) => a,
|
|
|
|
|
None => return None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let rtp_alloc = match rtp_pool.allocate().await {
|
|
|
|
|
Some(a) => a,
|
|
|
|
|
None => return None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let sip_call_id = generate_call_id(None);
|
|
|
|
|
|
|
|
|
|
// Create SipLeg for provider.
|
|
|
|
|
let leg_config = SipLegConfig {
|
|
|
|
|
lan_ip: lan_ip.clone(),
|
|
|
|
|
lan_port,
|
|
|
|
|
public_ip: public_ip.map(|s| s.to_string()),
|
|
|
|
|
sip_target: provider_dest,
|
|
|
|
|
username: Some(provider_config.username.clone()),
|
|
|
|
|
password: Some(provider_config.password.clone()),
|
|
|
|
|
registered_aor: Some(registered_aor.to_string()),
|
|
|
|
|
codecs: provider_config.codecs.clone(),
|
|
|
|
|
rtp_port: rtp_alloc.port,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let leg_id = format!("{call_id}-prov");
|
|
|
|
|
let mut sip_leg = SipLeg::new(leg_id.clone(), leg_config);
|
|
|
|
|
|
|
|
|
|
// Send INVITE.
|
|
|
|
|
let to_uri = format!("sip:{number}@{}", provider_config.domain);
|
|
|
|
|
sip_leg.send_invite(registered_aor, &to_uri, &sip_call_id, socket).await;
|
|
|
|
|
|
|
|
|
|
// Create call with mixer.
|
|
|
|
|
let (mixer_cmd_tx, mixer_task) = spawn_mixer(call_id.clone(), self.out_tx.clone());
|
|
|
|
|
let mut call = Call::new(
|
|
|
|
|
call_id.clone(),
|
|
|
|
|
CallDirection::Outbound,
|
|
|
|
|
provider_config.id.clone(),
|
|
|
|
|
mixer_cmd_tx,
|
|
|
|
|
mixer_task,
|
|
|
|
|
);
|
|
|
|
|
call.callee_number = Some(number.to_string());
|
|
|
|
|
|
|
|
|
|
let codec_pt = provider_config.codecs.first().copied().unwrap_or(9);
|
|
|
|
|
|
|
|
|
|
call.legs.insert(
|
|
|
|
|
leg_id.clone(),
|
|
|
|
|
LegInfo {
|
|
|
|
|
id: leg_id.clone(),
|
|
|
|
|
kind: LegKind::SipProvider,
|
|
|
|
|
state: LegState::Inviting,
|
|
|
|
|
codec_pt,
|
|
|
|
|
sip_leg: Some(sip_leg),
|
|
|
|
|
sip_call_id: Some(sip_call_id.clone()),
|
|
|
|
|
webrtc_session_id: None,
|
|
|
|
|
rtp_socket: Some(rtp_alloc.socket.clone()),
|
|
|
|
|
rtp_port: rtp_alloc.port,
|
|
|
|
|
remote_media: None,
|
|
|
|
|
signaling_addr: Some(provider_dest),
|
2026-04-10 14:54:21 +00:00
|
|
|
metadata: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Register for SIP routing.
|
|
|
|
|
self.sip_index
|
|
|
|
|
.insert(sip_call_id, (call_id.clone(), leg_id));
|
|
|
|
|
|
|
|
|
|
self.calls.insert(call_id.clone(), call);
|
|
|
|
|
Some(call_id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
/// Create a device-originated outbound call (device → provider) using hybrid B2BUA.
|
|
|
|
|
///
|
|
|
|
|
/// The device side is a simple passthrough leg (no SipLeg needed).
|
|
|
|
|
/// The provider side uses a full SipLeg for proper dialog management,
|
|
|
|
|
/// 407 auth, correct From URI, and public IP in SDP.
|
|
|
|
|
pub async fn create_device_outbound_call(
|
2026-04-10 09:57:27 +00:00
|
|
|
&mut self,
|
|
|
|
|
invite: &SipMessage,
|
|
|
|
|
from_addr: SocketAddr,
|
|
|
|
|
provider_config: &ProviderConfig,
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
rtp_pool: &mut RtpPortPool,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
public_ip: Option<&str>,
|
2026-04-10 14:54:21 +00:00
|
|
|
registered_aor: &str,
|
2026-04-10 09:57:27 +00:00
|
|
|
) -> Option<String> {
|
|
|
|
|
let call_id = self.next_call_id();
|
|
|
|
|
let lan_ip = &config.proxy.lan_ip;
|
|
|
|
|
let lan_port = config.proxy.lan_port;
|
2026-04-10 14:54:21 +00:00
|
|
|
let device_sip_call_id = invite.call_id().to_string();
|
|
|
|
|
|
|
|
|
|
let dialed_number = invite
|
|
|
|
|
.request_uri()
|
|
|
|
|
.and_then(|uri| SipMessage::extract_uri(uri))
|
|
|
|
|
.unwrap_or(invite.request_uri().unwrap_or(""))
|
|
|
|
|
.to_string();
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let provider_dest: SocketAddr = match provider_config.outbound_proxy.to_socket_addr() {
|
2026-04-10 09:57:27 +00:00
|
|
|
Some(a) => a,
|
|
|
|
|
None => return None,
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// Send 100 Trying to device immediately to stop retransmissions.
|
|
|
|
|
let trying = SipMessage::create_response(100, "Trying", invite, None);
|
|
|
|
|
let _ = socket.send_to(&trying.serialize(), from_addr).await;
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Allocate RTP ports for both legs.
|
|
|
|
|
let device_rtp = match rtp_pool.allocate().await {
|
2026-04-10 09:57:27 +00:00
|
|
|
Some(a) => a,
|
|
|
|
|
None => return None,
|
|
|
|
|
};
|
2026-04-10 12:52:48 +00:00
|
|
|
let provider_rtp = match rtp_pool.allocate().await {
|
|
|
|
|
Some(a) => a,
|
|
|
|
|
None => return None,
|
2026-04-10 09:57:27 +00:00
|
|
|
};
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let codec_pt = provider_config.codecs.first().copied().unwrap_or(9);
|
|
|
|
|
|
|
|
|
|
// Create call with mixer.
|
|
|
|
|
let (mixer_cmd_tx, mixer_task) = spawn_mixer(call_id.clone(), self.out_tx.clone());
|
|
|
|
|
let mut call = Call::new(
|
|
|
|
|
call_id.clone(),
|
|
|
|
|
CallDirection::Outbound,
|
|
|
|
|
provider_config.id.clone(),
|
|
|
|
|
mixer_cmd_tx,
|
|
|
|
|
mixer_task,
|
|
|
|
|
);
|
2026-04-10 14:54:21 +00:00
|
|
|
call.callee_number = Some(dialed_number.clone());
|
|
|
|
|
call.device_invite = Some(invite.clone());
|
2026-04-10 12:52:48 +00:00
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// --- Device leg (passthrough, no SipLeg) ---
|
2026-04-10 12:52:48 +00:00
|
|
|
let device_leg_id = format!("{call_id}-dev");
|
|
|
|
|
let mut device_media: Option<SocketAddr> = None;
|
|
|
|
|
if invite.has_sdp_body() {
|
|
|
|
|
if let Some(ep) = parse_sdp_endpoint(&invite.body) {
|
|
|
|
|
if let Ok(addr) = format!("{}:{}", ep.address, ep.port).parse() {
|
|
|
|
|
device_media = Some(addr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
call.legs.insert(
|
|
|
|
|
device_leg_id.clone(),
|
|
|
|
|
LegInfo {
|
|
|
|
|
id: device_leg_id.clone(),
|
|
|
|
|
kind: LegKind::SipDevice,
|
2026-04-10 14:54:21 +00:00
|
|
|
state: LegState::Inviting, // Not connected yet — waiting for provider answer
|
2026-04-10 12:52:48 +00:00
|
|
|
codec_pt,
|
|
|
|
|
sip_leg: None,
|
2026-04-10 14:54:21 +00:00
|
|
|
sip_call_id: Some(device_sip_call_id.clone()),
|
2026-04-10 12:52:48 +00:00
|
|
|
webrtc_session_id: None,
|
|
|
|
|
rtp_socket: Some(device_rtp.socket.clone()),
|
|
|
|
|
rtp_port: device_rtp.port,
|
|
|
|
|
remote_media: device_media,
|
|
|
|
|
signaling_addr: Some(from_addr),
|
2026-04-10 14:54:21 +00:00
|
|
|
metadata: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// Register device's SIP Call-ID → device leg.
|
|
|
|
|
self.sip_index
|
|
|
|
|
.insert(device_sip_call_id, (call_id.clone(), device_leg_id));
|
|
|
|
|
|
|
|
|
|
// --- Provider leg (B2BUA with SipLeg) ---
|
2026-04-10 12:52:48 +00:00
|
|
|
let provider_leg_id = format!("{call_id}-prov");
|
2026-04-10 14:54:21 +00:00
|
|
|
let provider_sip_call_id = generate_call_id(None);
|
|
|
|
|
|
|
|
|
|
let leg_config = SipLegConfig {
|
|
|
|
|
lan_ip: lan_ip.clone(),
|
|
|
|
|
lan_port,
|
|
|
|
|
public_ip: public_ip.map(|s| s.to_string()),
|
|
|
|
|
sip_target: provider_dest,
|
|
|
|
|
username: Some(provider_config.username.clone()),
|
|
|
|
|
password: Some(provider_config.password.clone()),
|
|
|
|
|
registered_aor: Some(registered_aor.to_string()),
|
|
|
|
|
codecs: provider_config.codecs.clone(),
|
|
|
|
|
rtp_port: provider_rtp.port,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut sip_leg = SipLeg::new(provider_leg_id.clone(), leg_config);
|
|
|
|
|
|
|
|
|
|
// Build proper To URI and send INVITE.
|
|
|
|
|
let to_uri = format!("sip:{}@{}", dialed_number, provider_config.domain);
|
|
|
|
|
sip_leg.send_invite(registered_aor, &to_uri, &provider_sip_call_id, socket).await;
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
call.legs.insert(
|
|
|
|
|
provider_leg_id.clone(),
|
|
|
|
|
LegInfo {
|
|
|
|
|
id: provider_leg_id.clone(),
|
|
|
|
|
kind: LegKind::SipProvider,
|
|
|
|
|
state: LegState::Inviting,
|
|
|
|
|
codec_pt,
|
2026-04-10 14:54:21 +00:00
|
|
|
sip_leg: Some(sip_leg),
|
|
|
|
|
sip_call_id: Some(provider_sip_call_id.clone()),
|
2026-04-10 12:52:48 +00:00
|
|
|
webrtc_session_id: None,
|
|
|
|
|
rtp_socket: Some(provider_rtp.socket.clone()),
|
|
|
|
|
rtp_port: provider_rtp.port,
|
|
|
|
|
remote_media: None,
|
|
|
|
|
signaling_addr: Some(provider_dest),
|
2026-04-10 14:54:21 +00:00
|
|
|
metadata: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-10 14:54:21 +00:00
|
|
|
// Register provider's SIP Call-ID → provider leg.
|
2026-04-10 12:52:48 +00:00
|
|
|
self.sip_index
|
2026-04-10 14:54:21 +00:00
|
|
|
.insert(provider_sip_call_id, (call_id.clone(), provider_leg_id));
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
self.calls.insert(call_id.clone(), call);
|
2026-04-10 09:57:27 +00:00
|
|
|
Some(call_id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Leg management (mid-call add/remove)
|
|
|
|
|
// -----------------------------------------------------------------------
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
/// Add a SIP leg to an existing call (e.g., add external participant).
|
|
|
|
|
pub async fn add_external_leg(
|
|
|
|
|
&mut self,
|
|
|
|
|
call_id: &str,
|
|
|
|
|
number: &str,
|
|
|
|
|
provider_config: &ProviderConfig,
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
rtp_pool: &mut RtpPortPool,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
public_ip: Option<&str>,
|
|
|
|
|
registered_aor: &str,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let call = self.calls.get(call_id)?;
|
|
|
|
|
let lan_ip = &config.proxy.lan_ip;
|
|
|
|
|
let lan_port = config.proxy.lan_port;
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let provider_dest: SocketAddr = provider_config.outbound_proxy.to_socket_addr()?;
|
|
|
|
|
let rtp_alloc = rtp_pool.allocate().await?;
|
|
|
|
|
let sip_call_id = generate_call_id(None);
|
|
|
|
|
let leg_id = self.next_leg_id();
|
|
|
|
|
|
|
|
|
|
let leg_config = SipLegConfig {
|
|
|
|
|
lan_ip: lan_ip.clone(),
|
|
|
|
|
lan_port,
|
|
|
|
|
public_ip: public_ip.map(|s| s.to_string()),
|
|
|
|
|
sip_target: provider_dest,
|
|
|
|
|
username: Some(provider_config.username.clone()),
|
|
|
|
|
password: Some(provider_config.password.clone()),
|
|
|
|
|
registered_aor: Some(registered_aor.to_string()),
|
|
|
|
|
codecs: provider_config.codecs.clone(),
|
|
|
|
|
rtp_port: rtp_alloc.port,
|
2026-04-10 09:57:27 +00:00
|
|
|
};
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let mut sip_leg = SipLeg::new(leg_id.clone(), leg_config);
|
|
|
|
|
let to_uri = format!("sip:{number}@{}", provider_config.domain);
|
|
|
|
|
sip_leg.send_invite(registered_aor, &to_uri, &sip_call_id, socket).await;
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let codec_pt = provider_config.codecs.first().copied().unwrap_or(9);
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let leg_info = LegInfo {
|
|
|
|
|
id: leg_id.clone(),
|
|
|
|
|
kind: LegKind::SipProvider,
|
|
|
|
|
state: LegState::Inviting,
|
|
|
|
|
codec_pt,
|
|
|
|
|
sip_leg: Some(sip_leg),
|
|
|
|
|
sip_call_id: Some(sip_call_id.clone()),
|
|
|
|
|
webrtc_session_id: None,
|
|
|
|
|
rtp_socket: Some(rtp_alloc.socket.clone()),
|
|
|
|
|
rtp_port: rtp_alloc.port,
|
|
|
|
|
remote_media: None,
|
|
|
|
|
signaling_addr: Some(provider_dest),
|
2026-04-10 14:54:21 +00:00
|
|
|
metadata: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
};
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
self.sip_index
|
|
|
|
|
.insert(sip_call_id, (call_id.to_string(), leg_id.clone()));
|
|
|
|
|
|
|
|
|
|
let call = self.calls.get_mut(call_id).unwrap();
|
|
|
|
|
call.legs.insert(leg_id.clone(), leg_info);
|
2026-04-10 09:57:27 +00:00
|
|
|
|
|
|
|
|
emit_event(
|
|
|
|
|
&self.out_tx,
|
2026-04-10 12:52:48 +00:00
|
|
|
"leg_added",
|
2026-04-10 09:57:27 +00:00
|
|
|
serde_json::json!({
|
2026-04-10 12:52:48 +00:00
|
|
|
"call_id": call_id,
|
|
|
|
|
"leg_id": leg_id,
|
|
|
|
|
"kind": "sip-provider",
|
|
|
|
|
"state": "inviting",
|
|
|
|
|
"number": number,
|
2026-04-10 09:57:27 +00:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
Some(leg_id)
|
2026-04-10 12:19:20 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
/// Remove a leg from a call.
|
|
|
|
|
pub async fn remove_leg(
|
2026-04-10 12:19:20 +00:00
|
|
|
&mut self,
|
2026-04-10 12:52:48 +00:00
|
|
|
call_id: &str,
|
|
|
|
|
leg_id: &str,
|
2026-04-10 12:19:20 +00:00
|
|
|
socket: &UdpSocket,
|
|
|
|
|
) -> bool {
|
2026-04-10 12:52:48 +00:00
|
|
|
let call = match self.calls.get_mut(call_id) {
|
2026-04-10 12:19:20 +00:00
|
|
|
Some(c) => c,
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Remove from mixer.
|
|
|
|
|
call.remove_leg_from_mixer(leg_id).await;
|
2026-04-10 12:19:20 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Send BYE if it's a SIP leg.
|
|
|
|
|
if let Some(leg) = call.legs.get_mut(leg_id) {
|
|
|
|
|
if let Some(sip_leg) = &mut leg.sip_leg {
|
|
|
|
|
if let Some(hangup_bytes) = sip_leg.build_hangup() {
|
|
|
|
|
let _ = socket.send_to(&hangup_bytes, sip_leg.config.sip_target).await;
|
2026-04-10 12:19:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 12:52:48 +00:00
|
|
|
leg.state = LegState::Terminated;
|
|
|
|
|
|
|
|
|
|
// Clean up SIP index.
|
|
|
|
|
if let Some(sip_cid) = &leg.sip_call_id {
|
|
|
|
|
self.sip_index.remove(sip_cid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit_event(
|
|
|
|
|
&self.out_tx,
|
|
|
|
|
"leg_removed",
|
|
|
|
|
serde_json::json!({ "call_id": call_id, "leg_id": leg_id }),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If fewer than 2 active legs remain, end the call.
|
|
|
|
|
let active_legs = call
|
|
|
|
|
.legs
|
|
|
|
|
.values()
|
|
|
|
|
.filter(|l| l.state != LegState::Terminated)
|
|
|
|
|
.count();
|
|
|
|
|
if active_legs <= 1 {
|
|
|
|
|
let duration = call.duration_secs();
|
|
|
|
|
emit_event(
|
|
|
|
|
&self.out_tx,
|
|
|
|
|
"call_ended",
|
|
|
|
|
serde_json::json!({ "call_id": call_id, "reason": "last_leg", "duration": duration }),
|
|
|
|
|
);
|
|
|
|
|
self.terminate_call(call_id).await;
|
2026-04-10 12:19:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Hangup + cleanup
|
|
|
|
|
// -----------------------------------------------------------------------
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
/// Hangup a call by internal call ID.
|
|
|
|
|
pub async fn hangup(&mut self, call_id: &str, socket: &UdpSocket) -> bool {
|
|
|
|
|
let call = match self.calls.get_mut(call_id) {
|
|
|
|
|
Some(c) => c,
|
|
|
|
|
None => return false,
|
2026-04-10 11:36:18 +00:00
|
|
|
};
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
if call.state == CallState::Terminated {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let duration = call.duration_secs();
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Send BYE to all SIP legs.
|
|
|
|
|
for leg in call.legs.values_mut() {
|
|
|
|
|
if leg.state == LegState::Terminated {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Some(sip_leg) = &mut leg.sip_leg {
|
|
|
|
|
if let Some(hangup_bytes) = sip_leg.build_hangup() {
|
|
|
|
|
let _ = socket.send_to(&hangup_bytes, sip_leg.config.sip_target).await;
|
|
|
|
|
}
|
|
|
|
|
} else if let Some(addr) = leg.signaling_addr {
|
|
|
|
|
// Passthrough leg — send a simple BYE.
|
|
|
|
|
if let Some(sip_cid) = &leg.sip_call_id {
|
|
|
|
|
let bye = format!(
|
|
|
|
|
"BYE sip:hangup SIP/2.0\r\n\
|
|
|
|
|
Via: SIP/2.0/UDP 0.0.0.0:0;branch=z9hG4bK-hangup\r\n\
|
|
|
|
|
Call-ID: {sip_cid}\r\n\
|
|
|
|
|
CSeq: 99 BYE\r\n\
|
|
|
|
|
Max-Forwards: 70\r\n\
|
|
|
|
|
Content-Length: 0\r\n\r\n"
|
|
|
|
|
);
|
|
|
|
|
let _ = socket.send_to(bye.as_bytes(), addr).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
leg.state = LegState::Terminated;
|
|
|
|
|
}
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
emit_event(
|
|
|
|
|
&self.out_tx,
|
|
|
|
|
"call_ended",
|
|
|
|
|
serde_json::json!({ "call_id": call_id, "reason": "hangup_command", "duration": duration }),
|
|
|
|
|
);
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
self.terminate_call(call_id).await;
|
|
|
|
|
true
|
|
|
|
|
}
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
/// Clean up a terminated call: shutdown mixer, remove from indexes.
|
|
|
|
|
async fn terminate_call(&mut self, call_id: &str) {
|
|
|
|
|
if let Some(mut call) = self.calls.remove(call_id) {
|
|
|
|
|
call.state = CallState::Terminated;
|
|
|
|
|
call.shutdown_mixer().await;
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Remove all SIP index entries for this call.
|
|
|
|
|
self.sip_index.retain(|_, (cid, _)| cid != call_id);
|
|
|
|
|
}
|
2026-04-10 11:36:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Voicemail
|
|
|
|
|
// -----------------------------------------------------------------------
|
2026-04-10 11:36:18 +00:00
|
|
|
|
|
|
|
|
async fn route_to_voicemail(
|
|
|
|
|
&mut self,
|
|
|
|
|
call_id: &str,
|
|
|
|
|
invite: &SipMessage,
|
|
|
|
|
from_addr: SocketAddr,
|
|
|
|
|
caller_number: &str,
|
|
|
|
|
provider_id: &str,
|
|
|
|
|
provider_config: &ProviderConfig,
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
rtp_pool: &mut RtpPortPool,
|
|
|
|
|
socket: &UdpSocket,
|
|
|
|
|
public_ip: Option<&str>,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let lan_ip = &config.proxy.lan_ip;
|
|
|
|
|
let pub_ip = public_ip.unwrap_or(lan_ip.as_str());
|
|
|
|
|
|
|
|
|
|
let rtp_alloc = match rtp_pool.allocate().await {
|
|
|
|
|
Some(a) => a,
|
|
|
|
|
None => {
|
2026-04-10 12:52:48 +00:00
|
|
|
let resp = SipMessage::create_response(503, "Service Unavailable", invite, None);
|
2026-04-10 11:36:18 +00:00
|
|
|
let _ = socket.send_to(&resp.serialize(), from_addr).await;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
let codec_pt = provider_config.codecs.first().copied().unwrap_or(9);
|
2026-04-10 11:36:18 +00:00
|
|
|
|
|
|
|
|
let sdp = sip_proto::helpers::build_sdp(&sip_proto::helpers::SdpOptions {
|
|
|
|
|
ip: pub_ip,
|
|
|
|
|
port: rtp_alloc.port,
|
|
|
|
|
payload_types: &provider_config.codecs,
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let response = SipMessage::create_response(
|
2026-04-10 12:52:48 +00:00
|
|
|
200, "OK", invite,
|
2026-04-10 11:36:18 +00:00
|
|
|
Some(sip_proto::message::ResponseOptions {
|
|
|
|
|
to_tag: Some(sip_proto::helpers::generate_tag()),
|
|
|
|
|
contact: Some(format!("<sip:{}:{}>", lan_ip, config.proxy.lan_port)),
|
|
|
|
|
body: Some(sdp),
|
|
|
|
|
content_type: Some("application/sdp".to_string()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let _ = socket.send_to(&response.serialize(), from_addr).await;
|
|
|
|
|
|
|
|
|
|
let provider_media = if invite.has_sdp_body() {
|
2026-04-10 12:52:48 +00:00
|
|
|
parse_sdp_endpoint(&invite.body)
|
2026-04-10 11:36:18 +00:00
|
|
|
.and_then(|ep| format!("{}:{}", ep.address, ep.port).parse().ok())
|
|
|
|
|
} else {
|
2026-04-10 12:52:48 +00:00
|
|
|
Some(from_addr)
|
2026-04-10 11:36:18 +00:00
|
|
|
};
|
|
|
|
|
let provider_media = provider_media.unwrap_or(from_addr);
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Create a minimal call for BYE routing.
|
|
|
|
|
let (mixer_cmd_tx, mixer_task) = spawn_mixer(call_id.to_string(), self.out_tx.clone());
|
|
|
|
|
let mut call = Call::new(
|
|
|
|
|
call_id.to_string(),
|
|
|
|
|
CallDirection::Inbound,
|
|
|
|
|
provider_id.to_string(),
|
|
|
|
|
mixer_cmd_tx,
|
|
|
|
|
mixer_task,
|
|
|
|
|
);
|
|
|
|
|
call.state = CallState::Voicemail;
|
|
|
|
|
call.caller_number = Some(caller_number.to_string());
|
|
|
|
|
|
|
|
|
|
let provider_leg_id = format!("{call_id}-prov");
|
|
|
|
|
call.legs.insert(
|
|
|
|
|
provider_leg_id.clone(),
|
|
|
|
|
LegInfo {
|
|
|
|
|
id: provider_leg_id.clone(),
|
|
|
|
|
kind: LegKind::SipProvider,
|
|
|
|
|
state: LegState::Connected,
|
|
|
|
|
codec_pt,
|
|
|
|
|
sip_leg: None,
|
|
|
|
|
sip_call_id: Some(invite.call_id().to_string()),
|
|
|
|
|
webrtc_session_id: None,
|
|
|
|
|
rtp_socket: Some(rtp_alloc.socket.clone()),
|
|
|
|
|
rtp_port: rtp_alloc.port,
|
|
|
|
|
remote_media: Some(provider_media),
|
|
|
|
|
signaling_addr: Some(from_addr),
|
2026-04-10 14:54:21 +00:00
|
|
|
metadata: HashMap::new(),
|
2026-04-10 12:52:48 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self.sip_index.insert(
|
|
|
|
|
invite.call_id().to_string(),
|
|
|
|
|
(call_id.to_string(), provider_leg_id),
|
|
|
|
|
);
|
|
|
|
|
self.calls.insert(call_id.to_string(), call);
|
2026-04-10 11:36:18 +00:00
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// Build recording path.
|
2026-04-10 11:36:18 +00:00
|
|
|
let timestamp = std::time::SystemTime::now()
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_millis();
|
2026-04-10 12:52:48 +00:00
|
|
|
let recording_dir = "nogit/voicemail/default".to_string();
|
2026-04-10 11:36:18 +00:00
|
|
|
let recording_path = format!("{recording_dir}/msg-{timestamp}.wav");
|
|
|
|
|
let greeting_wav = find_greeting_wav();
|
|
|
|
|
|
|
|
|
|
let out_tx = self.out_tx.clone();
|
|
|
|
|
let call_id_owned = call_id.to_string();
|
|
|
|
|
let caller_owned = caller_number.to_string();
|
|
|
|
|
let rtp_socket = rtp_alloc.socket;
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
crate::voicemail::run_voicemail_session(
|
2026-04-10 12:52:48 +00:00
|
|
|
rtp_socket, provider_media, codec_pt,
|
|
|
|
|
greeting_wav, recording_path, 120_000,
|
|
|
|
|
call_id_owned, caller_owned, out_tx,
|
2026-04-10 11:36:18 +00:00
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Some(call_id.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:48 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Internal helpers
|
|
|
|
|
// -----------------------------------------------------------------------
|
2026-04-10 09:57:27 +00:00
|
|
|
|
|
|
|
|
fn resolve_first_device(&self, config: &AppConfig, registrar: &Registrar) -> Option<SocketAddr> {
|
|
|
|
|
for device in &config.devices {
|
|
|
|
|
if let Some(addr) = registrar.get_device_contact(&device.id) {
|
|
|
|
|
return Some(addr);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 12:52:48 +00:00
|
|
|
None
|
2026-04-10 11:36:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn find_greeting_wav() -> Option<String> {
|
|
|
|
|
let candidates = [
|
|
|
|
|
".nogit/voicemail/default/greeting.wav",
|
|
|
|
|
".nogit/voicemail/greeting.wav",
|
|
|
|
|
];
|
|
|
|
|
for path in &candidates {
|
|
|
|
|
if std::path::Path::new(path).exists() {
|
|
|
|
|
return Some(path.to_string());
|
|
|
|
|
}
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
2026-04-10 12:52:48 +00:00
|
|
|
None
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|