WebRTC Cheatsheet
Connection States
Use this WebRTC reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Three Independent State Machines
RTCPeerConnection exposes three separate state properties — they evolve independently and all must be understood:
| Property | Tracks | Final states |
|---|---|---|
signalingState | SDP negotiation progress | "stable", "closed" |
iceConnectionState | ICE connectivity checks | "connected", "completed", "failed", "closed" |
connectionState | Overall DTLS + ICE rollup | "connected", "failed", "closed" |
Use
connectionStatefor UI (it's the definitive rollup). UseiceConnectionStatefor diagnostics.
signalingState
Tracks where you are in the offer/answer exchange.
| Value | Meaning |
|---|---|
"stable" | No negotiation in progress; safe to send data |
"have-local-offer" | Local offer set; waiting for remote answer |
"have-remote-offer" | Remote offer received; need to call createAnswer |
"have-local-pranswer" | Local provisional answer set |
"have-remote-pranswer" | Remote provisional answer received |
"closed" | Connection closed |
pc.onsignalingstatechange = () => {
console.log('Signaling state:', pc.signalingState);
};
// Guard renegotiation
pc.onnegotiationneeded = async () => {
if (pc.signalingState !== 'stable') return; // skip if mid-exchange
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: pc.localDescription });
};iceConnectionState
Tracks ICE candidate pair selection.
| Value | Meaning |
|---|---|
"new" | ICE not yet started |
"checking" | Sending STUN connectivity checks |
"connected" | A usable pair was found; data can flow |
"completed" | All checks done; optimal pair selected |
"failed" | No working pair found after all checks |
"disconnected" | Temporary loss of connectivity (may self-heal) |
"closed" | Connection closed |
pc.oniceconnectionstatechange = () => {
const state = pc.iceConnectionState;
console.log('ICE:', state);
if (state === 'failed') {
// Attempt ICE restart
restartIce();
}
if (state === 'disconnected') {
// Don't close yet — give it ~5s to reconnect
startDisconnectTimer();
}
if (state === 'connected' || state === 'completed') {
clearDisconnectTimer();
}
};connectionState
The high-level rollup: reflects the combined state of ICE + DTLS.
| Value | Meaning |
|---|---|
"new" | Not started |
"connecting" | Establishing ICE and DTLS |
"connected" | Fully established; media/data flowing |
"disconnected" | Connection interrupted; may self-heal |
"failed" | Could not connect or connection broke permanently |
"closed" | pc.close() was called |
pc.onconnectionstatechange = () => {
const state = pc.connectionState;
switch (state) {
case 'connected':
showUI('Connected');
break;
case 'disconnected':
showUI('Reconnecting…');
scheduleIceRestart();
break;
case 'failed':
showUI('Connection failed');
pc.close();
createNewConnection(); // full reconnect
break;
case 'closed':
cleanup();
break;
}
};iceGatheringState
Tracks ICE candidate collection progress.
| Value | Meaning |
|---|---|
"new" | Gathering not started |
"gathering" | Actively collecting candidates |
"complete" | All candidates have been gathered |
pc.onicegatheringstatechange = () => {
console.log('Gathering:', pc.iceGatheringState);
};
// Wait for all candidates before sending offer (non-trickle ICE)
function waitForGathering(pc) {
return new Promise(resolve => {
if (pc.iceGatheringState === 'complete') return resolve();
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === 'complete') resolve();
};
});
}
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await waitForGathering(pc);
// Now pc.localDescription contains all candidates
signaling.send({ type: 'offer', sdp: pc.localDescription });State Transition Diagram
connectionState: new → connecting → connected → disconnected ⟶ failed ↘ ↗ closed ←─────────────────┘ (pc.close())
iceConnectionState:
new → checking → connected → completed
↓ ↓
failed disconnected → failed
↓ ↓
closed closedDataChannel readyState
dc.readyState; // 'connecting' | 'open' | 'closing' | 'closed' dc.onopen = () => { /* safe to send */ }; dc.onclose = () => { /* channel gone */ }; dc.onerror = (e) => { console.error(e.error); };
| Value | Meaning |
|---|---|
"connecting" | Channel created, waiting for negotiation |
"open" | Ready — send() works |
"closing" | dc.close() called, draining |
"closed" | Channel terminated |
MediaStreamTrack readyState
track.readyState; // 'live' | 'ended' track.onended = () => { /* device disconnected or stop() called */ }; track.onmute = () => { /* sending silence/black */ }; track.onunmute = () => { /* sending real data again */ };
Comprehensive State Monitor
function monitorConnection(pc, label = 'pc') { const log = (prop, val) => console.log(`[${label}] ${prop}: ${val}`); pc.onsignalingstatechange = () => log('signalingState', pc.signalingState); pc.oniceconnectionstatechange = () => log('iceConnectionState', pc.iceConnectionState); pc.onicegatheringstatechange = () => log('iceGatheringState', pc.iceGatheringState); pc.onconnectionstatechange = () => log('connectionState', pc.connectionState); pc.onicecandidateerror = (e) => log('iceCandidateError', `${e.errorCode}: ${e.errorText}`); pc.ontrack = ({ track, streams }) => { log('track', `${track.kind} ${track.id}`); track.onended = () => log('trackEnded', track.kind); track.onmute = () => log('trackMuted', track.kind); track.onunmute = () => log('trackUnmuted', track.kind); }; } monitorConnection(pc, 'caller');
Reconnection Strategy
let iceRestartTimeout; pc.onconnectionstatechange = async () => { if (pc.connectionState === 'disconnected') { // Give the browser 5 s to self-heal iceRestartTimeout = setTimeout(async () => { if (pc.connectionState !== 'connected') { await triggerIceRestart(); } }, 5000); } if (pc.connectionState === 'connected') { clearTimeout(iceRestartTimeout); } if (pc.connectionState === 'failed') { clearTimeout(iceRestartTimeout); // ICE restart no longer possible from 'failed'; need a new PeerConnection pc.close(); reconnect(); } }; async function triggerIceRestart() { const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); signaling.send({ type: 'offer', sdp: pc.localDescription }); }