⏱️ Lectura: 16 min

Two browsers can exchange video, audio, and files without a single byte of that conversation passing through a central server: that’s what WebRTC makes possible. The technology was born at Google in 2011, became a W3C and IETF standard, and today runs natively in Chrome, Firefox, Safari, and Edge with no plugins or additional installations.

📑 En este artículo
  1. TL;DR
  2. What WebRTC Is and Why It Matters
  3. How WebRTC Works in Detail
    1. Signaling: The Step WebRTC Doesn’t Standardize
    2. ICE: Finding the Network Route
    3. DTLS-SRTP: Mandatory Encryption, Not Optional
  4. Practical Examples: From the Minimal Video Call to the Data Channel
    1. Example 1: Capturing the Camera and Displaying It On Screen
    2. Example 2: Negotiating the Connection with RTCPeerConnection
    3. Example 3: Transferring a File with RTCDataChannel
  5. Getting Started: Setting Up a Minimal Signaling Server
  6. Real-World Use Cases
  7. Common Mistakes and Best Practices
  8. Comparison with Alternatives
  9. Going Deeper: Production Architectures
  10. Frequently Asked Questions
    1. Does WebRTC need a server?
    2. Is WebRTC communication secure?
    3. What’s the difference between STUN and TURN?
    4. Does WebRTC work in all browsers?
    5. Can I use WebRTC just to transfer files, without video?
    6. Why doesn’t my test video call connect between two different networks?
  11. References

The key lies in three pieces working together: the STUN and TURN servers that solve the NAT traversal problem, the ICE protocol that tests network routes until it finds the most direct one, and SDP, the text format that describes which codecs and parameters each endpoint supports before connecting.

TL;DR

  • WebRTC connects two browsers directly with RTCPeerConnection, no plugins or native apps required.
  • STUN discovers the public IP behind a NAT; TURN relays traffic when no direct route exists.
  • ICE tests every available network candidate and automatically picks the fastest route.
  • SDP describes in plain text the codecs and parameters each browser supports before connecting.
  • RTCDataChannel lets you send files and arbitrary data over the same encrypted channel as video.
  • DTLS-SRTP encrypts all WebRTC traffic mandatorily, with no option to disable it.
  • chrome://webrtc-internals shows live which ICE candidate won the negotiation for a call.

What WebRTC Is and Why It Matters

WebRTC (Web Real-Time Communication) is a set of JavaScript APIs and a network protocol stack that lets two browsers establish a direct connection to exchange audio, video, and arbitrary data, without needing a plugin, a native app, or, in the ideal case, a server that relays the content. The project started at Google as open source, and since then the specification has been jointly maintained by the W3C (the browser API) and the IETF (the underlying network protocols).

Before WebRTC, any video call in the browser depended on plugins like Flash, or on the entire conversation passing through a central server that forwarded every audio and video packet. That has a real cost: every minute of a call consumes server bandwidth, and that bandwidth has to be paid for. WebRTC solves this problem by establishing, whenever the network allows it, a direct P2P (peer-to-peer) route between the two browsers. The server only steps in at the start, to coordinate the negotiation, and can then disconnect without the call dropping.

WebRTC’s importance isn’t just about cost: it’s also about latency and privacy. A direct P2P route eliminates a full network hop, which reduces latency in video calls and in latency-sensitive applications like remote device control or cloud gaming. And in terms of privacy, if the signaling server never sees the audio or video content and only coordinates connection metadata, the exposure surface is much smaller than that of a centralized system.

Conceptual diagram of two browsers connected via WebRTC
The P2P connection prevents a central server from processing every byte of video. Foto de Team Nocoloco en Unsplash

How WebRTC Works in Detail

A WebRTC connection is built in three phases that largely happen in parallel: signaling, ICE negotiation, and secure channel establishment.

Signaling: The Step WebRTC Doesn’t Standardize

Before two browsers can talk to each other they need to exchange certain information: which codecs they support, which resolutions they prefer, and the network addresses to try connecting through. This initial exchange is called signaling, and interestingly it happens outside of WebRTC: the spec leaves the signaling transport undefined, so each application picks its own (WebSocket, HTTP, or even a message copied by hand for a test). The only thing that travels over that channel are SDP messages (Session Description Protocol, RFC 8866), a plain text format that describes the session: available codecs, resolution, and encryption parameters.

