feat(fax): add fax routing, job tracking, inbox management, and T.38/UDPTL media support

This commit is contained in:
2026-04-20 20:43:42 +00:00
parent 3c010a3b1b
commit d2c18a4ebb
27 changed files with 4247 additions and 280 deletions
+52 -1
View File
@@ -11,7 +11,7 @@ use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Instant;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
pub type LegId = String;
@@ -114,6 +114,13 @@ pub struct LegInfo {
pub kind: LegKind,
pub state: LegState,
pub codec_pt: u8,
/// Media transport currently negotiated for this leg.
///
/// `rtp` covers classic SIP audio media, `t38-udptl` covers T.38 fax,
/// `webrtc` is used for browser legs, and `internal` for proxy-local media/tool paths.
pub media_protocol: &'static str,
/// Whether this leg is currently wired into an active media bridge.
pub media_io_active: bool,
/// For SIP legs: the SIP dialog manager (handles 407 auth, BYE, etc).
pub sip_leg: Option<SipLeg>,
@@ -146,6 +153,15 @@ pub struct LegInfo {
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Clone)]
pub struct PendingDialogBridge {
pub source_leg_id: LegId,
pub target_leg_id: LegId,
pub source_request: SipMessage,
pub target_request: SipMessage,
pub method: String,
}
/// A multiparty call with N legs and a central mixer.
pub struct Call {
// Duplicated from the HashMap key in CallManager. Kept for future
@@ -169,12 +185,21 @@ pub struct Call {
/// Used to construct proper 180/200/error responses back to the device.
pub device_invite: Option<SipMessage>,
/// Pending in-dialog B2BUA transaction bridged across two different SIP dialogs.
pub pending_dialog_bridge: Option<PendingDialogBridge>,
/// All legs in this call, keyed by leg ID.
pub legs: HashMap<LegId, LegInfo>,
/// Channel to send commands to the mixer task.
pub mixer_cmd_tx: mpsc::Sender<MixerCommand>,
/// Active passthrough media bridge mode, if any.
pub media_bridge_mode: Option<String>,
/// Cancellation handles for non-mixer passthrough media tasks.
media_bridge_cancel_txs: Vec<watch::Sender<bool>>,
/// Handle to the mixer task (aborted on call teardown).
mixer_task: Option<JoinHandle<()>>,
}
@@ -196,8 +221,11 @@ impl Call {
callee_number: None,
provider_id,
device_invite: None,
pending_dialog_bridge: None,
legs: HashMap::new(),
mixer_cmd_tx,
media_bridge_mode: None,
media_bridge_cancel_txs: Vec::new(),
mixer_task: Some(mixer_task),
}
}
@@ -235,8 +263,31 @@ impl Call {
self.created_at.elapsed().as_secs()
}
pub fn clear_media_bridge(&mut self) {
for cancel_tx in self.media_bridge_cancel_txs.drain(..) {
let _ = cancel_tx.send(true);
}
self.media_bridge_mode = None;
}
pub fn install_media_bridge(
&mut self,
mode: &str,
cancel_txs: Vec<watch::Sender<bool>>,
) {
self.clear_media_bridge();
self.media_bridge_mode = Some(mode.to_string());
self.media_bridge_cancel_txs = cancel_txs;
}
pub fn note_mixer_bridge(&mut self, mode: &str) {
self.clear_media_bridge();
self.media_bridge_mode = Some(mode.to_string());
}
/// Shut down the mixer and abort its task.
pub async fn shutdown_mixer(&mut self) {
self.clear_media_bridge();
let _ = self.mixer_cmd_tx.send(MixerCommand::Shutdown).await;
if let Some(handle) = self.mixer_task.take() {
handle.abort();