WebRTC Video Calls Implementation

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementing WebRTC for Video Calls on Website

WebRTC not just "add video calls". Complete protocol suite: ICE, STUN, TURN, SDP-negotiation, DTLS-SRTP and media pipeline via getUserMedia. Without understanding each component, get calls working only local network or failing behind NAT.

WebRTC Stack Components

Browser provides RTCPeerConnection — central object managing everything: ICE candidates, media streams, encryption. Needs signaling server — WebRTC doesn't define how two clients exchange SDP offers. Separate task.

Typical production stack:

Component Options
Signaling server Socket.IO, WebSocket (Go/Node), Phoenix Channels
ICE/STUN coturn, Twilio STUN, Google STUN
TURN server coturn on VPS, Twilio TURN, Xirsys
Media server (SFU) mediasoup, Janus, LiveKit, Jitsi Videobridge
Client library native RTCPeerConnection or simple-peer, mediasoup-client

For P2P calls (up to 4 participants) SFU unnecessary. At 5+ participants mesh topology creates n(n-1)/2 connections per person — unacceptable. Need SFU.

P2P Call Architecture

Alice                Signal Server              Bob
  |------ offer SDP -------->|                   |
  |                          |------ offer SDP ->|
  |<----- answer SDP --------|                   |
  |                          |<---- answer SDP --|
  |<========= ICE candidates exchange =========>|
  |<============== DTLS handshake ==============>|
  |<======= encrypted RTP/RTCP media stream ====>|

Each browser collects ICE candidates: host (local IP), srflx (public IP via STUN), relay (via TURN). TURN relay needed ~15–20% connections — corporate firewalls, symmetric NAT.

ICE and TURN: Why Nothing Works Without

STUN answers "what's my external IP?". TURN proxies media when direct connection impossible. coturn — standard self-hosted choice:

# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
realm=yourdomain.com
server-name=yourdomain.com
lt-cred-mech
use-auth-secret
static-auth-secret=YOUR_SECRET
total-quota=100
bps-capacity=0
stale-nonce=600
cert=/etc/letsencrypt/live/yourdomain.com/fullchain.pem
pkey=/etc/letsencrypt/live/yourdomain.com/privkey.pem

TURN via TLS on 443 port bypasses most corporate restrictions.

Signaling Server on Node.js + Socket.IO

io.on('connection', (socket) => {
  socket.on('join-room', (roomId, userId) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-connected', userId);

    socket.on('offer', (offer, targetId) => {
      io.to(targetId).emit('offer', offer, socket.id);
    });

    socket.on('answer', (answer, targetId) => {
      io.to(targetId).emit('answer', answer, socket.id);
    });

    socket.on('ice-candidate', (candidate, targetId) => {
      io.to(targetId).emit('ice-candidate', candidate, socket.id);
    });

    socket.on('disconnect', () => {
      socket.to(roomId).emit('user-disconnected', userId);
    });
  });
});

RTCPeerConnection on Client

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.yourdomain.com:3478' },
    {
      urls: 'turn:turn.yourdomain.com:3478',
      username: generateTurnUsername(ttl),
      credential: generateTurnCredential(username, secret),
    },
  ],
  iceTransportPolicy: 'all', // 'relay' to force TURN
});

// Add media
const stream = await navigator.mediaDevices.getUserMedia({
  video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } },
  audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 48000 },
});

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

// Negotiation
pc.onicecandidate = ({ candidate }) => {
  if (candidate) socket.emit('ice-candidate', candidate, targetId);
};

pc.onnegotiationneeded = async () => {
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  socket.emit('offer', offer, targetId);
};

Codecs and Quality

Browsers negotiate codecs via SDP. Video: VP8, VP9 or H.264; audio: Opus. Force preferred codec via SDP manipulation:

function preferCodec(sdp, codecName) {
  const lines = sdp.split('\n');
  // Find PT and move to start of m= section
  // ...
  return lines.join('\n');
}

const offer = await pc.createOffer();
offer.sdp = preferCodec(offer.sdp, 'VP9'); // VP9 — best quality per bitrate
await pc.setLocalDescription(offer);

Adaptive bitrate via RTCRtpSender.setParameters:

const sender = pc.getSenders().find(s => s.track.kind === 'video');
const params = sender.getParameters();
params.encodings[0].maxBitrate = 800000; // 800 kbps
await sender.setParameters(params);

Simulcast for Scalable Conferences

With SFU (mediasoup, LiveKit) use Simulcast — client sends multiple streams different resolutions:

pc.addTransceiver(videoTrack, {
  direction: 'sendonly',
  sendEncodings: [
    { rid: 'low',  maxBitrate: 150000, scaleResolutionDownBy: 4 },
    { rid: 'mid',  maxBitrate: 500000, scaleResolutionDownBy: 2 },
    { rid: 'high', maxBitrate: 1500000 },
  ],
});

SFU picks needed layer per receiver by bandwidth.

Recording Calls

Server-side recording via SFU preferable. Client-side:

const recorder = new MediaRecorder(stream, {
  mimeType: 'video/webm;codecs=vp9,opus',
  videoBitsPerSecond: 2500000,
});

const chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => {
  const blob = new Blob(chunks, { type: 'video/webm' });
  uploadToServer(blob);
};

recorder.start(1000);

Diagnostics and Monitoring

getStats() — main debugging tool:

setInterval(async () => {
  const stats = await pc.getStats();
  stats.forEach(report => {
    if (report.type === 'inbound-rtp' && report.kind === 'video') {
      console.log({
        packetsLost: report.packetsLost,
        jitter: report.jitter,
        framesDecoded: report.framesDecoded,
        framesPerSecond: report.framesPerSecond,
      });
    }
  });
}, 2000);

For production monitoring — Datadog WebRTC or open webrtc-internals (chrome://webrtc-internals) during debug.

Timeline and Effort

  • P2P video call with signaling server — 3–5 days (two participants, basic controls)
  • Group calls via SFU (mediasoup/LiveKit) — 2–3 weeks (server setup, scaling, rooms)
  • Recording + postprocessing — plus 1 week
  • Full conference platform (rooms, chat, screenshare, recording) — 6–10 weeks

Compatibility

WebRTC supported all modern browsers. Safari supports from version 11, but has limitations: no Insertable Streams support in older versions, renegotiation issues. iOS requires native app or PWA — browser WebRTC on iOS Safari works from version 14.5.