The exchange follows the offer/answer pattern: the browser initiating the call generates an offer and sends it over the signaling channel; the other end responds with an answer. Only after that exchange do both browsers know which parameters the connection will use.

ICE: Finding the Network Route

Once the SDP has been negotiated, each browser starts gathering ICE candidates (Interactive Connectivity Establishment, RFC 8445): network addresses through which it could try to connect to the other end. There are three types.

  • Host candidate: the machine’s local IP, directly on the LAN.
  • Server-reflexive candidate: the public IP the outside world sees, discovered by asking a STUN server which address and port the traffic is arriving from.
  • Relay candidate: an address on a TURN server that agrees to relay traffic when no direct route works.

Each browser sends its candidate list to the other over the signaling channel, and both run connectivity checks: they test every combination of local and remote candidate until they find a pair that works. If a direct route exists, ICE picks it for having the lowest latency. Only if all direct routes fail, typical in networks with symmetric NAT or restrictive corporate firewalls, does ICE fall back to the relay candidate via TURN.

sequenceDiagram
    participant A as Browser A
    participant S as Signaling server
    participant B as Browser B
    A->>S: sends SDP offer
    S-->>B: forwards offer
    B->>S: sends SDP answer
    S-->>A: forwards answer
    A->>B: exchanges ICE candidates
    Note over A,B: P2P connection established, media flows directly

DTLS-SRTP: Mandatory Encryption, Not Optional

Once ICE has chosen a route, WebRTC negotiates an encryption layer before sending the first byte of media: DTLS (Datagram TLS) for the key handshake, and SRTP to encrypt audio and video in transit. This layer is defined in the WebRTC security architecture (RFC 8827) and is mandatory: there is no unencrypted mode in the spec, not even for testing on a local network. It’s a deliberate design decision: since WebRTC runs in the browser, any unencrypted traffic would be trivially interceptable by any network proxy in between.

Practical Examples: From the Minimal Video Call to the Data Channel

Example 1: Capturing the Camera and Displaying It On Screen

The first step of any WebRTC app, even before connecting to another browser, is requesting permission to use the camera and microphone with getUserMedia.

const videoPreview = document.querySelector("#video-local");

async function startCamera() {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: { width: 640, height: 480 },
    audio: true,
  });
  videoPreview.srcObject = stream;
  return stream;
}

startCamera();

This block opens the browser’s permission prompt, captures 640×480 video and microphone audio, and assigns them to the <video> element with id video-local to show the local preview. None of this involves another browser yet: it’s pure MediaDevices.

Example 2: Negotiating the Connection with RTCPeerConnection

With the local stream in hand, the next step is to create an RTCPeerConnection, add the audio and video tracks to it, and handle the offer/answer exchange through a signaling server, here represented as signalingSocket, an ordinary WebSocket.

const ICE_SERVERS = {
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" },
    {
      urls: "turn:turn.myapp.com:3478",
      username: "turnUsername",
      credential: "turnCredential",
    },
  ],
};

const peerConnection = new RTCPeerConnection(ICE_SERVERS);

stream.getTracks().forEach((track) => peerConnection.addTrack(track, stream));

peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    signalingSocket.send(JSON.stringify({ type: "candidate", candidate: event.candidate }));
  }
};

peerConnection.ontrack = (event) => {
  remoteVideo.srcObject = event.streams[0];
};

async function startCall() {
  const offer = await peerConnection.createOffer();
  await peerConnection.setLocalDescription(offer);
  signalingSocket.send(JSON.stringify({ type: "offer", sdp: offer }));
}

This code registers a public Google STUN server and a custom TURN server as a fallback, adds the local stream’s tracks to the connection, and defines what to do when its own ICE candidates arrive (send them to the other end) or remote tracks arrive (display them in a second <video>). startCall() generates the offer and sends it over the signaling channel. The other browser would respond with an answer following the same pattern, and both would call setRemoteDescription with the received SDP.

Example 3: Transferring a File with RTCDataChannel

WebRTC isn’t just audio and video: the same RTCPeerConnection can open an arbitrary data channel, encrypted with the same DTLS and transported over SCTP.

const fileChannel = peerConnection.createDataChannel("file-transfer");

fileChannel.binaryType = "arraybuffer";

fileChannel.onopen = () => console.log("data channel ready");

