WebRTC lets two browsers send audio, video and data straight to each other, with no server carrying the conversation. Many browser video calls run on it, and setting one up takes four pieces: signalling, STUN, TURN and ICE.
Why not send video through a server?
The obvious way to build a video call is to have each browser upload its camera feed to a server, and have the server forward it to the other side. That works, but it has two costs.
- Latency: every packet makes an extra trip, out to the server and back down again. In a conversation, a few hundred extra milliseconds is enough to make people talk over each other.
- Money: video is heavy. A server that relays every call pays for every byte, in both directions, for as long as the call lasts.
WebRTC (Web Real-Time Communication) takes the server out of the middle. Once a call is set up, the two browsers send media directly to each other: peer to peer. Servers still help the browsers find each other, but they don't carry the call.
WebRTC is built into every major browser, and you use it through JavaScript APIs such as getUserMedia (to get the camera and microphone) and RTCPeerConnection (to connect to the other peer).
Signalling: the introduction
Two browsers can't connect to each other out of nowhere. Each has to learn what media the other can send and receive, and where on the network it might be reachable. Swapping that information is called signalling.
WebRTC deliberately doesn't say how signalling should work. You bring your own channel. A WebSocket server works, and so do plain HTTP requests. All the channel has to do is pass messages from one peer to the other.
Offers, answers and candidates
The messages follow a fixed pattern:
- The caller creates an offer, a description of the session written in SDP (Session Description Protocol), and sends it through the signalling channel.
- The other peer replies with an answer, its own SDP saying what it accepts.
- Both sides send ICE candidates as they find them: the IP addresses and ports where they might be reachable.
Once the browsers have connected, the signalling server has done its job. The audio and video never pass through it. It only comes back if the session needs to change, such as when someone adds a screen share.
Getting past NATs and firewalls
Connecting two browsers directly sounds simple until you remember how home and office networks work. Most devices sit behind a router that uses NAT (Network Address Translation): your laptop has a private address like 192.168.1.20, and the router shows a single public address to the internet. Your laptop doesn't know its own public address, and the router won't let unexpected traffic in. Firewalls add their own rules on top.
WebRTC uses three pieces to get through this.
STUN: what does my address look like from outside?
A STUN server answers one question: 'what public IP address and port do you see me coming from?' The browser sends it a small request, and the server replies with the address it saw. That gives the browser a candidate it can hand to the other peer.
STUN is cheap to run, because it only answers that question. No media goes through it.
TURN: the relay of last resort
Some networks are too strict for a direct connection: corporate firewalls that block most UDP traffic, or NATs that pick a new public port for every destination. For those, a TURN server relays the media. Both browsers send to the TURN server, and it forwards each packet to the other side.
TURN gets through almost any network, but it brings back the costs WebRTC was trying to avoid. Every byte of the call goes through your server, which adds latency and a bandwidth bill, so TURN is the fallback.
ICE: trying every path
ICE (Interactive Connectivity Establishment) ties it together. Each browser collects its candidates: its local addresses, its public address from STUN, and a relay address from TURN. The two sides swap candidates over signalling, test pairs of them with small connectivity checks, and choose the best pair that works. A direct path wins when it gets through, and ICE uses the relay only when nothing else connects.
Here is a call being set up, first over a direct path and then through a relay:
Setting up a WebRTC call
Step 1 of 12: Browser A asks a STUN server which public address it appears to come from.
Encrypted by default
Encryption in WebRTC isn't optional. Every connection starts with a DTLS handshake, which is TLS adapted for UDP, and the browsers agree their keys with a Diffie-Hellman style exchange. Audio and video then travel over SRTP (Secure Real-time Transport Protocol), using keys from that handshake.
That holds even when a TURN server relays the call. The relay forwards packets it can't read, so someone on the same network, or the person running the relay, sees only scrambled bytes.
Browsers add another guard: a page can only use the camera or microphone after the user grants permission, and only on a secure origin (HTTPS, or localhost while developing).
A minimal call in JavaScript
Here is the shape of a one-to-one call. signal stands in for your signalling channel, say a small wrapper around a WebSocket that sends and receives JSON objects.
First, create the connection and tell it which STUN and TURN servers it may use:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.example.org:3478" },
{
urls: "turn:turn.example.org:3478",
username: "user-123",
credential: "short-lived-token",
},
],
});The caller adds its camera and microphone, sends candidates as ICE finds them, and makes the offer:
const stream = await navigator.mediaDevices
.getUserMedia({ video: true, audio: true });
for (const track of stream.getTracks()) {
pc.addTrack(track, stream);
}
pc.onicecandidate = ({ candidate }) => {
if (candidate) signal.send({ candidate });
};
pc.ontrack = ({ streams }) => {
remoteVideo.srcObject = streams[0];
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signal.send({ description: pc.localDescription });Both sides handle what arrives from the other. The receiver answers an offer, and both add the other's candidates:
signal.onmessage = async (msg) => {
if (msg.description) {
await pc.setRemoteDescription(msg.description);
if (msg.description.type === "offer") {
await pc.setLocalDescription();
signal.send({
description: pc.localDescription,
});
}
} else if (msg.candidate) {
await pc.addIceCandidate(msg.candidate);
}
};Calling setLocalDescription() with no argument after receiving an offer makes the browser create the answer for you. The receiver also calls getUserMedia and addTrack if it wants to send video back. Sending candidates one at a time as they arrive, instead of waiting for all of them, is called trickle ICE, and it makes calls connect faster.
More than audio and video
RTCPeerConnection can also open data channels with pc.createDataChannel("chat"). A data channel sends text or binary messages peer to peer, over the same encrypted connection. Multiplayer games and in-call chat use them. You can choose whether a channel guarantees delivery and order, or drops late messages to keep latency low.
When WebRTC fits, and when it doesn't
WebRTC is the right tool when two or a few people need to talk in real time: calls, screen sharing, live tutoring, peer-to-peer games.
It gets harder as the group grows. In a pure peer-to-peer group call (a mesh), every person uploads a separate copy of their video to every other person, so a call of six means sending five streams at once. Most group-call products add a server called an SFU (Selective Forwarding Unit): each person sends one stream to it, and it forwards that stream to everyone else. The SFU is a WebRTC peer itself, so encryption then ends at the server, unless the app adds its own end-to-end layer on top.
It is the wrong tool for broadcasting to thousands of viewers, where a small delay is acceptable. Streaming formats built on HTTP are cheaper and scale better there.
Common mistakes
- Shipping without TURN: calls work on your home network and in testing, then fail for users on strict corporate or mobile networks. A public STUN server alone isn't enough.
- Long-lived TURN credentials in the page: anyone can read them and relay their traffic through your server at your expense. Have your backend hand out short-lived credentials.
- Thinking WebRTC needs no servers: it always needs signalling, and usually STUN and TURN. What it avoids is servers in the media path.
- Adding candidates too early:
addIceCandidatefails if the remote description hasn't been set yet. Queue candidates that arrive first, and add them once it has. - Testing camera access over plain HTTP:
getUserMediais blocked outside a secure origin.
Key takeaways
- WebRTC sends audio, video and data directly between browsers, so the conversation skips the server.
- A signalling channel you build yourself carries the offer, the answer and the ICE candidates, then steps aside.
- STUN tells a browser its public address. TURN relays media when no direct path works, at a cost in bandwidth.
- ICE tries every candidate pair and picks the best path that connects.
- All WebRTC media is encrypted by default, with DTLS and SRTP, even through a relay.