Skip to main content

o_sfu_core/
lib.rs

1//! Room state, routing and media transport orchestration.
2//!
3//! `o-sfu-core` bridges the server runtime, the pure `o-sfu-router` state machine
4//! and the `str0m`-backed media transport. It keeps room admission, user media
5//! intent and RTC worker details behind [`SfuCore`](prelude::SfuCore) and
6//! [`MediaSession`](prelude::MediaSession).
7//!
8//! # Public Surface
9//!
10//! - [`prelude`] contains caller-facing configuration, media intent,
11//!   [`SfuCore`](prelude::SfuCore) and [`MediaSession`](prelude::MediaSession).
12//! - [`server`] contains runtime construction, room integration, transport,
13//!   diagnostics and metrics.
14//! - Fundamental identifiers and [`Bitrate`] remain at the crate root.
15//!
16//! # Architecture
17//!
18//! ```text
19//! server runtime
20//!   -> SfuCore::admit_user
21//!   -> MediaSession
22//!   -> room state and source policy
23//!   -> MediaTransport
24//!   -> RTC workers
25//! ```
26//!
27//! [`MediaTransport`](server::transport::MediaTransport) starts the worker threads
28//! and binds their UDP sockets. Room operations release state locks before awaiting
29//! transport work. Source policy maps layout intent, active-speaker observations
30//! and receiver bandwidth to route activity and packet gates. Worker-local packet
31//! loops then demultiplex UDP and forward RTP through those gates. The private
32//! `rtc::codec` boundary contains capability projection plus codec-specific packet
33//! inspection and rewrite, so source policy does not branch on payload details.
34//!
35//! # Server Construction
36//!
37//! The server builds one [`MediaTransport`](server::transport::MediaTransport)
38//! from owner configuration and shared process services.
39//!
40//! ```no_run
41//! use std::{
42//!     net::{IpAddr, Ipv4Addr},
43//!     sync::Arc,
44//! };
45//!
46//! use o_sfu_core::{
47//!     prelude::{
48//!         Bitrate, CodecPreferences, MediaCodecFlags, RtcPortRange, RtcUdpIoBackend,
49//!         SessionBitrateLimits, VideoBitrateLimits,
50//!     },
51//!     server::{
52//!         metrics::RuntimeMetrics,
53//!         packet_sinks::RoomPacketSinkRegistry,
54//!         transport::{MediaTransport, MediaTransportConfig, MediaTransportDeps},
55//!     },
56//! };
57//!
58//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
59//! let config = MediaTransportConfig {
60//!     worker_count: 1,
61//!     announced_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
62//!     bitrate_limits: SessionBitrateLimits::new(
63//!         Bitrate::from_mbps(3),
64//!         Bitrate::from_mbps(3),
65//!     ),
66//!     video_bitrate_limits: VideoBitrateLimits::default(),
67//!     rtc_port_range: RtcPortRange::try_new(40_000, 40_099)?,
68//!     rtc_udp_io_backend: RtcUdpIoBackend::Tokio,
69//!     rtc_tcp_config: None,
70//!     codec_flags: MediaCodecFlags::default(),
71//!     codec_preferences: CodecPreferences::default(),
72//!     media_quality_interval: None,
73//! };
74//! let deps = MediaTransportDeps {
75//!     packet_sink_registry: Arc::new(RoomPacketSinkRegistry::default()),
76//!     metrics: Arc::new(RuntimeMetrics::default()),
77//! };
78//!
79//! let transport = MediaTransport::build(config, deps)?;
80//!
81//! # let _transport = transport;
82//! # Ok(())
83//! # }
84//! ```
85//!
86//! [`MediaTransport::build`](server::transport::MediaTransport::build) returns
87//! after every worker runtime has bound its UDP socket. Session-local RTC state
88//! remains lazy.
89//!
90//! # Session Negotiation
91//!
92//! Negotiation is serialized through `&mut MediaSession`. A publish received
93//! while an offer is pending is queued. Applying the answer returns a follow-up
94//! offer when that intent needs another SDP round.
95//!
96//! ```no_run
97//! use o_sfu_core::prelude::{
98//!     MediaSession, NegotiationOffer, SessionError, SourcePublishIntent,
99//! };
100//!
101//! # async fn exchange(_: NegotiationOffer) -> String { String::new() }
102//! async fn publish_source(
103//!     mut session: MediaSession,
104//!     intent: SourcePublishIntent,
105//! ) -> Result<(), SessionError> {
106//!     let Some(initial_offer) = session.establish().await? else {
107//!         return Ok(());
108//!     };
109//!     let initial_answer = exchange(initial_offer).await;
110//!
111//!     // Publish before answering so this intent queues behind the in-flight SDP round.
112//!     let _queued_without_offer = session.publish(intent).await?;
113//!
114//!     let Some(follow_up_offer) = session.answer(&initial_answer).await? else {
115//!         return Ok(());
116//!     };
117//!
118//!     let follow_up_answer = exchange(follow_up_offer).await;
119//!
120//!     let _next_offer = session.answer(&follow_up_answer).await?;
121//!     Ok(())
122//! }
123//! ```
124
125use std::fmt::{self, Display, Formatter};
126
127pub use o_sfu_router::{ConnectionId, MediaWorkerId};
128
129mod engine;
130mod options;
131pub mod prelude;
132pub mod server;
133mod sfu;
134
135pub(crate) use options::{
136    AudioCodecPreference, CodecPreferences, MediaCodecFlags, RoomMediaLimits, RoomWorkerPolicy,
137    RtcPortRange, RtcTcpConfig, RtcUdpIoBackend, RuntimeFeatureFlags, SessionBitrateLimits,
138    VideoAdaptationTuning, VideoBitrateLimits, VideoCodecPreference,
139};
140
141/// Media bitrate stored as bits per second (not bytes per second).
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
143pub struct Bitrate(u64);
144
145impl Bitrate {
146    #[must_use]
147    pub const fn from_bps(value: u64) -> Self {
148        Self(value)
149    }
150
151    #[must_use]
152    pub const fn from_kbps(value: u64) -> Self {
153        Self(value.saturating_mul(1_000))
154    }
155
156    #[must_use]
157    pub const fn from_mbps(value: u64) -> Self {
158        Self(value.saturating_mul(1_000_000))
159    }
160
161    #[must_use]
162    pub const fn zero() -> Self {
163        Self(0)
164    }
165
166    #[must_use]
167    pub const fn as_bps(self) -> u64 {
168        self.0
169    }
170
171    #[must_use]
172    pub const fn saturating_add(self, other: Self) -> Self {
173        Self(self.0.saturating_add(other.0))
174    }
175
176    #[must_use]
177    pub const fn saturating_sub(self, other: Self) -> Self {
178        Self(self.0.saturating_sub(other.0))
179    }
180
181    #[must_use]
182    pub const fn divided_by(self, divisor: u64) -> Self {
183        match self.0.checked_div(divisor) {
184            Some(value) => Self(value),
185            None => Self::zero(),
186        }
187    }
188}
189
190/// Process-local generation tag for one room lifecycle.
191///
192/// The tag separates one runtime allocation from its application room identity
193/// in transport keys and telemetry.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
195pub struct RoomInstanceId(u64);
196
197impl RoomInstanceId {
198    /// Allocates the next room generation tag.
199    ///
200    /// The counter saturates, so repeated calls return `u64::MAX` after
201    /// exhaustion.
202    #[must_use]
203    pub fn allocate(next_room_instance_id: &mut u64) -> Self {
204        let room_instance_id = Self(*next_room_instance_id);
205        *next_room_instance_id = next_room_instance_id.saturating_add(1);
206        room_instance_id
207    }
208
209    #[must_use]
210    pub const fn from_raw(raw: u64) -> Self {
211        Self(raw)
212    }
213
214    #[must_use]
215    pub const fn as_u64(self) -> u64 {
216        self.0
217    }
218}
219
220impl Display for RoomInstanceId {
221    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
222        self.0.fmt(formatter)
223    }
224}