Skip to main content

Chapter 13: JavaScript WebRTC - Complete Guide to Peer-to-Peer Communication

WebRTC (Web Real-Time Communication) is a powerful technology that enables direct peer-to-peer communication between browsers without requiring intermediate servers for data transmission. It's essential for building real-time applications like video calls, audio chats, file sharing, and collaborative tools.

Why WebRTC is Essential in JavaScript

WebRTC in JavaScript is crucial because it:

  • Enables Direct Communication: Establishes peer-to-peer connections without server intermediaries
  • Supports Real-time Media: Handles audio, video, and data transmission with low latency
  • Reduces Server Load: Direct connections reduce bandwidth and processing requirements
  • Enables Rich Applications: Powers video calls, screen sharing, file transfers, and gaming
  • Provides High Quality: Optimized for real-time media with adaptive quality
  • Supports Multiple Platforms: Works across browsers and mobile devices

Learning Objectives

Through this chapter, you will master:

  • WebRTC connection establishment and signaling
  • Audio and video streaming
  • Data channel communication
  • Screen sharing and media capture
  • Connection management and error handling
  • Security considerations and best practices
  • Building real-time communication applications

WebRTC Basics

Understanding WebRTC Architecture

// WebRTC connection flow
class WebRTCConnection {
constructor() {
this.localPeerConnection = null;
this.remotePeerConnection = null;
this.localStream = null;
this.remoteStream = null;
this.signalingChannel = null;

this.initializePeerConnection();
}

initializePeerConnection() {
// Create RTCPeerConnection with configuration
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};

this.localPeerConnection = new RTCPeerConnection(configuration);
this.setupPeerConnectionHandlers();
}

setupPeerConnectionHandlers() {
// Handle ICE candidates
this.localPeerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.sendSignalingMessage({
type: 'ice-candidate',
candidate: event.candidate
});
}
};

// Handle remote stream
this.localPeerConnection.ontrack = (event) => {
this.remoteStream = event.streams[0];
this.displayRemoteStream();
};

// Handle connection state changes
this.localPeerConnection.onconnectionstatechange = () => {
console.log('Connection state:', this.localPeerConnection.connectionState);
};

// Handle ICE connection state changes
this.localPeerConnection.oniceconnectionstatechange = () => {
console.log('ICE connection state:', this.localPeerConnection.iceConnectionState);
};
}

sendSignalingMessage(message) {
// Send signaling message through WebSocket or other channel
if (this.signalingChannel) {
this.signalingChannel.send(JSON.stringify(message));
}
}
}

Basic Peer Connection Setup

// Simple WebRTC peer connection
class SimpleWebRTC {
constructor() {
this.peerConnection = null;
this.localStream = null;
this.remoteVideo = null;
}

async initialize() {
try {
// Get user media
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});

// Display local stream
this.displayLocalStream();

// Create peer connection
this.createPeerConnection();

} catch (error) {
console.error('Error accessing media devices:', error);
}
}

createPeerConnection() {
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};

this.peerConnection = new RTCPeerConnection(configuration);

// Add local stream to peer connection
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});

// Handle remote stream
this.peerConnection.ontrack = (event) => {
const remoteStream = event.streams[0];
this.displayRemoteStream(remoteStream);
};

// Handle ICE candidates
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
console.log('ICE candidate:', event.candidate);
// Send to remote peer through signaling server
}
};
}

displayLocalStream() {
const localVideo = document.getElementById('localVideo');
if (localVideo) {
localVideo.srcObject = this.localStream;
}
}

displayRemoteStream(stream) {
const remoteVideo = document.getElementById('remoteVideo');
if (remoteVideo) {
remoteVideo.srcObject = stream;
}
}

async createOffer() {
try {
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
return offer;
} catch (error) {
console.error('Error creating offer:', error);
}
}

async createAnswer() {
try {
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
return answer;
} catch (error) {
console.error('Error creating answer:', error);
}
}

