WebRTC Cheatsheet
Connection States
Use this WebRTC reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
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 }); }