The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Create a peer connection | const pc = new RTCPeerConnection({ iceServers }) | View examples |
| Use expiring TURN credentials | iceServers: [{ urls: 'turn:turn.example.com:3478', username, credential }] | View examples |
| Add a local track | pc.addTrack(track, localStream) | View examples |
| Create a transceiver | pc.addTransceiver('video', { direction: 'recvonly' }) | View examples |
| Render remote media | pc.ontrack = event => { remoteVideo.srcObject = event.streams[0] ?? new MediaStream([event.track]) } | View examples |
| Create and signal an offer | await pc.setLocalDescription(); signal({ description: pc.localDescription }) | View examples |
| Apply a remote description | await pc.setRemoteDescription(description) | View examples |
| Signal ICE candidates | pc.onicecandidate = ({ candidate }) => candidate && signal({ candidate }) | View examples |
| Apply a remote candidate | await pc.addIceCandidate(candidate) | View examples |
| Restart failed ICE | pc.restartIce() | View examples |
| Create a data channel | const channel = pc.createDataChannel('chat', { ordered: true }) | View examples |
| Bound buffered data | channel.bufferedAmountLowThreshold = 65536 | View examples |
| Read connection statistics | const report = await pc.getStats() | View examples |
| Close the session | pc.getSenders().forEach(({ track }) => track?.stop()); pc.close() | View examples |
RTCPeerConnection transports real-time media and arbitrary data between peers. The browser implements ICE, DTLS, SRTP, SCTP, congestion control, and codecs, while the application must provide authenticated signaling and usually TURN relay service. Treat SDP and ICE candidates as sensitive session data, use the perfect-negotiation pattern to resolve glare, and close every track, channel, and connection when the call ends.
Step by step
Detailed examples
Configure relay service and authenticated signaling
WebRTC deliberately does not define signaling. Exchange descriptions and candidates through an authenticated, authorization-checked HTTPS or WSS service, bind messages to a call and participant, validate sizes and types, and prevent one user from signaling another call. STUN discovers addresses; TURN relays traffic when direct paths fail. Operate TURN over UDP and TCP/TLS where required, issue short-lived credentials, monitor capacity, and never embed durable relay secrets in JavaScript. WebRTC media is encrypted in transit, but peer identity and application authorization remain your responsibility.
const response = await fetch('/api/calls/rtc-config', {
credentials: 'same-origin',
headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error('RTC configuration unavailable');
const { iceServers } = await response.json();
const pc = new RTCPeerConnection({ iceServers }); Model media with tracks and transceivers
Acquire media only after a clear user action in a secure context. addTrack() creates or reuses a transceiver; addTransceiver() is better when direction or an inactive slot must be established before a track exists. The track event can have an empty streams array, so construct a MediaStream when necessary. replaceTrack() can switch cameras without renegotiation when the new track is compatible. Stop local capture tracks when no longer needed and respond to their ended events.
const local = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
for (const track of local.getTracks()) pc.addTrack(track, local);
const remote = new MediaStream();
pc.ontrack = event => {
if (!remote.getTrackById(event.track.id)) remote.addTrack(event.track);
remoteVideo.srcObject = event.streams[0] ?? remote;
}; Use perfect negotiation to survive simultaneous offers
The signaling state machine rejects descriptions applied in the wrong order. Perfect negotiation assigns one peer as polite and uses makingOffer, ignoreOffer, and the signalingState to resolve glare. setLocalDescription() without arguments automatically creates the description appropriate to the current state. Serialize signaling work, ignore candidates belonging to an intentionally ignored offer, and never modify SDP strings unless a documented interoperability requirement makes it unavoidable.
let makingOffer = false;
let ignoreOffer = false;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signal({ description: pc.localDescription });
} finally { makingOffer = false; }
};
async function receiveDescription(description, polite) {
const collision = description.type === 'offer' &&
(makingOffer || pc.signalingState !== 'stable');
ignoreOffer = !polite && collision;
if (ignoreOffer) return;
await pc.setRemoteDescription(description);
if (description.type === 'offer') {
await pc.setLocalDescription();
signal({ description: pc.localDescription });
}
} Trickle ICE candidates and recover deliberately
Send each non-null icecandidate promptly. Buffer inbound candidates until their remote description has been applied; addIceCandidate() otherwise may fail or associate data incorrectly. Candidate strings can expose network information, so protect and minimize signaling logs. connectionState summarizes the whole connection, while iceConnectionState isolates ICE. A transient disconnected state is not necessarily failure; use a grace period. On failed, request restartIce(), let negotiationneeded produce a new offer, and cap retries.
const pending = [];
async function receiveCandidate(candidate) {
if (!pc.remoteDescription) pending.push(candidate);
else if (!ignoreOffer) await pc.addIceCandidate(candidate);
}
async function flushCandidates() {
for (const candidate of pending.splice(0)) await pc.addIceCandidate(candidate);
}
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'failed') pc.restartIce();
}; Apply backpressure and protocol framing to data channels
RTCDataChannel messages can be strings, Blob objects, or ArrayBuffer data. Define an application protocol with a version, message kinds, size limits, and validation; receiving JSON does not make it trusted. Large sends accumulate in bufferedAmount and can exhaust memory, so stop above a high-water mark and resume after bufferedamountlow. Ordered reliable delivery is the default; maxRetransmits and maxPacketLifeTime trade reliability for latency and are mutually exclusive.
async function sendWhenReady(channel, payload) {
const highWater = 256 * 1024;
channel.bufferedAmountLowThreshold = 64 * 1024;
if (channel.bufferedAmount > highWater) {
await new Promise(resolve => channel.addEventListener('bufferedamountlow', resolve, { once: true }));
}
if (channel.readyState !== 'open') throw new Error('Channel is not open');
channel.send(JSON.stringify({ version: 1, ...payload }));
} Observe the selected path and clean up every resource
getStats() exposes implementation-defined collections keyed by stable IDs within a report. Follow the nominated or selected candidate-pair references rather than assuming a report order, and aggregate bytes over time to compute rates. Do not send raw candidate addresses or full SDP to analytics. On hang-up, detach handlers, close data channels, stop capture tracks the application owns, clear media element srcObject values, and close the connection. close() does not itself stop MediaStreamTrack capture.
function hangUp() {
pc.ontrack = pc.onicecandidate = pc.onnegotiationneeded = null;
for (const channel of dataChannels) channel.close();
for (const sender of pc.getSenders()) sender.track?.stop();
remoteVideo.srcObject = null;
localVideo.srcObject = null;
pc.close();
} Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- World Wide Web ConsortiumWebRTC: Real-Time Communication in Browsersw3.org
- World Wide Web ConsortiumIdentifiers for WebRTC's Statistics APIw3.org
- Internet Engineering Task ForceInteractive Connectivity Establishment (ICE): A Protocol for NAT Traversalrfc-editor.org
- Internet Engineering Task ForceTraversal Using Relays around NAT (TURN)rfc-editor.org
- World Wide Web ConsortiumMedia Capture and Streamsw3.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