async setRemoteDescription(description) {
try {
await this.peerConnection.setRemoteDescription(description);
} catch (error) {
console.error('Error setting remote description:', error);
}
}

async addIceCandidate(candidate) {
try {
await this.peerConnection.addIceCandidate(candidate);
} catch (error) {
console.error('Error adding ICE candidate:', error);
}
}
}

// Usage
const webrtc = new SimpleWebRTC();
webrtc.initialize();

Audio and Video Streaming

Media Capture and Display

class MediaManager {
constructor() {
this.localStream = null;
this.remoteStream = null;
this.constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
};
}

async startLocalMedia() {
try {
this.localStream = await navigator.mediaDevices.getUserMedia(this.constraints);
this.displayLocalMedia();
return this.localStream;
} catch (error) {
console.error('Error accessing media devices:', error);
throw error;
}
}

displayLocalMedia() {
const localVideo = document.getElementById('localVideo');
if (localVideo) {
localVideo.srcObject = this.localStream;
localVideo.muted = true; // Prevent echo
}
}

displayRemoteMedia(stream) {
this.remoteStream = stream;
const remoteVideo = document.getElementById('remoteVideo');
if (remoteVideo) {
remoteVideo.srcObject = stream;
}
}

// Toggle audio/video
toggleAudio() {
if (this.localStream) {
const audioTrack = this.localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
return audioTrack.enabled;
}
}
return false;
}

toggleVideo() {
if (this.localStream) {
const videoTrack = this.localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
return videoTrack.enabled;
}
}
return false;
}

// Change camera
async switchCamera() {
if (this.localStream) {
const videoTrack = this.localStream.getVideoTracks()[0];
if (videoTrack) {
const settings = videoTrack.getSettings();
const facingMode = settings.facingMode === 'user' ? 'environment' : 'user';

try {
const newStream = await navigator.mediaDevices.getUserMedia({
video: { ...this.constraints.video, facingMode }
});

// Replace video track
const newVideoTrack = newStream.getVideoTracks()[0];
const sender = this.peerConnection.getSenders().find(s =>
s.track && s.track.kind === 'video'
);

if (sender) {
await sender.replaceTrack(newVideoTrack);
}

// Stop old track
videoTrack.stop();

} catch (error) {
console.error('Error switching camera:', error);
}
}
}
}

stopLocalMedia() {
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
this.localStream = null;
}
}
}

// Usage
const mediaManager = new MediaManager();
mediaManager.startLocalMedia();

Advanced Media Controls

class AdvancedMediaControls {
constructor(peerConnection) {
this.peerConnection = peerConnection;
this.mediaStream = null;
this.audioContext = null;
this.audioAnalyser = null;
this.audioSource = null;
}

async setupAudioAnalysis() {
if (this.mediaStream) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.audioSource = this.audioContext.createMediaStreamSource(this.mediaStream);
this.audioAnalyser = this.audioContext.createAnalyser();

this.audioAnalyser.fftSize = 256;
this.audioSource.connect(this.audioAnalyser);

this.startAudioVisualization();
}
}

startAudioVisualization() {
const canvas = document.getElementById('audioVisualizer');
const ctx = canvas.getContext('2d');
const bufferLength = this.audioAnalyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);

const draw = () => {
requestAnimationFrame(draw);

this.audioAnalyser.getByteFrequencyData(dataArray);

ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, canvas.width, canvas.height);

const barWidth = (canvas.width / bufferLength) * 2.5;
let barHeight;
let x = 0;

for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i];

ctx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);

x += barWidth + 1;
}
};

draw();
}

// Audio level monitoring
getAudioLevel() {
if (this.audioAnalyser) {
const dataArray = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioAnalyser.getByteFrequencyData(dataArray);

let sum = 0;
for (let i = 0; i < dataArray.length; i++) {
sum += dataArray[i];
}

return sum / dataArray.length;
}
return 0;
}

