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.

ByteFE
ByteFE
ByteFE
Building a 1‑to‑1 WebRTC Real‑Time Audio/Video Call in the Browser

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptreal-time communicationWebRTCSTUNRTCPeerConnectionTURN
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.