Skip to main content

rvoip_core/bridge/
mod.rs

1//! Cross-transport bridge primitive.
2//!
3//! Per CARVE_PLAN §3 (BridgeManager row): in this PR series the stored handle
4//! stays as `media-core`'s [`BridgeHandle`] — current shape, just relocated.
5//! A `BridgeKind` enum (to discriminate SIP fast-path vs. cross-transport
6//! bridge) is deferred until the cross-transport frame-pump (INTERFACE_DESIGN
7//! §10.2) actually lands.
8//!
9//! The Phase-1 DashMap-of-bridges + by-owner-index shape from
10//! `PERFORMANCE_PLAN.md` is preserved exactly. [`BridgeManager`] is generic
11//! over the owner-key type so `orchestration-core` can keep using `CallId`
12//! while a future cross-transport orchestrator uses [`crate::ConnectionId`].
13
14use crate::ids::{BridgeId, ConnectionId};
15use dashmap::DashMap;
16use std::hash::Hash;
17use std::sync::Arc;
18
19pub use rvoip_media_core::relay::controller::{BridgeError, BridgeHandle};
20
21pub mod cross_handle;
22pub mod frame_pump;
23
24pub use cross_handle::CrossBridgeHandle;
25
26/// Enabled media directions for one two-Connection bridge.
27///
28/// The names are relative to the exact `a` and `b` Connection arguments. A
29/// disabled direction neither acquires that source's single-consumer receiver
30/// nor installs a sink route. Application-data bridging remains independent
31/// and bidirectional.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct DirectionalMediaBridgePlan {
34    a_to_b: bool,
35    b_to_a: bool,
36}
37
38impl DirectionalMediaBridgePlan {
39    /// Build a validated directional plan. A bridge with no media direction
40    /// is rejected rather than reserving both Connections without a route.
41    pub fn new(a_to_b: bool, b_to_a: bool) -> crate::Result<Self> {
42        if !a_to_b && !b_to_a {
43            return Err(crate::RvoipError::AdmissionRejected(
44                "media bridge must enable at least one direction",
45            ));
46        }
47        Ok(Self { a_to_b, b_to_a })
48    }
49
50    #[must_use]
51    pub const fn bidirectional() -> Self {
52        Self {
53            a_to_b: true,
54            b_to_a: true,
55        }
56    }
57
58    #[must_use]
59    pub const fn a_to_b(self) -> bool {
60        self.a_to_b
61    }
62
63    #[must_use]
64    pub const fn b_to_a(self) -> bool {
65        self.b_to_a
66    }
67}
68
69/// Map an `audio_codecs` codec name (per CONVERSATION_PROTOCOL.md §8)
70/// to its media-graph codec key.
71///
72/// Most keys are the codec's conventional RTP payload type. `pcm_s16le`
73/// uses the internal-only key 120; transports must not advertise or emit it.
74///
75/// Returns `None` for codec names not in the table so the bridge layer
76/// can produce a clear "unsupported codec" diagnostic
77/// ([`crate::RvoipError::UnsupportedCodec`]) instead of forwarding an
78/// arbitrary dynamic PT (e.g. `96`) and getting a generic transcoder
79/// error several layers down.
80pub fn codec_to_pt(name: &str) -> Option<u8> {
81    match name.to_ascii_lowercase().as_str() {
82        "pcmu" | "g.711-mu" | "g711-mu" | "g711-u" => Some(0),
83        "pcma" | "g.711-a" | "g711-a" => Some(8),
84        "g729" | "g.729" => Some(18),
85        "opus" => Some(111),
86        "pcm_s16le" | "pcm-s16le" => Some(rvoip_media_core::codec::audio::payload_type::PCM_S16LE),
87        _ => None,
88    }
89}
90
91#[cfg(test)]
92mod codec_mapping_tests {
93    use super::codec_to_pt;
94    use rvoip_media_core::codec::audio::payload_type::PCM_S16LE;
95
96    #[test]
97    fn maps_internal_pcm_name_to_reserved_key() {
98        assert_eq!(codec_to_pt("pcm_s16le"), Some(PCM_S16LE));
99        assert_eq!(codec_to_pt("PCM_S16LE"), Some(PCM_S16LE));
100        assert_eq!(PCM_S16LE, 120);
101    }
102}
103
104/// Per-process registry of active media bridges.
105///
106/// Backed by `DashMap` for lock-free concurrent reads/writes plus a secondary
107/// `by_owner` index for O(1) "which bridge is this owner in?" lookups (used
108/// by hangup / teardown paths). The primary map stores the handle alongside
109/// the owning key so [`BridgeManager::remove`] can keep both indices coherent
110/// without an extra parameter.
111///
112/// Generic parameters:
113/// - `K`: owner key (e.g. `ConnectionId` for cross-transport, `CallId` for
114///   SIP-only orchestration-core).
115/// - `I`: bridge identifier type (defaults to rvoip-core's
116///   [`BridgeId`]; orchestration-core overrides with its own `BridgeId` for
117///   the carve transition).
118pub struct BridgeManager<K = ConnectionId, I = BridgeId>
119where
120    K: Eq + Hash + Clone,
121    I: Eq + Hash + Clone,
122{
123    bridges: Arc<DashMap<I, (BridgeHandle, K)>>,
124    by_owner: Arc<DashMap<K, I>>,
125}
126
127impl<K, I> Clone for BridgeManager<K, I>
128where
129    K: Eq + Hash + Clone,
130    I: Eq + Hash + Clone,
131{
132    fn clone(&self) -> Self {
133        Self {
134            bridges: Arc::clone(&self.bridges),
135            by_owner: Arc::clone(&self.by_owner),
136        }
137    }
138}
139
140impl<K, I> Default for BridgeManager<K, I>
141where
142    K: Eq + Hash + Clone,
143    I: Eq + Hash + Clone,
144{
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl<K, I> BridgeManager<K, I>
151where
152    K: Eq + Hash + Clone,
153    I: Eq + Hash + Clone,
154{
155    pub fn new() -> Self {
156        Self {
157            bridges: Arc::new(DashMap::new()),
158            by_owner: Arc::new(DashMap::new()),
159        }
160    }
161
162    pub fn insert(&self, bridge_id: I, owner: K, handle: BridgeHandle) {
163        self.by_owner.insert(owner.clone(), bridge_id.clone());
164        self.bridges.insert(bridge_id, (handle, owner));
165    }
166
167    /// Removes the bridge and returns the [`BridgeHandle`] (whose `Drop`
168    /// tears the bridge down). Keeps the secondary index coherent.
169    pub fn remove(&self, bridge_id: &I) -> Option<BridgeHandle> {
170        let (_, (handle, owner)) = self.bridges.remove(bridge_id)?;
171        self.by_owner
172            .remove_if(&owner, |_, registered| registered == bridge_id);
173        Some(handle)
174    }
175
176    /// O(1) lookup: which active bridge (if any) is this owner in?
177    pub fn bridge_for_owner(&self, owner: &K) -> Option<I> {
178        self.by_owner.get(owner).map(|entry| entry.value().clone())
179    }
180
181    pub fn len(&self) -> usize {
182        self.bridges.len()
183    }
184
185    pub fn is_empty(&self) -> bool {
186        self.bridges.is_empty()
187    }
188}