Skip to content

Commit b5df892

Browse files
committed
refactor(media): extract media engine from SIP dependencies
- Move LegId from call::domain to media::leg_id, call re-exports it - Split 1296-line media/mod.rs into track, rtc_track, file_track, rtp_track_builder, media_stream modules - Convert Mutex<HashMap<>> to DashMap in MediaStream, ConferenceAudioMixer - Move MediaMixer/MixerRegistry/SupervisorMixerMode to proxy/proxy_call - Remove SipFlow capture from MediaEngine command dispatch - Group BridgePeer 30+ caller_*/callee_* paired fields into sides[2] array - Replace tokio::sync::Mutex with parking_lot::Mutex in bridge, forwarding_track, conference_mixer - Extract DTMF types (DtmfDetector, PayloadMapping, DtmfSink) into media/dtmf.rs - Add 20 new unit tests + 5 performance benchmarks for media engine - Add session_id tracing span to all MediaCommand dispatch paths - Remove FileTrack lifetime_guard (PeerConnection::close is idempotent) - Remove all compiler warnings
1 parent e970eb8 commit b5df892

25 files changed

Lines changed: 2352 additions & 2579 deletions

src/call/domain/leg.rs

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,9 @@ use serde::{Deserialize, Serialize};
55
/// Audio frame for mixer input (PCM 16bit, 8kHz)
66
pub type PcmAudioFrame = Vec<i16>;
77

8-
/// Unique identifier for a call leg (participant in a session)
9-
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10-
#[serde(transparent)]
11-
pub struct LegId(pub String);
12-
13-
impl LegId {
14-
pub fn new(id: impl Into<String>) -> Self {
15-
Self(id.into())
16-
}
17-
18-
pub fn as_str(&self) -> &str {
19-
&self.0
20-
}
21-
}
22-
23-
impl std::fmt::Display for LegId {
24-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25-
write!(f, "{}", self.0)
26-
}
27-
}
28-
29-
impl From<String> for LegId {
30-
fn from(s: String) -> Self {
31-
Self(s)
32-
}
33-
}
34-
35-
impl From<&str> for LegId {
36-
fn from(s: &str) -> Self {
37-
Self(s.to_string())
38-
}
39-
}
40-
41-
impl From<LegId> for String {
42-
fn from(leg_id: LegId) -> Self {
43-
leg_id.0
44-
}
45-
}
8+
/// Re-exported from `media::leg_id` so the entire codebase uses one definition
9+
/// without circular dependencies.
10+
pub use crate::media::LegId;
4611

4712
/// State of a single leg (participant) in a session
4813
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]

src/media/bridge.rs

Lines changed: 223 additions & 372 deletions
Large diffs are not rendered by default.

src/media/conference_mixer.rs

