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
Full-Featured Video Call System
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