// Video quality adjustment
adjustVideoQuality(quality) {
const videoTrack = this.mediaStream?.getVideoTracks()[0];
if (videoTrack) {
const constraints = {
width: { ideal: quality.width },
height: { ideal: quality.height },
frameRate: { ideal: quality.frameRate }
};

videoTrack.applyConstraints(constraints).catch(error => {
console.error('Error adjusting video quality:', error);
});
}
}

// Bandwidth adaptation
async adaptToBandwidth() {
const stats = await this.peerConnection.getStats();
let inboundStats = null;
let outboundStats = null;

stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
inboundStats = report;
} else if (report.type === 'outbound-rtp' && report.mediaType === 'video') {
outboundStats = report;
}
});

if (inboundStats && outboundStats) {
const bandwidth = this.calculateBandwidth(inboundStats, outboundStats);
this.adjustQualityBasedOnBandwidth(bandwidth);
}
}

calculateBandwidth(inboundStats, outboundStats) {
// Calculate available bandwidth based on stats
// This is a simplified calculation
return {
inbound: inboundStats.bytesReceived,
outbound: outboundStats.bytesSent
};
}

adjustQualityBasedOnBandwidth(bandwidth) {
// Adjust video quality based on available bandwidth
if (bandwidth.inbound < 100000) { // Low bandwidth
this.adjustVideoQuality({ width: 640, height: 480, frameRate: 15 });
} else if (bandwidth.inbound < 500000) { // Medium bandwidth
this.adjustVideoQuality({ width: 1280, height: 720, frameRate: 24 });
} else { // High bandwidth
this.adjustVideoQuality({ width: 1920, height: 1080, frameRate: 30 });
}
}
}

Data Channels

Basic Data Channel Communication

class DataChannelManager {
constructor(peerConnection) {
this.peerConnection = peerConnection;
this.dataChannel = null;
this.remoteDataChannel = null;
}

createDataChannel(label, options = {}) {
const defaultOptions = {
ordered: true,
maxRetransmits: 3
};

this.dataChannel = this.peerConnection.createDataChannel(label, {
...defaultOptions,
...options
});

this.setupDataChannelHandlers();
return this.dataChannel;
}

setupDataChannelHandlers() {
this.dataChannel.onopen = () => {
console.log('Data channel opened');
};

this.dataChannel.onmessage = (event) => {
console.log('Received message:', event.data);
this.handleMessage(event.data);
};

this.dataChannel.onclose = () => {
console.log('Data channel closed');
};

this.dataChannel.onerror = (error) => {
console.error('Data channel error:', error);
};
}

setupRemoteDataChannelHandler() {
this.peerConnection.ondatachannel = (event) => {
this.remoteDataChannel = event.channel;

this.remoteDataChannel.onopen = () => {
console.log('Remote data channel opened');
};

this.remoteDataChannel.onmessage = (event) => {
console.log('Received remote message:', event.data);
this.handleMessage(event.data);
};
};
}

sendMessage(message) {
if (this.dataChannel && this.dataChannel.readyState === 'open') {
this.dataChannel.send(message);
} else {
console.warn('Data channel not ready');
}
}

sendJsonMessage(data) {
this.sendMessage(JSON.stringify(data));
}

handleMessage(data) {
try {
const message = JSON.parse(data);
this.processMessage(message);
} catch (error) {
// Handle non-JSON messages
console.log('Received text message:', data);
}
}

processMessage(message) {
switch (message.type) {
case 'chat':
this.displayChatMessage(message);
break;
case 'file':
this.handleFileTransfer(message);
break;
case 'control':
this.handleControlMessage(message);
break;
default:
console.log('Unknown message type:', message.type);
}
}

displayChatMessage(message) {
const chatContainer = document.getElementById('chatContainer');
const messageElement = document.createElement('div');
messageElement.textContent = `${message.user}: ${message.text}`;
chatContainer.appendChild(messageElement);
}

handleFileTransfer(message) {
// Handle file transfer through data channel
console.log('File transfer:', message);
}

handleControlMessage(message) {
// Handle control messages (play, pause, etc.)
console.log('Control message:', message);
}
}

