# oxpulse-sfu-kit
[](https://github.com/anatolykoptev/oxpulse-sfu-kit/actions/workflows/ci.yml)
[](https://crates.io/crates/oxpulse-sfu-kit)
[](https://docs.rs/oxpulse-sfu-kit)
[](LICENSE-MIT)
[](#status)
Reusable multi-client SFU primitives built on top of [str0m](https://github.com/algesten/str0m).
str0m is a sans-I/O Rust WebRTC library — you plug in your own networking.
This crate adds the multi-client glue: per-peer state machines, UDP packet routing,
event fanout, simulcast layer forwarding, and bandwidth-adaptive layer selection.
## What this gives you
- **`Client`** — per-peer state machine wrapping `str0m::Rtc`
- **`Registry`** — room-level UDP routing and event fanout
- **`Propagated`** — event enum flowing between registry and clients
- **`LayerSelector` + `BestFitSelector`** — per-subscriber simulcast layer selection using desired layer + publisher's active RIDs
- **`ClientOrigin::RelayFromSfu`** — cascade SFU support: mark a client as an upstream relay, reroute keyframe requests and Dynacast hints upstream
- **Optional `pacer`** — BWE-adaptive layer switching (3-up/instant-down hysteresis, audio-only mode below 80 kbps)
- **Optional `av1-dd`** — AV1 Dependency Descriptor parser, per-subscriber temporal-layer drop gate
- **Optional `vfm`** — RFC 9626 Video Frame Marking for H.264/VP9/HEVC temporal-layer drop
- **Optional `active-speaker`** — dominant speaker detection with confidence margin
- **Optional `metrics-prometheus`** — Prometheus gauges including per-peer speaker activity scores
## Usage
```toml
[dependencies]
oxpulse-sfu-kit = "0.5"
```
Minimal run loop:
```rust,no_run
use oxpulse_sfu_kit::{SfuConfig, udp_loop};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = SfuConfig { udp_port: 3478, ..SfuConfig::default() };
let shutdown = async { tokio::signal::ctrl_c().await.unwrap() };
udp_loop::run_udp_loop(config, shutdown).await
}
```
Insert a peer after completing ICE/DTLS signaling:
```rust,no_run
use oxpulse_sfu_kit::{Client, Registry, SfuRtcBuilder, SfuMetrics};
use std::sync::Arc;
let mut registry = Registry::new(Arc::new(SfuMetrics::new_default()));
let rtc = SfuRtcBuilder::new().build();
let client = Client::new(rtc, Arc::new(SfuMetrics::new_default()));
registry.insert(client);
```
Mark a client as a relay from another SFU edge (call **before** `registry.insert`):
```rust,no_run
use oxpulse_sfu_kit::{Client, ClientOrigin};
let mut relay_client = Client::new(rtc, metrics);
relay_client.set_origin(ClientOrigin::RelayFromSfu("edge-eu-1".to_string()));
registry.insert(relay_client);
// Keyframe requests for relay-originated tracks now emit
// Propagated::UpstreamKeyframeRequest instead of sending PLI/FIR to the relay.
```
## Feature flags
| `kalman-bwe` | GoogCC-inspired Kalman delay + loss-based BWE. `BandwidthEstimator` with TWCC ingestion. `Registry::update_pacer_layers` for automatic layer selection. Enable with `pacer` for full adaptive forwarding. |
| | BWE-adaptive layer switching via (LiveKit-style 3-up/instant-down hysteresis). Adds at 80 kbps threshold. allows runtime tuning of all thresholds. |
| | GoogCC v2 per-subscriber estimator: (linear regression) + (loss-based AIMD). integrates with as an additional bitrate ceiling. |
| `av1-dd` | AV1 Dependency Descriptor parser (`av1::dependency_descriptor`). `SfuMediaPayload::av1_dd()` accessor. `Client::set_max_temporal_layer(u8)` per-subscriber drop gate. |
| `vfm` | RFC 9626 Video Frame Marking parser for H.264/VP9/HEVC. `SfuMediaPayload::vfm_frame_marking()`. `Client::set_max_vfm_temporal_layer(u8)`. |
| `active-speaker` | Dominant speaker tracking via [`rust-dominant-speaker`](https://crates.io/crates/rust-dominant-speaker). `Propagated::ActiveSpeakerChanged { peer_id, confidence }`. `Registry::tick_active_speaker` / `record_audio_level` / `peer_audio_scores`. |
| `metrics-prometheus` | Prometheus counters on `SfuMetrics`, including per-peer BWE, loss, RTT, and speaker activity gauges. |
| `googcc-bwe` | GoogCC v2 per-subscriber estimator: `TrendlineDetector` (linear regression, 20-packet window) + `AimdController` (loss-based AIMD +8%/x0.85). `GoogCcEstimator` integrates as an additional bitrate ceiling in `PerSubscriber::combined_bps`. |
| `test-utils` | Test seam helpers (`test_seed` module, `Registry::*_for_tests` methods). |
## Audio quality guidance
### Publisher-side noise filtering
For cleaner dominant-speaker elections, publishers should filter audio through a
noise suppressor before computing the RFC 6464 level:
- **RNNoise** (`xiph/rnnoise`, BSD-3-Clause) — DSP/DNN hybrid, runs on mobile.
- **ten-vad** (`TEN-framework/ten-vad`, MIT) — small CPU-friendly VAD alternative.
### Opus DRED (Deep REDundancy)
Opus DRED (libopus ≥ 1.4, shipping in recent Chromium) embeds a neural-decoded
redundant stream at ≈1 kbps overhead. The SFU forwards it transparently — no kit
changes required. Signal DRED capability with `Propagated::AudioCodecHint`.
### End-to-end encryption (SFrame)
The kit forwards RTP payloads opaquely — SFrame (RFC 9605) frames pass through
unchanged. Use `KeyEpoch` from `crate::sframe` to forward the key-epoch RTP
header extension. Key distribution (MLS RFC 9420) is your signalling layer's
responsibility.
## Not included (by design)
- Signaling (bring your own — WebSocket, HTTP, gRPC)
- TURN server (run coturn or similar)
- End-to-end encryption payload processing (use SFrame; see `sframe::KeyEpoch`)
- Server-side audio/video mixing (MCU mode)
- WHIP / WHEP ingestion endpoints
## Examples
```sh
cargo run --example basic-sfu --features active-speaker,metrics-prometheus
```
See `examples/basic-sfu.rs` for a complete single-node SFU with a Prometheus
`/metrics` endpoint.
## Capacity testing
`examples/synthetic_room.rs` is a self-contained load generator that spawns N
synthetic peers in one process — no real network, no DTLS handshake. It drives the
kit's fanout dispatch, per-subscriber simulcast layer-filter, and `delivered_media`
counters directly via the `test-utils` seam, then reports peak RSS, CPU%, total
packets forwarded, and p50/p95/p99 fanout latency.
```sh
cargo run --release --example synthetic_room \
--features active-speaker,metrics-prometheus,kalman-bwe,googcc-bwe,pacer,test-utils \
-- --peers 8 --duration-secs 30 --packet-rate-pps 30 --bitrate-bps 1500000
```
Output is grep-friendly for D-Lite 4 scripting:
```
SYNTHETIC_ROOM_RESULT peers=8 duration_s=30 packets_forwarded=72000 peak_rss_mb=124 cpu_percent=18.3 latency_p50_us=85 latency_p95_us=210 latency_p99_us=450
```
**What is NOT measured:** str0m DTLS/ICE/SRTP, real UDP kernel path, actual wire
bitrate (synthetic payload is always 4 bytes), GoogCC/pacer adapting to real TWCC
feedback. See the example's module doc for full scope.
## Relationship to str0m
We build on str0m's `Rtc` state machine. We do not replace it — we connect
multiple instances together for multi-party rooms. All credit for the underlying
protocol work goes to [Martin Algesten](https://github.com/algesten) and the
str0m contributors.
## Extracted from
Originally built as part of [OxPulse Chat](https://oxpulse.chat). Published
standalone for the broader Rust WebRTC ecosystem.
## Status
API is stabilising through v0.x. Minor breaking changes may occur between minors;
check `CHANGELOG.md` before upgrading. Stability commitment from v1.0.
## License
Dual MIT / Apache-2.0. See [LICENSE-MIT](LICENSE-MIT) and
[LICENSE-APACHE](LICENSE-APACHE).