The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create a peer connectionconst pc = new RTCPeerConnection({ iceServers })View examples
Use expiring TURN credentialsiceServers: [{ urls: 'turn:turn.example.com:3478', username, credential }]View examples
Add a local trackpc.addTrack(track, localStream)View examples
Create a transceiverpc.addTransceiver('video', { direction: 'recvonly' })View examples
Render remote mediapc.ontrack = event => { remoteVideo.srcObject = event.streams[0] ?? new MediaStream([event.track]) }View examples
Create and signal an offerawait pc.setLocalDescription(); signal({ description: pc.localDescription })View examples
Apply a remote descriptionawait pc.setRemoteDescription(description)View examples
Signal ICE candidatespc.onicecandidate = ({ candidate }) => candidate && signal({ candidate })View examples
Apply a remote candidateawait pc.addIceCandidate(candidate)View examples
Restart failed ICEpc.restartIce()View examples
Create a data channelconst channel = pc.createDataChannel('chat', { ordered: true })View examples
Bound buffered datachannel.bufferedAmountLowThreshold = 65536View examples
Read connection statisticsconst report = await pc.getStats()View examples
Close the sessionpc.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

01

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.

Fetch time-limited connection configuration
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 });
Back to quick reference ↑
02

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.

Publish local media and collect remote tracks
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;
};
Back to quick reference ↑
03

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.

Implement the core perfect-negotiation roles
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 });
  }
}
Back to quick reference ↑
04

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.

Buffer candidates and restart only after failure
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();
};
Back to quick reference ↑
05

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.

Wait for the channel buffer before sending
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 }));
}
Back to quick reference ↑
06

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.

Perform deterministic call teardown
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();
}
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. World Wide Web ConsortiumWebRTC: Real-Time Communication in Browsersw3.org
  2. World Wide Web ConsortiumIdentifiers for WebRTC's Statistics APIw3.org
  3. Internet Engineering Task ForceInteractive Connectivity Establishment (ICE): A Protocol for NAT Traversalrfc-editor.org
  4. Internet Engineering Task ForceTraversal Using Relays around NAT (TURN)rfc-editor.org
  5. 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.

Share feedback