From 93f671f1f9bf165c01da675ebcdb4f43727677ef Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 10 Apr 2026 12:52:48 +0000 Subject: [PATCH] feat(proxy-engine): add multiparty call mixing with dynamic SIP and WebRTC leg management --- changelog.md | 8 + nogit/voicemail/default/msg-1775825168199.wav | Bin 0 -> 318764 bytes rust/crates/proxy-engine/src/call.rs | 189 ++- rust/crates/proxy-engine/src/call_manager.rs | 1348 ++++++++++------- rust/crates/proxy-engine/src/leg_io.rs | 80 + rust/crates/proxy-engine/src/main.rs | 196 ++- rust/crates/proxy-engine/src/mixer.rs | 232 +++ rust/crates/proxy-engine/src/webrtc_engine.rs | 258 +--- ts/00_commitinfo_data.ts | 2 +- ts/frontend.ts | 19 +- ts/proxybridge.ts | 32 + ts/sipproxy.ts | 15 + ts_web/00_commitinfo_data.ts | 2 +- 13 files changed, 1572 insertions(+), 809 deletions(-) create mode 100644 nogit/voicemail/default/msg-1775825168199.wav create mode 100644 rust/crates/proxy-engine/src/leg_io.rs create mode 100644 rust/crates/proxy-engine/src/mixer.rs diff --git a/changelog.md b/changelog.md index 621c155..6c0cc2c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-10 - 1.14.0 - feat(proxy-engine) +add multiparty call mixing with dynamic SIP and WebRTC leg management + +- replace passthrough call handling with a mixer-backed call model that tracks multiple legs and exposes leg status in call state output +- add mixer and leg I/O infrastructure to bridge SIP RTP and WebRTC audio through channel-based mix-minus processing +- introduce add_leg and remove_leg proxy commands and wire frontend bridge APIs to manage external call legs +- emit leg lifecycle events for observability and mark unimplemented device-leg and transfer HTTP endpoints with 501 responses + ## 2026-04-10 - 1.13.0 - feat(proxy-engine,webrtc) add B2BUA SIP leg handling and WebRTC call bridging for outbound calls diff --git a/nogit/voicemail/default/msg-1775825168199.wav b/nogit/voicemail/default/msg-1775825168199.wav new file mode 100644 index 0000000000000000000000000000000000000000..7dc145ae4a378e924ad79d1b309613e63b963a9f GIT binary patch literal 318764 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
  • 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

    #jdYE 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π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 qnPNr3-p}pQT_v3L3&XD literal 0 HcmV?d00001 diff --git a/rust/crates/proxy-engine/src/call.rs b/rust/crates/proxy-engine/src/call.rs index 686bcdf..ba38c0c 100644 --- a/rust/crates/proxy-engine/src/call.rs +++ b/rust/crates/proxy-engine/src/call.rs @@ -1,12 +1,19 @@ -//! Call hub — owns legs and bridges media. +//! Call hub — owns N legs and a mixer task. //! -//! Each Call has a unique ID and tracks its state, direction, and associated -//! SIP Call-IDs for message routing. +//! Every call has a central mixer that provides mix-minus audio to all +//! participants. Legs can be added and removed dynamically mid-call. +use crate::mixer::{MixerCommand, RtpPacket}; +use crate::sip_leg::SipLeg; +use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use std::time::Instant; use tokio::net::UdpSocket; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + +pub type LegId = String; /// Call state machine. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -15,8 +22,6 @@ pub enum CallState { Ringing, Connected, Voicemail, - Ivr, - Terminating, Terminated, } @@ -27,8 +32,6 @@ impl CallState { Self::Ringing => "ringing", Self::Connected => "connected", Self::Voicemail => "voicemail", - Self::Ivr => "ivr", - Self::Terminating => "terminating", Self::Terminated => "terminated", } } @@ -49,43 +52,172 @@ impl CallDirection { } } -/// A passthrough call — both sides share the same SIP Call-ID. -/// The proxy rewrites SDP/Contact/Request-URI and relays RTP. -pub struct PassthroughCall { +/// The type of a call leg. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LegKind { + SipProvider, + SipDevice, + WebRtc, + Media, // voicemail playback, IVR, recording +} + +impl LegKind { + pub fn as_str(&self) -> &'static str { + match self { + Self::SipProvider => "sip-provider", + Self::SipDevice => "sip-device", + Self::WebRtc => "webrtc", + Self::Media => "media", + } + } +} + +/// Per-leg state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LegState { + Inviting, + Ringing, + Connected, + Terminated, +} + +impl LegState { + pub fn as_str(&self) -> &'static str { + match self { + Self::Inviting => "inviting", + Self::Ringing => "ringing", + Self::Connected => "connected", + Self::Terminated => "terminated", + } + } +} + +/// Information about a single leg in a call. +pub struct LegInfo { + pub id: LegId, + pub kind: LegKind, + pub state: LegState, + pub codec_pt: u8, + + /// For SIP legs: the SIP dialog manager (handles 407 auth, BYE, etc). + pub sip_leg: Option, + /// For SIP legs: the SIP Call-ID for message routing. + pub sip_call_id: Option, + /// For WebRTC legs: the session ID in WebRtcEngine. + pub webrtc_session_id: Option, + /// The RTP socket allocated for this leg. + pub rtp_socket: Option>, + /// The RTP port number. + pub rtp_port: u16, + /// The remote media endpoint (learned from SDP or address learning). + pub remote_media: Option, + /// SIP signaling address (provider or device). + pub signaling_addr: Option, +} + +/// A multiparty call with N legs and a central mixer. +pub struct Call { pub id: String, - pub sip_call_id: String, pub state: CallState, pub direction: CallDirection, pub created_at: Instant, - // Call metadata. + // Metadata. pub caller_number: Option, pub callee_number: Option, pub provider_id: String, - // Provider side. - pub provider_addr: SocketAddr, - pub provider_media: Option, + /// All legs in this call, keyed by leg ID. + pub legs: HashMap, - // Device side. - pub device_addr: SocketAddr, - pub device_media: Option, + /// Channel to send commands to the mixer task. + pub mixer_cmd_tx: mpsc::Sender, - // RTP relay. - pub rtp_port: u16, - pub rtp_socket: Arc, - - // Packet counters. - pub pkt_from_device: u64, - pub pkt_from_provider: u64, + /// Handle to the mixer task (aborted on call teardown). + mixer_task: Option>, } -impl PassthroughCall { +impl Call { + pub fn new( + id: String, + direction: CallDirection, + provider_id: String, + mixer_cmd_tx: mpsc::Sender, + mixer_task: JoinHandle<()>, + ) -> Self { + Self { + id, + state: CallState::SettingUp, + direction, + created_at: Instant::now(), + caller_number: None, + callee_number: None, + provider_id, + legs: HashMap::new(), + mixer_cmd_tx, + mixer_task: Some(mixer_task), + } + } + + /// Add a leg to the mixer. Sends the AddLeg command with channel endpoints. + pub async fn add_leg_to_mixer( + &self, + leg_id: &str, + codec_pt: u8, + inbound_rx: mpsc::Receiver, + outbound_tx: mpsc::Sender>, + ) { + let _ = self + .mixer_cmd_tx + .send(MixerCommand::AddLeg { + leg_id: leg_id.to_string(), + codec_pt, + inbound_rx, + outbound_tx, + }) + .await; + } + + /// Remove a leg from the mixer. + pub async fn remove_leg_from_mixer(&self, leg_id: &str) { + let _ = self + .mixer_cmd_tx + .send(MixerCommand::RemoveLeg { + leg_id: leg_id.to_string(), + }) + .await; + } + pub fn duration_secs(&self) -> u64 { self.created_at.elapsed().as_secs() } + /// Shut down the mixer and abort its task. + pub async fn shutdown_mixer(&mut self) { + let _ = self.mixer_cmd_tx.send(MixerCommand::Shutdown).await; + if let Some(handle) = self.mixer_task.take() { + handle.abort(); + } + } + + /// Produce a JSON status snapshot for the dashboard. pub fn to_status_json(&self) -> serde_json::Value { + let legs: Vec = self + .legs + .values() + .filter(|l| l.state != LegState::Terminated) + .map(|l| { + serde_json::json!({ + "id": l.id, + "type": l.kind.as_str(), + "state": l.state.as_str(), + "codec": sip_proto::helpers::codec_name(l.codec_pt), + "rtpPort": l.rtp_port, + "remoteMedia": l.remote_media.map(|a| format!("{}:{}", a.ip(), a.port())), + }) + }) + .collect(); + serde_json::json!({ "id": self.id, "state": self.state.as_str(), @@ -93,11 +225,8 @@ impl PassthroughCall { "callerNumber": self.caller_number, "calleeNumber": self.callee_number, "providerUsed": self.provider_id, - "createdAt": self.created_at.elapsed().as_millis(), "duration": self.duration_secs(), - "rtpPort": self.rtp_port, - "pktFromDevice": self.pkt_from_device, - "pktFromProvider": self.pkt_from_provider, + "legs": legs, }) } } diff --git a/rust/crates/proxy-engine/src/call_manager.rs b/rust/crates/proxy-engine/src/call_manager.rs index 906978a..a93b5b6 100644 --- a/rust/crates/proxy-engine/src/call_manager.rs +++ b/rust/crates/proxy-engine/src/call_manager.rs @@ -1,47 +1,31 @@ //! Call manager — central registry and orchestration for all calls. //! -//! Handles: -//! - Inbound passthrough calls (provider → proxy → device) -//! - Outbound passthrough calls (device → proxy → provider) -//! - SIP message routing by Call-ID -//! - BYE/CANCEL handling -//! - RTP relay setup -//! -//! Ported from ts/call/call-manager.ts (passthrough mode). +//! 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. -use crate::call::{CallDirection, CallState, PassthroughCall}; +use crate::call::{Call, CallDirection, CallState, LegId, LegInfo, LegKind, LegState}; use crate::config::{AppConfig, ProviderConfig}; -use crate::dtmf::DtmfDetector; use crate::ipc::{emit_event, OutTx}; +use crate::leg_io::{create_leg_channels, spawn_sip_inbound, spawn_sip_outbound}; +use crate::mixer::spawn_mixer; use crate::registrar::Registrar; use crate::rtp::RtpPortPool; -use crate::sip_leg::{LegState, SipLeg, SipLegAction, SipLegConfig}; -use sip_proto::helpers::parse_sdp_endpoint; +use crate::sip_leg::{SipLeg, SipLegAction, SipLegConfig}; +use sip_proto::helpers::{generate_call_id, parse_sdp_endpoint}; use sip_proto::message::SipMessage; use sip_proto::rewrite::{rewrite_sdp, rewrite_sip_uri}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; -use std::time::Instant; use tokio::net::UdpSocket; -/// A B2BUA call with a SipLeg for the provider side. -/// The other side is either a WebRTC session or another SipLeg. -pub struct B2buaCall { - pub id: String, - pub provider_leg: SipLeg, - pub webrtc_session_id: Option, - pub number: String, - pub created_at: std::time::Instant, - /// RTP socket allocated for the provider leg (used for WebRTC audio bridging). - pub rtp_socket: Option>, -} - pub struct CallManager { - /// Active passthrough calls, keyed by SIP Call-ID. - calls: HashMap, - /// Active B2BUA calls, keyed by SIP Call-ID of the provider leg. - b2bua_calls: HashMap, + /// All active calls, keyed by internal call ID. + pub calls: HashMap, + /// Index: SIP Call-ID → (internal call_id, leg_id). + /// Each SIP leg in a call has its own SIP Call-ID. + sip_index: HashMap, /// Call ID counter. next_call_num: u64, /// Output channel for events. @@ -52,13 +36,12 @@ impl CallManager { pub fn new(out_tx: OutTx) -> Self { Self { calls: HashMap::new(), - b2bua_calls: HashMap::new(), + sip_index: HashMap::new(), next_call_num: 0, out_tx, } } - /// Generate a unique call ID. fn next_call_id(&mut self) -> String { let id = format!( "call-{}-{}", @@ -72,55 +55,237 @@ impl CallManager { id } - /// Try to route a SIP message to an existing call. - /// Returns true if handled. + 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> { + 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 { + 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. pub async fn route_sip_message( &mut self, msg: &SipMessage, from_addr: SocketAddr, socket: &UdpSocket, config: &AppConfig, - _registrar: &Registrar, ) -> bool { let sip_call_id = msg.call_id().to_string(); - // Check B2BUA calls first (provider legs with dialog management). - if self.b2bua_calls.contains_key(&sip_call_id) { - return self.route_b2bua_message(&sip_call_id, msg, from_addr, socket).await; + 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; } - // Check passthrough calls. - if !self.calls.contains_key(&sip_call_id) { - return false; + // 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; + } + } + 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 + }; + + // Wire the leg to the mixer if remote media is known. + 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; + } + } + + 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); + 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; + } + _ => {} } - // Extract needed data from the call to avoid borrow conflicts. - let (call_id, provider_addr, device_addr, rtp_port, from_provider) = { - let call = self.calls.get(&sip_call_id).unwrap(); - let from_provider = from_addr.ip().to_string() == call.provider_addr.ip().to_string(); - ( - call.id.clone(), - call.provider_addr, - call.device_addr, - call.rtp_port, - from_provider, - ) + 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, }; let lan_ip = config.proxy.lan_ip.clone(); let lan_port = config.proxy.lan_port; + // 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); + if msg.is_request() { let method = msg.method().unwrap_or(""); - let forward_to = if from_provider { device_addr } else { provider_addr }; - // Handle BYE. if method == "BYE" { let ok = SipMessage::create_response(200, "OK", msg, None); let _ = socket.send_to(&ok.serialize(), from_addr).await; let _ = socket.send_to(&msg.serialize(), forward_to).await; - let duration = self.calls.get(&sip_call_id).unwrap().duration_secs(); + let duration = call.duration_secs(); emit_event( &self.out_tx, "call_ended", @@ -128,54 +293,48 @@ impl CallManager { "call_id": call_id, "reason": "bye", "duration": duration, - "from_side": if from_provider { "provider" } else { "device" }, }), ); - self.calls.get_mut(&sip_call_id).unwrap().state = CallState::Terminated; + self.terminate_call(call_id).await; return true; } - // Handle CANCEL. if method == "CANCEL" { let ok = SipMessage::create_response(200, "OK", msg, None); let _ = socket.send_to(&ok.serialize(), from_addr).await; let _ = socket.send_to(&msg.serialize(), forward_to).await; - let duration = self.calls.get(&sip_call_id).unwrap().duration_secs(); + let duration = call.duration_secs(); emit_event( &self.out_tx, "call_ended", - serde_json::json!({ - "call_id": call_id, "reason": "cancel", "duration": duration, - }), + serde_json::json!({ "call_id": call_id, "reason": "cancel", "duration": duration }), ); - self.calls.get_mut(&sip_call_id).unwrap().state = CallState::Terminated; + self.terminate_call(call_id).await; return true; } - // Handle INFO (DTMF relay). if method == "INFO" { let ok = SipMessage::create_response(200, "OK", msg, None); let _ = socket.send_to(&ok.serialize(), from_addr).await; - - // Detect DTMF from INFO body. - if let Some(ct) = msg.get_header("Content-Type") { - let mut detector = DtmfDetector::new(call_id.clone(), self.out_tx.clone()); - detector.process_sip_info(ct, &msg.body); - } return true; } // Forward other requests with SDP rewriting. let mut fwd = msg.clone(); - if from_provider { - rewrite_sdp_for_device(&mut fwd, &lan_ip, rtp_port); + // 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. if let Some(ruri) = fwd.request_uri().map(|s| s.to_string()) { - let new_ruri = rewrite_sip_uri(&ruri, &device_addr.ip().to_string(), device_addr.port()); + let new_ruri = rewrite_sip_uri(&ruri, &forward_to.ip().to_string(), forward_to.port()); fwd.set_request_uri(&new_ruri); } - } else { - rewrite_sdp_for_provider(&mut fwd, &lan_ip, rtp_port); } if fwd.is_dialog_establishing() { fwd.prepend_header("Record-Route", &format!("")); @@ -188,42 +347,67 @@ impl CallManager { if msg.is_response() { let code = msg.status_code().unwrap_or(0); let cseq_method = msg.cseq_method().unwrap_or("").to_uppercase(); - let forward_to = if from_provider { device_addr } else { provider_addr }; let mut fwd = msg.clone(); - if from_provider { - rewrite_sdp_for_device(&mut fwd, &lan_ip, rtp_port); - } else { - rewrite_sdp_for_provider(&mut fwd, &lan_ip, rtp_port); - if let Some(contact) = fwd.get_header("Contact").map(|s| s.to_string()) { - let new_contact = rewrite_sip_uri(&contact, &lan_ip, lan_port); - if new_contact != contact { - fwd.set_header("Contact", &new_contact); - } - } + // 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(); } - // State transitions. + // State transitions on INVITE responses. if cseq_method == "INVITE" { - let call = self.calls.get_mut(&sip_call_id).unwrap(); - if (code == 180 || code == 183) && call.state == CallState::SettingUp { - call.state = CallState::Ringing; - emit_event(&self.out_tx, "call_ringing", serde_json::json!({ "call_id": call_id })); + 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; + } } else if code >= 200 && code < 300 { - call.state = CallState::Connected; - emit_event(&self.out_tx, "call_answered", serde_json::json!({ "call_id": call_id })); + 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; } else if code >= 300 { let duration = call.duration_secs(); - call.state = CallState::Terminated; emit_event( &self.out_tx, "call_ended", - serde_json::json!({ - "call_id": call_id, - "reason": format!("rejected_{code}"), - "duration": duration, - }), + serde_json::json!({ "call_id": call_id, "reason": format!("rejected_{code}"), "duration": duration }), ); + // Don't terminate yet — let the forward happen first. } } @@ -234,7 +418,47 @@ impl CallManager { false } - /// Create an inbound passthrough call (provider → device). + /// 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, 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). pub async fn create_inbound_call( &mut self, invite: &SipMessage, @@ -250,6 +474,7 @@ impl CallManager { let call_id = self.next_call_id(); let lan_ip = &config.proxy.lan_ip; let lan_port = config.proxy.lan_port; + let sip_call_id = invite.call_id().to_string(); // Extract caller/callee info. let from_header = invite.get_header("From").unwrap_or(""); @@ -262,30 +487,30 @@ impl CallManager { .unwrap_or("") .to_string(); - // Resolve target device (first registered device). + // Resolve target device. let device_addr = match self.resolve_first_device(config, registrar) { Some(addr) => addr, None => { - // No device registered — route to voicemail. + // No device registered → voicemail. return self .route_to_voicemail( - &call_id, - invite, - from_addr, - &caller_number, - provider_id, - provider_config, - config, - rtp_pool, - socket, - public_ip, + &call_id, invite, from_addr, &caller_number, + provider_id, provider_config, config, rtp_pool, socket, public_ip, ) .await; } }; - // Allocate RTP port. - let rtp_alloc = match rtp_pool.allocate().await { + // Allocate RTP ports for both legs. + let provider_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; + } + }; + let device_rtp = match rtp_pool.allocate().await { Some(a) => a, None => { let resp = SipMessage::create_response(503, "Service Unavailable", invite, None); @@ -294,42 +519,73 @@ impl CallManager { } }; - // Create the call. - let mut call = PassthroughCall { - id: call_id.clone(), - sip_call_id: invite.call_id().to_string(), - state: CallState::Ringing, - direction: CallDirection::Inbound, - created_at: Instant::now(), - caller_number: Some(caller_number), - callee_number: Some(called_number), - provider_id: provider_id.to_string(), - provider_addr: from_addr, - provider_media: None, - device_addr, - device_media: None, - rtp_port: rtp_alloc.port, - rtp_socket: rtp_alloc.socket.clone(), - pkt_from_device: 0, - pkt_from_provider: 0, - }; + // 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; - // Extract provider media from SDP. + let codec_pt = provider_config.codecs.first().copied().unwrap_or(9); + + // Provider leg — extract media from SDP. + let mut provider_media: Option = 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() { - call.provider_media = Some(addr); + provider_media = Some(addr); } } } - // Start RTP relay. - let rtp_socket = rtp_alloc.socket.clone(); - let device_addr_for_relay = device_addr; - let provider_addr_for_relay = from_addr; - tokio::spawn(async move { - rtp_relay_loop(rtp_socket, device_addr_for_relay, provider_addr_for_relay).await; - }); + 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), + }, + ); + + // 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), + }, + ); + + // 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())); // Rewrite and forward INVITE to device. let mut fwd_invite = invite.clone(); @@ -340,264 +596,23 @@ impl CallManager { )); fwd_invite.prepend_header("Record-Route", &format!("")); + // Rewrite SDP: tell the device to send RTP to the device leg's port. if fwd_invite.has_sdp_body() { - let (new_body, original) = rewrite_sdp(&fwd_invite.body, lan_ip, rtp_alloc.port); + let (new_body, _) = rewrite_sdp(&fwd_invite.body, lan_ip, device_rtp.port); fwd_invite.body = new_body; fwd_invite.update_content_length(); - if let Some(ep) = original { - if let Ok(addr) = format!("{}:{}", ep.address, ep.port).parse() { - call.provider_media = Some(addr); - } - } } let _ = socket.send_to(&fwd_invite.serialize(), device_addr).await; // Store the call. - self.calls.insert(call.sip_call_id.clone(), call); + self.calls.insert(call_id.clone(), call); Some(call_id) } - /// Create an outbound passthrough call (device → provider). - pub async fn create_outbound_passthrough( - &mut self, - invite: &SipMessage, - from_addr: SocketAddr, - provider_config: &ProviderConfig, - config: &AppConfig, - rtp_pool: &mut RtpPortPool, - socket: &UdpSocket, - public_ip: Option<&str>, - ) -> Option { - let call_id = self.next_call_id(); - let lan_ip = &config.proxy.lan_ip; - let lan_port = config.proxy.lan_port; - let pub_ip = public_ip.unwrap_or(lan_ip.as_str()); - - let callee = invite.request_uri().unwrap_or("").to_string(); - - // Allocate RTP port. - let rtp_alloc = match rtp_pool.allocate().await { - Some(a) => a, - None => return None, - }; - - let provider_dest: SocketAddr = match provider_config.outbound_proxy.to_socket_addr() { - Some(a) => a, - None => return None, - }; - - let mut call = PassthroughCall { - id: call_id.clone(), - sip_call_id: invite.call_id().to_string(), - state: CallState::SettingUp, - direction: CallDirection::Outbound, - created_at: Instant::now(), - caller_number: None, - callee_number: Some(callee), - provider_id: provider_config.id.clone(), - provider_addr: provider_dest, - provider_media: None, - device_addr: from_addr, - device_media: None, - rtp_port: rtp_alloc.port, - rtp_socket: rtp_alloc.socket.clone(), - pkt_from_device: 0, - pkt_from_provider: 0, - }; - - // Start RTP relay. - let rtp_socket = rtp_alloc.socket.clone(); - let device_addr_for_relay = from_addr; - let provider_addr_for_relay = provider_dest; - tokio::spawn(async move { - rtp_relay_loop(rtp_socket, device_addr_for_relay, provider_addr_for_relay).await; - }); - - // Rewrite and forward INVITE to provider. - let mut fwd_invite = invite.clone(); - fwd_invite.prepend_header("Record-Route", &format!("")); - - // Rewrite Contact to public IP. - if let Some(contact) = fwd_invite.get_header("Contact").map(|s| s.to_string()) { - let new_contact = rewrite_sip_uri(&contact, pub_ip, lan_port); - if new_contact != contact { - fwd_invite.set_header("Contact", &new_contact); - } - } - - // Rewrite SDP. - if fwd_invite.has_sdp_body() { - let (new_body, original) = rewrite_sdp(&fwd_invite.body, pub_ip, rtp_alloc.port); - fwd_invite.body = new_body; - fwd_invite.update_content_length(); - if let Some(ep) = original { - if let Ok(addr) = format!("{}:{}", ep.address, ep.port).parse() { - call.device_media = Some(addr); - } - } - } - - let _ = socket.send_to(&fwd_invite.serialize(), provider_dest).await; - - self.calls.insert(call.sip_call_id.clone(), call); - Some(call_id) - } - - /// Hangup a call by call ID (from TypeScript command). - pub async fn hangup(&mut self, call_id: &str, socket: &UdpSocket) -> bool { - // Find the call by our internal call ID. - let sip_call_id = self - .calls - .iter() - .find(|(_, c)| c.id == call_id) - .map(|(k, _)| k.clone()); - - let sip_call_id = match sip_call_id { - Some(id) => id, - None => return false, - }; - - let call = match self.calls.get_mut(&sip_call_id) { - Some(c) => c, - None => return false, - }; - - if call.state == CallState::Terminated { - return false; - } - - // Build and send BYE to both sides. - // For passthrough, we build a simple BYE using the SIP Call-ID. - let bye_msg = 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: {}\r\n\ - CSeq: 99 BYE\r\n\ - Max-Forwards: 70\r\n\ - Content-Length: 0\r\n\r\n", - sip_call_id - ); - let bye_bytes = bye_msg.as_bytes(); - - let _ = socket.send_to(bye_bytes, call.provider_addr).await; - let _ = socket.send_to(bye_bytes, call.device_addr).await; - - call.state = CallState::Terminated; - - emit_event( - &self.out_tx, - "call_ended", - serde_json::json!({ - "call_id": call.id, - "reason": "hangup_command", - "duration": call.duration_secs(), - }), - ); - - true - } - - /// Get all active call statuses. - pub fn get_all_statuses(&self) -> Vec { - self.calls - .values() - .filter(|c| c.state != CallState::Terminated) - .map(|c| c.to_status_json()) - .collect() - } - - /// Clean up terminated calls. - pub fn cleanup_terminated(&mut self) { - self.calls.retain(|_, c| c.state != CallState::Terminated); - } - - /// Check if a SIP Call-ID belongs to any active call. - pub fn has_call(&self, sip_call_id: &str) -> bool { - self.calls.contains_key(sip_call_id) || self.b2bua_calls.contains_key(sip_call_id) - } - - /// Get the RTP socket for a B2BUA call (by our internal call ID). - /// Used by webrtc_link to set up the audio bridge. - pub fn get_b2bua_rtp_socket(&self, call_id: &str) -> Option> { - for b2bua in self.b2bua_calls.values() { - if b2bua.id == call_id { - return b2bua.rtp_socket.clone(); - } - } - None - } - - // --- B2BUA outbound call --- - - /// Route a SIP message to a B2BUA call's provider leg. - async fn route_b2bua_message( - &mut self, - sip_call_id: &str, - msg: &SipMessage, - from_addr: SocketAddr, - socket: &UdpSocket, - ) -> bool { - let b2bua = match self.b2bua_calls.get_mut(sip_call_id) { - Some(c) => c, - None => return false, - }; - - let call_id = b2bua.id.clone(); - let action = b2bua.provider_leg.handle_message(msg); - - match action { - SipLegAction::None => {} - SipLegAction::Send(buf) => { - let _ = socket.send_to(&buf, b2bua.provider_leg.config.sip_target).await; - } - SipLegAction::StateChange(LegState::Ringing) => { - emit_event(&self.out_tx, "call_ringing", serde_json::json!({ "call_id": call_id })); - } - SipLegAction::ConnectedWithAck(ack_buf) => { - let _ = socket.send_to(&ack_buf, b2bua.provider_leg.config.sip_target).await; - let remote = b2bua.provider_leg.remote_media; - let sip_pt = b2bua.provider_leg.config.codecs.first().copied().unwrap_or(9); - 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, - })); - } - SipLegAction::Terminated(reason) => { - let duration = b2bua.created_at.elapsed().as_secs(); - emit_event(&self.out_tx, "call_ended", serde_json::json!({ - "call_id": call_id, "reason": reason, "duration": duration, - })); - self.b2bua_calls.remove(sip_call_id); - return true; - } - SipLegAction::SendAndTerminate(buf, reason) => { - let _ = socket.send_to(&buf, from_addr).await; - let duration = b2bua.created_at.elapsed().as_secs(); - emit_event(&self.out_tx, "call_ended", serde_json::json!({ - "call_id": call_id, "reason": reason, "duration": duration, - })); - self.b2bua_calls.remove(sip_call_id); - return true; - } - SipLegAction::AuthRetry { ack_407, invite_with_auth } => { - let target = b2bua.provider_leg.config.sip_target; - if let Some(ack) = ack_407 { - let _ = socket.send_to(&ack, target).await; - } - let _ = socket.send_to(&invite_with_auth, target).await; - } - _ => {} - } - - true - } - - /// Initiate an outbound call from the dashboard using B2BUA mode. - /// Creates a SipLeg for the provider side with proper dialog + auth handling. + /// 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, @@ -617,16 +632,14 @@ impl CallManager { None => return None, }; - // Allocate RTP port for the provider leg. let rtp_alloc = match rtp_pool.allocate().await { Some(a) => a, None => return None, }; - // Build the SIP Call-ID for the provider dialog. - let sip_call_id = sip_proto::helpers::generate_call_id(None); + let sip_call_id = generate_call_id(None); - // Create a SipLeg with provider credentials for auth handling. + // Create SipLeg for provider. let leg_config = SipLegConfig { lan_ip: lan_ip.clone(), lan_port, @@ -639,29 +652,372 @@ impl CallManager { rtp_port: rtp_alloc.port, }; - let mut leg = SipLeg::new(format!("{call_id}-prov"), leg_config); + let leg_id = format!("{call_id}-prov"); + let mut sip_leg = SipLeg::new(leg_id.clone(), leg_config); - // Send the INVITE. + // Send INVITE. let to_uri = format!("sip:{number}@{}", provider_config.domain); - leg.send_invite(registered_aor, &to_uri, &sip_call_id, socket).await; + sip_leg.send_invite(registered_aor, &to_uri, &sip_call_id, socket).await; - // Store as B2BUA call. - let b2bua = B2buaCall { - id: call_id.clone(), - provider_leg: leg, - webrtc_session_id: None, - number: number.to_string(), - created_at: std::time::Instant::now(), - rtp_socket: Some(rtp_alloc.socket.clone()), - }; - self.b2bua_calls.insert(sip_call_id, b2bua); + // 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), + }, + ); + + // 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) } - // --- Voicemail --- + /// Create an outbound passthrough call (device → provider). + pub async fn create_outbound_passthrough( + &mut self, + invite: &SipMessage, + from_addr: SocketAddr, + provider_config: &ProviderConfig, + config: &AppConfig, + rtp_pool: &mut RtpPortPool, + socket: &UdpSocket, + public_ip: Option<&str>, + ) -> Option { + let call_id = self.next_call_id(); + let lan_ip = &config.proxy.lan_ip; + let lan_port = config.proxy.lan_port; + let pub_ip = public_ip.unwrap_or(lan_ip.as_str()); + let sip_call_id = invite.call_id().to_string(); + let callee = invite.request_uri().unwrap_or("").to_string(); + + let provider_dest: SocketAddr = match provider_config.outbound_proxy.to_socket_addr() { + Some(a) => a, + None => return None, + }; + + // Allocate RTP ports for both legs. + let device_rtp = match rtp_pool.allocate().await { + Some(a) => a, + None => return None, + }; + let provider_rtp = match rtp_pool.allocate().await { + Some(a) => a, + None => return None, + }; + + 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, + ); + call.callee_number = Some(callee); + + // Device leg. + let device_leg_id = format!("{call_id}-dev"); + let mut device_media: Option = 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); + } + } + } + + call.legs.insert( + device_leg_id.clone(), + LegInfo { + id: device_leg_id.clone(), + kind: LegKind::SipDevice, + state: LegState::Connected, + codec_pt, + sip_leg: None, + sip_call_id: Some(sip_call_id.clone()), + 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), + }, + ); + + // Provider leg. + 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::Inviting, + 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: None, + signaling_addr: Some(provider_dest), + }, + ); + + self.sip_index + .insert(sip_call_id.clone(), (call_id.clone(), device_leg_id)); + + // Forward INVITE to provider with SDP rewriting. + let mut fwd_invite = invite.clone(); + fwd_invite.prepend_header("Record-Route", &format!("")); + + if let Some(contact) = fwd_invite.get_header("Contact").map(|s| s.to_string()) { + let new_contact = rewrite_sip_uri(&contact, pub_ip, lan_port); + if new_contact != contact { + fwd_invite.set_header("Contact", &new_contact); + } + } + + // Tell provider to send RTP to our provider_rtp port. + if fwd_invite.has_sdp_body() { + let (new_body, _) = rewrite_sdp(&fwd_invite.body, pub_ip, provider_rtp.port); + fwd_invite.body = new_body; + fwd_invite.update_content_length(); + } + + let _ = socket.send_to(&fwd_invite.serialize(), provider_dest).await; + + self.calls.insert(call_id.clone(), call); + Some(call_id) + } + + // ----------------------------------------------------------------------- + // Leg management (mid-call add/remove) + // ----------------------------------------------------------------------- + + /// 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 { + let call = self.calls.get(call_id)?; + let lan_ip = &config.proxy.lan_ip; + let lan_port = config.proxy.lan_port; + + 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, + }; + + 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; + + let codec_pt = provider_config.codecs.first().copied().unwrap_or(9); + + 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), + }; + + 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); + + emit_event( + &self.out_tx, + "leg_added", + serde_json::json!({ + "call_id": call_id, + "leg_id": leg_id, + "kind": "sip-provider", + "state": "inviting", + "number": number, + }), + ); + + Some(leg_id) + } + + /// Remove a leg from a call. + pub async fn remove_leg( + &mut self, + call_id: &str, + leg_id: &str, + socket: &UdpSocket, + ) -> bool { + let call = match self.calls.get_mut(call_id) { + Some(c) => c, + None => return false, + }; + + // Remove from mixer. + call.remove_leg_from_mixer(leg_id).await; + + // 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; + } + } + 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; + } + + true + } + + // ----------------------------------------------------------------------- + // Hangup + cleanup + // ----------------------------------------------------------------------- + + /// 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, + }; + + if call.state == CallState::Terminated { + return false; + } + + let duration = call.duration_secs(); + + // 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; + } + + emit_event( + &self.out_tx, + "call_ended", + serde_json::json!({ "call_id": call_id, "reason": "hangup_command", "duration": duration }), + ); + + self.terminate_call(call_id).await; + true + } + + /// 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; + + // Remove all SIP index entries for this call. + self.sip_index.retain(|_, (cid, _)| cid != call_id); + } + } + + // ----------------------------------------------------------------------- + // Voicemail + // ----------------------------------------------------------------------- - /// Route a call to voicemail: answer the INVITE, play greeting, record message. async fn route_to_voicemail( &mut self, call_id: &str, @@ -678,21 +1034,17 @@ impl CallManager { let lan_ip = &config.proxy.lan_ip; let pub_ip = public_ip.unwrap_or(lan_ip.as_str()); - // Allocate RTP port for the voicemail session. let rtp_alloc = match rtp_pool.allocate().await { Some(a) => a, None => { - let resp = - SipMessage::create_response(503, "Service Unavailable", invite, None); + let resp = SipMessage::create_response(503, "Service Unavailable", invite, None); let _ = socket.send_to(&resp.serialize(), from_addr).await; return None; } }; - // Determine provider's preferred codec. - let codec_pt = provider_config.codecs.first().copied().unwrap_or(9); // default G.722 + let codec_pt = provider_config.codecs.first().copied().unwrap_or(9); - // Build SDP with our RTP port. let sdp = sip_proto::helpers::build_sdp(&sip_proto::helpers::SdpOptions { ip: pub_ip, port: rtp_alloc.port, @@ -700,11 +1052,8 @@ impl CallManager { ..Default::default() }); - // Answer the INVITE with 200 OK. let response = SipMessage::create_response( - 200, - "OK", - invite, + 200, "OK", invite, Some(sip_proto::message::ResponseOptions { to_tag: Some(sip_proto::helpers::generate_tag()), contact: Some(format!("", lan_ip, config.proxy.lan_port)), @@ -715,63 +1064,68 @@ impl CallManager { ); let _ = socket.send_to(&response.serialize(), from_addr).await; - // Extract provider media from original SDP. let provider_media = if invite.has_sdp_body() { - sip_proto::helpers::parse_sdp_endpoint(&invite.body) + parse_sdp_endpoint(&invite.body) .and_then(|ep| format!("{}:{}", ep.address, ep.port).parse().ok()) } else { - Some(from_addr) // fallback to signaling address + Some(from_addr) }; let provider_media = provider_media.unwrap_or(from_addr); - // Create a voicemail call entry for BYE routing. - let call = PassthroughCall { - id: call_id.to_string(), - sip_call_id: invite.call_id().to_string(), - state: CallState::Voicemail, - direction: CallDirection::Inbound, - created_at: std::time::Instant::now(), - caller_number: Some(caller_number.to_string()), - callee_number: None, - provider_id: provider_id.to_string(), - provider_addr: from_addr, - provider_media: Some(provider_media), - device_addr: from_addr, // no device — just use provider addr as placeholder - device_media: None, - rtp_port: rtp_alloc.port, - rtp_socket: rtp_alloc.socket.clone(), - pkt_from_device: 0, - pkt_from_provider: 0, - }; - self.calls.insert(invite.call_id().to_string(), call); + // 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()); - // Build recording file path. + 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), + }, + ); + + self.sip_index.insert( + invite.call_id().to_string(), + (call_id.to_string(), provider_leg_id), + ); + self.calls.insert(call_id.to_string(), call); + + // Build recording path. let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis(); - let recording_dir = format!(".nogit/voicemail/default"); + let recording_dir = "nogit/voicemail/default".to_string(); let recording_path = format!("{recording_dir}/msg-{timestamp}.wav"); - - // Look for a greeting WAV file. let greeting_wav = find_greeting_wav(); - // Spawn the voicemail session. 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( - rtp_socket, - provider_media, - codec_pt, - greeting_wav, - recording_path, - 120_000, // max 120 seconds - call_id_owned, - caller_owned, - out_tx, + rtp_socket, provider_media, codec_pt, + greeting_wav, recording_path, 120_000, + call_id_owned, caller_owned, out_tx, ) .await; }); @@ -779,7 +1133,9 @@ impl CallManager { Some(call_id.to_string()) } - // --- Internal helpers --- + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- fn resolve_first_device(&self, config: &AppConfig, registrar: &Registrar) -> Option { for device in &config.devices { @@ -787,13 +1143,11 @@ impl CallManager { return Some(addr); } } - None // No device registered — caller goes to voicemail. + None } } -/// Find a voicemail greeting WAV file. fn find_greeting_wav() -> Option { - // Check common locations for a pre-generated greeting. let candidates = [ ".nogit/voicemail/default/greeting.wav", ".nogit/voicemail/greeting.wav", @@ -803,83 +1157,5 @@ fn find_greeting_wav() -> Option { return Some(path.to_string()); } } - None // No greeting found — voicemail will just play the beep. -} - -/// Rewrite SDP for provider→device direction (use LAN IP). -fn rewrite_sdp_for_device(msg: &mut SipMessage, lan_ip: &str, rtp_port: u16) { - if msg.has_sdp_body() { - let (new_body, _original) = rewrite_sdp(&msg.body, lan_ip, rtp_port); - msg.body = new_body; - msg.update_content_length(); - } -} - -/// Rewrite SDP for device→provider direction (use public IP). -fn rewrite_sdp_for_provider(msg: &mut SipMessage, pub_ip: &str, rtp_port: u16) { - if msg.has_sdp_body() { - let (new_body, _original) = rewrite_sdp(&msg.body, pub_ip, rtp_port); - msg.body = new_body; - msg.update_content_length(); - } -} - -/// Bidirectional RTP relay loop. -/// Receives packets on the relay socket and forwards based on source address. -async fn rtp_relay_loop( - socket: Arc, - device_addr: SocketAddr, - provider_addr: SocketAddr, -) { - let mut buf = vec![0u8; 65535]; - let device_ip = device_addr.ip().to_string(); - let provider_ip = provider_addr.ip().to_string(); - - // Track learned media endpoints (may differ from signaling addresses). - let mut learned_device: Option = None; - let mut learned_provider: Option = None; - - loop { - match socket.recv_from(&mut buf).await { - Ok((n, from)) => { - let data = &buf[..n]; - let from_ip = from.ip().to_string(); - - if from_ip == device_ip || learned_device.map(|d| d == from).unwrap_or(false) { - // From device → forward to provider. - if learned_device.is_none() { - learned_device = Some(from); - } - if let Some(target) = learned_provider { - let _ = socket.send_to(data, target).await; - } else { - // Provider media not yet learned; try signaling address. - let _ = socket.send_to(data, provider_addr).await; - } - } else if from_ip == provider_ip - || learned_provider.map(|p| p == from).unwrap_or(false) - { - // From provider → forward to device. - if learned_provider.is_none() { - learned_provider = Some(from); - } - if let Some(target) = learned_device { - let _ = socket.send_to(data, target).await; - } else { - let _ = socket.send_to(data, device_addr).await; - } - } else { - // Unknown source — try to identify by known device addresses. - // For now, assume it's the device if not from provider IP range. - if learned_device.is_none() { - learned_device = Some(from); - } - } - } - Err(_) => { - // Socket closed or error — exit relay. - break; - } - } - } + None } diff --git a/rust/crates/proxy-engine/src/leg_io.rs b/rust/crates/proxy-engine/src/leg_io.rs new file mode 100644 index 0000000..1b4ac9a --- /dev/null +++ b/rust/crates/proxy-engine/src/leg_io.rs @@ -0,0 +1,80 @@ +//! Leg I/O task spawners. +//! +//! Each SIP leg gets two tasks: +//! - Inbound: recv_from on RTP socket → strip header → send RtpPacket to mixer channel +//! - Outbound: recv encoded RTP from mixer channel → send_to remote media endpoint +//! +//! WebRTC leg I/O is handled inside webrtc_engine.rs (on_track + track.write). + +use crate::mixer::RtpPacket; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; +use tokio::sync::mpsc; + +/// Channel pair for connecting a leg to the mixer. +pub struct LegChannels { + /// Mixer receives decoded packets from this leg. + pub inbound_tx: mpsc::Sender, + pub inbound_rx: mpsc::Receiver, + /// Mixer sends encoded RTP to this leg. + pub outbound_tx: mpsc::Sender>, + pub outbound_rx: mpsc::Receiver>, +} + +/// Create a channel pair for a leg. +pub fn create_leg_channels() -> LegChannels { + let (inbound_tx, inbound_rx) = mpsc::channel::(64); + let (outbound_tx, outbound_rx) = mpsc::channel::>(8); + LegChannels { + inbound_tx, + inbound_rx, + outbound_tx, + outbound_rx, + } +} + +/// Spawn the inbound I/O task for a SIP leg. +/// Reads RTP from the socket, strips the 12-byte header, sends payload to the mixer. +/// Returns the JoinHandle (exits when the inbound_tx channel is dropped). +pub fn spawn_sip_inbound( + rtp_socket: Arc, + inbound_tx: mpsc::Sender, +) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + let mut buf = vec![0u8; 1500]; + loop { + match rtp_socket.recv_from(&mut buf).await { + Ok((n, _from)) => { + if n < 12 { + continue; // Too small for RTP header. + } + let pt = buf[1] & 0x7F; + let payload = buf[12..n].to_vec(); + if payload.is_empty() { + continue; + } + if inbound_tx.send(RtpPacket { payload, payload_type: pt }).await.is_err() { + break; // Channel closed — leg removed. + } + } + Err(_) => break, // Socket error. + } + } + }) +} + +/// Spawn the outbound I/O task for a SIP leg. +/// Reads encoded RTP packets from the mixer and sends them to the remote media endpoint. +/// Returns the JoinHandle (exits when the outbound_rx channel is closed). +pub fn spawn_sip_outbound( + rtp_socket: Arc, + remote_media: SocketAddr, + mut outbound_rx: mpsc::Receiver>, +) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + while let Some(rtp_data) = outbound_rx.recv().await { + let _ = rtp_socket.send_to(&rtp_data, remote_media).await; + } + }) +} diff --git a/rust/crates/proxy-engine/src/main.rs b/rust/crates/proxy-engine/src/main.rs index 048e139..cee40c8 100644 --- a/rust/crates/proxy-engine/src/main.rs +++ b/rust/crates/proxy-engine/src/main.rs @@ -12,6 +12,8 @@ mod call_manager; mod config; mod dtmf; mod ipc; +mod leg_io; +mod mixer; mod provider; mod recorder; mod registrar; @@ -131,11 +133,13 @@ async fn handle_command( "hangup" => handle_hangup(engine, out_tx, &cmd).await, "make_call" => handle_make_call(engine, out_tx, &cmd).await, "get_status" => handle_get_status(engine, out_tx, &cmd).await, + "add_leg" => handle_add_leg(engine, out_tx, &cmd).await, + "remove_leg" => handle_remove_leg(engine, out_tx, &cmd).await, // WebRTC commands — lock webrtc only (no engine contention). "webrtc_offer" => handle_webrtc_offer(webrtc, out_tx, &cmd).await, "webrtc_ice" => handle_webrtc_ice(webrtc, out_tx, &cmd).await, "webrtc_close" => handle_webrtc_close(webrtc, out_tx, &cmd).await, - // webrtc_link needs both: engine (for RTP socket) and webrtc (for session). + // webrtc_link needs both: engine (for mixer channels) and webrtc (for session). "webrtc_link" => handle_webrtc_link(engine, webrtc, out_tx, &cmd).await, _ => respond_err(out_tx, &cmd.id, &format!("unknown command: {}", cmd.method)), } @@ -259,14 +263,11 @@ async fn handle_sip_packet( } // 3. Route to existing call by SIP Call-ID. - // Check if this Call-ID belongs to an active call (avoids borrow conflict). if eng.call_mgr.has_call(msg.call_id()) { let config_ref = eng.config.as_ref().unwrap().clone(); - // Temporarily take registrar to avoid overlapping borrows. - let registrar_dummy = Registrar::new(eng.out_tx.clone()); if eng .call_mgr - .route_sip_message(&msg, from_addr, socket, &config_ref, ®istrar_dummy) + .route_sip_message(&msg, from_addr, socket, &config_ref) .await { return; @@ -578,8 +579,8 @@ async fn handle_webrtc_ice(webrtc: Arc>, out_tx: &OutTx, cmd } } -/// Handle `webrtc_link` — link a WebRTC session to a SIP call for audio bridging. -/// Briefly locks engine to get the RTP socket, then locks webrtc to set up the bridge. +/// Handle `webrtc_link` — link a WebRTC session to a call's mixer for audio bridging. +/// Creates channels, adds WebRTC leg to the call, wires the WebRTC engine. /// Locks are never held simultaneously — no deadlock possible. async fn handle_webrtc_link( engine: Arc>, @@ -595,44 +596,67 @@ async fn handle_webrtc_link( Some(s) => s.to_string(), None => { respond_err(out_tx, &cmd.id, "missing call_id"); return; } }; - let provider_addr = match cmd.params.get("provider_media_addr").and_then(|v| v.as_str()) { - Some(s) => s.to_string(), - None => { respond_err(out_tx, &cmd.id, "missing provider_media_addr"); return; } - }; - let provider_port = match cmd.params.get("provider_media_port").and_then(|v| v.as_u64()) { - Some(p) => p as u16, - None => { respond_err(out_tx, &cmd.id, "missing provider_media_port"); return; } - }; - let sip_pt = cmd.params.get("sip_pt").and_then(|v| v.as_u64()).unwrap_or(9) as u8; - let provider_media: SocketAddr = match format!("{provider_addr}:{provider_port}").parse() { - Ok(a) => a, - Err(e) => { respond_err(out_tx, &cmd.id, &format!("bad address: {e}")); return; } - }; + // Create channels for the WebRTC leg. + let channels = crate::leg_io::create_leg_channels(); - // Briefly lock engine to get the B2BUA call's RTP socket. - let rtp_socket = { + // Briefly lock engine to add the WebRTC leg to the call's mixer. + { let eng = engine.lock().await; - eng.call_mgr.get_b2bua_rtp_socket(&call_id) - }; // engine lock released here + let call = match eng.call_mgr.calls.get(&call_id) { + Some(c) => c, + None => { + respond_err(out_tx, &cmd.id, &format!("call {call_id} not found")); + return; + } + }; + // Add to mixer via channel. + call.add_leg_to_mixer( + &session_id, + codec_lib::PT_OPUS, + channels.inbound_rx, + channels.outbound_tx, + ) + .await; + } // engine lock released - let rtp_socket = match rtp_socket { - Some(s) => s, - None => { - respond_err(out_tx, &cmd.id, &format!("call {call_id} not found or no RTP socket")); - return; - } - }; - - let bridge_info = crate::webrtc_engine::SipBridgeInfo { - provider_media, - sip_pt, - rtp_socket, - }; - - // Lock webrtc to set up the audio bridge. + // Lock webrtc to wire the channels. let mut wrtc = webrtc.lock().await; - if wrtc.link_to_sip(&session_id, &call_id, bridge_info).await { + if wrtc + .link_to_mixer(&session_id, &call_id, channels.inbound_tx, channels.outbound_rx) + .await + { + // Also store the WebRTC leg info in the call. + drop(wrtc); // Release webrtc lock before re-acquiring engine. + { + let mut eng = engine.lock().await; + if let Some(call) = eng.call_mgr.calls.get_mut(&call_id) { + call.legs.insert( + session_id.clone(), + crate::call::LegInfo { + id: session_id.clone(), + kind: crate::call::LegKind::WebRtc, + state: crate::call::LegState::Connected, + codec_pt: codec_lib::PT_OPUS, + sip_leg: None, + sip_call_id: None, + webrtc_session_id: Some(session_id.clone()), + rtp_socket: None, + rtp_port: 0, + remote_media: None, + signaling_addr: None, + }, + ); + } + } + + emit_event(out_tx, "leg_added", serde_json::json!({ + "call_id": call_id, + "leg_id": session_id, + "kind": "webrtc", + "state": "connected", + })); + respond_ok(out_tx, &cmd.id, serde_json::json!({ "session_id": session_id, "call_id": call_id, @@ -643,6 +667,98 @@ async fn handle_webrtc_link( } } +/// Handle `add_leg` — add a new SIP leg to an existing call. +async fn handle_add_leg(engine: Arc>, out_tx: &OutTx, cmd: &Command) { + let call_id = match cmd.params.get("call_id").and_then(|v| v.as_str()) { + Some(s) => s.to_string(), + None => { respond_err(out_tx, &cmd.id, "missing call_id"); return; } + }; + let number = match cmd.params.get("number").and_then(|v| v.as_str()) { + Some(n) => n.to_string(), + None => { respond_err(out_tx, &cmd.id, "missing number"); return; } + }; + let provider_id = cmd.params.get("provider_id").and_then(|v| v.as_str()); + + let mut eng = engine.lock().await; + let config_ref = match &eng.config { + Some(c) => c.clone(), + None => { respond_err(out_tx, &cmd.id, "not configured"); return; } + }; + + // Resolve provider. + let provider_config = if let Some(pid) = provider_id { + config_ref.providers.iter().find(|p| p.id == pid).cloned() + } else { + config_ref.resolve_outbound_route(&number, None, &|_| true).map(|r| r.provider) + }; + + let provider_config = match provider_config { + Some(p) => p, + None => { respond_err(out_tx, &cmd.id, "no provider available"); return; } + }; + + // Get registered AOR. + let registered_aor = if let Some(ps_arc) = eng.provider_mgr.find_by_address( + &provider_config.outbound_proxy.to_socket_addr().unwrap_or_else(|| "0.0.0.0:0".parse().unwrap()) + ).await { + let ps = ps_arc.lock().await; + ps.registered_aor.clone() + } else { + format!("sip:{}@{}", provider_config.username, provider_config.domain) + }; + + let public_ip = if let Some(ps_arc) = eng.provider_mgr.find_by_address( + &provider_config.outbound_proxy.to_socket_addr().unwrap_or_else(|| "0.0.0.0:0".parse().unwrap()) + ).await { + let ps = ps_arc.lock().await; + ps.public_ip.clone() + } else { + None + }; + + let socket = match &eng.transport { + Some(t) => t.socket(), + None => { respond_err(out_tx, &cmd.id, "not initialized"); return; } + }; + + let ProxyEngine { ref mut call_mgr, ref mut rtp_pool, .. } = *eng; + let rtp_pool = rtp_pool.as_mut().unwrap(); + + let leg_id = call_mgr.add_external_leg( + &call_id, &number, &provider_config, &config_ref, + rtp_pool, &socket, public_ip.as_deref(), ®istered_aor, + ).await; + + match leg_id { + Some(lid) => respond_ok(out_tx, &cmd.id, serde_json::json!({ "leg_id": lid })), + None => respond_err(out_tx, &cmd.id, "failed to add leg"), + } +} + +/// Handle `remove_leg` — remove a leg from a call. +async fn handle_remove_leg(engine: Arc>, out_tx: &OutTx, cmd: &Command) { + let call_id = match cmd.params.get("call_id").and_then(|v| v.as_str()) { + Some(s) => s.to_string(), + None => { respond_err(out_tx, &cmd.id, "missing call_id"); return; } + }; + let leg_id = match cmd.params.get("leg_id").and_then(|v| v.as_str()) { + Some(s) => s.to_string(), + None => { respond_err(out_tx, &cmd.id, "missing leg_id"); return; } + }; + + let mut eng = engine.lock().await; + let socket = match &eng.transport { + Some(t) => t.socket(), + None => { respond_err(out_tx, &cmd.id, "not initialized"); return; } + }; + + if eng.call_mgr.remove_leg(&call_id, &leg_id, &socket).await { + respond_ok(out_tx, &cmd.id, serde_json::json!({})); + } else { + respond_err(out_tx, &cmd.id, &format!("call/leg not found")); + } +} + /// Handle `webrtc_close` — close a WebRTC session. /// Uses only the WebRTC lock. async fn handle_webrtc_close(webrtc: Arc>, out_tx: &OutTx, cmd: &Command) { diff --git a/rust/crates/proxy-engine/src/mixer.rs b/rust/crates/proxy-engine/src/mixer.rs new file mode 100644 index 0000000..878356d --- /dev/null +++ b/rust/crates/proxy-engine/src/mixer.rs @@ -0,0 +1,232 @@ +//! Audio mixer — mix-minus engine for multiparty calls. +//! +//! Each Call spawns one mixer task. Legs communicate with the mixer via +//! tokio mpsc channels — no shared mutable state, no lock contention. +//! +//! The mixer runs a 20ms tick loop: +//! 1. Drain inbound channels, decode to PCM, resample to 16kHz +//! 2. Compute total mix (sum of all legs' PCM as i32) +//! 3. For each leg: mix-minus = total - own, resample to leg codec rate, encode, send + +use crate::ipc::{emit_event, OutTx}; +use crate::rtp::{build_rtp_header, rtp_clock_increment}; +use codec_lib::{codec_sample_rate, TranscodeState}; +use std::collections::HashMap; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tokio::time::{self, Duration, MissedTickBehavior}; + +/// Mixing sample rate — 16kHz. G.722 is native, G.711 needs 2× upsample, Opus needs 3× downsample. +const MIX_RATE: u32 = 16000; +/// Samples per 20ms frame at the mixing rate. +const MIX_FRAME_SIZE: usize = 320; // 16000 * 0.020 + +/// A raw RTP payload received from a leg (no RTP header). +pub struct RtpPacket { + pub payload: Vec, + pub payload_type: u8, +} + +/// Commands sent to the mixer task via a control channel. +pub enum MixerCommand { + /// Add a new leg to the mix. + AddLeg { + leg_id: String, + codec_pt: u8, + inbound_rx: mpsc::Receiver, + outbound_tx: mpsc::Sender>, + }, + /// Remove a leg from the mix (channels are dropped, I/O tasks exit). + RemoveLeg { leg_id: String }, + /// Shut down the mixer. + Shutdown, +} + +/// Internal per-leg state inside the mixer. +struct MixerLegSlot { + codec_pt: u8, + transcoder: TranscodeState, + inbound_rx: mpsc::Receiver, + outbound_tx: mpsc::Sender>, + /// Last decoded PCM frame at MIX_RATE (320 samples). Used for mix-minus. + last_pcm_frame: Vec, + /// Number of consecutive ticks with no inbound packet. + silent_ticks: u32, + // RTP output state. + rtp_seq: u16, + rtp_ts: u32, + rtp_ssrc: u32, +} + +/// Spawn the mixer task for a call. Returns the command sender and task handle. +pub fn spawn_mixer( + call_id: String, + out_tx: OutTx, +) -> (mpsc::Sender, JoinHandle<()>) { + let (cmd_tx, cmd_rx) = mpsc::channel::(32); + + let handle = tokio::spawn(async move { + mixer_loop(call_id, cmd_rx, out_tx).await; + }); + + (cmd_tx, handle) +} + +/// The 20ms mixing loop. +async fn mixer_loop( + call_id: String, + mut cmd_rx: mpsc::Receiver, + out_tx: OutTx, +) { + let mut legs: HashMap = HashMap::new(); + let mut interval = time::interval(Duration::from_millis(20)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + interval.tick().await; + + // 1. Process control commands (non-blocking). + loop { + match cmd_rx.try_recv() { + Ok(MixerCommand::AddLeg { + leg_id, + codec_pt, + inbound_rx, + outbound_tx, + }) => { + let transcoder = match TranscodeState::new() { + Ok(t) => t, + Err(e) => { + emit_event( + &out_tx, + "mixer_error", + serde_json::json!({ + "call_id": call_id, + "leg_id": leg_id, + "error": format!("codec init: {e}"), + }), + ); + continue; + } + }; + legs.insert( + leg_id, + MixerLegSlot { + codec_pt, + transcoder, + inbound_rx, + outbound_tx, + last_pcm_frame: vec![0i16; MIX_FRAME_SIZE], + silent_ticks: 0, + rtp_seq: 0, + rtp_ts: 0, + rtp_ssrc: rand::random(), + }, + ); + } + Ok(MixerCommand::RemoveLeg { leg_id }) => { + legs.remove(&leg_id); + // Channels drop → I/O tasks exit cleanly. + } + Ok(MixerCommand::Shutdown) => return, + Err(mpsc::error::TryRecvError::Empty) => break, + Err(mpsc::error::TryRecvError::Disconnected) => return, + } + } + + if legs.is_empty() { + continue; + } + + // 2. Drain inbound packets, decode to 16kHz PCM. + let leg_ids: Vec = legs.keys().cloned().collect(); + for lid in &leg_ids { + let slot = legs.get_mut(lid).unwrap(); + + // Drain channel, keep only the latest packet (simple jitter handling). + let mut latest: Option = None; + loop { + match slot.inbound_rx.try_recv() { + Ok(pkt) => latest = Some(pkt), + Err(_) => break, + } + } + + if let Some(pkt) = latest { + slot.silent_ticks = 0; + match slot.transcoder.decode_to_pcm(&pkt.payload, pkt.payload_type) { + Ok((pcm, rate)) => { + // Resample to mixing rate if needed. + let pcm_mix = if rate == MIX_RATE { + pcm + } else { + slot.transcoder + .resample(&pcm, rate, MIX_RATE) + .unwrap_or_else(|_| vec![0i16; MIX_FRAME_SIZE]) + }; + // Pad or truncate to exactly MIX_FRAME_SIZE. + let mut frame = pcm_mix; + frame.resize(MIX_FRAME_SIZE, 0); + slot.last_pcm_frame = frame; + } + Err(_) => { + // Decode failed — use silence. + slot.last_pcm_frame = vec![0i16; MIX_FRAME_SIZE]; + } + } + } else { + slot.silent_ticks += 1; + // After 150 ticks (3 seconds) of silence, zero out to avoid stale audio. + if slot.silent_ticks > 150 { + slot.last_pcm_frame = vec![0i16; MIX_FRAME_SIZE]; + } + } + } + + // 3. Compute total mix (sum of all legs as i32 to avoid overflow). + let mut total_mix = vec![0i32; MIX_FRAME_SIZE]; + for slot in legs.values() { + for (i, &s) in slot.last_pcm_frame.iter().enumerate().take(MIX_FRAME_SIZE) { + total_mix[i] += s as i32; + } + } + + // 4. For each leg: mix-minus, resample, encode, send. + for slot in legs.values_mut() { + // Mix-minus: total minus this leg's own contribution. + let mut mix_minus = Vec::with_capacity(MIX_FRAME_SIZE); + for i in 0..MIX_FRAME_SIZE { + let sample = + (total_mix[i] - slot.last_pcm_frame[i] as i32).clamp(-32768, 32767) as i16; + mix_minus.push(sample); + } + + // Resample from 16kHz to the leg's codec native rate. + let target_rate = codec_sample_rate(slot.codec_pt); + let resampled = if target_rate == MIX_RATE { + mix_minus + } else { + slot.transcoder + .resample(&mix_minus, MIX_RATE, target_rate) + .unwrap_or_default() + }; + + // Encode to the leg's codec. + let encoded = match slot.transcoder.encode_from_pcm(&resampled, slot.codec_pt) { + Ok(e) if !e.is_empty() => e, + _ => continue, + }; + + // Build RTP packet with header. + let header = build_rtp_header(slot.codec_pt, slot.rtp_seq, slot.rtp_ts, slot.rtp_ssrc); + let mut rtp = header.to_vec(); + rtp.extend_from_slice(&encoded); + + slot.rtp_seq = slot.rtp_seq.wrapping_add(1); + slot.rtp_ts = slot.rtp_ts.wrapping_add(rtp_clock_increment(slot.codec_pt)); + + // Non-blocking send — drop frame if channel is full. + let _ = slot.outbound_tx.try_send(rtp); + } + } +} diff --git a/rust/crates/proxy-engine/src/webrtc_engine.rs b/rust/crates/proxy-engine/src/webrtc_engine.rs index 467b1c3..40558f4 100644 --- a/rust/crates/proxy-engine/src/webrtc_engine.rs +++ b/rust/crates/proxy-engine/src/webrtc_engine.rs @@ -1,16 +1,17 @@ -//! WebRTC engine — manages browser PeerConnections with SIP audio bridging. +//! WebRTC engine — manages browser PeerConnections. //! -//! Browser Opus audio → Rust PeerConnection → transcode via codec-lib → SIP RTP -//! SIP RTP → transcode via codec-lib → Rust PeerConnection → Browser Opus +//! Audio bridging is now channel-based: +//! - Browser Opus audio → on_track → mixer inbound channel +//! - Mixer outbound channel → Opus RTP → TrackLocalStaticRTP → browser +//! +//! The mixer handles all transcoding. The WebRTC engine just shuttles raw Opus. use crate::ipc::{emit_event, OutTx}; -use crate::rtp::{build_rtp_header, rtp_clock_increment}; -use codec_lib::{TranscodeState, PT_G722, PT_OPUS}; +use crate::mixer::RtpPacket; +use codec_lib::PT_OPUS; use std::collections::HashMap; -use std::net::SocketAddr; use std::sync::Arc; -use tokio::net::UdpSocket; -use tokio::sync::Mutex; +use tokio::sync::{mpsc, Mutex}; use webrtc::api::media_engine::MediaEngine; use webrtc::api::APIBuilder; use webrtc::ice_transport::ice_candidate::RTCIceCandidateInit; @@ -22,26 +23,14 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; -/// SIP-side bridge info for a WebRTC session. -#[derive(Clone)] -pub struct SipBridgeInfo { - /// Provider's media endpoint (RTP destination). - pub provider_media: SocketAddr, - /// Provider's codec payload type (e.g. 9 for G.722). - pub sip_pt: u8, - /// The allocated RTP socket for bidirectional audio with the provider. - /// This is the socket whose port was advertised in SDP, so the provider - /// sends RTP here and expects RTP from this port. - pub rtp_socket: Arc, -} - /// A managed WebRTC session. struct WebRtcSession { pc: Arc, local_track: Arc, call_id: Option, - /// SIP bridge — set when the session is linked to a call. - sip_bridge: Arc>>, + /// Channel sender for forwarding browser Opus audio to the mixer. + /// Set when the session is linked to a call via link_to_mixer(). + mixer_tx: Arc>>>, } /// Manages all WebRTC sessions. @@ -58,7 +47,7 @@ impl WebRtcEngine { } } - /// Handle a WebRTC offer from a browser. + /// Handle a WebRTC offer from a browser — create PeerConnection, return SDP answer. pub async fn handle_offer( &mut self, session_id: &str, @@ -101,8 +90,9 @@ impl WebRtcEngine { .await .map_err(|e| format!("add track: {e}"))?; - // Shared SIP bridge info (populated when linked to a call). - let sip_bridge: Arc>> = Arc::new(Mutex::new(None)); + // Shared mixer channel sender (populated when linked to a call). + let mixer_tx: Arc>>> = + Arc::new(Mutex::new(None)); // ICE candidate handler. let out_tx_ice = self.out_tx.clone(); @@ -153,14 +143,14 @@ impl WebRtcEngine { })); // Track handler — receives Opus audio from the browser. - // When SIP bridge is set, transcodes and forwards to provider. + // Forwards raw Opus payload to the mixer channel (when linked). let out_tx_track = self.out_tx.clone(); let sid_track = session_id.to_string(); - let sip_bridge_for_track = sip_bridge.clone(); + let mixer_tx_for_track = mixer_tx.clone(); pc.on_track(Box::new(move |track, _receiver, _transceiver| { let out_tx = out_tx_track.clone(); let sid = sid_track.clone(); - let bridge = sip_bridge_for_track.clone(); + let mixer_tx = mixer_tx_for_track.clone(); Box::pin(async move { let codec_info = track.codec(); emit_event( @@ -173,8 +163,8 @@ impl WebRtcEngine { }), ); - // Spawn the browser→SIP audio forwarding task. - tokio::spawn(browser_to_sip_loop(track, bridge, out_tx, sid)); + // Spawn browser→mixer forwarding task. + tokio::spawn(browser_to_mixer_loop(track, mixer_tx, out_tx, sid)); }) })); @@ -201,43 +191,41 @@ impl WebRtcEngine { pc, local_track, call_id: None, - sip_bridge, + mixer_tx, }, ); Ok(answer_sdp) } - /// Link a WebRTC session to a SIP call — sets up bidirectional audio bridge. - /// - Browser→SIP: already running via on_track handler, will start forwarding - /// once bridge info is set. - /// - SIP→Browser: spawned here, reads from the RTP socket and sends to browser. - pub async fn link_to_sip( + /// Link a WebRTC session to a call's mixer via channels. + /// - `inbound_tx`: browser audio goes TO the mixer through this channel + /// - `outbound_rx`: mixed audio comes FROM the mixer through this channel + pub async fn link_to_mixer( &mut self, session_id: &str, call_id: &str, - bridge_info: SipBridgeInfo, + inbound_tx: mpsc::Sender, + outbound_rx: mpsc::Receiver>, ) -> bool { - if let Some(session) = self.sessions.get_mut(session_id) { - session.call_id = Some(call_id.to_string()); + let session = match self.sessions.get_mut(session_id) { + Some(s) => s, + None => return false, + }; - // Spawn SIP → browser audio loop (provider RTP → transcode → Opus → WebRTC track). - let local_track = session.local_track.clone(); - let rtp_socket = bridge_info.rtp_socket.clone(); - let sip_pt = bridge_info.sip_pt; - let out_tx = self.out_tx.clone(); - let sid = session_id.to_string(); - tokio::spawn(sip_to_browser_loop( - rtp_socket, local_track, sip_pt, out_tx, sid, - )); + session.call_id = Some(call_id.to_string()); - // Set bridge info — this unblocks the browser→SIP loop (already running). - let mut bridge = session.sip_bridge.lock().await; - *bridge = Some(bridge_info); - true - } else { - false + // Set the mixer sender so the on_track loop starts forwarding. + { + let mut tx = session.mixer_tx.lock().await; + *tx = Some(inbound_tx); } + + // Spawn mixer→browser outbound task. + let local_track = session.local_track.clone(); + tokio::spawn(mixer_to_browser_loop(outbound_rx, local_track)); + + true } pub async fn add_ice_candidate( @@ -272,90 +260,48 @@ impl WebRtcEngine { } Ok(()) } - - pub fn has_session(&self, session_id: &str) -> bool { - self.sessions.contains_key(session_id) - } } -/// Browser → SIP audio forwarding loop. -/// Reads Opus RTP from the browser, transcodes to the SIP codec, sends to provider. -async fn browser_to_sip_loop( +/// Browser → Mixer audio forwarding loop. +/// Reads Opus RTP from the browser track, sends raw Opus payload to the mixer channel. +async fn browser_to_mixer_loop( track: Arc, - sip_bridge: Arc>>, + mixer_tx: Arc>>>, out_tx: OutTx, session_id: String, ) { - // Create a persistent codec state for this direction. - let mut transcoder = match TranscodeState::new() { - Ok(t) => t, - Err(e) => { - emit_event( - &out_tx, - "webrtc_error", - serde_json::json!({ "session_id": session_id, "error": format!("codec init: {e}") }), - ); - return; - } - }; - let mut buf = vec![0u8; 1500]; let mut count = 0u64; - let mut to_sip_seq: u16 = 0; - let mut to_sip_ts: u32 = 0; - let to_sip_ssrc: u32 = rand::random(); loop { match track.read(&mut buf).await { Ok((rtp_packet, _attributes)) => { count += 1; - // Get the SIP bridge info (may not be set yet if call isn't linked). - let bridge = sip_bridge.lock().await; - let bridge_info = match bridge.as_ref() { - Some(b) => b.clone(), - None => continue, // Not linked to a SIP call yet — drop the packet. - }; - drop(bridge); // Release lock before doing I/O. - - // Extract Opus payload from the RTP packet (skip 12-byte header). let payload = &rtp_packet.payload; if payload.is_empty() { continue; } - // Transcode Opus → SIP codec (e.g. G.722). - let sip_payload = match transcoder.transcode( - payload, - PT_OPUS, - bridge_info.sip_pt, - Some("to_sip"), - ) { - Ok(p) if !p.is_empty() => p, - _ => continue, - }; - - // Build SIP RTP packet. - let header = build_rtp_header(bridge_info.sip_pt, to_sip_seq, to_sip_ts, to_sip_ssrc); - let mut sip_rtp = header.to_vec(); - sip_rtp.extend_from_slice(&sip_payload); - - to_sip_seq = to_sip_seq.wrapping_add(1); - to_sip_ts = to_sip_ts.wrapping_add(rtp_clock_increment(bridge_info.sip_pt)); - - // Send to provider via the RTP socket (correct source port matching our SDP). - let _ = bridge_info - .rtp_socket - .send_to(&sip_rtp, bridge_info.provider_media) - .await; + // Send raw Opus payload to mixer (if linked). + let tx = mixer_tx.lock().await; + if let Some(ref tx) = *tx { + let _ = tx + .send(RtpPacket { + payload: payload.to_vec(), + payload_type: PT_OPUS, + }) + .await; + } + drop(tx); if count == 1 || count == 50 || count % 500 == 0 { emit_event( &out_tx, - "webrtc_audio_tx", + "webrtc_audio_rx", serde_json::json!({ "session_id": session_id, - "direction": "browser_to_sip", + "direction": "browser_to_mixer", "packet_count": count, }), ); @@ -366,85 +312,13 @@ async fn browser_to_sip_loop( } } -/// SIP → Browser audio forwarding loop. -/// Reads RTP from the provider (via the allocated RTP socket), transcodes to Opus, -/// and writes to the WebRTC local track for delivery to the browser. -async fn sip_to_browser_loop( - rtp_socket: Arc, +/// Mixer → Browser audio forwarding loop. +/// Reads Opus-encoded RTP packets from the mixer and writes to the WebRTC track. +async fn mixer_to_browser_loop( + mut outbound_rx: mpsc::Receiver>, local_track: Arc, - sip_pt: u8, - out_tx: OutTx, - session_id: String, ) { - let mut transcoder = match TranscodeState::new() { - Ok(t) => t, - Err(e) => { - emit_event( - &out_tx, - "webrtc_error", - serde_json::json!({ - "session_id": session_id, - "error": format!("sip_to_browser codec init: {e}"), - }), - ); - return; - } - }; - - let mut buf = vec![0u8; 1500]; - let mut count = 0u64; - let mut seq: u16 = 0; - let mut ts: u32 = 0; - let ssrc: u32 = rand::random(); - - loop { - match rtp_socket.recv_from(&mut buf).await { - Ok((n, _from)) => { - if n < 12 { - continue; // Too small for RTP header. - } - count += 1; - - // Extract payload (skip 12-byte RTP header). - let payload = &buf[12..n]; - if payload.is_empty() { - continue; - } - - // Transcode SIP codec → Opus. - let opus_payload = match transcoder.transcode( - payload, - sip_pt, - PT_OPUS, - Some("sip_to_browser"), - ) { - Ok(p) if !p.is_empty() => p, - _ => continue, - }; - - // Build Opus RTP packet. - let header = build_rtp_header(PT_OPUS, seq, ts, ssrc); - let mut packet = header.to_vec(); - packet.extend_from_slice(&opus_payload); - - seq = seq.wrapping_add(1); - ts = ts.wrapping_add(960); // Opus: 48000 Hz × 20ms = 960 samples - - let _ = local_track.write(&packet).await; - - if count == 1 || count == 50 || count % 500 == 0 { - emit_event( - &out_tx, - "webrtc_audio_rx", - serde_json::json!({ - "session_id": session_id, - "direction": "sip_to_browser", - "packet_count": count, - }), - ); - } - } - Err(_) => break, // Socket closed. - } + while let Some(rtp_data) = outbound_rx.recv().await { + let _ = local_track.write(&rtp_data).await; } } diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index dc3db2a..07d68dd 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: 'siprouter', - version: '1.13.0', + version: '1.14.0', description: 'undefined' } diff --git a/ts/frontend.ts b/ts/frontend.ts index e4bf19b..2a1b3bb 100644 --- a/ts/frontend.ts +++ b/ts/frontend.ts @@ -128,14 +128,14 @@ async function handleRequest( } } - // API: add leg to call. + // API: add leg to call (device — not yet implemented, needs device-to-call routing). if (url.pathname.startsWith('/api/call/') && url.pathname.endsWith('/addleg') && method === 'POST') { try { const callId = url.pathname.split('/')[3]; const body = await readJsonBody(req); if (!body?.deviceId) return sendJson(res, { ok: false, error: 'missing deviceId' }, 400); - const ok = callManager?.addDeviceToCall(callId, body.deviceId) ?? false; - return sendJson(res, { ok }); + // TODO: implement device leg addition (needs SIP INVITE to device). + return sendJson(res, { ok: false, error: 'not yet implemented' }, 501); } catch (e: any) { return sendJson(res, { ok: false, error: e.message }, 400); } @@ -147,8 +147,9 @@ async function handleRequest( const callId = url.pathname.split('/')[3]; const body = await readJsonBody(req); if (!body?.number) return sendJson(res, { ok: false, error: 'missing number' }, 400); - const ok = callManager?.addExternalToCall(callId, body.number, body.providerId) ?? false; - return sendJson(res, { ok }); + const { addLeg: addLegFn } = await import('./proxybridge.ts'); + const legId = await addLegFn(callId, body.number, body.providerId); + return sendJson(res, { ok: !!legId, legId }); } catch (e: any) { return sendJson(res, { ok: false, error: e.message }, 400); } @@ -160,22 +161,22 @@ async function handleRequest( const callId = url.pathname.split('/')[3]; const body = await readJsonBody(req); if (!body?.legId) return sendJson(res, { ok: false, error: 'missing legId' }, 400); - const ok = callManager?.removeLegFromCall(callId, body.legId) ?? false; + const { removeLeg: removeLegFn } = await import('./proxybridge.ts'); + const ok = await removeLegFn(callId, body.legId); return sendJson(res, { ok }); } catch (e: any) { return sendJson(res, { ok: false, error: e.message }, 400); } } - // API: transfer leg. + // API: transfer leg (not yet implemented). if (url.pathname === '/api/transfer' && method === 'POST') { try { const body = await readJsonBody(req); if (!body?.sourceCallId || !body?.legId || !body?.targetCallId) { return sendJson(res, { ok: false, error: 'missing sourceCallId, legId, or targetCallId' }, 400); } - const ok = callManager?.transferLeg(body.sourceCallId, body.legId, body.targetCallId) ?? false; - return sendJson(res, { ok }); + return sendJson(res, { ok: false, error: 'not yet implemented' }, 501); } catch (e: any) { return sendJson(res, { ok: false, error: e.message }, 400); } diff --git a/ts/proxybridge.ts b/ts/proxybridge.ts index c363f61..fafcb85 100644 --- a/ts/proxybridge.ts +++ b/ts/proxybridge.ts @@ -238,6 +238,38 @@ export async function webrtcLink(sessionId: string, callId: string, providerMedi } } +/** + * Add an external SIP leg to an existing call (multiparty). + */ +export async function addLeg(callId: string, number: string, providerId?: string): Promise { + if (!bridge || !initialized) return null; + try { + const result = await bridge.sendCommand('add_leg', { + call_id: callId, + number, + provider_id: providerId, + } as any); + return (result as any)?.leg_id || null; + } catch (e: any) { + logFn?.(`[proxy-engine] add_leg error: ${e?.message || e}`); + return null; + } +} + +/** + * Remove a leg from a call. + */ +export async function removeLeg(callId: string, legId: string): Promise { + if (!bridge || !initialized) return false; + try { + await bridge.sendCommand('remove_leg', { call_id: callId, leg_id: legId } as any); + return true; + } catch (e: any) { + logFn?.(`[proxy-engine] remove_leg error: ${e?.message || e}`); + return false; + } +} + /** * Close a WebRTC session. */ diff --git a/ts/sipproxy.ts b/ts/sipproxy.ts index 16bfc0b..2ee994c 100644 --- a/ts/sipproxy.ts +++ b/ts/sipproxy.ts @@ -39,6 +39,8 @@ import { webrtcIce, webrtcLink, webrtcClose, + addLeg, + removeLeg, } from './proxybridge.ts'; import type { IIncomingCallEvent, @@ -359,6 +361,19 @@ async function startProxyEngine(): Promise { log(`[sip] unhandled ${data.method_or_status} Call-ID=${data.call_id?.slice(0, 20)} from=${data.from_addr}:${data.from_port}`); }); + // Leg events (multiparty). + onProxyEvent('leg_added', (data: any) => { + log(`[leg] added: call=${data.call_id} leg=${data.leg_id} kind=${data.kind} state=${data.state}`); + }); + + onProxyEvent('leg_removed', (data: any) => { + log(`[leg] removed: call=${data.call_id} leg=${data.leg_id}`); + }); + + onProxyEvent('leg_state_changed', (data: any) => { + log(`[leg] state: call=${data.call_id} leg=${data.leg_id} → ${data.state}`); + }); + // WebRTC events from Rust — forward ICE candidates to browser via WebSocket. onProxyEvent('webrtc_ice_candidate', (data: any) => { // Find the browser's WebSocket by session ID and send the ICE candidate. diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index dc3db2a..07d68dd 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: 'siprouter', - version: '1.13.0', + version: '1.14.0', description: 'undefined' }