// Usage
const dataChannelManager = new DataChannelManager(peerConnection);
dataChannelManager.createDataChannel('chat');
dataChannelManager.setupRemoteDataChannelHandler();

// Send chat message
dataChannelManager.sendJsonMessage({
type: 'chat',
user: 'John',
text: 'Hello!'
});

File Transfer via Data Channel

class FileTransferManager {
constructor(dataChannel) {
this.dataChannel = dataChannel;
this.chunkSize = 16384; // 16KB chunks
this.transfers = new Map();
}

sendFile(file) {
const transferId = Date.now().toString();
const fileInfo = {
id: transferId,
name: file.name,
size: file.size,
type: file.type,
chunks: Math.ceil(file.size / this.chunkSize),
sentChunks: 0
};

this.transfers.set(transferId, fileInfo);

// Send file info
this.dataChannel.send(JSON.stringify({
type: 'file_info',
...fileInfo
}));

// Send file chunks
this.sendFileChunks(file, transferId);
}

async sendFileChunks(file, transferId) {
const fileInfo = this.transfers.get(transferId);
let offset = 0;

while (offset < file.size) {
const chunk = file.slice(offset, offset + this.chunkSize);
const arrayBuffer = await chunk.arrayBuffer();

// Send chunk
this.dataChannel.send(JSON.stringify({
type: 'file_chunk',
transferId: transferId,
chunkIndex: Math.floor(offset / this.chunkSize),
data: Array.from(new Uint8Array(arrayBuffer))
}));

offset += this.chunkSize;
fileInfo.sentChunks++;

// Update progress
this.updateProgress(transferId, fileInfo.sentChunks, fileInfo.chunks);

// Small delay to prevent overwhelming the channel
await new Promise(resolve => setTimeout(resolve, 10));
}

// Send completion message
this.dataChannel.send(JSON.stringify({
type: 'file_complete',
transferId: transferId
}));
}

handleFileInfo(fileInfo) {
const transferId = fileInfo.id;
this.transfers.set(transferId, {
...fileInfo,
receivedChunks: 0,
chunks: new Array(fileInfo.chunks)
});

console.log(`Receiving file: ${fileInfo.name} (${fileInfo.size} bytes)`);
}

handleFileChunk(chunkData) {
const transferId = chunkData.transferId;
const transfer = this.transfers.get(transferId);

if (transfer) {
transfer.chunks[chunkData.chunkIndex] = new Uint8Array(chunkData.data);
transfer.receivedChunks++;

this.updateProgress(transferId, transfer.receivedChunks, transfer.chunks.length);

if (transfer.receivedChunks === transfer.chunks.length) {
this.reconstructFile(transfer);
}
}
}

reconstructFile(transfer) {
const totalSize = transfer.size;
const reconstructed = new Uint8Array(totalSize);
let offset = 0;

for (let i = 0; i < transfer.chunks.length; i++) {
const chunk = transfer.chunks[i];
reconstructed.set(chunk, offset);
offset += chunk.length;
}

// Create blob and download
const blob = new Blob([reconstructed], { type: transfer.type });
const url = URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = transfer.name;
a.click();

URL.revokeObjectURL(url);
this.transfers.delete(transfer.id);

console.log(`File received: ${transfer.name}`);
}

updateProgress(transferId, current, total) {
const progress = (current / total) * 100;
console.log(`Transfer ${transferId}: ${progress.toFixed(1)}%`);
}
}

// Usage
const fileTransferManager = new FileTransferManager(dataChannel);

// Send file
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
fileTransferManager.sendFile(file);
}
});

Screen Sharing

Screen Capture and Sharing

