Architecture
Each peer connection is one task, the driver, on a private tokio runtime. The driver owns everything that mutates the connection, so the rules of the sans-I/O str0m stack hold in exactly one place.
The two traits
PeerEngine creates peer connections. Peer is one connection, with methods that mirror RTCPeerConnection: descriptions, candidates, data channels, transceivers, media frames, DTMF and stats. A consumer depends on the traits, so another engine could stand behind them. native::NativeEngine is the one qrtc ships.
The driver
The driver owns the str0m::Rtc, its sockets, the JSEP state and the data channel queues. Every mutation of the Rtc is followed by a drain step. The drain sends queued packets, delivers events and arms the next timeout. Work that may block, such as DNS resolution and mDNS lookups, runs as separate tasks that report back to the driver.
NativePeer is a cheap handle. Each method sends a command to the driver and waits for the answer, which is why every method blocks. Engine events reach the consumer's EventSink from engine threads, in order per peer connection.
Sockets and candidates
The engine binds one UDP socket per usable interface address, and each becomes a host candidate. Loopback is used only when nothing else exists, as in an isolated CI container. Server-reflexive candidates come from STUN Binding requests on those sockets. Relayed candidates come from TURN allocations over UDP, or over a stream to the TURN server when the URL asks for TCP or TLS.
Host candidates leave the engine with a random <uuid>.local name in place of the address, as browsers do, so the page, the signalling server and a remote peer that never connects do not learn the interface addresses. One mDNS responder per engine listens on 224.0.0.251:5353 and ff02::fb:5353 and answers A/AAAA queries for the names of live peer connections, at most one multicast answer per name per second. Remote .local candidates are resolved with a multicast query before they reach the ICE agent; a name registered by this engine resolves without the network. If no mDNS socket can be opened, host candidates carry plain addresses and a warning is logged.
The JSEP layer
str0m generates the SDP. qrtc rewrites only what W3C semantics decide and str0m cannot know:
- the direction attribute of each media section, since an answer narrows str0m's maximal direction to the transceiver's;
a=msid, carrying the page's MediaStream and track ids, which Matrixsdp_stream_metadatakeys on;- the payload list, when the consumer called
setCodecPreferences().
Setting a local description that differs from the last offer or answer is checked semantically, as Chrome and Safari do. Line order, whitespace, candidates and codec parameters may differ, because matrix-js-sdk re-serialises every local description. Structural edits are refused: setup role, ICE credentials, fingerprints, media sections and directions.
Media paths
Video is encoded and decoded by the consumer, for example with WebCodecs in a web page. The engine moves encoded frames: it packetizes them into RTP, reassembles received frames, and asks for keyframes. Audio is processed in the engine, because echo cancellation needs the far-end signal: the consumer pushes and receives 48 kHz PCM. Audio covers the details.
Where each piece lives
| Module | Role |
|---|---|
native/mod.rs | NativeEngine, NativePeer and the command channel |
native/driver.rs | The per-connection driver |
native/jsep.rs | SDP section parsing and the rewrites above |
native/audio.rs | Opus, audio processing, jitter buffers, playout clock |
native/net.rs | Interface discovery and host sockets |
native/stun.rs, turn.rs, turn_stream.rs | STUN/TURN codec, TURN client state machine, TCP and TLS transports |
native/mdns.rs | mDNS responder for our host candidate names and resolver for remote .local candidates |
native/pq.rs | Post-quantum and hybrid key exchange groups |