Wasm + WebRTC
What is WebRTC?
WebRTC (Web Real-Time Communication) enables peer-to-peer audio, video, and data transfer directly between browsers — with no server relay. Rust/Wasm can drive the entire client-side logic: encoding, protocol handling, and application state.
Traditional Client-Server WebRTC Peer-to-Peer
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│Peer A│────>│Server│<────│Peer B│ │Peer A│<========>│Peer B│
└──────┘ └──────┘ └──────┘ └──────┘ direct └──────┘
▲ │
All data Lower latency
goes through No server cost
server E2E encryptedWebRTC Architecture Overview
A WebRTC connection involves several components:
┌─────────────────────────────────────────────────────────┐
│ Browser (Peer A) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Your Rust │──>│RTCPeerConn. │──>│ ICE Agent │ │
│ │ Wasm Code │ │ (browser) │ │ (STUN/TURN) │ │
│ └─────────────┘ └──────────────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌─────────────┐ ┌──────────────┐ │ │
│ │Data Channel │ │ Media Stream │ │ │
│ │ (text/bin) │ │ (audio/video)│ │ │
│ └─────────────┘ └──────────────┘ │ │
└───────────────────────────────────────────────┼─────────┘
│
┌────────────────────────┘
│ STUN / TURN / Direct
▼
┌─────────────────────────────────────────────────────────┐
│ Browser (Peer B) │
│ (mirror of above) │
└─────────────────────────────────────────────────────────┘The Signaling Process
Before peers can connect directly, they must exchange offers, answers, and ICE candidates through a signaling server (typically a WebSocket server):
Alice Signaling Server Bob
│ │ │
│── 1. Create Offer ───────> │ │
│ │── 2. Forward Offer ──> │
│ │ │
│ │ <── 3. Create Answer ──│
│ <── 4. Forward Answer ──── │ │
│ │ │
│── 5. ICE Candidate ──────> │ │
│ │── 6. Forward ICE ────> │
│ │ │
│ │ <── 7. ICE Candidate ──│
│ <── 8. Forward ICE ─────── │ │
│ │ │
│ <══════════ Direct P2P Connection ═════════════════> │
│ (signaling server no longer needed) │SDP (Session Description Protocol)
The offer and answer are SDP strings that describe capabilities:
v=0
o=- 4567890 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:abcd
a=ice-pwd:efghijklmnop
a=fingerprint:sha-256 AA:BB:CC:...
a=setup:actpass
a=mid:0
a=sctp-port:5000WebRTC in Rust/Wasm with web-sys
The web-sys crate provides bindings to all WebRTC browser APIs:
[dependencies.web-sys]
version = "0.3"
features = [
"RtcPeerConnection",
"RtcPeerConnectionIceEvent",
"RtcSessionDescriptionInit",
"RtcSdpType",
"RtcDataChannel",
"RtcDataChannelInit",
"RtcDataChannelEvent",
"RtcIceCandidateInit",
"RtcConfiguration",
"RtcIceServer",
"MessageEvent",
]Creating a Peer Connection
use web_sys::{RtcPeerConnection, RtcConfiguration, RtcIceServer};
use wasm_bindgen::prelude::*;
use js_sys::{Array, Object, Reflect};
fn create_peer_connection() -> Result<RtcPeerConnection, JsValue> {
// Configure STUN/TURN servers
let ice_server = Object::new();
Reflect::set(&ice_server, &"urls".into(),
&"stun:stun.l.google.com:19302".into())?;
let ice_servers = Array::new();
ice_servers.push(&ice_server);
let config = RtcConfiguration::new();
config.set_ice_servers(&ice_servers);
RtcPeerConnection::new_with_configuration(&config)
}Creating an Offer
use wasm_bindgen_futures::JsFuture;
async fn create_offer(pc: &RtcPeerConnection) -> Result<String, JsValue> {
let offer = JsFuture::from(pc.create_offer()).await?;
let offer_sdp = Reflect::get(&offer, &"sdp".into())?
.as_string()
.unwrap();
let mut desc = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
desc.sdp(&offer_sdp);
JsFuture::from(pc.set_local_description(&desc)).await?;
Ok(offer_sdp)
}Setting Up a Data Channel
fn setup_data_channel(pc: &RtcPeerConnection) -> RtcDataChannel {
let mut init = RtcDataChannelInit::new();
init.ordered(true);
let channel = pc.create_data_channel_with_data_channel_dict("chat", &init);
// Handle incoming messages
let onmessage = Closure::wrap(Box::new(move |evt: MessageEvent| {
if let Some(text) = evt.data().as_string() {
web_sys::console::log_1(&format!("Received: {}", text).into());
}
}) as Box<dyn FnMut(_)>);
channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
onmessage.forget();
// Handle channel open
let channel_clone = channel.clone();
let onopen = Closure::wrap(Box::new(move |_: JsValue| {
channel_clone.send_with_str("Hello from Rust/Wasm!").unwrap();
}) as Box<dyn FnMut(_)>);
channel.set_onopen(Some(onopen.as_ref().unchecked_ref()));
onopen.forget();
channel
}ICE: Finding a Path Between Peers
ICE (Interactive Connectivity Establishment) discovers the best network path between peers:
ICE Candidate Types (in preference order):
1. Host Candidate ← Direct LAN connection
┌──────┐ ┌──────┐
│Peer A│═══════════════│Peer B│
└──────┘ same network └──────┘
2. Server-Reflexive (srflx) ← Via STUN (learns public IP)
┌──────┐ ┌──────┐ ┌──────┐
│Peer A│────>│ STUN │<────│Peer B│
└──────┘ │Server│ └──────┘
NAT ─ ─ ─ └──────┘ ─ ─ ─ NAT
Peer A ═════════════════════ Peer B
(through NAT)
3. Relay (relay) ← Via TURN (relayed traffic)
┌──────┐ ┌──────┐ ┌──────┐
│Peer A│────>│ TURN │<────│Peer B│
└──────┘ │Server│ └──────┘
└──────┘
(all data relayed)Handling ICE Candidates in Rust
fn setup_ice_handling(pc: &RtcPeerConnection, ws: &WebSocket) {
let ws_clone = ws.clone();
let onicecandidate = Closure::wrap(Box::new(move |evt: RtcPeerConnectionIceEvent| {
if let Some(candidate) = evt.candidate() {
// Serialize and send via signaling server
let msg = format!(
r#"{{"type":"ice","candidate":"{}","sdpMid":"{}","sdpMLineIndex":{}}}"#,
candidate.candidate(),
candidate.sdp_mid().unwrap_or_default(),
candidate.sdp_m_line_index().unwrap_or(0)
);
ws_clone.send_with_str(&msg).unwrap();
}
}) as Box<dyn FnMut(_)>);
pc.set_onicecandidate(Some(onicecandidate.as_ref().unchecked_ref()));
onicecandidate.forget();
}Data Channel Message Types
Data channels support both text and binary data:
// Text message
channel.send_with_str("Hello!")?;
// Binary message (e.g., game state, file chunks)
let data: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04];
let array = js_sys::Uint8Array::from(&data[..]);
channel.send_with_array_buffer(&array.buffer())?;Ordered vs Unordered Channels
| Property | Ordered (ordered: true) |
Unordered (ordered: false) |
|---|---|---|
| Message order | Guaranteed | May arrive out of order |
| Latency | Higher (head-of-line) | Lower |
| Use case | Chat, commands | Game state, video frames |
| Retransmission | Yes | Optional (maxRetransmits) |
// Unreliable, unordered — ideal for real-time game state
let mut init = RtcDataChannelInit::new();
init.ordered(false);
init.max_retransmits(0); // fire-and-forget
let game_channel = pc.create_data_channel_with_data_channel_dict("game-state", &init);Signaling with WebSocket
A minimal signaling server relays SDP and ICE messages between peers:
// Client-side: connect to signaling server
use web_sys::WebSocket;
let ws = WebSocket::new("wss://signal.example.com/room/abc")?;
let onmessage = Closure::wrap(Box::new(move |evt: MessageEvent| {
let text = evt.data().as_string().unwrap();
// Parse and handle: offer, answer, or ICE candidate
// Then call pc.set_remote_description() or pc.add_ice_candidate()
}) as Box<dyn FnMut(_)>);
ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
onmessage.forget();Signaling Protocol Example
// Alice -> Server -> Bob
{"type": "offer", "sdp": "v=0\r\no=..."}
// Bob -> Server -> Alice
{"type": "answer", "sdp": "v=0\r\no=..."}
// Both directions
{"type": "ice", "candidate": "candidate:1 1 UDP ...",
"sdpMid": "0", "sdpMLineIndex": 0}Connection Lifecycle
State Machine:
┌─────┐ create offer/answer ┌────────────┐ ICE done ┌───────────┐
│ New │───────────────────────>│ Connecting │───────────>│ Connected │
└─────┘ └────────────┘ └─────┬─────┘
│ │
│ failure │ network
▼ │ change
┌──────────┐ ▼
│ Failed │ ┌──────────────┐
└──────────┘ │Disconnected │
└──────┬───────┘
│ ICE
│ restart
▼
┌───────────┐
│ Connected │
└───────────┘Performance Tips for Rust/Wasm WebRTC
Process binary data in Rust — decode video frames, compress game state, encrypt payloads on the Wasm side before sending through data channels.
Use
SharedArrayBufferfor zero-copy data transfer between Wasm and JavaScript (requires cross-origin isolation headers).Batch small messages — each
send()call has overhead. Pack multiple game events into a single binary message.Use unordered channels for real-time data where the latest state matters more than every update.
| Pattern | Latency | Reliability | Use Case |
|---|---|---|---|
| Ordered + reliable | High | Full | Chat, file transfer |
| Ordered + maxRetransmits(3) | Medium | Partial | Important game events |
| Unordered + maxRetransmits(0) | Lowest | None | Position updates |
Summary
WebRTC lets your Rust/Wasm application communicate peer-to-peer without a relay server. The signaling phase (offers, answers, ICE candidates) happens through a WebSocket server, but once connected, all data flows directly between browsers. Data channels give you flexible, low-latency messaging — choose ordered/reliable for correctness-critical data and unordered/unreliable for real-time streams.