Skip to main content

o_sfu/
lib.rs

1//! A Selective Forwarding Unit (SFU) for audio/video calls.
2//!
3//! A SFU receives each participant's media once and selectively forwards it to
4//! the others, so an N-party call costs one upload per sender rather than one
5//! per listener and no stream is transcoded or mixed. `o-sfu` runs this model as
6//! a dedicated server that handles room admission, routing topology, media
7//! policy, packet forwarding, signaling and telemetry. Applications provision
8//! rooms over HTTP, browsers connect over WebSocket and media travels over UDP
9//! with `str0m` terminating ICE, DTLS and SRTP.
10//!
11//! # Core Concepts
12//!
13//! `o-sfu` separates the **control plane** for admission and room policy, the
14//! **routing plane** for user-to-connection placement and the **packet plane**
15//! for RTP forwarding on worker loops.
16//!
17//! - **[`Runtime`]**: Owns the process lifecycle, the HTTP/WebSocket servers and graceful shutdown.
18//! - **[`core::server::room::Room`]**: The control plane boundary for a set of participants. It commits membership and media relationships.
19//! - **[`o_sfu_router::Router`]**: The routing plane, a pure, sans-I/O engine owning the placement graph that maps users to connections.
20//! - **[`core::prelude::MediaSession`]**: Orchestrates a user's connection, bridging room intent to transport effects.
21//! - **[`core::server::transport::MediaTransport`]**: Owns the media workers and hides their threading model. Each worker holds a packet loop and applies projected routes to incoming datagrams.
22//!
23//! # Architecture
24//!
25//! Control and routing decisions happen above the packet loops, which apply the
26//! resulting transport state to UDP datagrams.
27//!
28//! ```text
29//! HTTP control API ----------------------> RoomManager
30//!                                               |
31//! WebSocket session -> SfuCore -> MediaSession  |
32//!                              \                /
33//!                               +----> Room <----+
34//!                                      |
35//!                                      +<----> Router
36//!                                      |
37//!                                      v
38//!                                MediaTransport
39//!                                 |    |    |
40//!                                 v    v    v
41//!                          RTC workers / packet loops
42//!                         |            |              |
43//!                      UDP:40001   UDP:40002      UDP:40003
44//!                         |            |              |
45//!                         v            v              v
46//!                      fanout        fanout        fanout
47//! ```
48//!
49//! # Admission Edge
50//!
51//! Applications provision rooms through
52//! [`RoomManager::serve_room`](core::server::room::RoomManager::serve_room).
53//! WebSocket clients join through
54//! [`SfuCore::admit_user`](core::prelude::SfuCore::admit_user). Both paths require
55//! JWT authentication before admission.
56//!
57//! ```text
58//!                     +----------------------------------------+
59//!                     |     Incoming HTTP / WebSocket I/O      |
60//!                     +----------------------------------------+
61//!                                         |
62//!                                         v
63//!                     +----------------------------------------+
64//!                     |              Axum Router               |
65//!                     +----------------------------------------+
66//!                                         |
67//!        +--------------------+-----------+-----------+--------------------+
68//!        |                    |                       |                    |
69//!        v                    v                       v                    v
70//!  [ GET /v1/noop ]  [ GET /v1/channel ]     [ WebSocket / ]   [ Operator Routes ]   <-- Routes
71//!  (Public Liveness) [ POST /v1/disconnect ] (Signaling Path)  - GET /v1/stats
72//!        |                    |                     |          - GET /metrics
73//!        |                    |                     |          - GET /internal/...
74//!        |                    v                     v                  |
75//!        |           +---------------+     +---------------+   +---------------+
76//!        |           | VerifiedRoom  |     | Upgrade       |   | OperatorAccess|     <-- Extractors
77//!        |           | VerifiedClaims|     | ConnectInfo   |   | (Route Layer) |
78//!        |           | (JWT Header)  |     | 1st-Frame JWT |   | (Bearer/Local)|
79//!        |           +---------------+     +---------------+   +---------------+
80//!        |                    |                     |                  |
81//!        v                    v                     v                  v
82//!  +---------------+ +---------------+     +---------------+   +---------------+
83//!  | noop          | | room          |     | upgrade       |   | stats         |     <-- Handlers
84//!  |               | | disconnect    |     | -> admit_user |   | metrics       |
85//!  | -> 200 JSON   | | -> Room State |     | -> WS Session |   | diagnostics_* |
86//!  +---------------+ +---------------+     +---------------+   +---------------+
87//! ```
88//!
89//! - **HTTP**: Parses server-to-server requests using [`http::CreateRoomQuery`]. Verifies [`auth::HttpRoomClaims`]. The request that creates the current room fixes its signing key.
90//! - **WebSocket**: Client connection frames are decoded by [`websocket::decode_auth_payload_text`]. A hint selects a candidate key. Claims are verified, normalized into [`auth::WebSocketConnectClaims`] and trusted for access.
91//!
92//! # Security Model
93//!
94//! `o-sfu` secures two planes independently. Application-layer JWTs gate room
95//! admission on the control plane. `str0m` encrypts media on the packet plane
96//! with DTLS-SRTP. Signaling transport confidentiality is terminated at the
97//! deployment edge rather than in process.
98//!
99//! ```text
100//! control plane    JWT HS256           admission trust
101//! packet plane     DTLS-SRTP (str0m)   media confidentiality
102//! signaling wire   TLS at edge         transport confidentiality
103//! ```
104//!
105//! ## JWT Admission
106//!
107//! Tokens are `HS256` only. [`auth::verify`] rejects any other `alg`, checks the
108//! HMAC in constant time and enforces `exp`, `nbf` plus an `iat` future-skew
109//! bound. It caps token size at [`auth::MAX_JWT_TOKEN_BYTES`]. Two keys scope
110//! trust:
111//!
112//! - **Server-to-server key**: `AUTH_KEY` (base64, at least 32 bytes) verifies
113//!   the HTTP [`http::CreateRoomQuery`] path through [`auth::HttpRoomClaims`] and
114//!   [`auth::HttpDisconnectClaims`]. See [`config`].
115//! - **Per-room key**: the request that creates the current room pins the signing
116//!   key from the `key` or `keySeed` claim in [`auth::HttpRoomClaims`]. For more
117//!   security, prefer the `keySeed` claim, which derives a per-room key with the
118//!   `AUTH_KEY` and provided seed using the following KDF:
119//!   ```text
120//!   room_key = Base64StdPad(HMAC-SHA256(
121//!                  key = Base64Decode(AUTH_KEY),
122//!                  message = Base64Decode(keySeed)
123//!              ))
124//!   ```
125//!   WebSocket [`auth::WebSocketConnectClaims`] verify against that room key,
126//!   never against `AUTH_KEY`.
127//!
128//! Token carriage differs per surface: HTTP room creation uses the
129//! `Authorization` header, HTTP disconnect uses the request body and the
130//! WebSocket client sends a first-frame auth envelope decoded by
131//! [`websocket::decode_auth_payload_text`]. An unverified room id selects only a
132//! candidate key, then the same token is re-verified against it. Modern
133//! [`auth::WebSocketConnectClaims`] must name the selected room. Legacy Odoo
134//! tokens select it through the auth envelope's `channel` and are normalized
135//! only after verification with that room's key.
136//!
137//! Admission establishes identity and room scope. It does not enforce the
138//! per-user `permissions` claim, which room state collapses to a marker.
139//!
140//! ## Signaling Ingress
141//!
142//! Every authenticated client frame passes through the same decoder as the auth
143//! frame, [`websocket::decode_client_batch`], which bounds parser work with
144//! static caps: [`websocket::MAX_CLIENT_FRAME_BYTES`] per frame and
145//! [`websocket::MAX_CLIENT_BATCH_ENVELOPES`] per batch. The Axum upgrade applies
146//! the frame cap at the socket and the decoder re-checks it. Oversized frames,
147//! oversized batches, malformed JSON, ambiguous routing metadata and unknown
148//! protocol tags reject as [`websocket::ClientBatchDecodeError`] and close the
149//! socket with a protocol-error code.
150//!
151//! Two further bounds guard against resource exhaustion:
152//!
153//! - **Pre-auth admission**: global and per-origin permits cap concurrent
154//!   unauthenticated sockets and return `503` once exhausted. See [`config`].
155//! - **Outbound backpressure**: per-user fanout is a bounded queue by message
156//!   count and by bytes. A consumer that falls behind is closed rather than
157//!   buffered without limit.
158//!
159//! A first-frame auth timeout rejects clients that never authenticate. A
160//! ping/pong health loop closes clients that stop responding. There is no
161//! per-session request-rate budget: the size, count and backpressure caps bound
162//! the work rather than metering a rate.
163//!
164//! ## Media Transport
165//!
166//! `str0m` terminates ICE, DTLS and SRTP over UDP with the `aws-lc-rs` crypto
167//! backend. `o-sfu` builds and drives the `str0m` session but implements no DTLS
168//! or SRTP itself: it forwards already-decrypted RTP between sessions and hands
169//! outbound RTP back to `str0m` for SRTP protection.
170//!
171//! - **Keying**: the DTLS handshake derives SRTP keys per RFC 5764 DTLS-SRTP.
172//! - **Certificate**: `str0m` generates a self-signed certificate when each RTC
173//!   session is built and advertises its SHA-256 fingerprint in the SDP offer.
174//!   Accepting the answer stores the expected remote fingerprint. The DTLS
175//!   handshake verifies it against the peer certificate.
176//! - **ICE**: `o-sfu` runs ICE-lite with `a=setup:actpass` and advertises
177//!   `ANNOUNCED_IP`, so media UDP must reach the host directly.
178//!
179//! ## Signaling Transport
180//!
181//! HTTP and WebSocket are served in plaintext in process. HTTPS and WSS are
182//! terminated by an external reverse proxy, so a forwarded scheme and client
183//! address are trusted only when the proxy is trusted through [`config`]
184//! (`PROXY`). Operator route access is documented by [`http`].
185//!
186//! # Room and Router Ownership
187//!
188//! Room transitions produce typed commits while holding short exclusive state
189//! locks. `RoomEffects` consumes deferred transport, source-policy and WebSocket
190//! output work after the lock is released.
191//!
192//! ```text
193//! room state lock held                  lock released
194//! +--------------------------------+    +------------------------------+
195//! | validate user and connection   |    | MediaTransport commands      |
196//! | commit room topology           |    | source-policy turn           |
197//! | capture transition commit      |--->| websocket output             |
198//! |                                |    | idempotent teardown          |
199//! +--------------------------------+    +------------------------------+
200//! ```
201//!
202//! [`o_sfu_router::Router`] owns exact user-to-connection placement. Receiver shadows are foreign local sessions derived from active consumer dependencies, disappearing with their final consumer.
203//!
204//! # Signaling and Client Bundle
205//!
206//! Browsers use `SfuClient` for connection, publication, subscription and room
207//! control. Signaling state stays in [`o_sfu_protocol::host::ProtocolCore`] and
208//! yields ordered [`o_sfu_protocol::host::Command`] values. The WASM bridge
209//! serializes those commands then maps protocol events to Odoo bundle updates
210//! that drive browser `WebSocket`, `RTCPeerConnection` and timer APIs.
211//!
212//! ```text
213//! SfuClient (public API)
214//!        |
215//!        v
216//! BrowserRuntime
217//!        |
218//!        v
219//! ProtocolCore (sans-I/O) -> Vec<Command>
220//!        |
221//!        v
222//! WASM serialization -> TypeScript command union
223//!        |
224//!        v
225//! BrowserRuntime -> WebSocket, RTCPeerConnection, timers
226//!        ^                                        |
227//!        +--------------- browser events ---------+
228//! ```
229//!
230//! # Packet Path
231//!
232//! [`core::server::transport::MediaTransport`] owns the media workers, which hold the packet loops. These loops receive UDP datagrams, drive WebRTC state (`str0m`), apply route tables and forward RTP.
233//!
234//! ```text
235//! UDP datagram
236//!     |
237//!     v
238//! worker ingress and `str0m` drain
239//!     |
240//!     v
241//! packet facts (source, RID and codec)
242//!     |
243//!     +-> origin packet sinks
244//!     |
245//!     +-> source and destination packet gates
246//!              |
247//!              +-> relay fanout
248//!              +-> local RTC -> RTP identity and codec rewrite
249//! ```
250//!
251//! `str0m` handles ICE, DTLS and SRTP. The private `rtc::codec` boundary keeps
252//! codec branching out of route planning. Origin packet sinks precede route
253//! gates so recording can observe a publisher without active receivers. Source,
254//! relay and receiver gates then narrow routed fanout. Same-process relays share
255//! payload data with another worker for local delivery.
256//!
257//! Worker BWE and audio observations feed into room source policy, which updates route gates for later packets.
258//!
259//! ```text
260//! worker BWE and audio observations
261//!                |
262//!                v
263//!        room source policy
264//!                |
265//!                v
266//!   route gates for later packets
267//! ```
268//!
269//! # Shutdown and Teardown
270//!
271//! Teardown is explicit async work. [`Runtime::serve_listener`] stops listener acceptance, drains tracked web sockets and stops background tasks within [`config::HttpConfig::shutdown_timeout_ms`].
272//!
273//! ```text
274//! Runtime::serve_listener
275//!     |
276//!     +-> stop listener
277//!     +-> close tracker and cancel sessions
278//!     +-> wait for tracker emptiness
279//!     +-> stop source-policy sync and media workers
280//! ```
281//!
282//! Missing worker-local sessions or media during teardown are successful no-ops. Unavailable workers or ownership mismatches are terminal.
283//!
284//! # Observability
285//!
286//! Monitored through the [`o_sfu_telemetry`] sub-crate. See [`http::telemetry`] for the HTTP contracts.
287//!
288//! - **Metrics**: [`http::telemetry::metrics`] exposes Prometheus text exposition.
289//! - **Diagnostics**: [`http::telemetry::diagnostics`] exposes JSON state summaries.
290//!
291//! # Scaling
292//!
293//! Rooms use one [`o_sfu_router::Router`] facade and can opt into additional same-process local routers through [`config::RoomWorkerPolicy`].
294//! Joins stay on an assigned healthy packet loop. When no assigned worker has a
295//! known delay below the configured threshold, a join can attach a healthy
296//! worker not yet assigned to the room.
297//!
298//! # Feature Flags
299//!
300//! Core media behavior is configured at runtime through [`config`], not Cargo features.
301//! The default feature `otel-tracing` enables OpenTelemetry tracing support through [`o_sfu_telemetry::TraceExportConfig`]. Other features are used strictly for tests and benchmarking.
302//!
303//! # Sub-crates
304//!
305//! | Crate | Role |
306//! | --- | --- |
307//! | [`o_sfu_rfc`] | RFC-backed JWT, RTP, RTCP, SDP and WebRTC consts/types |
308//! | `o-sfu-model` | Shared call data ([`o_sfu_protocol::wire::UserId`], etc.) |
309//! | [`o_sfu_router`] | Sans-I/O [`o_sfu_router::Router`] facade for room placement and routed media lifetimes |
310//! | [`o_sfu_core`] | Room engine, [`core::prelude::SourcePolicy`], recording taps and [`core::server::transport::MediaTransport`] projection |
311//! | [`o_sfu_protocol`] | Sans-I/O [`o_sfu_protocol::host::ProtocolCore`] and typed commands |
312//! | [`o_sfu_telemetry`] | Tracing setup, metrics, diagnostics response types and graph payloads |
313//!
314//! # Reading Map
315//!
316//! - [`run`] and [`Runtime`] own boot, serving, background tasks and shutdown.
317//! - [`config`] is the environment-to-runtime boundary.
318//! - [`auth`], [`http`] and [`websocket`] form the admission edge.
319//! - [`crate::core`] turns accepted control-plane intent into room mutations and transport effects.
320//! - [`o_sfu_protocol::host::ProtocolCore`] keeps browser signaling sans-I/O.
321//! - [`core::server::transport::MediaTransport`] owns the media workers and hides their threading model.
322pub mod config;
323pub mod core {
324    pub use o_sfu_core::{prelude, server};
325}
326pub(crate) mod application;
327mod runtime;
328
329pub mod auth {
330    pub use crate::runtime::auth::{
331        AuthenticationError, HttpDisconnectClaims, HttpRoomClaims, MAX_JWT_TOKEN_BYTES,
332        RegisteredJwtClaims, WebSocketConnectClaims, sign, verify,
333    };
334}
335
336/// HTTP route and payload contracts.
337///
338/// `/v1/stats`, `/metrics` and diagnostics require the configured
339/// [`crate::config::DiagnosticsConfig::auth_token`] on every listener. Without
340/// one, the actual listener must be loopback. Missing or invalid tokens return
341/// `401 Unauthorized`. Tokenless non-loopback access returns `403 Forbidden`.
342pub mod http {
343    pub use crate::runtime::{
344        http_server::contract::{
345            CreateRoomQuery, IncomingBitRateStatsResponse, NoopResponse, RoomResponse,
346            StatsResponse, route,
347        },
348        request_origin::{RequestOrigin, resolve_request_origin},
349    };
350
351    /// Operator-facing metrics and diagnostics contracts.
352    pub mod telemetry {
353        /// Prometheus metric scrape contract.
354        ///
355        /// `GET` [`metrics::PATH`] returns `200 OK` Prometheus text exposition
356        /// with [`metrics::CONTENT_TYPE`].
357        ///
358        /// This is a scrape endpoint.
359        /// Configure Prometheus to scrape [`metrics::PATH`], then issue `PromQL`
360        /// queries to Prometheus.
361        /// Histogram families render `<name>_bucket` with the additional `le`
362        /// label plus `<name>_sum` and `<name>_count`.
363        ///
364        /// # Scrape and Query
365        ///
366        /// ```yaml
367        /// scrape_configs:
368        ///   - job_name: o-sfu
369        ///     scheme: https
370        ///     metrics_path: /metrics
371        ///     authorization:
372        ///       type: Bearer
373        ///       credentials_file: /run/secrets/o_sfu_diagnostics_token
374        ///     tls_config:
375        ///       ca_file: /run/secrets/o_sfu_observability_ca
376        ///       server_name: o-sfu-observability.internal
377        ///     static_configs:
378        ///       - targets: ["o-sfu-observability.internal:443"]
379        /// ```
380        ///
381        /// The endpoint returns Prometheus text exposition.
382        ///
383        /// ```text
384        /// # HELP osfu_rooms_active Current number of active rooms owned by this runtime.
385        /// # TYPE osfu_rooms_active gauge
386        /// osfu_rooms_active 3
387        /// # TYPE osfu_worker_rtp_packets_total counter
388        /// osfu_worker_rtp_packets_total{media_worker_id="0",direction="ingress"} 1240
389        /// ```
390        ///
391        /// Query clients send `PromQL` to the Prometheus-compatible backend
392        /// rather than to o-sfu.
393        /// These examples cover a gauge, counter and histogram.
394        ///
395        /// ```promql
396        /// sum(osfu_users_active)
397        /// sum by (stage) (rate(osfu_ws_connections_total[5m]))
398        /// histogram_quantile(
399        ///   0.95,
400        ///   sum by (le, route) (rate(osfu_http_request_duration_seconds_bucket[10m]))
401        /// )
402        /// ```
403        ///
404        /// Read the [`metrics::MetricName`] variants below to find every
405        /// exported name and its meaning.
406        /// Query [`metrics::PATH`] to see each family's `HELP`, `TYPE`, label
407        /// keys and current label values before building selectors.
408        pub mod metrics {
409            pub use o_sfu_telemetry::{
410                metrics::MetricName, prometheus::PROMETHEUS_CONTENT_TYPE as CONTENT_TYPE,
411            };
412
413            pub use crate::http::route::METRICS as PATH;
414        }
415
416        /// JSON diagnostics contract.
417        ///
418        /// Every constant in [`diagnostics::route`] is a `GET` endpoint returning
419        /// `200 OK` JSON on success.
420        ///
421        /// # Routes and Parameters
422        ///
423        /// | request | JSON response | parameter source |
424        /// | --- | --- | --- |
425        /// | `GET /internal/diagnostics/summary` | one [`diagnostics::DiagnosticsSummaryResponse`] | none |
426        /// | `GET /internal/diagnostics/rooms` | array of [`diagnostics::DiagnosticsRoomSummary`] | none |
427        /// | `GET /internal/diagnostics/workers` | array of [`diagnostics::DiagnosticsWorkerSummary`] | none |
428        /// | `GET /internal/diagnostics/rooms/{uuid}` | one [`diagnostics::DiagnosticsRoomDetail`] | `uuid` from the rooms response |
429        /// | `GET /internal/diagnostics/rooms/{uuid}/users` | array of [`diagnostics::DiagnosticsUserSummary`] | `uuid` from the rooms response |
430        /// | `GET /internal/diagnostics/rooms/{uuid}/users/{id}` | one [`diagnostics::DiagnosticsUserDetail`] | `uuid` from rooms and `userKey` from room users |
431        /// | `GET /internal/diagnostics/node-graph/rooms/{uuid}` | `{ "nodes": [], "edges": [] }` | `uuid` from the rooms response |
432        /// | `GET /internal/diagnostics/node-graph/rooms/{uuid}/users/{id}` | `{ "nodes": [], "edges": [] }` | `uuid` from rooms and `userKey` from room users |
433        ///
434        /// `userId` may be a JSON number or string.
435        /// `userKey` is always the string to put into `{id}`.
436        /// URL-encode both path values before substitution.
437        ///
438        /// # Summary Request and Response over HTTPS
439        ///
440        /// ```text
441        /// GET /internal/diagnostics/summary HTTP/1.1
442        /// Host: o-sfu-observability.internal
443        /// Authorization: Bearer <diagnostics-token>
444        /// Accept: application/json
445        ///
446        /// HTTP/1.1 200 OK
447        /// Content-Type: application/json
448        ///
449        /// {
450        ///   "roomsActive": 1,
451        ///   "publicationsActive": 1,
452        ///   "recordingRoomsActive": 0,
453        ///   "usersActive": 2,
454        ///   "subscriptionsActive": 1,
455        ///   "transport": {
456        ///     "connectedUsers": 2,
457        ///     "disconnectedUsers": 0,
458        ///     "totalUsers": 2,
459        ///     "unknownUsers": 0
460        ///   }
461        /// }
462        /// ```
463        ///
464        /// # JavaScript Fetch Example
465        ///
466        /// ```javascript
467        /// const origin = "https://o-sfu-observability.internal";
468        /// const headers = {
469        ///   Authorization: `Bearer ${process.env.DIAGNOSTICS_AUTH_TOKEN}`,
470        /// };
471        ///
472        /// async function getJson(path) {
473        ///   const response = await fetch(`${origin}${path}`, { headers });
474        ///   if (!response.ok) {
475        ///     throw new Error(`${response.status} ${await response.text()}`);
476        ///   }
477        ///   return response.json();
478        /// }
479        ///
480        /// async function main() {
481        ///   const rooms = await getJson("/internal/diagnostics/rooms");
482        ///   const roomUuid = encodeURIComponent(rooms[0].uuid);
483        ///   const room = await getJson(`/internal/diagnostics/rooms/${roomUuid}`);
484        ///   const users = await getJson(`/internal/diagnostics/rooms/${roomUuid}/users`);
485        ///   const userKey = encodeURIComponent(users[0].userKey);
486        ///   const graph = await getJson(
487        ///     `/internal/diagnostics/node-graph/rooms/${roomUuid}/users/${userKey}`,
488        ///   );
489        ///
490        ///   console.log(room.summary, room.users, room.sources);
491        ///   console.log(graph.nodes, graph.edges);
492        /// }
493        ///
494        /// main().catch((error) => {
495        ///   console.error(error);
496        ///   process.exitCode = 1;
497        /// });
498        /// ```
499        ///
500        /// The rooms response has this shape.
501        ///
502        /// ```json
503        /// [
504        ///   {
505        ///     "createDate": "2026-07-15T10:20:30.000Z",
506        ///     "mediaWorkerId": 0,
507        ///     "publicationCount": 1,
508        ///     "recordingState": {
509        ///       "recording": false,
510        ///       "audio": false,
511        ///       "transcription": false,
512        ///       "video": false
513        ///     },
514        ///     "remoteAddress": "203.0.113.10",
515        ///     "sourceCount": 1,
516        ///     "userCount": 2,
517        ///     "subscriptionCount": 1,
518        ///     "transport": {
519        ///       "connectedUsers": 2,
520        ///       "disconnectedUsers": 0,
521        ///       "totalUsers": 2,
522        ///       "unknownUsers": 0
523        ///     },
524        ///     "uuid": "550e8400-e29b-41d4-a716-446655440000",
525        ///     "webRtcEnabled": true
526        ///   }
527        /// ]
528        /// ```
529        ///
530        /// The room users response has this shape.
531        ///
532        /// ```json
533        /// [
534        ///   {
535        ///     "audioIncomingBitrateBps": 32000,
536        ///     "cameraIncomingBitrateBps": 600000,
537        ///     "connectionId": 91,
538        ///     "health": "connected",
539        ///     "incomingBitrateBps": 632000,
540        ///     "mediaWorkerId": 0,
541        ///     "publicationCount": 2,
542        ///     "roomId": "550e8400-e29b-41d4-a716-446655440000",
543        ///     "screenIncomingBitrateBps": 0,
544        ///     "subscriptionCount": 1,
545        ///     "userId": 42,
546        ///     "userKey": "42"
547        ///   }
548        /// ]
549        /// ```
550        ///
551        /// The response structs below list every field in each payload.
552        /// Wire names are `camelCase` unless a field documents an exception.
553        ///
554        /// User detail is room-scoped because the same user key can be active
555        /// in several rooms.
556        pub mod diagnostics {
557            pub use o_sfu_telemetry::diagnostics::{
558                DiagnosticsRoomDetail, DiagnosticsRoomSummary, DiagnosticsSummaryResponse,
559                DiagnosticsUserDetail, DiagnosticsUserSummary, DiagnosticsWorkerSummary,
560            };
561
562            pub use crate::http::route::diagnostics as route;
563        }
564    }
565}
566
567pub mod websocket {
568    pub use crate::runtime::websocket_server::{
569        ClientBatchDecodeError, ClientBatchDecodeFailureKind, MAX_CLIENT_BATCH_ENVELOPES,
570        MAX_CLIENT_FRAME_BYTES, decode_auth_payload_text, decode_client_batch,
571    };
572}
573
574pub use self::runtime::{Runtime, ServeError, run};