Lines changed: 49 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
//! This module provides real-time audio mixing for conference calls.
44
//! It connects MediaPeers to the mixer and routes mixed audio back to participants.
55
6-
use crate::call::domain::LegId;
6+
use crate::media::LegId;
77
use crate::media::mixer::AudioMixer;
88
use anyhow::{Result, anyhow};
99
use audio_codec::CodecType;
10+
use dashmap::DashMap;
1011
use parking_lot::Mutex as ParkMutex;
11-
use std::collections::HashMap;
1212
use std::sync::Arc;
1313
use tokio::sync::mpsc;
1414
use tokio_util::sync::CancellationToken;
@@ -77,8 +77,8 @@ pub struct ConferenceParticipantAudio {
7777
pub struct ConferenceAudioMixer {
7878
/// Conference ID
7979
conf_id: String,
80-
/// Participant audio channels
81-
participants: Arc<tokio::sync::Mutex<HashMap<LegId, ConferenceParticipantAudio>>>,
80+
/// Participant audio channels (DashMap for concurrent access)
81+
participants: Arc<DashMap<LegId, ConferenceParticipantAudio>>,
8282
/// Cached participant count for sync access
8383
participant_count: Arc<std::sync::atomic::AtomicUsize>,
8484
/// Audio sample rate
@@ -91,10 +91,10 @@ pub struct ConferenceAudioMixer {
9191
mixing_task: Arc<ParkMutex<Option<tokio::task::JoinHandle<()>>>>,
9292
/// Per-(source, destination) gain overrides for supervisor modes.
9393
/// Key: (src_leg_id, dst_leg_id), Value: gain (0.0 = silent, 1.0 = normal)
94-
route_gains: Arc<tokio::sync::Mutex<HashMap<(LegId, LegId), f32>>>,
94+
route_gains: Arc<DashMap<(LegId, LegId), f32>>,
9595
/// Optional recorder tap: each participant's input PCM is cloned here
9696
/// before mixing so the recorder can write per-leg audio.
97-
recorder_sink: Arc<tokio::sync::Mutex<Option<mpsc::Sender<RecorderFrame>>>>,
97+
recorder_sink: Arc<parking_lot::Mutex<Option<mpsc::Sender<RecorderFrame>>>>,
9898
}
9999

100100
impl std::fmt::Debug for ConferenceAudioMixer {
@@ -123,22 +123,22 @@ impl ConferenceAudioMixer {
123123

124124
Self {
125125
conf_id,
126-
participants: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
126+
participants: Arc::new(DashMap::new()),
127127
participant_count: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
128128
sample_rate,
129129
frame_size,
130130
cancel_token: CancellationToken::new(),
131131
mixing_task: Arc::new(ParkMutex::new(None)),
132-
route_gains: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
133-
recorder_sink: Arc::new(tokio::sync::Mutex::new(None)),
132+
route_gains: Arc::new(DashMap::new()),
133+
recorder_sink: Arc::new(parking_lot::Mutex::new(None)),
134134
}
135135
}
136136

137137
/// Install a recorder tap. Every 20ms tick, the mixer will send each
138138
/// participant's raw input PCM to the provided channel **before**
139139
/// N-1 mixing, so the recorder can write per-leg audio.
140140
pub async fn set_recorder_sink(&self, sink: Option<mpsc::Sender<RecorderFrame>>) {
141-
let mut guard = self.recorder_sink.lock().await;
141+
let mut guard = self.recorder_sink.lock();
142142
*guard = sink;
143143
}
144144

@@ -150,14 +150,11 @@ impl ConferenceAudioMixer {
150150
codec: CodecType,
151151
) -> Result<(mpsc::Sender<AudioFrame>, mpsc::Receiver<AudioFrame>)> {
152152
// Reject duplicate participants
153-
{
154-
let participants = self.participants.lock().await;
155-
if participants.contains_key(&leg_id) {
156-
return Err(anyhow!(
157-
"Participant {} already exists in conference",
158-
leg_id
159-
));
160-
}
153+
if self.participants.contains_key(&leg_id) {
154+
return Err(anyhow!(
155+
"Participant {} already exists in conference",
156+
leg_id
157+
));
161158
}
162159

163160
let (input_tx, input_rx) = mpsc::channel::<AudioFrame>(100);
@@ -171,10 +168,7 @@ impl ConferenceAudioMixer {
171168
muted: false,
172169
};
173170

174-
{
175-
let mut participants = self.participants.lock().await;
176-
participants.insert(leg_id.clone(), participant);
177-
}
171+
self.participants.insert(leg_id.clone(), participant);
178172

179173
// Update cached count
180174
self.participant_count
@@ -194,10 +188,7 @@ impl ConferenceAudioMixer {
194188

195189
/// Remove a participant from the conference
196190
pub async fn remove_participant(&self, leg_id: &LegId) -> Result<()> {
197-
let was_present = {
198-
let mut participants = self.participants.lock().await;
199-
participants.remove(leg_id).is_some()
200-
};
191+
let was_present = self.participants.remove(leg_id).is_some();
201192

202193
// Only adjust the count if the participant actually existed, to avoid
203194
// underflowing to usize::MAX on duplicate/erroneous remove calls (which
@@ -209,12 +200,10 @@ impl ConferenceAudioMixer {
209200
// Prune any route gains that referenced the leaving leg (as source
210201
// or destination), so the routing table does not grow monotonically
211202
// as participants churn through a long-running conference.
212-
let mut gains = self.route_gains.lock().await;
213-
let before = gains.len();
214-
gains.retain(|(src, dst), _| src != leg_id && dst != leg_id);
215-
let pruned = before - gains.len();
203+
let before = self.route_gains.len();
204+
self.route_gains.retain(|(src, dst), _| src != leg_id && dst != leg_id);
205+
let pruned = before - self.route_gains.len();
216206
if pruned > 0 {
217-
drop(gains);
218207
debug!(
219208
conf_id = %self.conf_id,
220209
leg_id = %leg_id,
@@ -238,8 +227,7 @@ impl ConferenceAudioMixer {
238227

239228
/// Mute/unmute a participant
240229
pub async fn set_muted(&self, leg_id: &LegId, muted: bool) -> Result<()> {
241-
let mut participants = self.participants.lock().await;
242-
if let Some(participant) = participants.get_mut(leg_id) {
230+
if let Some(mut participant) = self.participants.get_mut(leg_id) {
243231
participant.muted = muted;
244232
info!(
245233
conf_id = %self.conf_id,
@@ -254,11 +242,10 @@ impl ConferenceAudioMixer {
254242
/// Set per-route gain for supervisor modes.
255243
/// A gain of 0.0 means the source participant is silent for the destination.
256244
pub async fn set_route_gain(&self, src: &LegId, dst: &LegId, gain: f32) {
257-
let mut gains = self.route_gains.lock().await;
258245
if (gain - 1.0).abs() < f32::EPSILON {
259-
gains.remove(&(src.clone(), dst.clone()));
246+
self.route_gains.remove(&(src.clone(), dst.clone()));
260247
} else {
261-
gains.insert((src.clone(), dst.clone()), gain);
248+
self.route_gains.insert((src.clone(), dst.clone()), gain);
262249
}
263250
info!(
264251
conf_id = %self.conf_id,
@@ -271,25 +258,22 @@ impl ConferenceAudioMixer {
271258

272259
/// Clear all route gains (reset to default N-1 mixing).
273260
pub async fn clear_route_gains(&self) {
274-
let mut gains = self.route_gains.lock().await;
275-
gains.clear();
261+
self.route_gains.clear();
276262
info!(conf_id = %self.conf_id, "Route gains cleared");
277263
}
278264

279265
/// Update audio routing for all participants
280266
/// Each participant hears all other participants (N-1 mixing)
281267
async fn update_routing(&self) -> Result<()> {
282-
let participants = self.participants.lock().await;
283-
let leg_ids: Vec<LegId> = participants.keys().cloned().collect();
284-
drop(participants);
268+
let participant_count = self.participants.len();
285269

286270
// ConferenceAudioMixer uses its own mixing loop (N-1 mixing)
287271
// Each participant receives mixed audio from all other participants
288272
// The actual mixing happens in mixing_loop(), not via MediaMixer routing
289273

290274
debug!(
291275
conf_id = %self.conf_id,
292-
participant_count = leg_ids.len(),
276+
participant_count,
293277
"Updated conference routing"
294278
);
295279

@@ -346,12 +330,12 @@ impl ConferenceAudioMixer {
346330
/// Collects audio from all participants, mixes, and distributes
347331
async fn mixing_loop(
348332
conf_id: String,
349-
participants: Arc<tokio::sync::Mutex<HashMap<LegId, ConferenceParticipantAudio>>>,
333+
participants: Arc<DashMap<LegId, ConferenceParticipantAudio>>,
350334
cancel_token: CancellationToken,
351335
frame_size: usize,
352336
sample_rate: u32,
353-
route_gains: Arc<tokio::sync::Mutex<HashMap<(LegId, LegId), f32>>>,
354-
recorder_sink: Arc<tokio::sync::Mutex<Option<mpsc::Sender<RecorderFrame>>>>,
337+
route_gains: Arc<DashMap<(LegId, LegId), f32>>,
338+
recorder_sink: Arc<parking_lot::Mutex<Option<mpsc::Sender<RecorderFrame>>>>,
355339
) {
356340
let interval_ms = (frame_size as f64 / sample_rate as f64 * 1000.0) as u64;
357341
let interval = tokio::time::Duration::from_millis(interval_ms.max(1));
@@ -373,39 +357,28 @@ impl ConferenceAudioMixer {
373357
break;
374358
}
375359
_ = tokio::time::sleep(interval) => {
376-
let participant_audio = {
377-
let mut participants_guard = participants.lock().await;
378-
let mut frames = HashMap::new();
379-
380-
for (leg_id, participant) in participants_guard.iter_mut() {
381-
loop {
382-
match participant.input_rx.try_recv() {
383-
Ok(frame) => {
384-
if !participant.muted {
385-
frames.insert(leg_id.clone(), frame);
386-
}
387-
}
388-
Err(mpsc::error::TryRecvError::Empty) => {
389-
break;
390-
}
391-
Err(mpsc::error::TryRecvError::Disconnected) => {
392-
break;
360+
// Phase 1: drain latest audio from every participant (no locks held across await)
361+
let mut participant_audio = std::collections::HashMap::new();
362+
for mut entry in participants.iter_mut() {
363+
loop {
364+
match entry.input_rx.try_recv() {
365+
Ok(frame) => {
366+
if !entry.muted {
367+
participant_audio.insert(entry.key().clone(), frame);
393368
}
394369
}
370+
Err(mpsc::error::TryRecvError::Empty) => break,
371+
Err(mpsc::error::TryRecvError::Disconnected) => break,
395372
}
396373
}
374+
}
397375

398-
frames
399-
};
400-
401-
let gains_map = route_gains.lock().await;
402-
let participants_guard = participants.lock().await;
403-
let participant_ids: Vec<LegId> = participants_guard.keys().cloned().collect();
404-
drop(participants_guard);
376+
let participant_ids: Vec<LegId> =
377+
participants.iter().map(|e| e.key().clone()).collect();
405378

406379
// ── Recorder tap: forward raw per-leg PCM before mixing ─────
407380
if !participant_audio.is_empty() {
408-
let sink_guard = recorder_sink.lock().await;
381+
let sink_guard = recorder_sink.lock();
409382
if let Some(ref sink) = *sink_guard {
410383
for (leg_id, frame) in &participant_audio {
411384
let rf = RecorderFrame {
@@ -427,9 +400,9 @@ impl ConferenceAudioMixer {
427400

428401
for (input_leg, frame) in &participant_audio {
429402
if input_leg != output_leg {
430-
let gain = gains_map
403+
let gain = route_gains
431404
.get(&(input_leg.clone(), output_leg.clone()))
432-
.copied()
405+
.map(|r| *r)
433406
.unwrap_or(1.0);
434407
if gain > 0.0 {
435408
input_frames.push(frame.samples.clone());
@@ -452,10 +425,10 @@ impl ConferenceAudioMixer {
452425

453426
let output_frame = AudioFrame::new(mixed_samples, sample_rate);
454427

455-
let output_tx = {
456-
let participants_guard = participants.lock().await;
457-
participants_guard.get(output_leg).map(|p| p.output_tx.clone())
458-
};
428+
// Get the output_tx with a short-lived DashMap lookup
429+
let output_tx = participants
430+
.get(output_leg)
431+
.map(|e| e.output_tx.clone());
459432

460433
if let Some(tx) = output_tx
461434
&& tx.send(output_frame).await.is_err() {

src/media/dtmf.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::sync::Arc;
2+
3+
use crate::media::bridge::BridgeEndpoint;
4+
5+
/// Callback invoked when a DTMF digit is detected from a bridge endpoint.
6+
pub type DtmfHandler = Arc<dyn Fn(char) + Send + Sync + 'static>;
7+
8+
/// Per-endpoint DTMF sink — which payload types carry telephone-events
9+
/// and where to forward detected digits.
10+
#[derive(Clone)]
11+
pub struct DtmfSink {
12+
pub endpoint: BridgeEndpoint,
13+
pub payload_types: Vec<u8>,
14+
pub handler: DtmfHandler,
15+
}
16+
17+
/// Maps a source telephone-event payload type to a target payload type,
18+
/// adjusting clock rate for the other side of the bridge (e.g. Opus 48kHz
19+
/// → PCMU 8kHz).
20+
#[derive(Clone, Debug, PartialEq, Eq)]
21+
pub struct PayloadMapping {
22+
pub source_pt: u8,
23+
pub target_pt: u8,
24+
pub source_clock_rate: u32,
25+
pub target_clock_rate: u32,
26+
}
27+
28+
/// Key used to deduplicate repeated telephone-event packets.
29+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30+
struct DtmfEventKey {
31+
digit_code: u8,
32+
rtp_timestamp: u32,
33+
}
34+
35+
/// Stateful deduplicator for RFC 2833 telephone-event packets.
36+
///
37+
/// The same DTMF digit may arrive in multiple RTP packets with different
38+
/// timestamps (start, continue, end). This detector emits a `char` only
39+
/// once per (digit_code, rtp_timestamp) pair so that duplicate RTP packets
40+
/// (e.g. from a retransmission or from both the recorder tap and the
41+
/// forwarding path) do not produce duplicate digits.
42+
#[derive(Debug, Default)]
43+
pub struct DtmfDetector {
44+
last_event: Option<DtmfEventKey>,
45+
}
46+
47+
impl DtmfDetector {
48+
pub fn observe(&mut self, payload: &[u8], rtp_timestamp: u32) -> Option<char> {
49+
if payload.len() < 4 {
50+
return None;
51+
}
52+
53+
let digit_code = payload[0];
54+
let digit = crate::media::telephone_event::dtmf_code_to_char(digit_code)?;
55+
56+
let event = DtmfEventKey {
57+
digit_code,
58+
rtp_timestamp,
59+
};
60+
61+
if self.last_event == Some(event) {
62+
return None;
63+
}
64+
65+
self.last_event = Some(event);
66+
Some(digit)
67+
}
68+
}

0 commit comments

Comments
 (0)