End of Life for Twilio Programmable Video - Upgrade to VideoSDKLearn More

How to Build LibDataChannel WebRTC App with JavaScript?

Learn how to build a WebRTC application using LibDataChannel and Node.js. This guide covers installation, setup, coding, and implementation of real-time communication features.

Introduction to LibDataChannel WebRTC

What is LibDataChannel?

LibDataChannel is an open-source WebRTC data channel library designed to facilitate real-time peer-to-peer communication. WebRTC (Web Real-Time Communication) is a powerful technology that enables audio, video, and data sharing between browsers and devices without requiring an intermediary server. It is a cornerstone for applications like video conferencing, file sharing, and real-time collaboration tools.

Importance of WebRTC for Real-Time Communication

WebRTC has revolutionized the way we interact online by providing a standardized set of protocols for real-time communication. It eliminates the need for plugins or external software, ensuring seamless and direct media exchange between users. This technology has become critical for developing modern communication applications, allowing developers to create interactive and engaging user experiences.

Integration of LibDataChannel with Node.js Server

Integrating LibDataChannel with a Node.js server combines the efficiency of Node.js in handling asynchronous operations with the robust capabilities of WebRTC for real-time data transmission. Node.js, known for its non-blocking I/O operations and event-driven architecture, serves as an excellent backend for WebRTC applications. By leveraging LibDataChannel, developers can establish secure, high-performance data channels that facilitate peer-to-peer communication directly from the server side.
In the following sections, we will dive into the steps required to set up a LibDataChannel application with Node.js, explore the project structure, and implement key features to create a fully functional real-time communication platform. Whether you're building a simple chat application or a complex collaborative tool, this guide will provide you with the foundation needed to harness the power of WebRTC using LibDataChannel.

Getting Started with the Code

Create a New LibDataChannel App

To start building your LibDataChannel application, you need to set up your development environment and initialize a new Node.js project. Follow these steps to create a new LibDataChannel app:

[a] Install Node.js

Ensure that Node.js is installed on your system. You can download it from the

official Node.js website

.

[b] Initialize a New Project

Open your terminal and create a new directory for your project. Navigate to the directory and initialize a new Node.js project using npm (Node Package Manager):

bash

1   mkdir libdatachannel-app
2   cd libdatachannel-app
3   npm init -y
This command creates a package.json file, which will manage your project's dependencies and scripts.

Install LibDataChannel

LibDataChannel is a C++ library, so to use it with Node.js, you need to install bindings that allow Node.js to interact with the library. One way to do this is to use the node-webrtc package, which provides WebRTC bindings for Node.js. Here's how to install it:

[a] Install Node-webrtc

In your project directory, run the following command to install the necessary package:

bash

1   npm install wrtc

[b] Install Additional Dependencies:

Depending on your development environment, you may need to install additional dependencies for building native modules. For instance, on Ubuntu, you might need to install build-essential and other libraries:

bash

1   sudo apt-get install build-essential

Structure of the Project

Organizing your project directory is crucial for maintaining clean and manageable code. Here is a suggested directory structure for your LibDataChannel application:
1libdatachannel-app/
2├── node_modules/
3├── src/
4│   ├── main.js
5│   ├── config.js
6│   ├── controllers/
7│   │   └── signalingController.js
8│   └── views/
9│       └── join.html
10├── .gitignore
11├── package.json
12└── README.md
  • src/: Contains all the source files.
  • main.js: The main entry point of your application.
  • config.js: Configuration file for your application settings.
  • controllers/: Contains controllers for handling different parts of the application logic.
  • views/: Contains HTML files for the front-end views.

App Architecture

libdatachannel-webrtc
Understanding the architecture of your LibDataChannel application helps in implementing features systematically. Here’s a high-level overview of the architecture:

Server-Side (Node.js)

  • Signaling Server: Manages the signaling process, helping peers discover each other and establish a connection. This is implemented in signalingController.js.
  • Data Channel Management: Handles the creation, maintenance, and closure of data channels using LibDataChannel.

