DEV Community

snehaa1989
snehaa1989

Posted on

Building a Real-Time Video Conferencing Web App with WebRTC, Node.js, and Socket.IO

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
    });
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Offer/Answer: One peer creates an offer, the other responds with an answer
  2. ICE Candidates: Network connection information exchanged between peers
  3. Room Management: Socket.IO rooms help organize participants

Peer Connection Flow

  1. User A creates RTCPeerConnection
  2. User A creates offer and sends via Socket.IO
  3. User B receives offer, creates answer
  4. Both exchange ICE candidates
  5. Direct peer-to-peer connection established

Advanced Features to Consider

  1. Recording: Use MediaRecorder API to record sessions
  2. Chat: Add text messaging with Socket.IO
  3. File Sharing: Implement file transfer capabilities
  4. Breakout Rooms: Create sub-rooms within main conference
  5. Virtual Backgrounds: Use canvas manipulation for backgrounds
  6. Bandwidth Management: Adapt video quality based on network conditions

Deployment Considerations

  1. HTTPS Required: WebRTC only works on secure connections
  2. STUN/TURN Servers: Consider using paid TURN servers for better connectivity
  3. Scaling: For large applications, consider Redis for Socket.IO scaling
  4. Monitoring: Implement logging and monitoring for connection quality

Troubleshooting Common Issues

  1. Connection Failed: Check STUN/TURN server configuration
  2. No Video/Audio: Ensure browser permissions are granted
  3. One-Way Audio: Check NAT traversal and firewall settings
  4. 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)