async function sendFile(file) {
  const buffer = await file.arrayBuffer();
  const chunkSize = 16 * 1024;
  for (let offset = 0; offset < buffer.byteLength; offset += chunkSize) {
    fileChannel.send(buffer.slice(offset, offset + chunkSize));
  }
}

This example splits a file into 16 KB chunks, the recommended size to avoid overwhelming the channel’s internal buffer, and sends them one by one as soon as the channel reports an open state. P2P transfer tools that avoid uploading the file to an intermediary server rely on exactly this mechanism.

Getting Started: Setting Up a Minimal Signaling Server

WebRTC doesn’t define the signaling transport, so you need to write one. The simplest possible option is a WebSocket server in Node.js that forwards messages between connected clients.

npm install ws
// signaling-server.js
const { WebSocketServer } = require("ws");

const server = new WebSocketServer({ port: 8080 });
const clients = new Set();

server.on("connection", (socket) => {
  clients.add(socket);
  socket.on("message", (message) => {
    for (const otherClient of clients) {
      if (otherClient !== socket && otherClient.readyState === 1) {
        otherClient.send(message.toString());
      }
    }
  });
  socket.on("close", () => clients.delete(socket));
});

With node signaling-server.js running on port 8080, any message a client sends (offer, answer, or an ICE candidate) is automatically forwarded to the rest of the connected clients. For production you’d need to add rooms and authentication, but this under-20-line base is enough to test the full offer/answer/ICE flow between two browser tabs.

For the TURN server, the most widely used open source option is coturn, which installs with apt install coturn on Debian/Ubuntu and exposes both STUN and TURN from the same binary.

Chrome panel showing the state of a WebRTC connection
chrome://webrtc-internals exposes live every ICE candidate tested. Foto de Sufyan en Unsplash

Real-World Use Cases

The most visible application of WebRTC is web video call clients: Google Meet, and the web versions of Zoom and Microsoft Teams run their media transport over WebRTC, although they add their own server layer for large group calls. Real-world usage goes far beyond video.

  • Telemedicine: remote medical consultations where low latency and mandatory encryption are requirements, not extras.
  • Cloud gaming and remote control: streaming keyboard and mouse input with minimal latency using RTCDataChannel.
  • P2P file transfer: sending files directly between browsers without uploading them to any intermediary server.
  • IoT device streaming: cameras and robots that stream video directly to the operator’s browser.
  • Decentralized applications: P2P messaging and collaboration aiming to minimize dependence on central infrastructure.

Common Mistakes and Best Practices

⚠️ Watch out: getUserMedia only works in a secure context (HTTPS) or on localhost. Serving a WebRTC demo over plain HTTP from a network IP fails silently, with no clear browser warning.

The most frequent mistake when starting out with WebRTC is assuming the P2P connection always gets established. On networks with symmetric NAT, common in corporate networks and some mobile carriers, no server-reflexive candidate works, and the only option is to fall back to the TURN relay. If the application doesn’t configure a TURN server, those users simply can’t connect, with no obvious error message beyond the ICE state never reaching connected.

Another common mistake is not checking the actual connection state before assuming it’s already connected. Listening to oniceconnectionstatechange is the correct approach.

peerConnection.oniceconnectionstatechange = () => {
  console.log("ICE state:", peerConnection.iceConnectionState);
};

When that state reaches "connected" or "completed", the route has already been chosen and media is flowing. To confirm this in more detail, chrome://webrtc-internals in Chrome (or about:webrtc in Firefox) shows live which candidate won the negotiation, how many bytes were sent, and whether the route is direct or via TURN. You can also query this programmatically with peerConnection.getStats(), which returns a report with the active candidate pair and its nominated: true flag.

Other frequent gotchas: browsers block video autoplay with audio if the element doesn’t have the muted attribute until there’s user interaction; forgetting to close the RTCPeerConnection with .close() and stop the tracks with track.stop() leaves the camera on; and assuming the P2P (mesh) topology scales to large group video calls: upload bandwidth grows with each additional participant, so from around 4 to 6 people onward it’s worth migrating to an SFU architecture.

Comparison with Alternatives