Client-Side (HTML/JavaScript)

  • Join Screen: A simple HTML page (join.html) where users can enter details to join the communication session.
  • WebRTC API: Uses the WebRTC API provided by node-webrtc to handle peer connections and data transmission.
With the environment set up and the project structure in place, we can now move on to writing the initial server code. In the next sections, we will dive into setting up main.js, wiring up the components, and implementing the core functionalities of our WebRTC application using LibDataChannel.

Step 1: Get Started with main.js

Setting Up main.js

The main.js file serves as the entry point for your Node.js application. It initializes the server, sets up the necessary configurations, and handles the basic routing for your application. Follow these steps to set up main.js:

[a] Create the main.js File

Inside the src/ directory, create a file named main.js.

bash

1   touch src/main.js

[b] Import Necessary Modules

Open main.js in your favorite code editor and start by importing the required modules. We will need http, express, and wrtc (node-webrtc) to set up our server and handle WebRTC connections.

JavaScript

1   const http = require('http');
2   const express = require('express');
3   const wrtc = require('wrtc');

[c] Initialize Express Application

Initialize an Express application and set up a basic HTTP server.

JavaScript

1   const app = express();
2   const server = http.createServer(app);

[d] Configure Express Middleware

Configure middleware to serve static files from the views directory and parse JSON payloads.

JavaScript

1   app.use(express.static('src/views'));
2   app.use(express.json());

[e] Set Up Basic Routing

Define a basic route to serve the join screen HTML file.

JavaScript

1   app.get('/', (req, res) => {
2       res.sendFile(__dirname + '/views/join.html');
3   });

[f] Start the Server

Set the server to listen on a specified port (e.g., 3000).

JavaScript

1   const PORT = process.env.PORT || 3000;
2
3   server.listen(PORT, () => {
4       console.log(`Server is running on port ${PORT}`);
5   });

[g] Signaling Controller

Set up the signaling controller to manage WebRTC signaling messages. Create a new file signalingController.js inside the controllers directory.

bash

1   touch src/controllers/signalingController.js
Inside signalingController.js, add the following code to handle WebRTC signaling:

JavaScript

1   const peers = {};
2
3   const handleSignaling = (socket) => {
4       socket.on('offer', (data) => {
5           const { offer, roomId } = data;
6           if (!peers[roomId]) peers[roomId] = {};
7           peers[roomId].offer = offer;
8           socket.broadcast.emit('offer', data);
9       });
10
11       socket.on('answer', (data) => {
12           const { answer, roomId } = data;
13           if (peers[roomId]) {
14               peers[roomId].answer = answer;
15               socket.broadcast.emit('answer', data);
16           }
17       });
18
19       socket.on('candidate', (data) => {
20           const { candidate, roomId } = data;
21           if (peers[roomId]) {
22               socket.broadcast.emit('candidate', data);
23           }
24       });
25   };
26
27   module.exports = { handleSignaling };

[h] Integrate Signaling with Main.js

Integrate the signaling controller in main.js. Install socket.io to facilitate real-time communication.

bash

1   npm install socket.io
Then, modify main.js to include Socket.io and the signaling controller:

JavaScript

1   const io = require('socket.io')(server);
2   const { handleSignaling } = require('./controllers/signalingController');
3
4   io.on('connection', (socket) => {
5       console.log('New user connected');
6       handleSignaling(socket);
7   });
Here is the complete main.js file for reference:

JavaScript

1const http = require('http');
2const express = require('express');
3const wrtc = require('wrtc');
4const io = require('socket.io')(server);
5const { handleSignaling } = require('./controllers/signalingController');
6
7const app = express();
8const server = http.createServer(app);
9
10app.use(express.static('src/views'));
11app.use(express.json());
12
13app.get('/', (req, res) => {
14    res.sendFile(__dirname + '/views/join.html');
15});
16
17io.on('connection', (socket) => {
18    console.log('New user connected');
19    handleSignaling(socket);
20});
21
22const PORT = process.env.PORT || 3000;
23
24server.listen(PORT, () => {
25    console.log(`Server is running on port ${PORT}`);
26});
With the server set up and basic routing in place, we are ready to move on to designing the application's wireframe and implementing the join screen in the next sections. This foundation will enable us to build a robust WebRTC application using LibDataChannel and Node.js.

