o_sfu_router/lib.rs
1//! Pure topology and capability negotiation engine for O-SFU.
2//!
3//! `o-sfu-router` is the isolated, deterministic brain of the SFU. It models room
4//! membership, multi-worker router placements, producer-consumer dependency graphs,
5//! and typed RTP codec negotiation without touching networks, threads, or raw wire protocols.
6//!
7//! # Why is the Router Isolated?
8//!
9//! Traditional SFU architectures often interleave signaling protocols (SDP offer/answer),
10//! routing topology, and RTP packet loops into monolithic async engines. `o-sfu-router`
11//! enforces a strict architectural boundary:
12//!
13//! 1. **Complete Determinism (Zero I/O)**: Contains zero async runtimes, zero threads,
14//! and zero socket syscalls. All state transitions are synchronous and deterministic.
15//! 2. **SDP-Free Domain Modeling**: Sits cleanly behind `o-sfu-core`'s SDP edge. It does not
16//! parse raw SDP text or manage ICE candidates; it operates on strongly-typed topology
17//! and RTP models ([`rtp::MediaStream`], [`rtp::MediaCapabilities`]).
18//! 3. **Isolated Placement & Teardowns**: Tracks cross-router subscriptions via foreign session
19//! shadows. When a client reconnects or leaves, the router cleans its dependent graph
20//! without affecting other active publishers.
21//! 4. **Direct Testability**: Because the crate has zero I/O, complex multi-router reconnects,
22//! cascading disconnects, and codec negotiation edge cases can be tested directly.
23//!
24//! # System Architecture
25//!
26//! ```text
27//! +------------------------------------------+
28//! | Signaling Edge / Clients |
29//! | (HTTP, WebSocket, SDP Offer/Answer Bags) |
30//! +------------------------------------------+
31//! |
32//! core adapts SDP to | request placement,
33//! MediaStream / Caps | publish, subscribe
34//! v
35//! +===================================================================================+
36//! | o-sfu-router (Pure Core) |
37//! | |
38//! | * 100% Synchronous & Deterministic (Zero async, Zero I/O, Zero RTP transport) |
39//! | |
40//! | +-------------------------------------+ +------------------------------------+ |
41//! | | Multi-Router Routing Topology | | Typed RTP Capability Matching | |
42//! | | - User -> Connection -> Home Router | | - Ingress normalization | |
43//! | | - Producer -> Dependent Consumers | | - Egress codec intersection | |
44//! | | - Cross-Router Session Shadows | | - RFC 4588 RTX `apt` remapping | |
45//! | +-------------------------------------+ +------------------------------------+ |
46//! +===================================================================================+
47//! |
48//! routed identities, worker | deterministic
49//! lookups, RTP specs | graph mutations
50//! v
51//! +-----------------------------------------------------------------------------------+
52//! | o-sfu-core (Runtime & Engine) |
53//! | |
54//! | * Async Tokio Runtimes, Media Transport Workers, UDP Demuxing, str0m Packet Loop |
55//! +-----------------------------------------------------------------------------------+
56//! ```
57//!
58//! The [`Router`] struct is the sole stateful facade. It owns the exact user-to-connection
59//! placement relation and manages local and foreign session graphs across attached routers.
60//!
61//! # Examples
62//!
63//! ```
64//! # use o_sfu_model::UserId;
65//! # use o_sfu_router::{
66//! # ConnectionId, ConsumerId, MediaWorkerId, ProducerId, Router, RouterError, RouterId,
67//! # rtp::MediaCapabilities,
68//! # topology::{RouterPlacement, RouterPlacements},
69//! # };
70//!
71//! # fn main() -> Result<(), RouterError> {
72//! # fn placement(router: u64, worker: usize) -> RouterPlacement {
73//! # RouterPlacement {
74//! # router: RouterId(router),
75//! # media_worker: MediaWorkerId::from_raw(worker),
76//! # }
77//! # }
78//! let source = placement(1, 0);
79//! let first_receiver = placement(2, 1);
80//! let next_receiver = placement(3, 2);
81//! let placements = RouterPlacements::new(source, vec![first_receiver, next_receiver]);
82//! let mut router = Router::with_placements(placements, MediaCapabilities::default());
83//! let publisher = UserId::from(1_i64);
84//! let receiver = UserId::from(2_i64);
85//! let publisher_connection = ConnectionId::from_raw(10);
86//! let first_receiver_connection = ConnectionId::from_raw(20);
87//! router.commit_session_placement(&publisher, publisher_connection, source)?;
88//! router.commit_session_placement(&receiver, first_receiver_connection, first_receiver)?;
89//! let producer = router.add_producer(&publisher, ProducerId(30))?;
90//! let stale = router.add_consumer(&receiver, ConsumerId(40), producer)?;
91//!
92//! assert_eq!(stale.router_id(), producer.router_id());
93//! assert_eq!(stale.connection_id(), first_receiver_connection);
94//!
95//! let next_receiver_connection = ConnectionId::from_raw(21);
96//! router.commit_session_placement(&receiver, next_receiver_connection, next_receiver)?;
97//! assert_eq!(
98//! router.remove_consumer(stale),
99//! Err(RouterError::MissingConsumer(stale)),
100//! );
101//!
102//! let current = router.add_consumer(&receiver, ConsumerId(41), producer)?;
103//! assert_eq!(current.router_id(), producer.router_id());
104//! assert_eq!(current.connection_id(), next_receiver_connection);
105//! # Ok(())
106//! # }
107//! ```
108
109mod model;
110#[cfg(any(test, feature = "test-support"))]
111#[path = "TESTS/test_support/mod.rs"]
112pub mod test_support;
113
114/// typed router and media identifiers
115pub mod ids {
116 pub use crate::model::ids::*;
117}
118
119/// typed RTP values used at the router boundary
120pub mod rtp {
121 pub use crate::model::{MediaKind, rtp::*};
122}
123
124/// producer and consumer RTP negotiation
125///
126/// Producer parameters are first normalized against router capabilities. The
127/// consumer is then negotiated from that router-visible stream
128///
129/// # Examples
130///
131/// ```
132/// # use o_sfu_rfc::rtp::{
133/// # CodecName, RTP_VIDEO_CLOCK_RATE_HZ as VIDEO_CLOCK_RATE_HZ, codec_name, fmtp,
134/// # };
135/// # use o_sfu_router::{
136/// # MediaKind,
137/// # negotiation::{
138/// # RtpNegotiationError, derive_consumable_rtp_parameters,
139/// # negotiate_consumer_rtp_parameters,
140/// # },
141/// # rtp::{
142/// # MediaCapabilities, MediaCodecCapability, MediaFormat, MediaStream, PayloadType,
143/// # RtcpFeedback, RtcpFeedbackKind,
144/// # },
145/// # };
146/// # fn main() -> Result<(), RtpNegotiationError> {
147/// let producer = MediaStream::new(
148/// vec![
149/// MediaFormat::new(MediaKind::Video, CodecName::Rtx, PayloadType::new(97), VIDEO_CLOCK_RATE_HZ)
150/// .with_parameter(fmtp::RTX_ASSOCIATION, "96"),
151/// MediaFormat::new(MediaKind::Video, CodecName::Vp8, PayloadType::new(96), VIDEO_CLOCK_RATE_HZ)
152/// .with_rtcp_feedback(RtcpFeedback::new(RtcpFeedbackKind::Nack, None)),
153/// ],
154/// vec![],
155/// vec![],
156/// );
157/// let router = MediaCapabilities::new(
158/// vec![
159/// MediaCodecCapability::new(MediaKind::Video, CodecName::Vp8, VIDEO_CLOCK_RATE_HZ)
160/// .with_payload_type(PayloadType::new(100))
161/// .with_rtcp_feedback(RtcpFeedback::new(RtcpFeedbackKind::Nack, None)),
162/// MediaCodecCapability::new(MediaKind::Video, CodecName::Rtx, VIDEO_CLOCK_RATE_HZ)
163/// .with_payload_type(PayloadType::new(101))
164/// .with_parameter(fmtp::RTX_ASSOCIATION, "100"),
165/// ],
166/// vec![],
167/// );
168/// let consumer = MediaCapabilities::new(
169/// vec![
170/// MediaCodecCapability::new(MediaKind::Video, CodecName::Vp8, VIDEO_CLOCK_RATE_HZ)
171/// .with_rtcp_feedback(RtcpFeedback::new(RtcpFeedbackKind::Nack, None)),
172/// MediaCodecCapability::new(MediaKind::Video, CodecName::Rtx, VIDEO_CLOCK_RATE_HZ)
173/// .with_parameter(fmtp::RTX_ASSOCIATION, "100"),
174/// ],
175/// vec![],
176/// );
177///
178/// let consumable = derive_consumable_rtp_parameters(&producer, &router)?;
179/// let negotiated = negotiate_consumer_rtp_parameters(&consumable, &consumer)?;
180///
181/// assert_eq!(
182/// consumable
183/// .formats()
184/// .map(|format| (
185/// format.codec_name(),
186/// format.payload_type(),
187/// format.rtx_associated_payload_type(),
188/// ))
189/// .collect::<Vec<_>>(),
190/// vec![(codec_name::VP8, 100, None), (codec_name::RTX, 101, Some(100))],
191/// );
192/// assert_eq!(negotiated, consumable);
193/// # Ok(())
194/// # }
195/// ```
196pub mod negotiation {
197 #[cfg(any(test, feature = "test-support"))]
198 pub use crate::model::diagnostic::*;
199 pub use crate::model::rtp_negotiation::*;
200}
201
202/// placement and routed media identifiers used by [`Router`]
203pub mod topology {
204 pub use crate::model::topology::{
205 PlacementSnapshot, RoutedConsumerId, RoutedProducerId, RouterPlacement, RouterPlacements,
206 RouterPlacementsError,
207 };
208}
209
210pub use ids::{ConnectionId, ConsumerId, MediaWorkerId, ProducerId, RouterId};
211pub use model::{MediaKind, Router, RouterError};