OptionWhen to use itAdvantageLimitation
WebRTCReal-time video, audio, or data with minimal latencyDirect P2P route, mandatory encryption, no pluginsNon-standardized signaling, complex NAT traversal
WebSocketSimple bidirectional messaging, chat, notificationsEasy to implement, single central serverAll traffic always passes through the server
HTTP long pollingCompatibility with legacy infrastructure or restrictive proxiesWorks behind almost any firewallHigh latency, constant reconnection overhead
Pure QUIC / HTTP/3Low-latency client-server transfers without needing P2PMultiplexing without head-of-line blockingDoesn’t solve direct connection between two browsers

Going Deeper: Production Architectures

A pure P2P call (mesh topology) works well for two or three participants: each browser sends its stream directly to all the others. Upload cost grows with each additional participant, because each browser encodes and sends its own video once for every other participant in the call. That’s why, past a certain size, production applications migrate to an SFU (Selective Forwarding Unit): a server that receives a single stream from each participant and forwards it to everyone else without decoding it. There’s a third variant, the MCU (Multipoint Control Unit), which does decode and mix all the streams into one before forwarding it, at the cost of much more server-side computation.

flowchart LR
    subgraph Mesh["Mesh topology: pure P2P"]
    P1["Participant 1"] --- P2["Participant 2"]
    P2 --- P3["Participant 3"]
    P1 --- P3
    end
    subgraph SFU["SFU topology: media server"]
    Q1["Participant 1"] --> R["SFU Server"]
    Q2["Participant 2"] --> R
    Q3["Participant 3"] --> R
    R --> Q1
    R --> Q2
    R --> Q3
    end

Another advanced component is congestion control: WebRTC dynamically adjusts the video bitrate based on network conditions using algorithms like GCC (Google Congestion Control) on the sender side and REMB (Receiver Estimated Maximum Bitrate) as feedback from the receiver. When an app supports simulcast, the sending browser encodes the same video at 2 or 3 simultaneous resolutions, and the SFU decides which one to forward to each receiver based on their available bandwidth, without re-encoding anything.

flowchart TD
    A["Browser A"] --> B["STUN Server"]
    A --> C["TURN Server"]
    D["Browser B"] --> B
    D --> C
    A -.->|"direct P2P candidate"| D
    A -.->|"candidate via relay"| C
    C -.->|"media relay"| D
    subgraph ICE["ICE route discovery"]
    B
    C
    end
💡 Tip: TURN server credentials shouldn’t be static. Best practice is to generate a short-lived username and password (for example with HMAC over a timestamp) for each session, so a leaked credential stops working within minutes.

WebRTC isn’t the right answer for everything. For streaming from one sender to thousands of viewers, technologies like HLS over a CDN are still simpler and cheaper to scale, though with more latency. And running your own TURN server has a real bandwidth cost: every byte that passes through the relay is paid for by whoever operates the infrastructure, so on networks with restrictive NAT, WebRTC stops being free in terms of server cost.

📖 Summary on Telegram: View summary

Your next step: clone two browser tabs, spin up this article’s signaling server with node signaling-server.js, and open chrome://webrtc-internals in one of them to see live which ICE candidate wins the negotiation.

Frequently Asked Questions

Does WebRTC need a server?

Yes, but only for the initial signaling, meaning to exchange SDP and ICE candidates. Once the P2P connection is established, the signaling server can disconnect without cutting the call. Only if the network forces the use of TURN does a server stay involved for the whole session, relaying the traffic.

Is WebRTC communication secure?

Yes: DTLS-SRTP encryption is mandatory in the spec, there is no unencrypted mode, not even for test connections within the same local network.

What’s the difference between STUN and TURN?

STUN only helps discover the public IP and port the outside world sees behind a NAT; it doesn’t relay traffic. TURN does relay all the audio, video, or data when no direct route between the two browsers works.

Does WebRTC work in all browsers?

Chrome, Firefox, Safari, and Edge have supported it natively for years. In native mobile apps, not an embedded browser, the libwebrtc library is used, the same engine Chrome uses, with bindings for iOS and Android.

Can I use WebRTC just to transfer files, without video?

Yes: RTCDataChannel is a generic data channel over SCTP that doesn’t require a camera or microphone. You can open an RTCPeerConnection exclusively to exchange arbitrary data.

Why doesn’t my test video call connect between two different networks?

The most common reason is symmetric NAT on one of the two networks, which blocks server-reflexive candidates. The solution is to configure a TURN server as a fallback: without TURN, those connections fail with no explicit error, only the ICE state never reaching connected.

References

📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day. @programacion

Imagen destacada: Foto de Shubham Dhage en Unsplash


Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.