Step 2: Wireframe All the Components

Designing the Application

Creating a clear and detailed wireframe for your application is crucial for understanding the flow and interaction between different components. This step involves planning the user interface (UI) and the functional components required for a seamless WebRTC experience.

Drafting the Wireframe

Here’s a simple wireframe layout for a basic WebRTC application using LibDataChannel:

Join Screen

  • Elements:
    • Text input for user name or room ID.
    • "Join" button to enter the communication session.
  • Functionality:
    • Allows users to enter a room or session.
    • Initiates the connection process with the signaling server.

Main Communication Screen

  • Elements:
    • Video/Audio display area for local and remote streams.
    • Control buttons (mute, unmute, end call, etc.).
    • Chat box for text communication (optional).
  • Functionality:
    • Displays real-time video/audio streams.
    • Provides controls for managing the call and communication.

Listing All Components and Their Interactions

Client-Side Components

  • Join Screen: HTML form to capture user input and join the session.
  • Main Screen: Area to display video streams and control buttons.
  • WebRTC Connection Handling: JavaScript code to manage WebRTC connections, data channels, and media streams.

Server-Side Components

  • Signaling Server: Handles signaling messages (offer, answer, and ICE candidates) between peers.
  • Data Channel Management: Manages data channels for text communication or additional data sharing.

Implementing the Join Screen

[a] Create Join Screen HTML

Inside the views/ directory, create a file named join.html and add the following HTML code:

HTML

1   <!DOCTYPE html>
2   <html lang="en">
3   <head>
4       <meta charset="UTF-8">
5       <meta name="viewport" content="width=device-width, initial-scale=1.0">
6       <title>Join Room</title>
7       <style>
8           body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; }
9           #join-container { text-align: center; }
10           input { margin: 10px; padding: 10px; width: 200px; }
11           button { padding: 10px 20px; }
12       </style>
13   </head>
14   <body>
15       <div id="join-container">
16           <h1>Join a Room</h1>
17           <input type="text" id="room-id" placeholder="Enter Room ID">
18           <button id="join-btn">Join</button>
19       </div>
20       <script src="/socket.io/socket.io.js"></script>
21       <script>
22           const socket = io();
23           document.getElementById('join-btn').onclick = () => {
24               const roomId = document.getElementById('room-id').value;
25               if (roomId) {
26                   socket.emit('join', { roomId });
27               }
28           };
29       </script>
30   </body>
31   </html>
This HTML file sets up a basic join screen where users can enter a room ID and join the session.

[b] Handle Join Requests on Server

Modify signalingController.js to handle join requests from clients:

JavaScript

1   const peers = {};
2
3   const handleSignaling = (socket) => {
4       socket.on('join', (data) => {
5           const { roomId } = data;
6           if (!peers[roomId]) peers[roomId] = [];
7           peers[roomId].push(socket.id);
8           socket.join(roomId);
9           console.log(`User joined room: ${roomId}`);
10       });
11
12       socket.on('offer', (data) => {
13           const { offer, roomId } = data;
14           if (peers[roomId]) {
15               peers[roomId].forEach(peerId => {
16                   if (peerId !== socket.id) {
17                       socket.to(peerId).emit('offer', data);
18                   }
19               });
20           }
21       });
22
23       socket.on('answer', (data) => {
24           const { answer, roomId } = data;
25           if (peers[roomId]) {
26               peers[roomId].forEach(peerId => {
27                   if (peerId !== socket.id) {
28                       socket.to(peerId).emit('answer', data);
29                   }
30               });
31           }
32       });
33
34       socket.on('candidate', (data) => {
35           const { candidate, roomId } = data;
36           if (peers[roomId]) {
37               peers[roomId].forEach(peerId => {
38                   if (peerId !== socket.id) {
39                       socket.to(peerId).emit('candidate', data);
40                   }
41               });
42           }
43       });
44
45       socket.on('disconnect', () => {
46           for (const roomId in peers) {
47               peers[roomId] = peers[roomId].filter(peerId => peerId !== socket.id);
48           }
49           console.log('User disconnected');
50       });
51   };
52
53   module.exports = { handleSignaling };
This code ensures that users can join a room by entering a room ID. The server keeps track of connected peers and handles signaling messages between them. With this setup, we have a basic structure in place for users to join communication sessions.
In the next sections, we will implement the main communication screen, controls, and participant views, building upon this foundation to create a fully functional WebRTC application using LibDataChannel and Node.js.

