Closes the coverage gap the README flagged as "Next": two real str0m clients connect to the sidecar over loopback UDP (full ICE/DTLS/SRTP + the data-channel renegotiation the browser does), peer A sends Opus RTP, and we assert peer B RECEIVES the forwarded media. Pure `cargo test`, ~0.8s, no browser — proves the SFU data plane (accept offer → forward without decode) end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| dist | ||
| src | ||
| tests | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| build-release.sh | ||
README.md
blax-webrtc-sfu — the Rust data plane
The sidecar binary behind Blax\WebRtc\Media\RustMediaEngine: backend-hosted
rooms. PHP/Laravel owns the control plane (rooms, auth, signaling
orchestration, billing hooks); this process owns everything per-packet.
- ICE (Interactive Connectivity Establishment — finds a working network path to each browser), DTLS (the UDP TLS handshake that negotiates keys) and SRTP (the encrypted media packets) are terminated here via str0m, a sans-IO Rust WebRTC stack.
- SFU forwarding (Selective Forwarding Unit): each peer's Opus audio is
received once and forwarded to the other peers of its room without
decoding — one loop thread, one shared UDP socket,
rtc.accepts()demultiplexing. - Recording: per-peer Ogg/Opus written straight from the depayloaded stream — playable by ffmpeg/browsers, no transcoding, no client cooperation.
- Negotiation: the initial offer/answer arrives via the control socket
(from PHP signaling). Renegotiation as peers join rides each browser's
WebRTC data channel directly — PHP never relays it (see str0m's
chatexample, which this follows).
Control protocol (PHP ⇄ sidecar)
JSON-lines over a Unix socket. Requests carry an id echoed by the reply;
notices are pushed without one.
→ {"id":1,"cmd":"add_peer","room":"lobby","peer":"alice","offer":"v=0…","record":"/rec/alice.ogg"}
← {"id":1,"ok":true,"answer":"v=0…"}
→ {"id":2,"cmd":"mute_peer","room":"lobby","peer":"alice","muted":true}
→ {"id":3,"cmd":"record_start","room":"lobby","peer":"alice","path":"/rec/a.ogg"} (record_stop too)
→ {"id":4,"cmd":"remove_peer","room":"lobby","peer":"alice"}
→ {"id":5,"cmd":"stats"} ← rooms → peers → {connected, muted, seconds}
→ {"id":6,"cmd":"ping"} / {"id":7,"cmd":"shutdown"}
← {"event":"peer_connected","room":"lobby","peer":"alice"}
← {"event":"peer_left","room":"lobby","peer":"alice","seconds":42.5}
Build & test
cargo build && cargo test # 7 tests incl. an end-to-end control-socket run
./build-release.sh # static musl release + .sha256 into dist/
On NixOS: nix-shell -p gcc pkg-config openssl perl gnumake musl --run ./build-release.sh.
Releasing
BinaryManager::VERSION (PHP) pins the release tag it downloads from. To ship:
build dist/ artifacts (linux-x86_64 at minimum), attach both files to the
Forgejo release with that tag, and bump VERSION when the protocol changes.
PHP resolves binaries in this order: webrtc.sfu.binary override → a local
target/{release,debug} build (dev) → the downloaded install.
CLI
blax-webrtc-sfu --socket /run/sfu.sock --udp-ip 0.0.0.0 --udp-port 41000 --public-ip 203.0.113.9
--udp-port 0 (default) picks an ephemeral port — fine on one host, pin it in
production and publish it (UDP) on the container. --public-ip is what
browsers are told to reach in the ICE host candidate; it defaults to the
default-route interface address.