class ScreenShareManager {
constructor(peerConnection) {
this.peerConnection = peerConnection;
this.screenStream = null;
this.isSharing = false;
}

async startScreenShare() {
try {
// Request screen capture
this.screenStream = await navigator.mediaDevices.getDisplayMedia({
video: {
mediaSource: 'screen',
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 }
},
audio: true
});

// Display local screen share
this.displayScreenShare();

// Add screen stream to peer connection
this.addScreenStreamToPeerConnection();

// Handle screen share end
this.screenStream.getVideoTracks()[0].onended = () => {
this.stopScreenShare();
};

this.isSharing = true;
return this.screenStream;

} catch (error) {
console.error('Error starting screen share:', error);
throw error;
}
}

displayScreenShare() {
const screenVideo = document.getElementById('screenVideo');
if (screenVideo) {
screenVideo.srcObject = this.screenStream;
}
}

addScreenStreamToPeerConnection() {
if (this.peerConnection && this.screenStream) {
// Remove existing screen tracks
const senders = this.peerConnection.getSenders();
senders.forEach(sender => {
if (sender.track && sender.track.kind === 'video') {
this.peerConnection.removeTrack(sender);
}
});

// Add screen stream tracks
this.screenStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.screenStream);
});
}
}

stopScreenShare() {
if (this.screenStream) {
this.screenStream.getTracks().forEach(track => track.stop());
this.screenStream = null;
}

this.isSharing = false;

// Remove screen tracks from peer connection
const senders = this.peerConnection.getSenders();
senders.forEach(sender => {
if (sender.track && sender.track.kind === 'video') {
this.peerConnection.removeTrack(sender);
}
});
}

async switchToCamera() {
if (this.isSharing) {
this.stopScreenShare();
}

try {
const cameraStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});

// Add camera stream to peer connection
cameraStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, cameraStream);
});

return cameraStream;
} catch (error) {
console.error('Error switching to camera:', error);
throw error;
}
}

async shareApplication() {
try {
const appStream = await navigator.mediaDevices.getDisplayMedia({
video: {
mediaSource: 'application'
},
audio: true
});

this.addScreenStreamToPeerConnection();
return appStream;
} catch (error) {
console.error('Error sharing application:', error);
throw error;
}
}

async shareWindow() {
try {
const windowStream = await navigator.mediaDevices.getDisplayMedia({
video: {
mediaSource: 'window'
},
audio: true
});

this.addScreenStreamToPeerConnection();
return windowStream;
} catch (error) {
console.error('Error sharing window:', error);
throw error;
}
}
}

// Usage
const screenShareManager = new ScreenShareManager(peerConnection);

// Start screen share
document.getElementById('shareScreen').addEventListener('click', async () => {
try {
await screenShareManager.startScreenShare();
} catch (error) {
console.error('Screen share failed:', error);
}
});

// Stop screen share
document.getElementById('stopShare').addEventListener('click', () => {
screenShareManager.stopScreenShare();
});

Complete Video Call Application

class VideoCallApp {
constructor() {
this.peerConnection = null;
this.localStream = null;
this.remoteStream = null;
this.signalingChannel = null;
this.dataChannel = null;
this.isInitiator = false;
this.roomId = null;

this.initializeApp();
}

async initializeApp() {
await this.setupSignaling();
await this.setupMedia();
this.setupUI();
}

async setupSignaling() {
// WebSocket signaling channel
this.signalingChannel = new WebSocket('ws://localhost:8080');

this.signalingChannel.onopen = () => {
console.log('Signaling channel connected');
};

this.signalingChannel.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleSignalingMessage(message);
};
}

async setupMedia() {
try {
this.localStream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: true
});

this.displayLocalVideo();
} catch (error) {
console.error('Error accessing media:', error);
}
}

setupUI() {
// Setup UI event listeners
document.getElementById('startCall').addEventListener('click', () => {
this.startCall();
});

document.getElementById('endCall').addEventListener('click', () => {
this.endCall();
});

document.getElementById('muteAudio').addEventListener('click', () => {
this.toggleAudio();
});

document.getElementById('muteVideo').addEventListener('click', () => {
this.toggleVideo();
});

document.getElementById('shareScreen').addEventListener('click', () => {
this.shareScreen();
});
}