Step 3: Implement Join Screen

Join Screen Implementation

The join screen is the gateway for users to enter a WebRTC communication session. It captures user input, initiates the connection process, and transitions to the main communication interface. Let's implement the join screen functionality by setting up the HTML and JavaScript required to handle user interactions and signaling.

[a] HTML for Join Screen

In the views/ directory, we already created join.html with a basic form. Here it is again for reference:

HTML

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Join Room</title>
7    <style>
8        body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; }
9        #join-container { text-align: center; }
10        input { margin: 10px; padding: 10px; width: 200px; }
11        button { padding: 10px 20px; }
12    </style>
13</head>
14<body>
15    <div id="join-container">
16        <h1>Join a Room</h1>
17        <input type="text" id="room-id" placeholder="Enter Room ID">
18        <button id="join-btn">Join</button>
19    </div>
20    <script src="/socket.io/socket.io.js"></script>
21    <script>
22        const socket = io();
23        document.getElementById('join-btn').onclick = () => {
24            const roomId = document.getElementById('room-id').value;
25            if (roomId) {
26                socket.emit('join', { roomId });
27                localStorage.setItem('roomId', roomId);
28                window.location.href = '/main.html';
29            }
30        };
31    </script>
32</body>
33</html>
This HTML file includes a simple form where users can enter a room ID and click the "Join" button to initiate the connection process.

JavaScript for Handling User Inputs and Signaling

[b] Emit Join Event

When the user clicks the "Join" button, we emit a join event to the server with the room ID. We also store the room ID in local storage and redirect the user to main.html where the main communication interface will be displayed.

[c] Handle Join Requests on Server

Ensure the server-side signalingController.js is updated to handle the join event properly. Here’s the updated code for reference:

JavaScript

1   const peers = {};
2
3   const handleSignaling = (socket) => {
4       socket.on('join', (data) => {
5           const { roomId } = data;
6           if (!peers[roomId]) peers[roomId] = [];
7           peers[roomId].push(socket.id);
8           socket.join(roomId);
9           console.log(`User joined room: ${roomId}`);
10       });
11
12       socket.on('offer', (data) => {
13           const { offer, roomId } = data;
14           if (peers[roomId]) {
15               peers[roomId].forEach(peerId => {
16                   if (peerId !== socket.id) {
17                       socket.to(peerId).emit('offer', data);
18                   }
19               });
20           }
21       });
22
23       socket.on('answer', (data) => {
24           const { answer, roomId } = data;
25           if (peers[roomId]) {
26               peers[roomId].forEach(peerId => {
27                   if (peerId !== socket.id) {
28                       socket.to(peerId).emit('answer', data);
29                   }
30               });
31           }
32       });
33
34       socket.on('candidate', (data) => {
35           const { candidate, roomId } = data;
36           if (peers[roomId]) {
37               peers[roomId].forEach(peerId => {
38                   if (peerId !== socket.id) {
39                       socket.to(peerId).emit('candidate', data);
40                   }
41               });
42           }
43       });
44
45       socket.on('disconnect', () => {
46           for (const roomId in peers) {
47               peers[roomId] = peers[roomId].filter(peerId => peerId !== socket.id);
48           }
49           console.log('User disconnected');
50       });
51   };
52
53   module.exports = { handleSignaling };
This code ensures that users can join a room by entering a room ID, and the server keeps track of connected peers, handling signaling messages between them.

Create main.html for Main Communication Screen

Next, we need to create the main communication screen where the actual WebRTC connections will be established and managed. Create a new file named main.html in the views/ directory:

HTML

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Main Communication Screen</title>
7    <style>
8        body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; }
9        #video-container { display: flex; flex-wrap: wrap; justify-content: center; }
10        video { margin: 10px; width: 300px; height: 200px; background-color: black; }
11        #controls { margin-top: 20px; }
12        button { margin: 5px; padding: 10px 20px; }
13    </style>
14</head>
15<body>
16    <h1>Communication Room</h1>
17    <div id="video-container"></div>
18    <div id="controls">
19        <button id="mute-btn">Mute</button>
20        <button id="unmute-btn">Unmute</button>
21        <button id="end-call-btn">End Call</button>
22    </div>
23    <script src="/socket.io/socket.io.js"></script>
24    <script>
25        const socket = io();
26        const roomId = localStorage.getItem('roomId');
27        let localStream;
28
29        // Get user media (audio/video)
30        navigator.mediaDevices.getUserMedia({ video: true, audio: true })
31            .then(stream => {
32                localStream = stream;
33                const videoElement = document.createElement('video');
34                videoElement.srcObject = stream;
35                videoElement.autoplay = true;
36                document.getElementById('video-container').appendChild(videoElement);
37
38                // Join room and send offer
39                socket.emit('join', { roomId });
40                socket.emit('offer', { offer: stream, roomId });
41            })
42            .catch(error => {
43                console.error('Error accessing media devices.', error);
44            });
45
46        // Handle incoming offer
47        socket.on('offer', data => {
48            const videoElement = document.createElement('video');
49            videoElement.srcObject = data.offer;
50            videoElement.autoplay = true;
51            document.getElementById('video-container').appendChild(videoElement);
52        });
53
54        // Handle controls
55        document.getElementById('mute-btn').onclick = () => {
56            localStream.getAudioTracks()[0].enabled = false;
57        };
58        document.getElementById('unmute-btn').onclick = () => {
59            localStream.getAudioTracks()[0].enabled = true;
60        };
61        document.getElementById('end-call-btn').onclick = () => {
62            socket.emit('leave', { roomId });
63            localStream.getTracks().forEach(track => track.stop());
64            window.location.href = '/';
65        };
66    </script>
67</body>
68</html>
This main.html file sets up the main communication screen with video elements to display local and remote streams, and control buttons to mute, unmute, and end the call.
With the join screen and main communication screen in place, we have established the core functionality for users to enter a room and initiate WebRTC connections. In the next sections, we will implement additional controls and manage participant views to enhance the WebRTC application using LibDataChannel and Node.js.

Step 4: Implement Controls

Adding Control Features

To provide users with a full-featured WebRTC application, it's essential to implement control features that allow users to manage their audio and video streams effectively. In this section, we will add functionality for muting, unmuting, and ending calls.

Control Buttons Implementation

In main.html, we have already included buttons for muting, unmuting, and ending the call. Now, let's enhance the JavaScript code to handle these controls more effectively.

[a] Mute and Unmute Audio

will control the audio track of the local stream to mute and unmute the user's microphone.

[b] End Call

Ending the call will involve stopping all media tracks and informing the server and other participants.
Here’s the updated JavaScript code for main.html:

HTML

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Main Communication Screen</title>
7    <style>
8        body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; }
9        #video-container { display: flex; flex-wrap: wrap; justify-content: center; }
10        video { margin: 10px; width: 300px; height: 200px; background-color: black; }
11        #controls { margin-top: 20px; }
12        button { margin: 5px; padding: 10px 20px; }
13    </style>
14</head>
15<body>
16    <h1>Communication Room</h1>
17    <div id="video-container"></div>
18    <div id="controls">
19        <button id="mute-btn">Mute</button>
20        <button id="unmute-btn">Unmute</button>
21        <button id="end-call-btn">End Call</button>
22    </div>
23    <script src="/socket.io/socket.io.js"></script>
24    <script>
25        const socket = io();
26        const roomId = localStorage.getItem('roomId');
27        let localStream;
28        let peerConnections = {};
29
30        // Get user media (audio/video)
31        navigator.mediaDevices.getUserMedia({ video: true, audio: true })
32            .then(stream => {
33                localStream = stream;
34                const videoElement = document.createElement('video');
35                videoElement.srcObject = stream;
36                videoElement.autoplay = true;
37                videoElement.muted = true; // Mute local video to avoid feedback
38                document.getElementById('video-container').appendChild(videoElement);
39
40                // Join room and send offer
41                socket.emit('join', { roomId });
42            })
43            .catch(error => {
44                console.error('Error accessing media devices.', error);
45            });
46
47        // Handle offer from other peers
48        socket.on('offer', async data => {
49            const { offer, socketId } = data;
50            const peerConnection = new RTCPeerConnection();
51            peerConnections[socketId] = peerConnection;
52
53            peerConnection.onicecandidate = event => {
54                if (event.candidate) {
55                    socket.emit('candidate', { candidate: event.candidate, roomId });
56                }
57            };
58
59            peerConnection.ontrack = event => {
60                const videoElement = document.createElement('video');
61                videoElement.srcObject = event.streams[0];
62                videoElement.autoplay = true;
63                document.getElementById('video-container').appendChild(videoElement);
64            };
65
66            peerConnection.addStream(localStream);
67            await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
68            const answer = await peerConnection.createAnswer();
69            await peerConnection.setLocalDescription(answer);
70
71            socket.emit('answer', { answer, roomId });
72        });
73
74        // Handle answer from other peers
75        socket.on('answer', async data => {
76            const { answer, socketId } = data;
77            const peerConnection = peerConnections[socketId];
78            await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
79        });
80
81        // Handle ICE candidates
82        socket.on('candidate', async data => {
83            const { candidate, socketId } = data;
84            const peerConnection = peerConnections[socketId];
85            await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
86        });
87
88        // Handle controls
89        document.getElementById('mute-btn').onclick = () => {
90            localStream.getAudioTracks()[0].enabled = false;
91        };
92
93        document.getElementById('unmute-btn').onclick = () => {
94            localStream.getAudioTracks()[0].enabled = true;
95        };
96
97        document.getElementById('end-call-btn').onclick = () => {
98            socket.emit('leave', { roomId });
99            localStream.getTracks().forEach(track => track.stop());
100            Object.values(peerConnections).forEach(pc => pc.close());
101            window.location.href = '/';
102        };
103
104        // Handle peer leaving
105        socket.on('leave', data => {
106            const { socketId } = data;
107            const videoElement = document.getElementById(socketId);
108            if (videoElement) {
109                videoElement.srcObject.getTracks().forEach(track => track.stop());
110                videoElement.remove();
111            }
112            if (peerConnections[socketId]) {
113                peerConnections[socketId].close();
114                delete peerConnections[socketId];
115            }
116        });
117    </script>
118</body>
119</html>

Explanation

[a] Mute and Unmute Buttons

The mute-btn and unmute-btn buttons control the audio track of the local stream. When the mute button is clicked, the audio track is disabled, and when the unmute button is clicked, the audio track is enabled.

JavaScript

1   document.getElementById('mute-btn').onclick = () => {
2       localStream.getAudioTracks()[0].enabled = false;
3   };
4
5   document.getElementById('unmute-btn').onclick = () => {
6       localStream.getAudioTracks()[0].enabled = true;
7   };

[b] End Call Button

The end-call-btn button stops all media tracks, closes all peer connections, and notifies the server that the user is leaving the room. It then redirects the user back to the join screen.

JavaScript

1   document.getElementById('end-call-btn').onclick = () => {
2       socket.emit('leave', { roomId });
3       localStream.getTracks().forEach(track => track.stop());
4       Object.values(peerConnections).forEach(pc => pc.close());
5       window.location.href = '/';
6   };

[c] Handle Peer Leaving

When a peer leaves the room, the corresponding video element is removed, and the peer connection is closed and deleted from the peerConnections object.

JavaScript

1   socket.on('leave', data => {
2       const { socketId } = data;
3       const videoElement = document.getElementById(socketId);
4       if (videoElement) {
5           videoElement.srcObject.getTracks().forEach(track => track.stop());
6           videoElement.remove();
7       }
8       if (peerConnections[socketId]) {
9           peerConnections[socketId].close();
10           delete peerConnections[socketId];
11       }
12   });
With these control features implemented, users can now manage their audio and video streams effectively, enhancing the overall user experience of the WebRTC application. In the next section, we will implement the participant view to handle multiple participant streams seamlessly.

