How it works
One OS thread, one UDP socket, and one str0m state machine per person. str0m is sans-IO: it owns no sockets and spawns no tasks. soli-sfu feeds it packets and the clock, and it says what to send.
Two planes
browser ──HTTPS (soli-proxy)──▶ control API (axum, :9300) offers, groups
browser ──UDP, direct────────▶ media engine (str0m, :3478) RTP / RTCP, one portThe control plane is a small HTTP API for the offer/answer exchange and group updates. It runs on a two-thread async runtime and talks to the media thread over a channel. The media plane is the single UDP socket, and it never goes through a proxy.
The media loop
Every turn of the loop does four things, in order:
- Commands. Admit new sessions, apply
peerschanges, answer stats. - Drive. Poll each session until str0m returns a timeout: send what it asks to send, forward media events, relay keyframe requests. Time is fed to a session only when its own deadline is due.
- Read. Wait up to 20 ms for one datagram, then take whatever else is already queued without waiting, at most 64 per turn.
- Reap. Drop sessions whose ICE has died, and sessions that never connected within 30 seconds.
The read step is bounded on purpose. Media arrives every few milliseconds, so a loop that reads until the socket goes quiet never gets back to forwarding. A test floods the port faster than the tick and checks that commands are still answered.
Incoming datagrams are matched to a session by source address from a small cache; only the first packets from a new address, or STUN checks, ask every session whether the packet is theirs.
ICE-lite on one port
The SFU is always the reachable side, so it runs ICE-lite: it never
starts connectivity checks, it only answers them. Every session shares
the one UDP port, and each answer carries a single host candidate,
public_ip:udp_port. Browsers need no STUN servers, and TURN
only matters on networks that block UDP entirely.
All sessions share one DTLS certificate, generated at startup, so a new join costs no key generation on the media thread. Offers are parsed on the HTTP side before they reach it.
Slots instead of renegotiation
A new speaker normally means a new m-line for every listener, and an SDP renegotiation for each of them. soli-sfu avoids it:
- Each browser reserves
audio_slots(8) andvideo_slots(4) receive-only m-lines in its first offer. - The engine binds speakers to free slots as their packets arrive. A slot whose speaker has been silent for 3 seconds can be taken by a new one.
- A newly bound video slot asks the speaker for a keyframe, so the new viewer sees a picture at once. Keyframe requests to one track are spaced at least 500 ms apart.
- An offer with more m-lines than the configured counts is truncated to them.
Group-scoped forwarding
Each session carries a peers set: the user ids it should
hear. When a speaker's packet arrives, it goes to room-mates whose set
contains that speaker. null means everyone in the room,
which suits a plain meeting.
fn hears(peers: &Option<HashSet<String>>, speaker: &str) -> bool {
match peers {
None => true, // open room
Some(set) => set.contains(speaker), // a circle
}
}When a set shrinks, slots bound to people no longer heard are freed at once, so a newcomer finds room.
Session lifecycle
- Join. POST an offer. A second session for the same user and room replaces the first, so reconnecting is safe.
- Leave. DELETE it, or simply go away: ICE notices within seconds and the session is reaped, freeing every slot it fed.
- Never connect. A session with no completed ICE and DTLS after 30 seconds is dropped.
- Restart. State is in memory only. Live calls drop and clients rejoin.