Building a 1‑to‑1 WebRTC Real‑Time Audio/Video Call in the Browser
This article explains how to create a browser‑based 1‑to‑1 real‑time audio/video communication application using WebRTC APIs, covering media capture, SDP and ICE handling, signaling with socket.io, peer‑to‑peer connection setup, data channels, and NAT traversal techniques.
WebRTC provides a standard API that enables real‑time audio and video communication directly in web browsers; most browsers and operating systems support it, allowing developers to build 1‑to‑1 calls without external plugins.
Audio/Video Capture
The getUserMedia API obtains a MediaStream from the user's camera and microphone, which can be assigned to a video element's srcObject for local playback.
API:navigator.mediaDevices.getUserMedia
Parameters:constraints
Returns:Promise that resolves with a MediaStream object.
const localVideo = document.querySelector("video");
function gotLocalMediaStream(mediaStream) {
localVideo.srcObject = mediaStream;
}
navigator.mediaDevices
.getUserMedia({
video: {
width: 640,
height: 480,
frameRate: 15,
facingMode: 'enviroment', // use rear camera
deviceId: deviceId ? {exact: deviceId} : undefined
},
audio: false
})
.then(gotLocalMediaStream)
.catch(error => console.log("navigator.getUserMedia error: ", error));Connection Management
WebRTC uses RTCPeerConnection to manage network connections, media, and data. Key components include SDP (session description), ICE (candidate gathering), and STUN/TURN servers.
SDP (RTCSessionDescription)
SDP describes capabilities such as codecs and transport protocols. It consists of session‑level and media‑level sections as defined in RFC4566.
Session description (session level)
v= (protocol version)
o= (originator and session identifier)
s= (session name)
c=* (connection information)
t= (time the session is active)
a=* (session attributes)
Media description (media level)
m= (media name and transport address)
c=* (optional connection information)
a=* (media attributes)ICE Candidates (RTCIceCandidate)
ICE gathers possible network paths (host, srflx, relay) to traverse NATs. The onicecandidate event sends each candidate to the remote peer via a signaling server, and addIceCandidate adds received candidates.
API: pc.onicecandidate = eventHandler
pc.onicecandidate = function(event) {
if (event.candidate) {
// Send the candidate to the remote peer
} else {
// All ICE candidates have been sent
}
}
API: pc.addIceCandidate
pc.addIceCandidate(candidate).then(_ => {
// Candidate added successfully
}).catch(e => {
console.log("Error: Failure during addIceCandidate()");
});Signaling Server
SDP and ICE information are exchanged through a signaling server, commonly built with socket.io for real‑time messaging.
var express = require("express");
var app = express();
var http = require("http");
const { Server } = require("socket.io");
const httpServer = http.createServer(app);
const io = new Server(httpServer);
io.on("connection", (socket) => {
console.log("a user connected");
socket.on("message", (room, data) => {
socket.to(room).emit("message", room, data);
});
socket.on("join", (room) => {
socket.join(room);
});
});Peer‑to‑Peer Connection Flow
1. Peer A creates an RTCPeerConnection with STUN/TURN server configuration.
var pcConfig = {
iceServers: [
{
urls: "turn:stun.al.learningrtc.cn:3478",
credential: "mypasswd",
username: "garrylea"
},
{
urls: ["stun:stun.example.com", "stun:stun-1.example.com"]
}
]
};
pc = new RTCPeerConnection(pcConfig);2. A calls createOffer , sets the local description, and sends the SDP offer via the signaling server.
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendMessage(roomid, offer);3. B receives the offer, sets it as remote description, creates an answer, and sends it back.
await pc.setRemoteDescription(new RTCSessionDescription(e.data));
const answer = await pc.createAnswer();
sendMessage(roomid, answer);
await pc.setLocalDescription(answer);4. Both peers exchange ICE candidates (via onicecandidate and addIceCandidate ) until a viable path is found.
5. Media tracks are added with addTrack and received via the ontrack event.
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
pc.ontrack = (e) => {
if (e && e.streams) {
remoteVideo.srcObject = e.streams[0];
}
};Bidirectional Data Channel
A data channel can be created with pc.createDataChannel for low‑latency, server‑less communication.
const dc = pc.createDataChannel("chat");
dc.onmessage = receivemsg;
dc.onopen = () => console.log("datachannel open");
dc.onclose = () => console.log("datachannel close");
pc.ondatachannel = (e) => {
if (!dc) {
dc = e.channel;
dc.onmessage = receivemsg;
dc.onopen = dataChannelStateChange;
dc.onclose = dataChannelStateChange;
}
};NAT and ICE Supplement
NAT devices hide internal addresses; STUN discovers the public address, while TURN relays traffic when direct paths fail. ICE gathers three candidate types: host (local), srflx (server‑reflexive), and relay (TURN).
{
IP: xxx.xxx.xxx.xxx,
port: number,
type: host/srflx/relay,
priority: number,
protocol: UDP/TCP,
usernameFragment: string
}Result
The tutorial culminates in a functional demo where two browsers can exchange audio, video, and arbitrary data without external plugins.
Demo repository: https://code.byted.org/yuanyuan.wallace/WebRTC/tree/master/demo
References
MDN WebRTC API documentation, GitHub WebRTC‑P2P example, and related RFC4566 specifications.
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.