Get Free 10,000 Minutes Every Months

No credit card required to start.

Step 5: Implement Participant View

Participant View

In a WebRTC application, handling multiple participant streams is essential for a seamless communication experience. This section will focus on implementing the participant view, where each participant’s video stream is displayed on the main communication screen.

Rendering Participant Views

We will enhance the existing main.html to dynamically create and manage video elements for each participant. This involves setting up peer connections, handling media streams, and updating the UI accordingly.

Updated JavaScript Code

Here's the complete JavaScript code for main.html, including the implementation for managing multiple participant views:

HTML

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Main Communication Screen</title>
7    <style>
8        body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; }
9        #video-container { display: flex; flex-wrap: wrap; justify-content: center; }
10        video { margin: 10px; width: 300px; height: 200px; background-color: black; }
11        #controls { margin-top: 20px; }
12        button { margin: 5px; padding: 10px 20px; }
13    </style>
14</head>
15<body>
16    <h1>Communication Room</h1>
17    <div id="video-container"></div>
18    <div id="controls">
19        <button id="mute-btn">Mute</button>
20        <button id="unmute-btn">Unmute</button>
21        <button id="end-call-btn">End Call</button>
22    </div>
23    <script src="/socket.io/socket.io.js"></script>
24    <script>
25        const socket = io();
26        const roomId = localStorage.getItem('roomId');
27        let localStream;
28        let peerConnections = {};
29
30        // Get user media (audio/video)
31        navigator.mediaDevices.getUserMedia({ video: true, audio: true })
32            .then(stream => {
33                localStream = stream;
34                const videoElement = document.createElement('video');
35                videoElement.srcObject = stream;
36                videoElement.autoplay = true;
37                videoElement.muted = true; // Mute local video to avoid feedback
38                document.getElementById('video-container').appendChild(videoElement);
39
40                // Join room and send offer
41                socket.emit('join', { roomId });
42            })
43            .catch(error => {
44                console.error('Error accessing media devices.', error);
45            });
46
47        // Handle offer from other peers
48        socket.on('offer', async data => {
49            const { offer, socketId } = data;
50            const peerConnection = new RTCPeerConnection();
51            peerConnections[socketId] = peerConnection;
52
53            peerConnection.onicecandidate = event => {
54                if (event.candidate) {
55                    socket.emit('candidate', { candidate: event.candidate, roomId });
56                }
57            };
58
59            peerConnection.ontrack = event => {
60                const videoElement = document.createElement('video');
61                videoElement.srcObject = event.streams[0];
62                videoElement.autoplay = true;
63                videoElement.id = socketId;
64                document.getElementById('video-container').appendChild(videoElement);
65            };
66
67            peerConnection.addStream(localStream);
68            await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
69            const answer = await peerConnection.createAnswer();
70            await peerConnection.setLocalDescription(answer);
71
72            socket.emit('answer', { answer, roomId });
73        });
74
75        // Handle answer from other peers
76        socket.on('answer', async data => {
77            const { answer, socketId } = data;
78            const peerConnection = peerConnections[socketId];
79            await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
80        });
81
82        // Handle ICE candidates
83        socket.on('candidate', async data => {
84            const { candidate, socketId } = data;
85            const peerConnection = peerConnections[socketId];
86            await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
87        });
88
89        // Handle controls
90        document.getElementById('mute-btn').onclick = () => {
91            localStream.getAudioTracks()[0].enabled = false;
92        };
93
94        document.getElementById('unmute-btn').onclick = () => {
95            localStream.getAudioTracks()[0].enabled = true;
96        };
97
98        document.getElementById('end-call-btn').onclick = () => {
99            socket.emit('leave', { roomId });
100            localStream.getTracks().forEach(track => track.stop());
101            Object.values(peerConnections).forEach(pc => pc.close());
102            window.location.href = '/';
103        };
104
105        // Handle peer leaving
106        socket.on('leave', data => {
107            const { socketId } = data;
108            const videoElement = document.getElementById(socketId);
109            if (videoElement) {
110                videoElement.srcObject.getTracks().forEach(track => track.stop());
111                videoElement.remove();
112            }
113            if (peerConnections[socketId]) {
114                peerConnections[socketId].close();
115                delete peerConnections[socketId];
116            }
117        });
118    </script>
119</body>
120</html>

