Skip to main content

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};