On this pageOPENCLOSE
How WebRTC connects
Follow a call from permission prompt to media flowing: signaling, ICE, DTLS, SRTP, and what each side really does.
Goal
- 01See the complete path from a permission prompt to encrypted media leaving the browser.
- 02Separate what signaling does from what ICE, DTLS, SRTP, and the media path do.
- 03Know which component owns each failure so you stop debugging the wrong layer.
The concept, plainly
WebRTC is not a single connection. It is a negotiation followed by several layered protocols. The browser captures media as s inside a `MediaStream`. Your application creates an , attaches the tracks, and generates an offer describing codecs, directions, and transport parameters. The remote peer answers with its own SDP. Neither SDP contains a usable network path yet; both sides then gather s and test connectivity.
Signaling is deliberately unspecified. You can use WebSocket, HTTP, Slack messages, or a QR code, as long as offer, answer, and candidates reach the other peer in the right order. Most failures blamed on “WebRTC” are actually signaling bugs: the answer was set before the offer, a candidate arrived before the remote description, or the room used two different peer IDs. Your signaling messages should be explicit about type, sender, target, and payload.
ICE gathers **host** candidates from local interfaces, **server-reflexive** candidates from , and **relay** candidates from . It pairs candidates on both sides and sends STUN connectivity checks. The nominated pair becomes the media path. If a direct UDP path works, media flows peer-to-peer. If it does not, TURN relays encrypted packets without decrypting them. STUN is only for discovery; TURN costs bandwidth because every packet traverses your server.
Once a path is chosen, the peers run a handshake over it. The DTLS certificates and fingerprints are already present in the SDP, which binds the signaling exchange to the eventual encryption keys. DTLS derives keys for , so audio and video are encrypted end to end between the peers. If both peers are behind restrictive NATs and use TURN, the TURN server still sees only encrypted SRTP, not the media content.
The practical lifecycle is: capture, create connection, add tracks, negotiate, gather candidates, connect, then watch state changes. Always listen to `iceconnectionstatechange`, `connectionstatechange`, `icecandidateerror`, and `signalingstatechange` separately. A connection that stays in `new` never started negotiation. One stuck in `checking` has candidates but no working pair. One in `connected` but with no video usually has a track or autoplay problem, not a network problem.
See it move
Interactive diagram
The complete WebRTC connection flow
Capture local media
getUserMedia resolves after permission. Tracks exist locally, but no remote peer has received a single byte.
Signaling is blue; discovery and checks are neutral; media is orange. The relay path only appears when a direct pair fails.
Behind the scenes
Inside the browser
Chrome, Firefox, and Safari implement WebRTC in a media engine plus a network stack. The camera pipeline feeds an encoder; the encoder output becomes RTP packets; the network stack sends them over the nominated ICE pair. The browser also runs RTCP for feedback. getUserMedia permission is handled by the browser UI and the operating system, so a page can be perfectly coded and still fail because the OS denied microphone access. All of this requires a secure context: HTTPS or localhost.
What the server actually needs to do
A basic 1:1 call needs a signaling relay and credentials for STUN/TURN. It does not need to touch media. A production call also needs room authorization, TURN credential minting, presence, and observability. If you add recording, transcription, or an SFU, a media server becomes a participant and media deliberately changes path. That is a topology decision, not a requirement of WebRTC.
Media never travels in signaling
It is common to see logs full of SDP and assume the server is carrying the call. It is not. SDP is a few kilobytes; SRTP is a continuous stream. If you disable the signaling server after connection, an established direct call normally continues. If you stop TURN, relayed calls fail immediately. Knowing which component is in the media path makes incident response much faster.
Code walkthrough
The essential order of operations. Signaling is abstracted as a send function; ICE candidates are queued until the remote description exists.
export function createCall(send: (message: unknown) => void) {const pc = new RTCPeerConnection({iceServers: [{ urls: "stun:stun.example.com:3478" }],});const pendingCandidates: RTCIceCandidateInit[] = [];pc.onicecandidate = ({ candidate }) => {if (candidate) send({ type: "ice", candidate: candidate.toJSON() });};pc.ontrack = ({ streams }) => {remoteVideo.srcObject = streams[0] ?? null;};async function makeOffer(stream: MediaStream) {for (const track of stream.getTracks()) pc.addTrack(track, stream);const offer = await pc.createOffer();await pc.setLocalDescription(offer);send({ type: "offer", sdp: pc.localDescription });}async function acceptOffer(sdp: RTCSessionDescriptionInit) {await pc.setRemoteDescription(sdp);while (pendingCandidates.length) {await pc.addIceCandidate(pendingCandidates.shift()!);}const answer = await pc.createAnswer();await pc.setLocalDescription(answer);send({ type: "answer", sdp: pc.localDescription });}async function acceptAnswer(sdp: RTCSessionDescriptionInit) {await pc.setRemoteDescription(sdp);while (pendingCandidates.length) {await pc.addIceCandidate(pendingCandidates.shift()!);}}async function addIce(candidate: RTCIceCandidateInit) {if (!pc.remoteDescription) pendingCandidates.push(candidate);else await pc.addIceCandidate(candidate);}return { pc, makeOffer, acceptOffer, acceptAnswer, addIce };}
Do and don't
Do
Signal all three things
Offer, answer, and every ICE candidate must reach the peer. Candidate trickling needs a queue, not wishful ordering.
Log state transitions with context
Include call id, peer id, state, and timestamp. The first state that fails tells you which subsystem to inspect.
Provide TURN credentials
STUN alone works on friendly networks. Production needs TURN with short-lived credentials and enough relay capacity.
Clean up tracks and connections
Stop local tracks, close the peer connection, and clear video elements when leaving a call, not only when the user clicks “hang up”.
Don't
Don't send media through the signaling socket
WebSocket is for the small negotiation messages. Media belongs in SRTP; sending frames through JSON destroys latency and scalability.
Don't ignore ICE candidates that arrive early
Trickled candidates commonly arrive before setRemoteDescription. Dropping them leads to a connection that never leaves checking.
Don't test only on localhost
Host candidates make localhost work without STUN or TURN. Real networks expose NAT, firewalls, and proxy failures that localhost hides.
Don't force one codec or resolution blindly
SDP and constraints must match the use case. Over-constrained capture or unsupported codecs produce silent black frames, not a clean error.
Practice
Hands-on lab
Watch the handshake happen
Run two tabs or use loopback mode. The lab prints every signaling message, candidate type, selected pair, and state transition while media flows.
- 1Open the 1:1 lab and join a room from two tabs, or start loopback mode if you are alone.
- 2Read the signaling log in order: offer, answer, candidate groups. Match each message to the diagram.
- 3Inspect the ICE candidate list and identify the selected pair. Is it host, srflx, or relay?
- 4Open the stats panel and confirm that RTT and bitrate are coming from the selected pair, not the signaling socket.
Solo? Use the built-in loopback: two RTCPeerConnections in one tab, connected without a second person. The signaling log is still real.
Quick quiz
Knowledge check
0 of 5 answered
- 01
Which component carries audio and video after a WebRTC connection is established?
- 02
A connection stays in ICE checking. Which is the most likely class of problem?
- 03
What does STUN provide?
- 04
Why must ICE candidates be exchanged even though SDP is already exchanged?
- 05
Which is a true statement about TURN?
Key takeaways
- WebRTC is layered: signaling, ICE, DTLS, SRTP. Diagnose one layer at a time.01
- The signaling server is not in the media path unless you deliberately add a media server.02
- STUN discovers addresses; TURN relays packets; ICE decides which pair works.03
- A connected peer connection can still have no media. Track state, transceiver direction, and autoplay separately.04
Common errors and fixes
Terms in this lesson
Done with this lesson?
Mark it complete, then keep the thread.