Explanation

[a] Handling Multiple Streams

  • When a new peer joins the room and sends an offer, a new RTCPeerConnection is created and stored in the peerConnections object with the peer's socket ID as the key.
  • The ontrack event of the RTCPeerConnection handles incoming media streams. A new video element is created for each stream and added to the video-container div. The video element's ID is set to the peer's socket ID for easy reference.

JavaScript

1   peerConnection.ontrack = event => {
2       const videoElement = document.createElement('video');
3       videoElement.srcObject = event.streams[0];
4       videoElement.autoplay = true;
5       videoElement.id = socketId;
6       document.getElementById('video-container').appendChild(videoElement);
7   };

[b] Updating the UI

  • The video elements are dynamically created and appended to the video-container div when new streams are received.
  • When a peer leaves, the corresponding video element is removed from the UI.

JavaScript

1   socket.on('leave', data => {
2       const { socketId } = data;
3       const videoElement = document.getElementById(socketId);
4       if (videoElement) {
5           videoElement.srcObject.getTracks().forEach(track => track.stop());
6           videoElement.remove();
7       }
8       if (peerConnections[socketId]) {
9           peerConnections[socketId].close();
10           delete peerConnections[socketId];
11       }
12   });

[c] Managing Peer Connections

Peer connections are stored in the peerConnections object. When a peer leaves, the corresponding RTCPeerConnection is closed, and the entry is deleted from the peerConnections object.

JavaScript

1   if (peerConnections[socketId]) {
2       peerConnections[socketId].close();
3       delete peerConnections[socketId];
4   }
With these enhancements, the application can now handle multiple participant streams, dynamically update the UI, and manage peer connections efficiently. In the next section, we will focus on running and testing your WebRTC application using LibDataChannel and Node.js.

Step 6: Run Your Code Now

Running the Application

With all the components in place, it's time to run and test your WebRTC application. Follow these steps to start your Node.js server and test the WebRTC functionality using LibDataChannel.

Start the Server

Ensure that you are in the root directory of your project (where the package.json file is located). Open your terminal and run the following command to start the server:

bash

1   node src/main.js

Open the Application in a Browser

Open your web browser and navigate to http://localhost:3000. You should see the join screen where you can enter a room ID.

Test the Application

  • Join a Room: Enter a room ID and click the "Join" button. This will take you to the main communication screen.
  • Open Multiple Tabs: Open multiple tabs or browsers and join the same room ID to simulate multiple participants.
  • Check Controls: Use the mute, unmute, and end call buttons to ensure they work as expected.
  • Video Streams: Verify that video streams from all participants are displayed correctly.

Troubleshooting Common Issues

If you encounter any issues while running the application, here are some common problems and their solutions:

Media Devices Access Error

  • Ensure that your browser has permission to access the camera and microphone.
  • Check for any errors in the console related to media device access.

Signaling Issues

  • Verify that the signaling messages (offer, answer, and ICE candidates) are being exchanged correctly between peers.
  • Check the server logs for any errors related to socket connections.

ICE Candidate Issues

  • Ensure that the ICE candidates are being correctly exchanged and added to the peer connections.
  • Check for any network-related issues that might be blocking the connection.

Conclusion

In this guide, we have successfully built a WebRTC application using LibDataChannel and Node.js. We covered the setup of the development environment, project structure, server configuration, and implementation of key features such as the join screen, main communication interface, controls, and participant views. This application demonstrates the power of WebRTC for real-time communication and the flexibility of Node.js in handling asynchronous operations.

Want to level-up your learning? Subscribe now

Subscribe to our newsletter for more tech based insights

FAQ