async startCall() {
this.roomId = document.getElementById('roomId').value || 'default-room';
this.isInitiator = true;

await this.createPeerConnection();
this.createDataChannel();

// Join room
this.signalingChannel.send(JSON.stringify({
type: 'join',
roomId: this.roomId
}));
}

async createPeerConnection() {
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};

this.peerConnection = new RTCPeerConnection(configuration);

// Add local stream
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});

// Handle remote stream
this.peerConnection.ontrack = (event) => {
this.remoteStream = event.streams[0];
this.displayRemoteVideo();
};

// Handle ICE candidates
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.signalingChannel.send(JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate,
roomId: this.roomId
}));
}
};

// Handle connection state
this.peerConnection.onconnectionstatechange = () => {
console.log('Connection state:', this.peerConnection.connectionState);
this.updateConnectionStatus();
};
}

createDataChannel() {
this.dataChannel = this.peerConnection.createDataChannel('chat');

this.dataChannel.onopen = () => {
console.log('Data channel opened');
};

this.dataChannel.onmessage = (event) => {
this.handleChatMessage(event.data);
};

// Handle remote data channel
this.peerConnection.ondatachannel = (event) => {
const channel = event.channel;
channel.onmessage = (event) => {
this.handleChatMessage(event.data);
};
};
}

async handleSignalingMessage(message) {
switch (message.type) {
case 'user-joined':
if (this.isInitiator) {
await this.createOffer();
}
break;

case 'offer':
await this.handleOffer(message.offer);
break;

case 'answer':
await this.handleAnswer(message.answer);
break;

case 'ice-candidate':
await this.handleIceCandidate(message.candidate);
break;
}
}

async createOffer() {
try {
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);

this.signalingChannel.send(JSON.stringify({
type: 'offer',
offer: offer,
roomId: this.roomId
}));
} catch (error) {
console.error('Error creating offer:', error);
}
}

async handleOffer(offer) {
try {
await this.peerConnection.setRemoteDescription(offer);
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);

this.signalingChannel.send(JSON.stringify({
type: 'answer',
answer: answer,
roomId: this.roomId
}));
} catch (error) {
console.error('Error handling offer:', error);
}
}

async handleAnswer(answer) {
try {
await this.peerConnection.setRemoteDescription(answer);
} catch (error) {
console.error('Error handling answer:', error);
}
}

async handleIceCandidate(candidate) {
try {
await this.peerConnection.addIceCandidate(candidate);
} catch (error) {
console.error('Error handling ICE candidate:', error);
}
}

displayLocalVideo() {
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = this.localStream;
localVideo.muted = true;
}

displayRemoteVideo() {
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = this.remoteStream;
}

toggleAudio() {
const audioTrack = this.localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
this.updateAudioButton(audioTrack.enabled);
}
}

toggleVideo() {
const videoTrack = this.localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
this.updateVideoButton(videoTrack.enabled);
}
}

async shareScreen() {
try {
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true
});

// Replace video track
const videoTrack = screenStream.getVideoTracks()[0];
const sender = this.peerConnection.getSenders().find(s =>
s.track && s.track.kind === 'video'
);

if (sender) {
await sender.replaceTrack(videoTrack);
}

// Handle screen share end
videoTrack.onended = () => {
this.switchBackToCamera();
};

} catch (error) {
console.error('Error sharing screen:', error);
}
}

async switchBackToCamera() {
try {
const cameraStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});

const videoTrack = cameraStream.getVideoTracks()[0];
const sender = this.peerConnection.getSenders().find(s =>
s.track && s.track.kind === 'video'
);

if (sender) {
await sender.replaceTrack(videoTrack);
}
} catch (error) {
console.error('Error switching back to camera:', error);
}
}

sendChatMessage(message) {
if (this.dataChannel && this.dataChannel.readyState === 'open') {
this.dataChannel.send(JSON.stringify({
type: 'chat',
message: message,
timestamp: Date.now()
}));
}
}

