A hands-on WebRTC experiment for developers who learn by building. This repository implements peer-to-peer data channel communication from scratch β no frameworks, no abstractions, just raw WebRTC APIs with clear annotations.
| File | Purpose |
|---|---|
sender.html |
Manual mode β creates an SDP offer, sends messages via data channel |
receiver.html |
Manual mode β accepts the offer, creates an answer, receives messages |
auto.html |
Auto mode β WebSocket-based signaling, open in two tabs and it just works |
server.js |
Minimal WebSocket signaling server + static file server |
Perfect for understanding the signaling flow β you are the signaling server.
# Just open both files directly in your browser
open sender.html # or double-click
open receiver.htmlThen follow the 3-step flow on each page:
- Sender β Click "Create Offer" β Copy the offer JSON
- Receiver β Paste the offer β Click "Set Offer" β Copy the answer JSON
- Sender β Paste the answer β Connection established β Chat!
The signaling happens automatically via WebSocket β closer to how real apps work.
npm install # install ws dependency (one time)
npm start # start signaling server on port 8080Then open two tabs to http://localhost:8080. The first tab becomes the sender, the second becomes the receiver. Once connected, all chat messages flow directly peer-to-peer.
Two browsers want to talk directly. But they don't know each other's IP addresses, they're behind NATs/firewalls, and they need to agree on media/data formats. WebRTC solves all of this.
ββββββββββββ ββββββββββββ
β Sender β β Receiver β
β (Browser)β β (Browser)β
ββββββ¬ββββββ ββββββ¬ββββββ
β β
β 1. Create RTCPeerConnection β
β 2. Create Data Channel β
β 3. Create Offer (SDP) β
β 4. Set Local Description β
β β
β ββββββββ Offer (via signaling) βββββββββββββββΊ β
β β
β 5. Set Remote Description β
β 6. Create Answer (SDP) β
β 7. Set Local Description β
β β
β βββββββββ Answer (via signaling) ββββββββββββ β
β β
β 8. Set Remote Description β
β β
β βββββββββ ICE Candidates (both ways) βββββββββΊ β
β β
β βββββββ Direct P2P Connection Established βββββββ β
β β
β βββββββββββ Data Channel Messages βββββββββββββΊ β
β (no server involved anymore!) β
SDP is a text format that describes a peer's capabilities: what codecs it supports, what media it wants to send/receive, network information, etc.
v=0
o=- 123456 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
a=ice-ufrag:abcd
a=ice-pwd:efghijklmnop
a=fingerprint:sha-256 AA:BB:CC:...
a=setup:actpass
a=sctp-port:5000
The offer says "here's what I can do." The answer says "here's what I'll do from your options."
ICE finds the best network path between peers. It gathers candidates β possible ways to reach the peer:
| Candidate Type | What It Is | When It's Used |
|---|---|---|
| host | Local IP address (e.g., 192.168.1.5) |
Both peers on same local network |
| srflx (Server Reflexive) | Public IP discovered via STUN | Peers behind simple NAT |
| relay | Traffic relayed via TURN server | When direct connection is impossible |
ICE tries each candidate and picks the one with the lowest latency that actually works.
-
STUN (Session Traversal Utilities for NAT): A lightweight server that tells you your public IP. Like asking "what's my address?" to someone outside your house. Free STUN servers exist (e.g., Google's
stun:stun.l.google.com:19302). -
TURN (Traversal Using Relays around NAT): A relay server that forwards traffic when direct connection fails (~10-15% of cases). Costs bandwidth because all data flows through the server.
Direct (preferred): Peer A ββββββββββββββββ Peer B
Via TURN (fallback): Peer A βββββ TURN βββββ Peer B
WebRTC does NOT define how peers exchange offers/answers. That's "signaling" and it's your responsibility. Common approaches:
| Method | Complexity | Use Case |
|---|---|---|
| Copy-paste | None | Learning / debugging |
| WebSocket | Low | Small apps, demos |
| HTTP polling | Low | Serverless-friendly |
| Firebase/Supabase | Medium | Production apps without custom server |
| Custom server | High | Full control, scalability |
This repo demonstrates copy-paste (manual mode) and WebSocket (auto mode).
Data channels are like WebSockets, but peer-to-peer. They support:
- Reliable (TCP-like): Guaranteed delivery, ordered. Good for chat, file transfer.
- Unreliable (UDP-like): Fast, no guarantees. Good for gaming, live telemetry.
// Sender creates the channel
const channel = pc.createDataChannel("chat", {
ordered: true, // guaranteed order (default)
// maxRetransmits: 3, // or set for partial reliability
});
// Receiver gets it via event
pc.ondatachannel = (event) => {
const channel = event.channel;
channel.onmessage = (e) => console.log(e.data);
};Uses vanilla ICE (all candidates are gathered first, then embedded in the SDP). The full SDP with candidates is copied as one blob. Simpler to understand, slightly slower.
Sender You (clipboard) Receiver
β β β
βββ create offer ββββΊ β β
β (wait for ICE) β β
βββ copy full SDP βββΊ βββ paste βββββββββββββΊ β
β β βββ create answer
β β β (wait for ICE)
β βββ paste βββββ β βββ copy full SDP ββ β
βββ set answer β β
βββββββββββββββ Connected (P2P) ββββββββββββββββ β
Uses trickle ICE (candidates are sent as they're discovered). Faster connection establishment.
Sender Signaling Server Receiver
β β β
βββ offer βββββββββΊ βββ forward βββββββββΊ β
β β βββ answer
β βββ forward ββ β βββ answer βββββ β
βββ ICE candidate βΊ βββ forward βββββββββΊ β
β βββ forward ββ β βββ ICE candidate ββ β
ββββββββββββββββ Connected (P2P) ββββββββββββββββ
β (server no longer needed) β
webrtc/
βββ sender.html # Manual mode: creates offer, sends data
βββ receiver.html # Manual mode: answers offer, receives data
βββ auto.html # Auto mode: WebSocket signaling, open in 2 tabs
βββ server.js # Signaling server (WebSocket + HTTP)
βββ package.json # Dependencies (ws)
βββ .gitignore
βββ README.md # You are here
- WebRTC API β MDN β The definitive API reference
- WebRTC.org β Official project site with samples
- RTCPeerConnection β MDN β Core API docs
- RTCDataChannel β MDN β Data channel API
- WebRTC for the Curious β Free online book, explains protocols deeply
- WebRTC Samples β Official code samples for every feature
- WebRTC Internals β Chrome's built-in debugging tool (paste in Chrome address bar)
- WebRTC Crash Course β Traversy Media
- WebRTC Full Course β freeCodeCamp
- Peerjs, WebRTC β Fireship
- High Performance Browser Networking β Ch.18 WebRTC β Ilya Grigorik's deep dive
- RFC 8825 β WebRTC Overview β The actual protocol spec
- ICE RFC 8445 β How ICE works under the hood
- SDP Anatomy β webrtchacks β Understand every line of an SDP
- Chrome:
chrome://webrtc-internals/β Real-time stats, ICE candidate graph, SDP viewer - Firefox:
about:webrtcβ Similar debugging dashboard - Trickle ICE β Test your STUN/TURN configuration
| Library | Use Case |
|---|---|
| PeerJS | Simple P2P with managed signaling |
| simple-peer | Lightweight WebRTC wrapper |
| mediasoup | SFU for video conferencing |
| Livekit | Production-grade WebRTC infrastructure |
| Socket.io | Popular choice for signaling servers |
To test between two devices (e.g., laptop + phone on the same Wi-Fi):
- Start the server β it will print your local network IP:
npm start # Output: # Local: http://localhost:8080/ # Network: http://192.168.1.42:8080/
- Open the Network URL on both devices
- First device becomes the sender, second becomes the receiver
Different networks? The code includes a free TURN relay server (Open Relay) as a fallback, so connections work even when direct P2P is blocked by NAT or firewalls.
| Scenario | STUN only | STUN + TURN (current setup) |
|---|---|---|
| Same device, two tabs | β | β |
| Same Wi-Fi, two devices | β (usually) | β |
| Different networks | β | β |
| Behind strict corporate firewall | β | β (TCP TURN) |
For production, use a dedicated TURN service like Metered, Twilio, or Cloudflare Calls instead of the free Open Relay.
| Pitfall | Solution |
|---|---|
| Offer/answer copied before ICE gathering completes | Wait for onicecandidate to fire with null (vanilla ICE), or use trickle ICE |
Data channel created after createOffer() |
Always create the data channel before calling createOffer() |
| No STUN servers configured | Add at least one STUN server to iceServers config |
| Answer never sent back to sender | Both sides need setLocalDescription AND setRemoteDescription |
| Works on localhost but not over network | You likely need a TURN server for production |
ICE failed error |
Firewall/NAT too restrictive β configure a TURN server |
TL;DR: Manual copy-paste mode may fail in Brave. Use auto mode (
node server.jsβlocalhost:8080) instead, or disable mDNS anonymization.
Brave anonymizes local IP addresses by replacing them with .local mDNS hostnames (e.g., 944e5bd0-6704-48b8-9f13-2e5fd9f8a5ee.local instead of 192.168.1.5). This is a privacy feature to prevent WebRTC IP leaks.
Why manual mode breaks: In vanilla ICE, all ICE candidates are embedded in the SDP blob you copy-paste. When the other tab receives the SDP, it can't resolve the .local mDNS addresses β so ICE connectivity checks fail and the connection goes checking β disconnected.
Why auto mode works: In trickle ICE (auto mode via server.js), candidates are exchanged individually through the signaling server. The browser can resolve .local candidates in real-time over the local network, so the connection succeeds.
Fixes for manual mode (pick one):
- Use auto mode β
node server.jsβ openhttp://localhost:8080in two tabs - Disable mDNS anonymization β navigate to
brave://flags/#enable-webrtc-hide-local-ips-with-mdnsβ set to Disabled β relaunch Brave - Lower Brave Shields β click the lion icon in the address bar β toggle Shields off for the page
Safari exposes real local IPs by default, so manual mode works there without issue. Chrome behaves similarly to Brave if WebRTC IP handling is set to restrict in
chrome://flags.
Once you're comfortable with data channels, try extending this project:
- Add video/audio streaming with
getUserMedia()and media tracks - Implement a file transfer system over data channels
- Add TURN server support for production NAT traversal
- Build a multi-peer mesh network (connect 3+ peers)
- Replace WebSocket signaling with Firebase Realtime Database
- Deploy the signaling server and test across different networks
MIT β Use this however you want. If it helped you learn, consider giving it a star.
Built with curiosity by Ariful Islam.
