Introduction
Video conferencing has become essential in today's digital world. In this tutorial, we'll build a fully functional video conferencing application using WebRTC for peer-to-peer video streaming, Node.js for the backend, and Socket.IO for real-time signaling.
Prerequisites
- Basic knowledge of JavaScript, Node.js, and HTML/CSS
- Node.js installed on your machine
- Understanding of client-server architecture
What We'll Build
A video conferencing app with:
- Multiple participant video streams
- Audio/video controls
- Room-based meetings
- Real-time participant management
Project Structure
video-conferencing/
├── server/
│ ├── package.json
│ ├── server.js
│ └── public/
│ ├── index.html
│ ├── style.css
│ └── client.js
Step 1: Setting Up the Backend
First, let's create our Node.js server with Express and Socket.IO:
mkdir server && cd server
npm init -y
npm install express socket.io
server.js:
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
app.use(express.static('public'));
const rooms = {};
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join-room', (roomId, userId) => {
socket.join(roomId);
if (!rooms[roomId]) {
rooms[roomId] = [];
}
rooms[roomId].push(userId);
socket.to(roomId).emit('user-connected', userId);
socket.on('disconnect', () => {
socket.to(roomId).emit('user-disconnected', userId);
rooms[roomId] = rooms[roomId].filter(id => id !== userId);
});
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 2: Creating the Frontend
public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Conferencing App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>Video Conference Room</h1>
<div class="room-info">
<span id="room-id"></span>
<span id="participant-count">1 participant</span>
</div>
</header>
<main>
<div class="video-grid" id="video-grid">
<!-- Local video will be added here -->
</div>
<div class="controls">
<button id="mute-btn">🎤 Mute</button>
<button id="video-btn">📹 Video Off</button>
<button id="share-screen-btn">🖥️ Share Screen</button>
<button id="leave-btn">📞 Leave</button>
</div>
</main>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
public/style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #1a1a2e;
color: white;
height: 100vh;
overflow: hidden;
}
.container {
height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: #16213e;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.room-info {
display: flex;
gap: 2rem;
font-size: 0.9rem;
opacity: 0.8;
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
}
.video-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
overflow-y: auto;
}
.video-container {
position: relative;
background: #0f3460;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 16/9;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-label {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
}
.controls {
display: flex;
justify-content: center;
gap: 1rem;
padding: 1rem;
background: #16213e;
border-radius: 8px;
}
.controls button {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 6px;
background: #e94560;
color: white;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
}
.controls button:hover {
background: #c23652;
transform: translateY(-2px);
}
.controls button.active {
background: #27ae60;
}
.controls button.danger {
background: #e74c3c;
}
Step 3: Implementing WebRTC Logic
public/client.js:
const socket = io();
const videoGrid = document.getElementById('video-grid');
const roomId = window.location.pathname.split('/')[1] || 'default-room';
const userId = Math.random().toString(36).substr(2, 9);
document.getElementById('room-id').textContent = `Room: ${roomId}`;
let localStream;
let peerConnections = {};
let isMuted = false;
let isVideoOff = false;
// WebRTC configuration
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
// Initialize local media
async function initializeMedia() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
const localVideo = createVideoElement(userId, 'You', true);
localVideo.srcObject = localStream;
socket.emit('join-room', roomId, userId);
} catch (error) {
console.error('Error accessing media devices:', error);
alert('Unable to access camera/microphone');
}
}
// Create video element
function createVideoElement(userId, label, isLocal = false) {
const videoContainer = document.createElement('div');
videoContainer.className = 'video-container';
videoContainer.id = `video-${userId}`;
const video = document.createElement('video');
video.autoplay = true;
video.muted = isLocal;
video.playsinline = true;
const videoLabel = document.createElement('div');
videoLabel.className = 'video-label';
videoLabel.textContent = label;
videoContainer.appendChild(video);
videoContainer.appendChild(videoLabel);
videoGrid.appendChild(videoContainer);
return video;
}
// Create peer connection
function createPeerConnection(userId) {
const pc = new RTCPeerConnection(configuration);
// Add local stream to peer connection
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
// Handle incoming streams
pc.ontrack = (event) => {
const [remoteStream] = event.streams;
const remoteVideo = document.getElementById(`video-${userId}`);
if (remoteVideo && !remoteVideo.srcObject) {
remoteVideo.srcObject = remoteStream;
}
};
// Handle ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice-candidate', {
candidate: event.candidate,
to: userId
});
}
};
return pc;
}
// Socket event handlers
socket.on('user-connected', async (userId) => {
console.log('User connected:', userId);
const pc = createPeerConnection(userId);
peerConnections[userId] = pc;
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', {
offer: offer,
to: userId
});
createVideoElement(userId, `User ${userId.substr(0, 5)}`);
});
socket.on('user-disconnected', (userId) => {
console.log('User disconnected:', userId);
const videoElement = document.getElementById(`video-${userId}`);
if (videoElement) {
videoElement.remove();
}
if (peerConnections[userId]) {
peerConnections[userId].close();
delete peerConnections[userId];
}
});
socket.on('offer', async ({ offer, from }) => {
if (!peerConnections[from]) {
peerConnections[from] = createPeerConnection(from);
}
const pc = peerConnections[from];
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', {
answer: answer,
to: from
});
});
socket.on('answer', async ({ answer, from }) => {
const pc = peerConnections[from];
await pc.setRemoteDescription(new RTCSessionDescription(answer));
});
socket.on('ice-candidate', async ({ candidate, from }) => {
const pc = peerConnections[from];
if (pc) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
});
// Control buttons
document.getElementById('mute-btn').addEventListener('click', () => {
isMuted = !isMuted;
localStream.getAudioTracks().forEach(track => {
track.enabled = !isMuted;
});
const btn = document.getElementById('mute-btn');
btn.textContent = isMuted ? '🔇 Unmute' : '🎤 Mute';
btn.classList.toggle('active', isMuted);
});
document.getElementById('video-btn').addEventListener('click', () => {
isVideoOff = !isVideoOff;
localStream.getVideoTracks().forEach(track => {
track.enabled = !isVideoOff;
});
const btn = document.getElementById('video-btn');
btn.textContent = isVideoOff ? '📹 Video On' : '📹 Video Off';
btn.classList.toggle('active', isVideoOff);
});
document.getElementById('share-screen-btn').addEventListener('click', async () => {
try {
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true
});
const videoTrack = screenStream.getVideoTracks()[0];
// Replace video track in all peer connections
Object.values(peerConnections).forEach(pc => {
const sender = pc.getSenders().find(s =>
s.track && s.track.kind === 'video'
);
if (sender) {
sender.replaceTrack(videoTrack);
}
});
videoTrack.onended = () => {
// Restore camera video
localStream.getVideoTracks().forEach(track => {
Object.values(peerConnections).forEach(pc => {
const sender = pc.getSenders().find(s =>
s.track && s.track.kind === 'video'
);
if (sender) {
sender.replaceTrack(track);
}
});
});
};
} catch (error) {
console.error('Error sharing screen:', error);
}
});
document.getElementById('leave-btn').addEventListener('click', () => {
window.location.href = '/';
});
// Update participant count
function updateParticipantCount() {
const count = document.querySelectorAll('.video-container').length;
document.getElementById('participant-count').textContent =
`${count} participant${count !== 1 ? 's' : ''}`;
}
// Initialize the application
initializeMedia();
// Update participant count periodically
setInterval(updateParticipantCount, 1000);
Step 4: Enhanced Server with Signaling
Let's update our server to handle WebRTC signaling:
// Add these event handlers to your existing server.js
socket.on('offer', ({ offer, to }) => {
socket.to(to).emit('offer', {
offer: offer,
from: socket.id
});
});
socket.on('answer', ({ answer, to }) => {
socket.to(to).emit('answer', {
answer: answer,
from: socket.id
});
});
socket.on('ice-candidate', ({ candidate, to }) => {
socket.to(to).emit('ice-candidate', {
candidate: candidate,
from: socket.id
});
});
Step 5: Adding Room Management
Create a simple landing page for room creation:
public/room.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create or Join Room</title>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
}
.container {
background: white;
padding: 3rem;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
text-align: center;
max-width: 400px;
width: 90%;
}
h1 {
color: #333;
margin-bottom: 2rem;
}
.input-group {
margin-bottom: 1.5rem;
}
input {
width: 100%;
padding: 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 1rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.3s;
margin-bottom: 1rem;
}
button:hover {
background: #5a67d8;
}
.secondary {
background: #48bb78;
}
.secondary:hover {
background: #38a169;
}
</style>
</head>
<body>
<div class="container">
<h1>📹 Video Conference</h1>
<div class="input-group">
<input type="text" id="room-input" placeholder="Enter room name or ID">
</div>
<button onclick="createRoom()">Create New Room</button>
<button class="secondary" onclick="joinRoom()">Join Existing Room</button>
<div id="room-info" style="margin-top: 2rem; display: none;">
<p style="color: #666;">Share this room ID with others:</p>
<p id="room-id-display" style="font-weight: bold; color: #667eea; font-size: 1.2rem;"></p>
</div>
</div>
<script>
function generateRoomId() {
return Math.random().toString(36).substr(2, 9).toUpperCase();
}
function createRoom() {
const roomId = generateRoomId();
document.getElementById('room-input').value = roomId;
document.getElementById('room-info').style.display = 'block';
document.getElementById('room-id-display').textContent = roomId;
setTimeout(() => {
window.location.href = `/${roomId}`;
}, 2000);
}
function joinRoom() {
const roomId = document.getElementById('room-input').value.trim();
if (roomId) {
window.location.href = `/${roomId}`;
} else {
alert('Please enter a room ID');
}
}
// Check if there's a room in the URL
const pathRoom = window.location.pathname.substr(1);
if (pathRoom) {
window.location.href = `/${pathRoom}`;
}
</script>
</body>
</html>
Update your server.js to serve the room page:
// Add this route before the static middleware
app.get('/', (req, res) => {
res.sendFile(__dirname + '/public/room.html');
});
// Update the existing route for room pages
app.get('/:room', (req, res) => {
res.sendFile(__dirname + '/public/index.html');
});
Key Concepts Explained
WebRTC (Web Real-Time Communication)
WebRTC enables peer-to-peer audio, video, and data sharing between browsers without requiring plugins. Key components:
- RTCPeerConnection: The main interface for WebRTC connections
- MediaStream: Represents audio/video streams
- ICE (Interactive Connectivity Establishment): Handles NAT traversal
- STUN/TURN servers: Help establish connections across networks
Socket.IO Signaling
WebRTC requires a signaling server to exchange connection information:
- Offer/Answer: One peer creates an offer, the other responds with an answer
- ICE Candidates: Network connection information exchanged between peers
- Room Management: Socket.IO rooms help organize participants
Peer Connection Flow
- User A creates RTCPeerConnection
- User A creates offer and sends via Socket.IO
- User B receives offer, creates answer
- Both exchange ICE candidates
- Direct peer-to-peer connection established
Advanced Features to Consider
- Recording: Use MediaRecorder API to record sessions
- Chat: Add text messaging with Socket.IO
- File Sharing: Implement file transfer capabilities
- Breakout Rooms: Create sub-rooms within main conference
- Virtual Backgrounds: Use canvas manipulation for backgrounds
- Bandwidth Management: Adapt video quality based on network conditions
Deployment Considerations
- HTTPS Required: WebRTC only works on secure connections
- STUN/TURN Servers: Consider using paid TURN servers for better connectivity
- Scaling: For large applications, consider Redis for Socket.IO scaling
- Monitoring: Implement logging and monitoring for connection quality
Troubleshooting Common Issues
- Connection Failed: Check STUN/TURN server configuration
- No Video/Audio: Ensure browser permissions are granted
- One-Way Audio: Check NAT traversal and firewall settings
- Poor Quality: Implement bandwidth adaptation
Conclusion
We've built a functional video conferencing application using modern web technologies. This foundation can be extended with additional features like recording, chat, and advanced room management.
The combination of WebRTC for peer-to-peer media streaming and Socket.IO for signaling provides a robust architecture for real-time communication applications.
Next Steps
- Add user authentication
- Implement persistent room storage
- Add mobile responsiveness
- Integrate with calendar systems
- Add analytics and usage tracking
Top comments (0)