o_sfu/lib.rs
1//! Odoo's Selective Forwarding Unit (SFU) for audio/video calls.
2//!
3//! `o-sfu` receives participants' audio and video and forwards selected streams
4//! to other participants in the same room. Each sender uploads media to the SFU
5//! instead of sending a separate copy to every receiver.
6//!
7//! ```text
8//! Alice -- audio/video --> SFU --+--> Bob
9//! |
10//! +--> Carol
11//! ```
12//!
13//! A room groups the participants, published media and subscriptions for a call.
14//! One server can host independently authenticated rooms for multiple tenants,
15//! such as different Odoo databases. This fits the Odoo saas and Odoo.sh
16//! architecture.
17//!
18//! # A call through the SFU
19//!
20//! The application backend provisions a room and gives participants credentials
21//! to join it. Browser clients use WebSocket signaling to join, publish media
22//! and choose what to receive. Media travels separately from signaling, over WebRTC.
23//!
24//! ```text
25//! Odoo backend -- HTTP ------> room creation and access
26//! |
27//! v
28//! Browser <---- WebSocket ---> room membership and media requests
29//! |
30//! v
31//! Browser <---- WebRTC/UDP --> media workers <----> other browsers
32//! ```
33//!
34//! This separates decisions about the call from the work of forwarding its
35//! media. Signaling changes which streams should flow. Media workers deliver
36//! those streams as packets arrive.
37//!
38//! # Architecture
39//!
40//! [`Runtime`] brings together the HTTP/WebSocket servers, rooms and media
41//! workers. The HTTP control API manages rooms through
42//! [`RoomManager`](core::server::room::RoomManager). WebSocket sessions admit
43//! participants through [`SfuCore`](core::prelude::SfuCore) and use a
44//! [`MediaSession`](core::prelude::MediaSession) for each connection to publish
45//! media and manage subscriptions.
46//!
47//! ```text
48//! HTTP control API ------------------------------> RoomManager
49//! |
50//! WebSocket session -> SfuCore -> MediaSession |
51//! \ /
52//! +----> Room <----+
53//! |
54//! +<----> Router
55//! |
56//! v
57//! MediaTransport
58//! | | |
59//! v v v
60//! RTC workers / packet loops
61//! (one thread per worker)
62//! | | |
63//! UDP socket UDP socket UDP socket
64//! | | |
65//! v v v
66//! fanout fanout fanout
67//! UDP OUT UDP OUT UDP OUT
68//! ```
69//!
70//! A [`Room`](core::server::room::Room) manages who is present and which media
71//! they publish or receive. The [`Router`](o_sfu_router::Router) keeps
72//! track of the connections needed to deliver that media. Its decisions do not
73//! perform network I/O, so routing behavior can be tested without sockets.
74//!
75//! [`MediaTransport`](core::server::transport::MediaTransport) runs the worker
76//! threads and uses `str0m` for WebRTC. Room policy selects media using
77//! subscriptions, video layout, active speakers and receiver bandwidth. Each
78//! worker forwards incoming media to the selected receivers, shown as fanout
79//! in the diagram.
80//!
81//! Rooms start with one worker. [`config::RoomWorkerPolicy`] can spread new
82//! joins across additional workers in the same process. Participants still
83//! belong to one room and can receive media from participants on other workers.
84//!
85//! # Access and encryption
86//!
87//! The backend signs tokens for room creation and disconnect requests with
88//! the server's `AUTH_KEY`. Participant tokens are verified with their room's
89//! signing key. This lets a tenant authorize its participants without giving
90//! them the server key. See [`auth`] for token types and verification.
91//!
92//! Admission establishes participant identity and room scope. It does not
93//! enforce the per-user `permissions` claim supplied by the tenant.
94//!
95//! Signaling and media need separate transport protection:
96//!
97//! - An external reverse proxy terminates HTTPS and WSS. Proxy mode requires
98//! configured trusted proxies and the public edge must overwrite
99//! client-supplied forwarded host and protocol headers. See [`config`] and
100//! [`http::resolve_request_origin`].
101//! - `str0m` encrypts media between each browser and the SFU with DTLS-SRTP.
102//! Media UDP must reach the server's configured `ANNOUNCED_IP`.
103//!
104//! # Browser client
105//!
106//! The JavaScript bundle exposes `SfuClient`, the Odoo-facing API for joining
107//! calls, publishing media and subscribing to other participants. For example,
108//! with a room token supplied by the application:
109//!
110//! ```javascript
111//! import { SfuClient } from "/bundle/odoo_sfu.js";
112//!
113//! const sfu = new SfuClient();
114//! sfu.connect("https://sfu.example.com/ws", jsonWebToken, {
115//! channelUUID: "mail-channel-uuid",
116//! });
117//! ```
118//!
119//! `connect()` returns before authentication and media negotiation finish.
120//! The application observes `stateChange` and `handledError` for the result.
121//!
122//! The client shares its signaling logic through
123//! [`o_sfu_protocol::host::ProtocolCore`]. The browser adapter runs that Rust
124//! logic as WebAssembly and connects it to browser WebSocket and WebRTC APIs.
125//! This keeps protocol decisions separate from browser-specific operations.
126//!
127//! # Running and observing the server
128//!
129//! [`run`] starts the server from configuration. Use [`Runtime`] to embed it
130//! with a caller-provided listener and shutdown signal. [`config`] controls
131//! network addresses, admission limits and media worker policy at runtime.
132//! The default Cargo feature `otel-tracing` enables OpenTelemetry tracing.
133//!
134//! [`http::telemetry::metrics`] exposes Prometheus metrics for monitoring over
135//! time. [`http::telemetry::diagnostics`] exposes room, participant and worker
136//! state for investigating a call. These operator endpoints require separate
137//! access control, described in [`http`].
138//!
139//! # Where to read next
140//!
141//! | Area | Entry point |
142//! | --- | --- |
143//! | Configuration and server lifecycle | [`config`], [`Runtime`] and [`run`] |
144//! | Authentication and signaling messages | [`auth`], [`http`] and [`websocket`] |
145//! | Room behavior and media transport | [`o_sfu_core`] |
146//! | Connections and media routing | [`o_sfu_router`] |
147//! | Client signaling and Odoo bundle compatibility | [`o_sfu_protocol`] |
148//! | Metrics, diagnostics and tracing | [`o_sfu_telemetry`] |
149//! | Shared call identifiers | `o-sfu-model`, including [`o_sfu_protocol::wire::UserId`] |
150//! | Protocol constants and types | [`o_sfu_rfc`] |
151pub mod config;
152pub mod core {
153 pub use o_sfu_core::{prelude, server};
154}
155pub(crate) mod application;
156mod runtime;
157
158pub mod auth {
159 pub use crate::runtime::auth::{
160 AuthenticationError, HttpDisconnectClaims, HttpRoomClaims, MAX_JWT_TOKEN_BYTES,
161 RegisteredJwtClaims, WebSocketConnectClaims, sign, verify,
162 };
163}
164
165#[cfg(any(test, feature = "testing"))]
166pub mod test_support {
167 pub use crate::runtime::auth::test_support::TestHttpRoomClaims;
168}
169
170/// HTTP route and payload contracts.
171///
172/// `/v1/stats`, `/metrics` and diagnostics require the configured
173/// [`crate::config::DiagnosticsConfig::auth_token`] on every listener. Without
174/// one, the actual listener must be loopback. Missing or invalid tokens return
175/// `401 Unauthorized` with `WWW-Authenticate: Bearer realm="o-sfu"`.
176/// Tokenless non-loopback access returns `403 Forbidden`.
177pub mod http {
178 pub use crate::runtime::{
179 http_server::contract::{
180 CreateRoomQuery, IncomingBitRateStatsResponse, NoopResponse, RoomResponse,
181 StatsResponse, route,
182 },
183 request_origin::{RequestOrigin, resolve_request_origin},
184 };
185
186 /// Operator-facing metrics and diagnostics contracts.
187 pub mod telemetry {
188 /// Prometheus metrics for monitoring calls and server load.
189 ///
190 /// `GET` [`metrics::PATH`] returns `200 OK` in Prometheus text exposition
191 /// format with content type [`metrics::CONTENT_TYPE`]. Configure
192 /// Prometheus to scrape this endpoint. Send `PromQL` queries to
193 /// Prometheus, for example:
194 ///
195 /// ```promql
196 /// sum(osfu_users_active)
197 /// sum by (stage) (rate(osfu_ws_connections_total[5m]))
198 /// ```
199 ///
200 /// [`metrics::MetricName`] lists exported names and their meanings.
201 /// The scrape response includes each metric's `HELP`, `TYPE` and labels.
202 pub mod metrics {
203 pub use o_sfu_telemetry::{
204 metrics::MetricName, prometheus::PROMETHEUS_CONTENT_TYPE as CONTENT_TYPE,
205 };
206
207 pub use crate::http::route::METRICS as PATH;
208 }
209
210 /// JSON snapshots for investigating rooms, participants and workers.
211 ///
212 /// Each route in [`diagnostics::route`] accepts `GET` and returns
213 /// `200 OK` JSON on success. The response types document their fields.
214 ///
215 /// | Request | Response |
216 /// | --- | --- |
217 /// | `/internal/diagnostics/summary` | [`diagnostics::DiagnosticsSummaryResponse`] |
218 /// | `/internal/diagnostics/rooms` | Array of [`diagnostics::DiagnosticsRoomSummary`] |
219 /// | `/internal/diagnostics/workers` | Array of [`diagnostics::DiagnosticsWorkerSummary`] |
220 /// | `/internal/diagnostics/rooms/{uuid}` | [`diagnostics::DiagnosticsRoomDetail`] |
221 /// | `/internal/diagnostics/rooms/{uuid}/users` | Array of [`diagnostics::DiagnosticsUserSummary`] |
222 /// | `/internal/diagnostics/rooms/{uuid}/users/{id}` | [`diagnostics::DiagnosticsUserDetail`] |
223 ///
224 /// Use `uuid` from the rooms response and `userKey` from the room users
225 /// response. `userId` may be a number or string, while `userKey` is
226 /// always the string for `{id}`. URL-encode both path values.
227 ///
228 /// ```http
229 /// GET /internal/diagnostics/summary HTTP/1.1
230 /// Host: o-sfu-observability.internal
231 /// Authorization: Bearer <diagnostics-token>
232 /// Accept: application/json
233 /// ```
234 ///
235 /// Wire names use `camelCase` unless a field documents an exception.
236 /// User detail is room-scoped because the same user key can be active
237 /// in several rooms.
238 pub mod diagnostics {
239 pub use o_sfu_telemetry::diagnostics::{
240 DiagnosticsRoomDetail, DiagnosticsRoomSummary, DiagnosticsSummaryResponse,
241 DiagnosticsUserDetail, DiagnosticsUserSummary, DiagnosticsWorkerSummary,
242 };
243
244 pub use crate::http::route::diagnostics as route;
245 }
246 }
247}
248
249pub mod websocket {
250 pub use crate::runtime::websocket_server::{
251 ClientBatchDecodeError, ClientBatchDecodeFailureKind, MAX_CLIENT_BATCH_ENVELOPES,
252 MAX_CLIENT_FRAME_BYTES, decode_auth_payload_text, decode_client_batch,
253 };
254}
255
256pub use self::runtime::{Runtime, ServeError, run};