What Is WebRTC?
WebRTC (Web Real-Time Communication) is a free, open-source technology that enables peer-to-peer audio, video, and data communication directly inside web browsers without requiring any plugins, native apps, or third-party media servers. It is built into modern browsers like Chrome, Firefox, Safari, and Edge, and is standardized by the W3C and IETF.
At its core, WebRTC provides three primary APIs:
- getUserMedia β captures audio and video from the user's camera and microphone
- RTCPeerConnection β establishes and manages the peer-to-peer connection between two endpoints
- RTCDataChannel β enables arbitrary data to be sent directly between peers with low latency
For a video chat application, we primarily leverage the first two APIs to capture media streams and transmit them between participants in real time.
Why WebRTC Matters for Video Chat
π Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before WebRTC, building a real-time video chat application meant relying on proprietary protocols, browser plugins like Flash, or heavy native SDKs that required installation. WebRTC changed everything:
- Zero Installation β Users simply open a browser, grant camera permission, and the call begins
- Ultra-Low Latency β Peer-to-peer connections minimize delay because media travels directly between participants rather than routing through an intermediary server
- Privacy by Design β Media streams are encrypted end-to-end using DTLS-SRTP, meaning even signaling servers cannot access the audio or video content
- Cross-Platform β The same WebRTC code works across desktop and mobile browsers, and native libraries exist for iOS and Android
- Cost Efficiency β Since media flows peer-to-peer, you avoid the bandwidth costs associated with relaying streams through centralized media servers (for small group calls)
- Data Channels β Beyond video, you can send text, files, or game state data over the same peer connection
These advantages make WebRTC the de facto standard for modern video conferencing apps, telehealth platforms, remote collaboration tools, and live broadcasting solutions.
Core Architecture of a WebRTC Video Chat App
Understanding the architecture before diving into code is essential. A WebRTC video chat application consists of three main components:
1. The Signaling Server
WebRTC does not define how peers discover each other or negotiate the session. This is handled by a signaling server that relays session description metadata (SDP) and ICE candidates between peers. The signaling server can be built using WebSockets, HTTP long polling, or any bidirectional communication protocol. Importantly, the signaling server never touches the media streamsβit only exchanges control messages.
2. STUN and TURN Servers
Peer-to-peer connections face challenges when devices sit behind NATs or firewalls. STUN (Session Traversal Utilities for NAT) servers help peers discover their public IP address. When direct peer-to-peer communication fails (typically in 8-10% of calls), a TURN (Traversal Using Relays around NAT) server relays media traffic as a fallback. For production applications, you should deploy your own TURN server (using tools like Coturn) or use a reliable third-party provider.
3. RTCPeerConnection
This is the browser API that manages the entire peer-to-peer connection lifecycle: codec negotiation, media transport, encryption, bandwidth adaptation, and more. It takes the SDP offers and answers created via signaling and processes ICE candidates to establish the optimal network path between peers.
Step-by-Step Implementation
We will build a complete one-on-one video chat application with a Node.js signaling server and a vanilla HTML/JavaScript client. The code is production-ready in structure and covers all essential concepts.
Project Structure
Create a new directory with the following files:
video-chat-app/
βββ server.js # Node.js signaling server
βββ package.json # Dependencies
βββ public/
βββ index.html # Client-side application
Step 1: Initialize the Project and Install Dependencies
Create package.json:
{
"name": "webrtc-video-chat",
"version": "1.0.0",
"description": "A WebRTC video chat application",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"ws": "^8.14.2",
"uuid": "^9.0.0"
}
}
Run npm install to install Express, the WebSocket library (ws), and uuid for generating unique peer identifiers.
Step 2: Build the Signaling Server (server.js)
The signaling server handles three critical message types: offer, answer, and ice-candidate. It also manages room membership so two peers can find each other. Here is the complete server code:
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// Serve static files from the public directory
app.use(express.static('public'));
// Store active rooms and their participants
const rooms = new Map();
// Map each WebSocket connection to a client object
const clients = new Map();
wss.on('connection', (ws) => {
const clientId = uuidv4();
clients.set(ws, { id: clientId, roomId: null });
console.log(`Client connected: ${clientId}`);
ws.on('message', (message) => {
try {
const data = JSON.parse(message.toString());
handleMessage(ws, data);
} catch (err) {
console.error('Invalid message format:', err.message);
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
}
});
ws.on('close', () => {
const client = clients.get(ws);
if (client && client.roomId) {
leaveRoom(ws, client.roomId);
}
clients.delete(ws);
console.log(`Client disconnected: ${clientId}`);
});
ws.on('error', (error) => {
console.error(`WebSocket error for client ${clientId}:`, error.message);
});
});
function handleMessage(ws, data) {
const client = clients.get(ws);
const { type, roomId } = data;
switch (type) {
case 'join-room': {
const requestedRoomId = data.roomId || uuidv4().substring(0, 6);
joinRoom(ws, requestedRoomId);
break;
}
case 'offer': {
forwardToRoom(ws, data, 'offer');
break;
}
case 'answer': {
forwardToRoom(ws, data, 'answer');
break;
}
case 'ice-candidate': {
forwardToRoom(ws, data, 'ice-candidate');
break;
}
case 'hang-up': {
if (client && client.roomId) {
leaveRoom(ws, client.roomId);
}
break;
}
default: {
ws.send(JSON.stringify({
type: 'error',
message: `Unknown message type: ${type}`
}));
}
}
}
function joinRoom(ws, roomId) {
const client = clients.get(ws);
client.roomId = roomId;
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
}
const room = rooms.get(roomId);
const participantCount = room.size;
// Limit rooms to 2 participants for one-on-one chat
if (participantCount >= 2) {
ws.send(JSON.stringify({
type: 'room-full',
message: 'Room is full (max 2 participants)'
}));
return;
}
room.add(ws);
console.log(`Client ${client.id} joined room ${roomId} (${room.size} participants)`);
// Notify the joining client that they are connected
ws.send(JSON.stringify({
type: 'joined-room',
roomId: roomId,
participantCount: room.size
}));
// If there are now 2 participants, tell the new peer to create an offer
if (room.size === 2) {
// Find the other participant (not the one that just joined)
for (const peerWs of room) {
if (peerWs !== ws) {
// Tell the newly joined peer to initiate the connection
ws.send(JSON.stringify({
type: 'peer-connected',
peerId: clients.get(peerWs).id,
initiator: true
}));
// Tell the existing peer that someone joined
peerWs.send(JSON.stringify({
type: 'peer-connected',
peerId: client.id,
initiator: false
}));
}
}
}
}
function leaveRoom(ws, roomId) {
const client = clients.get(ws);
if (!roomId || !rooms.has(roomId)) return;
const room = rooms.get(roomId);
room.delete(ws);
console.log(`Client ${client.id} left room ${roomId}`);
// Notify remaining peers about the departure
for (const peerWs of room) {
peerWs.send(JSON.stringify({
type: 'peer-disconnected',
peerId: client.id
}));
}
// Clean up empty rooms
if (room.size === 0) {
rooms.delete(roomId);
console.log(`Room ${roomId} deleted (empty)`);
}
}
function forwardToRoom(ws, data, messageType) {
const client = clients.get(ws);
const roomId = client.roomId;
if (!roomId || !rooms.has(roomId)) {
ws.send(JSON.stringify({
type: 'error',
message: 'You are not in a room'
}));
return;
}
const room = rooms.get(roomId);
// Forward the message to all other participants in the room
for (const peerWs of room) {
if (peerWs !== ws && peerWs.readyState === WebSocket.OPEN) {
peerWs.send(JSON.stringify({
type: messageType,
[messageType === 'ice-candidate' ? 'candidate' : 'sdp']:
data[messageType === 'ice-candidate' ? 'candidate' : 'sdp'],
peerId: client.id,
roomId: roomId
}));
}
}
}
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Signaling server running on http://localhost:${PORT}`);
console.log(`WebSocket server ready at ws://localhost:${PORT}`);
});
This server handles room management, limits each room to two participants, and relays SDP offers, answers, and ICE candidates between peers without inspecting or modifying the content. When a peer disconnects, the remaining participant is notified so they can update their UI.
Step 3: Build the Client Interface (public/index.html)
The client handles media capture, peer connection establishment, ICE candidate exchange, and the offer/answer negotiation flow. Here is the complete HTML file with embedded CSS and JavaScript:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC Video Chat</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
h1 {
font-size: 2rem;
margin-bottom: 20px;
color: #a8dadc;
}
.container {
width: 100%;
max-width: 1200px;
display: flex;
flex-direction: column;
gap: 20px;
}
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
}
.video-box {
position: relative;
background: #16213e;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
aspect-ratio: 16 / 9;
}
.video-box video {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
}
.video-label {
position: absolute;
bottom: 12px;
left: 12px;
background: rgba(0, 0, 0, 0.6);
padding: 6px 14px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
color: #fff;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #888;
font-size: 1.2rem;
}
.controls {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 14px 28px;
border: none;
border-radius: 30px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
background: #2d3a5c;
color: #e0e0e0;
}
.btn:hover {
background: #3f5182;
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: #457b9d;
}
.btn-primary:hover {
background: #5a91b8;
}
.btn-danger {
background: #e63946;
}
.btn-danger:hover {
background: #f1515e;
}
.btn-success {
background: #2a9d8f;
}
.btn-success:hover {
background: #35b8a9;
}
.room-info {
display: flex;
gap: 16px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.room-id-display {
background: #16213e;
padding: 10px 20px;
border-radius: 8px;
font-family: monospace;
font-size: 1.1rem;
color: #a8dadc;
}
.status {
text-align: center;
font-size: 0.95rem;
color: #aaa;
min-height: 24px;
}
input {
padding: 12px 18px;
border-radius: 8px;
border: 2px solid #2d3a5c;
background: #16213e;
color: #e0e0e0;
font-size: 1rem;
width: 220px;
outline: none;
transition: border-color 0.2s;
}
input:focus {
border-color: #457b9d;
}
.hidden {
display: none !important;
}
</style>
</head>
<body>
<h1>π₯ WebRTC Video Chat</h1>
<div class="container">
<div class="room-info">
<input type="text" id="roomIdInput" placeholder="Enter Room ID to join..." maxlength="20">
<button class="btn btn-primary" id="joinBtn">Join Room</button>
<button class="btn btn-success" id="createBtn">Create New Room</button>
<span class="room-id-display hidden" id="roomIdDisplay"></span>
<button class="btn btn-danger hidden" id="hangupBtn">Hang Up</button>
</div>
<div class="video-grid">
<div class="video-box">
<video id="localVideo" autoplay muted playsinline></video>
<div class="video-label">You (Local)</div>
</div>
<div class="video-box">
<video id="remoteVideo" autoplay playsinline></video>
<div class="video-label">Remote Peer</div>
<div class="placeholder" id="remotePlaceholder">Waiting for peer to join...</div>
</div>
</div>
<div class="controls">
<button class="btn" id="muteBtn">π Mute Mic</button>
<button class="btn" id="cameraBtn">π· Stop Camera</button>
</div>
<p class="status" id="status">Enter a room ID or create a new one to get started.</p>
</div>
<script>
// --- Configuration ---
const SIGNALING_SERVER_URL = `ws://${window.location.host}`;
// STUN/TURN servers (use your own TURN server in production)
const ICE_SERVERS = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
// Add your TURN server credentials here for production
// {
// urls: 'turn:your-turn-server.com:3478',
// username: 'username',
// credential: 'password'
// }
]
};
// --- DOM Elements ---
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const remotePlaceholder = document.getElementById('remotePlaceholder');
const roomIdInput = document.getElementById('roomIdInput');
const roomIdDisplay = document.getElementById('roomIdDisplay');
const joinBtn = document.getElementById('joinBtn');
const createBtn = document.getElementById('createBtn');
const hangupBtn = document.getElementById('hangupBtn');
const muteBtn = document.getElementById('muteBtn');
const cameraBtn = document.getElementById('cameraBtn');
const statusEl = document.getElementById('status');
// --- State ---
let ws = null;
let localStream = null;
let peerConnection = null;
let currentRoomId = null;
let isInitiator = false;
let isMuted = false;
let isCameraOff = false;
let pendingCandidates = [];
// --- Helper: Update Status ---
function setStatus(message, isError = false) {
statusEl.textContent = message;
statusEl.style.color = isError ? '#e63946' : '#aaa';
}
// --- Helper: Toggle UI based on connection state ---
function setConnectedState(connected) {
if (connected) {
joinBtn.classList.add('hidden');
createBtn.classList.add('hidden');
roomIdInput.classList.add('hidden');
hangupBtn.classList.remove('hidden');
roomIdDisplay.classList.remove('hidden');
} else {
joinBtn.classList.remove('hidden');
createBtn.classList.remove('hidden');
roomIdInput.classList.remove('hidden');
hangupBtn.classList.add('hidden');
roomIdDisplay.classList.add('hidden');
}
}
// --- Step 1: Get local media ---
async function getLocalMedia() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
localVideo.srcObject = localStream;
setStatus('Camera and microphone ready.');
return true;
} catch (err) {
console.error('Error accessing media devices:', err);
setStatus('Failed to access camera/microphone. Please check permissions.', true);
return false;
}
}
// --- Step 2: Connect to signaling server ---
function connectToSignalingServer() {
return new Promise((resolve, reject) => {
ws = new WebSocket(SIGNALING_SERVER_URL);
ws.onopen = () => {
console.log('Connected to signaling server');
setStatus('Connected to signaling server.');
resolve();
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setStatus('Failed to connect to signaling server.', true);
reject(error);
};
ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
setStatus('Disconnected from signaling server. Please refresh.', true);
// Clean up if connection drops unexpectedly
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
remoteVideo.srcObject = null;
remotePlaceholder.style.display = 'flex';
setConnectedState(false);
};
ws.onmessage = (event) => {
handleSignalingMessage(JSON.parse(event.data));
};
});
}
// --- Step 3: Handle incoming signaling messages ---
function handleSignalingMessage(data) {
const { type } = data;
switch (type) {
case 'joined-room':
currentRoomId = data.roomId;
roomIdDisplay.textContent = `Room: ${currentRoomId}`;
setStatus(`Joined room ${currentRoomId}. Waiting for peer...`);
setConnectedState(true);
break;
case 'peer-connected':
setStatus('Peer joined! Establishing connection...');
if (data.initiator) {
isInitiator = true;
createOffer();
}
break;
case 'offer':
handleOffer(data);
break;
case 'answer':
handleAnswer(data);
break;
case 'ice-candidate':
handleIceCandidate(data);
break;
case 'peer-disconnected':
setStatus('Remote peer disconnected.');
closePeerConnection();
remoteVideo.srcObject = null;
remotePlaceholder.style.display = 'flex';
setConnectedState(true); // Stay in room, wait for new peer
break;
case 'room-full':
setStatus('Room is full. Please try another room ID.', true);
setConnectedState(false);
currentRoomId = null;
break;
case 'error':
setStatus(data.message, true);
break;
default:
console.log('Unknown message type:', type, data);
}
}
// --- Step 4: Create RTCPeerConnection ---
function createPeerConnection() {
if (peerConnection) {
peerConnection.close();
}
peerConnection = new RTCPeerConnection(ICE_SERVERS);
// Add local tracks to the peer connection
if (localStream) {
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
}
// Handle incoming remote tracks
peerConnection.ontrack = (event) => {
console.log('Received remote track:', event.track.kind);
if (event.streams && event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
remotePlaceholder.style.display = 'none';
setStatus('Video call established!');
}
};
// Handle ICE candidates (trickle ICE)
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
sendSignalingMessage({
type: 'ice-candidate',
candidate: event.candidate,
roomId: currentRoomId
});
}
};
// Handle ICE gathering state
peerConnection.onicegatheringstatechange = () => {
console.log('ICE gathering state:', peerConnection.iceGatheringState);
};
// Handle connection state changes
peerConnection.onconnectionstatechange = () => {
console.log('Connection state:', peerConnection.connectionState);
if (peerConnection.connectionState === 'connected') {
setStatus('Peer connection established! π');
} else if (peerConnection.connectionState === 'disconnected') {
setStatus('Peer connection lost. Attempting to reconnect...');
} else if (peerConnection.connectionState === 'failed') {
setStatus('Connection failed. Please try rejoining.', true);
closePeerConnection();
}
};
// Handle ICE connection state
peerConnection.oniceconnectionstatechange = () => {
console.log('ICE connection state:', peerConnection.iceConnectionState);
};
return peerConnection;
}
// --- Step 5: Create Offer (Initiator) ---
async function createOffer() {
createPeerConnection();
// Process any ICE candidates that arrived before the offer was ready
pendingCandidates = [];
try {
const offer = await peerConnection.createOffer({
offerToReceiveVideo: true,
offerToReceiveAudio: true
});
await peerConnection.setLocalDescription(offer);
sendSignalingMessage({
type: 'offer',
sdp: peerConnection.localDescription,
roomId: currentRoomId
});
setStatus('Offer sent. Waiting for answer...');
} catch (err) {
console.error('Error creating offer:', err);
setStatus('Failed to create offer.', true);
}
}
// --- Step 6: Handle Offer (Non-Initiator) ---
async function handleOffer(data) {
createPeerConnection();
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
// Process any queued ICE candidates
if (pendingCandidates.length > 0) {
for (const candidate of pendingCandidates) {
await peerConnection.addIceCandidate(candidate);
}
pendingCandidates = [];
}
const answer = await peerConnection.createAnswer({
offerToReceiveVideo: true,
offerToReceiveAudio: true
});
await peerConnection.setLocalDescription(answer);
sendSignalingMessage({
type: 'answer',
sdp: peerConnection.localDescription,
roomId: currentRoomId
});
setStatus('Answer sent. Establishing connection...');
} catch (err) {
console.error('Error handling offer:', err);
setStatus('Failed to process offer.', true);
}
}
// --- Step 7: Handle Answer ---
async function handleAnswer(data) {
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
// Process any queued ICE candidates
if (pendingCandidates.length > 0) {
for (const candidate of pendingCandidates) {
await peerConnection.addIceCandidate(candidate);
}
pendingCandidates = [];
}
setStatus('Answer received. Finalizing connection...');
} catch (err) {
console.error('Error handling answer:', err);
setStatus('Failed to process answer.', true);
}
}
// --- Step 8: Handle ICE Candidates ---
async function handleIceCandidate(data) {
const candidate = new RTCIceCandidate(data.candidate);
// If the remote description is not set yet, queue the candidate
if (!peerConnection || !peerConnection.remoteDescription) {
pendingCandidates.push(candidate);
console.log('Queued ICE candidate (remote description not set yet)');
return;
}
try {
await peerConnection.addIceCandidate(candidate);
console.log('Added ICE candidate');
} catch (err) {
console.error('Error adding ICE candidate:', err);
}
}
// --- Step 9: Send Signaling Message ---
function sendSignalingMessage(message) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
} else {
console.error('WebSocket is not open. Cannot send message.');
}
}
// --- Step 10: Close Peer Connection ---
function closePeerConnection() {
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
pendingCandidates = [];
isInitiator = false;
}
// --- Step 11: Hang Up ---
function hangUp() {
sendSignalingMessage({
type: 'hang-up',
roomId: currentRoomId
});
closePeerConnection();
remoteVideo.srcObject = null;
remotePlaceholder.style.display = 'flex';
currentRoomId = null;
setConnectedState(false);
setStatus('Call ended. Enter a room ID or create a new one.');
}
// --- Step 12: Join a Room ---
async function joinRoom(roomId) {
if (!localStream) {
const success = await getLocalMedia();
if (!success) return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
await connectToSignalingServer();
}
sendSignalingMessage({
type: 'join-room',
roomId: roomId
});
roomIdInput.value = '';
setStatus(`Joining room ${roomId}...`);
}
// --- Step 13: Create a Room ---
async function createRoom() {
if (!localStream) {
const success = await getLocalMedia();
if (!success) return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
await connectToSignalingServer();
}
// Generate a random room ID
const roomId = Math.random().toString(36).substring(2, 8).toUpperCase();
sendSignalingMessage({
type: 'join-room',
roomId: roomId
});
roomIdInput.value = '';
setStatus(`Creating room ${roomId}... Share this ID with your peer!`);
}
// --- Mute/Unmute ---
function toggleMute() {
if (!localStream) return;
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
isMuted = !audioTrack.enabled;
muteBtn.textContent = isMuted ? 'π Unmute Mic' : 'π Mute Mic';
}
}
// --- Camera On/Off ---
function toggleCamera() {
if (!localStream) return;
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
isCameraOff = !videoTrack.enabled;
cameraBtn.textContent = isCameraOff ? 'π· Start Camera' : 'π· Stop Camera';
// Show a dark overlay on local video when camera is off
localVideo.style.opacity = isCameraOff ? '0.3' : '1';
}
}
// --- Event Listeners ---
joinBtn.addEventListener('click', () => {
const roomId = roomIdInput.value.trim();
if (!roomId) {
setStatus('Please enter a room ID.', true);
return;
}
joinRoom(roomId);
});
createBtn.addEventListener('click', () => {
createRoom();
});
hangupBtn.addEventListener('click', () => {
hangUp();
});
muteBtn.addEventListener('click', toggleMute);
cameraBtn.addEventListener('click', toggleCamera);
// Allow pressing Enter to join a room
roomIdInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const roomId = roomIdInput.value.trim();
if (roomId) joinRoom(roomId);
}
});
// --- Initialization ---
async function initialize() {
try {
await getLocalMedia();
await connectToSignalingServer();
setStatus('Ready! Create a room or join an existing one.');
} catch (err) {
console.error('Initialization failed:', err);
setStatus('Failed to initialize. Please check your connection and permissions.', true);
}
}
// Start the application
initialize();
</script>
</body>
</html>
Understanding the Client-Side Flow
Let's walk through the key stages of the WebRTC connection lifecycle as implemented in the client code:
- Media Acquisition β
getUserMedia()requests camera and microphone access. The resultingMediaStreamis attached to the local<video>element and later added to the peer connection viaaddTrack(). - Signaling Connection β A WebSocket connection is established to the signaling server. This connection is used exclusively for control messages (join, offer, answer, ICE candidates