handleChatMessage(data) {
const message = JSON.parse(data);
if (message.type === 'chat') {
this.displayChatMessage(message);
}
}

displayChatMessage(message) {
const chatContainer = document.getElementById('chatContainer');
const messageElement = document.createElement('div');
messageElement.textContent = `${new Date(message.timestamp).toLocaleTimeString()}: ${message.message}`;
chatContainer.appendChild(messageElement);
}

updateConnectionStatus() {
const statusElement = document.getElementById('connectionStatus');
statusElement.textContent = this.peerConnection.connectionState;
}

updateAudioButton(enabled) {
const button = document.getElementById('muteAudio');
button.textContent = enabled ? 'Mute' : 'Unmute';
}

updateVideoButton(enabled) {
const button = document.getElementById('muteVideo');
button.textContent = enabled ? 'Hide Video' : 'Show Video';
}

endCall() {
if (this.peerConnection) {
this.peerConnection.close();
}

if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
}

if (this.signalingChannel) {
this.signalingChannel.close();
}
}
}

// Initialize the app
const videoCallApp = new VideoCallApp();

Best Practices

1. Error Handling

// Good: Comprehensive error handling
class RobustWebRTC {
constructor() {
this.peerConnection = null;
this.retryCount = 0;
this.maxRetries = 3;
}

async createPeerConnection() {
try {
this.peerConnection = new RTCPeerConnection(this.getConfiguration());
this.setupEventHandlers();
} catch (error) {
console.error('Failed to create peer connection:', error);
this.handleConnectionError(error);
}
}

handleConnectionError(error) {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
setTimeout(() => {
this.createPeerConnection();
}, 1000 * this.retryCount);
} else {
console.error('Max retries reached');
this.notifyUser('Connection failed. Please try again.');
}
}

getConfiguration() {
return {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
],
iceCandidatePoolSize: 10
};
}
}

// Avoid: No error handling
function badCreateConnection() {
const pc = new RTCPeerConnection();
// No error handling - bad practice
return pc;
}

2. Resource Management

// Good: Proper resource cleanup
class ResourceManager {
constructor() {
this.streams = [];
this.connections = [];
}

addStream(stream) {
this.streams.push(stream);
}

addConnection(connection) {
this.connections.push(connection);
}

cleanup() {
// Stop all streams
this.streams.forEach(stream => {
stream.getTracks().forEach(track => track.stop());
});

// Close all connections
this.connections.forEach(connection => {
connection.close();
});

// Clear arrays
this.streams = [];
this.connections = [];
}
}

// Avoid: Memory leaks
function badResourceManagement() {
const stream = navigator.mediaDevices.getUserMedia({ video: true });
// No cleanup - memory leak
}

3. Security Considerations

// Good: Secure WebRTC implementation
class SecureWebRTC {
constructor() {
this.allowedOrigins = ['https://yourdomain.com'];
this.maxConnections = 5;
this.connectionCount = 0;
}

validateOrigin(origin) {
return this.allowedOrigins.includes(origin);
}

canCreateConnection() {
return this.connectionCount < this.maxConnections;
}

createSecureConnection() {
if (!this.canCreateConnection()) {
throw new Error('Maximum connections reached');
}

const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
],
iceTransportPolicy: 'all',
bundlePolicy: 'balanced',
rtcpMuxPolicy: 'require'
};

return new RTCPeerConnection(configuration);
}
}

Summary

WebRTC is essential for real-time peer-to-peer communication:

  • Connection Management: Establish and maintain peer-to-peer connections
  • Media Streaming: Handle audio, video, and screen sharing
  • Data Channels: Enable real-time data exchange
  • Signaling: Coordinate connection establishment through signaling servers
  • Error Handling: Implement robust error handling and reconnection
  • Security: Follow security best practices for WebRTC applications
  • Performance: Optimize for low latency and high quality

Mastering WebRTC enables you to build sophisticated real-time communication applications that provide excellent user experiences through direct peer-to-peer connections.


This tutorial is part of the JavaScript Mastery series by syscook.dev