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

How to Build FreeSwitch WebRTC App with JavaScript?

Learn how to integrate FreeSwitch with WebRTC to build powerful real-time communication applications. This guide covers installation, configuration, control implementation, and participant management, ensuring a seamless and scalable communication solution.

Introduction to FreeSwitch WebRTC Technology

What is FreeSwitch WebRTC?

FreeSwitch WebRTC is a powerful integration of FreeSwitch, a versatile open-source telephony platform, with WebRTC, an advanced technology that enables real-time communication directly in web browsers. This combination allows developers to create sophisticated and scalable communication solutions that can handle a variety of tasks, such as PBX (Private Branch Exchange), IVR (Interactive Voice Response), and conference systems, bridging the gap between traditional telephony and modern web-based communication.

Overview of FreeSwitch

FreeSwitch is a robust, scalable open-source telephony platform designed to route and interconnect communication protocols. It supports a wide range of telephony applications, including PBX, VoIP gateways, and conferencing. Written in C, FreeSwitch is known for its performance and flexibility, making it a preferred choice for businesses and developers looking to build advanced telephony solutions.

Introduction to WebRTC

WebRTC (Web Real-Time Communication) is a cutting-edge technology that facilitates peer-to-peer communication directly within web browsers without the need for additional plugins or software. By leveraging WebRTC, developers can implement high-quality audio, video, and data sharing capabilities in web applications, enhancing user experiences with seamless, real-time interactions.

Importance and Benefits of Integrating FreeSwitch with WebRTC

Integrating FreeSwitch with WebRTC combines the best of both worlds—robust telephony capabilities with modern, real-time web communication. This integration provides several key benefits:
  • Enhanced Communication: By merging traditional telephony with web-based communication, users can seamlessly connect via voice, video, and messaging, regardless of their platform or device.
  • Scalability: FreeSwitch's architecture supports large-scale deployments, making it suitable for enterprise-level applications that require high performance and reliability.
  • Flexibility and Customization: Developers can tailor the solution to meet specific business needs, thanks to FreeSwitch's extensive configuration options and WebRTC's adaptable nature.
  • Cost-Effective: Utilizing open-source technologies like FreeSwitch and WebRTC can significantly reduce costs compared to proprietary telephony systems, while still offering high-quality service.
By leveraging FreeSwitch and WebRTC, businesses can build advanced communication systems that are both efficient and scalable, providing a superior user experience and meeting the demands of modern telephony requirements.

Getting Started with FreeSwitch WebRTC

Create a New FreeSwitch WebRTC App

Starting a new FreeSwitch WebRTC application involves setting up your development environment and ensuring you have the necessary prerequisites. Here’s a step-by-step guide to help you get started:

Setting Up Your Development Environment

Before diving into the code, ensure your development environment is ready. You’ll need:
  • A machine running a Unix-like operating system (Linux or macOS is recommended).
  • A stable internet connection to download necessary packages and dependencies.
  • Basic knowledge of C and web development (HTML, CSS, JavaScript).

Prerequisites for FreeSwitch and WebRTC

Ensure you have the following prerequisites installed on your system:
  • Git: For cloning repositories.
  • Build Tools: GCC, make, and other essential build tools.
  • CMake: For managing the build process.
  • OpenSSL: For secure communication.
  • Node.js and npm: For managing JavaScript dependencies.

Install FreeSwitch

Installing FreeSwitch is a straightforward process. Follow these steps to get FreeSwitch up and running on your system:

[a] Clone the FreeSwitch Repository

sh

1   git clone https://github.com/signalwire/freeswitch.git
2   cd freeswitch

[b] Install Dependencies

sh

1   sudo apt-get update
2   sudo apt-get install -y build-essential pkg-config libjpeg-dev \
3   libncurses5-dev libssl-dev libpcre3-dev libcurl4-openssl-dev \
4   libldns-dev libedit-dev libsqlite3-dev libspeex-dev \
5   libspeexdsp-dev libavformat-dev libswscale-dev \
6   libavresample-dev libopus-dev libsndfile1-dev

[c] Build FreeSwitch

sh

1   ./bootstrap.sh
2   ./configure
3   make
4   sudo make install
5   sudo make all cd-sounds-install cd-moh-install

[d] Configure FreeSwitch for WebRTC

Enable necessary modules in the modules.conf.xml file:

xml

1     <load module="mod_verto"/>
2     <load module="mod_sofia"/>

[e] Start FreeSwitch

sh

1   sudo freeswitch -nc

Configuring FreeSwitch for WebRTC

To enable WebRTC support in FreeSwitch, some additional configuration is required. This includes setting up the verto (WebRTC signaling protocol) module and ensuring secure WebSocket (WSS) communication.

Configure verto

  • Edit the verto.conf.xml file to set up verto parameters, such as SSL settings and binding ports.
  • Ensure the verto module is loaded in the modules.conf.xml file.

Secure Communication

  • Generate SSL certificates and configure them in the wss section of the sofia.conf.xml file.
  • Example SSL configuration:

xml

1     <param name="wss-binding" value=":7443"/>
2     <param name="cert-file" value="/path/to/your/cert.pem"/>
3     <param name="key-file" value="/path/to/your/key.pem"/>

Structure of the Project

Understanding the structure of your FreeSwitch WebRTC project is crucial for effective development and maintenance. Here’s an overview of key directories and files:
  • conf: Contains configuration files.
    • modules.conf.xml: Specifies which modules to load.
    • sofia.conf.xml: Configuration for SIP profiles and WebRTC.
    • verto.conf.xml: Configuration for the verto module.
  • scripts: Contains script files for various tasks.
  • htdocs: Holds static files such as HTML, CSS, and JavaScript for web-based interfaces.
  • logs: Stores log files generated by FreeSwitch.

App Architecture

freeswitch-webrtc
FreeSwitch WebRTC applications typically follow a modular architecture, separating core telephony functions from web-based components. Here’s an overview of the interaction between FreeSwitch and WebRTC:

Client-Side (Browser)

  • Uses WebRTC API for media capture and peer-to-peer communication.
  • JavaScript handles signaling and user interactions.

Server-Side (FreeSwitch)

  • Manages signaling using the verto module.
  • Routes and processes media streams.
  • Integrates with other telephony components (IVR, PBX).
By following this modular architecture, you can build scalable and maintainable FreeSwitch WebRTC applications that leverage the strengths of both FreeSwitch and WebRTC.
In the next part, we will delve into the detailed steps for configuring FreeSwitch to support WebRTC, starting with essential configuration files and parameters.

Step 1: Get Started with Configuration

Configuring FreeSwitch for WebRTC

To fully leverage the capabilities of FreeSwitch with WebRTC, proper configuration is essential. This section will guide you through the necessary steps to configure FreeSwitch for WebRTC, focusing on setting up the required modules and securing your configuration.

Setting Up Necessary Modules

FreeSwitch requires specific modules to handle WebRTC communication effectively. The key modules for WebRTC are mod_verto and mod_sofia. These modules manage signaling and media processing for WebRTC connections.

[a] Enable mod_verto and mod_sofia in modules.conf.xml

xml

1   <load module="mod_verto"/>
2   <load module="mod_sofia"/>

[b] Reload the Configuration

After editing the modules.conf.xml, reload the FreeSwitch configuration:

sh

1     fs_cli -x "reloadxml"

Configuration Files and Parameters

The following configuration files need to be modified to support WebRTC:

[a] verto.conf.xml

This file configures the verto module, which handles WebRTC signaling.
Example verto.conf.xml configuration:

xml

1     <settings>
2       <param name="http-port" value="8081"/>
3       <param name="ws-binding" value=":8082"/>
4       <param name="wss-binding" value=":8083"/>
5       <param name="tls-cert-dir" value="/usr/local/freeswitch/certs"/>
6     </settings>

sofia.conf.xml

This file configures SIP profiles, including settings for WebRTC.
Example sofia.conf.xml configuration:

xml

1     <profile name="external">
2       <param name="wss-binding" value=":7443"/>
3       <param name="cert-file" value="/usr/local/freeswitch/certs/server.pem"/>
4       <param name="key-file" value="/usr/local/freeswitch/certs/server.key"/>
5     </profile>

Securing the Configuration

Security is crucial when configuring FreeSwitch for WebRTC, especially when dealing with WebSocket Secure (WSS) connections. Ensure that all communications are encrypted using SSL/TLS.

[a] Generate SSL Certificates

Generate SSL certificates using a trusted Certificate Authority (CA) or create self-signed certificates for testing purposes.
Example command to generate a self-signed certificate:

sh

1     openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
2     -keyout /usr/local/freeswitch/certs/server.key \
3     -out /usr/local/freeswitch/certs/server.pem

[b] Configure SSL in sofia.conf.xml

Add the paths to your SSL certificate and key files in the sofia.conf.xml configuration:

xml

1     <param name="wss-binding" value=":7443"/>
2     <param name="cert-file" value="/usr/local/freeswitch/certs/server.pem"/>
3     <param name="key-file" value="/usr/local/freeswitch/certs/server.key"/>

[c] Enable Secure WebSocket (WSS)

Ensure that the wss-binding parameter is set correctly to bind WSS on the desired port.

Example Configuration for Secure WebRTC

Here’s a consolidated example of what your configuration files might look like:

verto.conf.xml

xml

1<settings>
2  <param name="http-port" value="8081"/>
3  <param name="ws-binding" value=":8082"/>
4  <param name="wss-binding" value=":8083"/>
5  <param name="tls-cert-dir" value="/usr/local/freeswitch/certs"/>
6</settings>

sofia.conf.xml

xml

1<profile name="external">
2  <param name="wss-binding" value=":7443"/>
3  <param name="cert-file" value="/usr/local/freeswitch/certs/server.pem"/>
4  <param name="key-file" value="/usr/local/freeswitch/certs/server.key"/>
5</profile>
By following these configuration steps, you can ensure that FreeSwitch is properly set up to handle WebRTC connections securely and efficiently. In the next part, we will cover how to design the wireframe for all components, ensuring a well-organized and user-friendly interface for your FreeSwitch WebRTC application.

Step 2: Wireframe All the Components

Designing the Wireframe

Creating a wireframe for your FreeSwitch WebRTC application is a crucial step in the development process. It helps you visualize the layout, design the user interface, and ensure all necessary components are included. This section will guide you through designing a wireframe for the key components of your application.

Implementing the Wireframe

Once the wireframe is designed, you can start implementing it using HTML, CSS, and JavaScript. Here’s an example of how to create the join screen:

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>FreeSwitch WebRTC Join Screen</title>
7    <link rel="stylesheet" href="styles.css">
8</head>
9<body>
10    <div class="join-screen">
11        <h1>FreeSwitch WebRTC</h1>
12        <form id="join-form">
13            <label for="username">Username:</label>
14            <input type="text" id="username" name="username" required>
15            <label for="room-id">Room ID:</label>
16            <input type="text" id="room-id" name="room-id" required>
17            <button type="submit">Join</button>
18        </form>
19    </div>
20</body>
21</html>

CSS

CSS

1body {
2    font-family: Arial, sans-serif;
3    display: flex;
4    justify-content: center;
5    align-items: center;
6    height: 100vh;
7    margin: 0;
8    background-color: #f0f0f0;
9}
10
11.join-screen {
12    background: white;
13    padding: 20px;
14    border-radius: 8px;
15    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
16    text-align: center;
17}
18
19.join-screen h1 {
20    margin-bottom: 20px;
21}
22
23.join-screen form {
24    display: flex;
25    flex-direction: column;
26    gap: 10px;
27}
28
29.join-screen label {
30    text-align: left;
31}
32
33.join-screen input {
34    padding: 10px;
35    border: 1px solid #ccc;
36    border-radius: 4px;
37}
38
39.join-screen button {
40    padding: 10px;
41    border: none;
42    border-radius: 4px;
43    background-color: #007bff;
44    color: white;
45    cursor: pointer;
46}
47
48.join-screen button:hover {
49    background-color: #0056b3;
50}
This example sets up a simple join screen with HTML and CSS. The form captures the user's name and room ID, preparing them to join a WebRTC session.
In the next part, we will implement the join screen functionality, ensuring users can connect to their WebRTC sessions seamlessly.

Step 3: Implement Join Screen

Creating the Join Screen

The join screen is a crucial part of your FreeSwitch WebRTC application. It serves as the entry point for users to connect to the WebRTC session. In this section, we will implement the join screen functionality using HTML, CSS, and JavaScript.

HTML Structure for the Join Screen

We have already created a basic HTML structure for the join screen in the previous part. 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>FreeSwitch WebRTC Join Screen</title>
7    <link rel="stylesheet" href="styles.css">
8</head>
9<body>
10    <div class="join-screen">
11        <h1>FreeSwitch WebRTC</h1>
12        <form id="join-form">
13            <label for="username">Username:</label>
14            <input type="text" id="username" name="username" required>
15            <label for="room-id">Room ID:</label>
16            <input type="text" id="room-id" name="room-id" required>
17            <button type="submit">Join</button>
18        </form>
19    </div>
20    <script src="script.js"></script>
21</body>
22</html>

CSS for Styling the Join Screen

The CSS provided earlier styles the join screen with a clean and user-friendly interface. Here it is again for reference:

CSS

1body {
2    font-family: Arial, sans-serif;
3    display: flex;
4    justify-content: center;
5    align-items: center;
6    height: 100vh;
7    margin: 0;
8    background-color: #f0f0f0;
9}
10
11.join-screen {
12    background: white;
13    padding: 20px;
14    border-radius: 8px;
15    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
16    text-align: center;
17}
18
19.join-screen h1 {
20    margin-bottom: 20px;
21}
22
23.join-screen form {
24    display: flex;
25    flex-direction: column;
26    gap: 10px;
27}
28
29.join-screen label {
30    text-align: left;
31}
32
33.join-screen input {
34    padding: 10px;
35    border: 1px solid #ccc;
36    border-radius: 4px;
37}
38
39.join-screen button {
40    padding: 10px;
41    border: none;
42    border-radius: 4px;
43    background-color: #007bff;
44    color: white;
45    cursor: pointer;
46}
47
48.join-screen button:hover {
49    background-color: #0056b3;
50}

JavaScript for Form Handling and WebRTC Connection

To make the join screen functional, we need to add JavaScript that handles form submission and initiates the WebRTC connection. We will use the Vert.x framework for handling WebRTC signaling.

JavaScript

1document.addEventListener("DOMContentLoaded", function () {
2    const joinForm = document.getElementById("join-form");
3
4    joinForm.addEventListener("submit", function (event) {
5        event.preventDefault();
6
7        const username = document.getElementById("username").value;
8        const roomId = document.getElementById("room-id").value;
9
10        // Validate input
11        if (!username || !roomId) {
12            alert("Please enter both username and room ID.");
13            return;
14        }
15
16        // Initiate WebRTC connection
17        joinRoom(username, roomId);
18    });
19
20    function joinRoom(username, roomId) {
21        // Replace with your actual WebRTC signaling server URL
22        const signalingServerUrl = "wss://your-signaling-server-url";
23        const ws = new WebSocket(signalingServerUrl);
24
25        ws.onopen = function () {
26            console.log("Connected to the signaling server");
27
28            // Send join message
29            const joinMessage = {
30                type: "join",
31                username: username,
32                roomId: roomId
33            };
34            ws.send(JSON.stringify(joinMessage));
35        };
36
37        ws.onmessage = function (message) {
38            console.log("Received message from server:", message.data);
39
40            const data = JSON.parse(message.data);
41            switch (data.type) {
42                case "joined":
43                    console.log(`${username} joined room ${roomId}`);
44                    // Proceed to the main communication interface
45                    loadMainInterface();
46                    break;
47                case "error":
48                    console.error("Error:", data.message);
49                    alert(data.message);
50                    break;
51                // Handle other message types as needed
52            }
53        };
54
55        ws.onerror = function (error) {
56            console.error("WebSocket error:", error);
57        };
58
59        ws.onclose = function () {
60            console.log("Disconnected from the signaling server");
61        };
62    }
63
64    function loadMainInterface() {
65        // Redirect or load the main communication interface
66        window.location.href = "main.html";
67    }
68});

Explanation of the JavaScript Code

Form Submission Handling

  • The script listens for the form submission event and prevents the default form submission behavior.
  • It retrieves the username and room ID from the input fields and validates them.

WebSocket Connection

  • A WebSocket connection is established to the WebRTC signaling server.
  • Upon connection, a join message is sent to the server with the user's details.

Handling Server Messages

  • The script listens for messages from the signaling server.
  • When a join confirmation is received, it proceeds to load the main communication interface.
  • Errors from the server are handled and displayed to the user.

Loading the Main Interface

  • The loadMainInterface function redirects the user to the main communication interface, where the WebRTC session will take place.
This completes the implementation of the join screen for your FreeSwitch WebRTC application. In the next part, we will move on to developing the control features for the main communication interface, ensuring users can manage their audio and video streams effectively.

Step 4: Implement Controls

Developing Control Features

Control features are essential for a user-friendly WebRTC application. They allow users to manage their audio and video streams, providing a more interactive and flexible communication experience. In this section, we will implement the core control features, including audio and video controls, mute/unmute functionality, and camera switch capabilities.

Audio and Video Controls

Implementing audio and video controls involves creating buttons that allow users to toggle their microphone and camera on or off. These controls will be integrated into the main communication interface.
First, add buttons for audio and video controls to your main communication interface HTML file:

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>FreeSwitch WebRTC Main Interface</title>
7    <link rel="stylesheet" href="styles.css">
8</head>
9<body>
10    <div class="main-interface">
11        <div id="local-video-container">
12            <video id="local-video" autoplay muted></video>
13        </div>
14        <div id="remote-video-container">
15            <!-- Remote video elements will be added here dynamically -->
16        </div>
17        <div class="control-panel">
18            <button id="mute-audio">Mute Audio</button>
19            <button id="toggle-video">Stop Video</button>
20            <button id="switch-camera">Switch Camera</button>
21            <button id="end-call">End Call</button>
22        </div>
23    </div>
24    <script src="webrtc.js"></script>
25</body>
26</html>

CSS

Add styling for the control panel to your styles.css file:

CSS

1.main-interface {
2    display: flex;
3    flex-direction: column;
4    align-items: center;
5    justify-content: center;
6    height: 100vh;
7    background-color: #f0f0f0;
8}
9
10#local-video-container,
11#remote-video-container {
12    width: 100%;
13    max-width: 600px;
14    margin-bottom: 20px;
15    position: relative;
16}
17
18#local-video,
19.remote-video {
20    width: 100%;
21    border-radius: 8px;
22    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
23}
24
25.control-panel {
26    display: flex;
27    gap: 10px;
28}
29
30.control-panel button {
31    padding: 10px 20px;
32    border: none;
33    border-radius: 4px;
34    background-color: #007bff;
35    color: white;
36    cursor: pointer;
37}
38
39.control-panel button:hover {
40    background-color: #0056b3;
41}

JavaScript for Implementing Controls

Next, we need to add JavaScript to handle the functionality of these controls.

JavaScript

1document.addEventListener("DOMContentLoaded", function () {
2    const muteAudioButton = document.getElementById("mute-audio");
3    const toggleVideoButton = document.getElementById("toggle-video");
4    const switchCameraButton = document.getElementById("switch-camera");
5    const endCallButton = document.getElementById("end-call");
6
7    let localStream;
8    let isAudioMuted = false;
9    let isVideoStopped = false;
10
11    // Get user media
12    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
13        .then(stream => {
14            localStream = stream;
15            document.getElementById("local-video").srcObject = stream;
16        })
17        .catch(error => {
18            console.error("Error accessing media devices.", error);
19        });
20
21    // Mute/Unmute Audio
22    muteAudioButton.addEventListener("click", function () {
23        isAudioMuted = !isAudioMuted;
24        localStream.getAudioTracks()[0].enabled = !isAudioMuted;
25        muteAudioButton.textContent = isAudioMuted ? "Unmute Audio" : "Mute Audio";
26    });
27
28    // Start/Stop Video
29    toggleVideoButton.addEventListener("click", function () {
30        isVideoStopped = !isVideoStopped;
31        localStream.getVideoTracks()[0].enabled = !isVideoStopped;
32        toggleVideoButton.textContent = isVideoStopped ? "Start Video" : "Stop Video";
33    });
34
35    // Switch Camera
36    switchCameraButton.addEventListener("click", async function () {
37        if (localStream) {
38            const videoTrack = localStream.getVideoTracks()[0];
39            const currentConstraints = videoTrack.getConstraints();
40
41            const devices = await navigator.mediaDevices.enumerateDevices();
42            const videoDevices = devices.filter(device => device.kind === 'videoinput');
43
44            if (videoDevices.length > 1) {
45                const currentDeviceId = videoTrack.getSettings().deviceId;
46                const newDeviceId = videoDevices.find(device => device.deviceId !== currentDeviceId).deviceId;
47
48                const newConstraints = {
49                    video: {
50                        deviceId: { exact: newDeviceId }
51                    }
52                };
53
54                const newStream = await navigator.mediaDevices.getUserMedia(newConstraints);
55                const newVideoTrack = newStream.getVideoTracks()[0];
56                localStream.removeTrack(videoTrack);
57                localStream.addTrack(newVideoTrack);
58
59                document.getElementById("local-video").srcObject = localStream;
60            }
61        }
62    });
63
64    // End Call
65    endCallButton.addEventListener("click", function () {
66        if (localStream) {
67            localStream.getTracks().forEach(track => track.stop());
68        }
69        // Implement additional logic to handle call termination
70        window.location.href = "join.html";
71    });
72});

Explanation of the JavaScript Code

Get User Media

  • The code requests access to the user's audio and video devices using navigator.mediaDevices.getUserMedia.
  • The local video stream is displayed in the video element.

Mute/Unmute Audio

  • The muteAudioButton toggles the audio track's enabled property.
  • The button text changes to reflect the current state.

Start/Stop Video

  • The toggleVideoButton toggles the video track's enabled property.
  • The button text changes to indicate whether the video is active or stopped.

Switch Camera

  • The switchCameraButton checks for available video input devices.
  • If more than one device is available, it switches between them.
  • The video stream is updated to reflect the new camera input.

End Call

  • The endCallButton stops all tracks of the local stream.
  • It then redirects the user back to the join screen or performs other call termination logic.
This completes the implementation of the control features for your FreeSwitch WebRTC application. These controls will enable users to manage their audio and video streams effectively, providing a better communication experience. In the next part, we will focus on implementing the participant view, allowing users to see and interact with multiple participants in a session.

Get Free 10,000 Minutes Every Months

No credit card required to start.

Step 5: Implement Participant View

Building the Participant View

The participant view is an essential component of your FreeSwitch WebRTC application. It allows users to see and interact with multiple participants in a session. In this section, we will implement the participant view, including the layout for displaying multiple video streams and managing participant streams and layouts.

Displaying Multiple Participants

To display multiple participants, we need to dynamically create video elements for each participant and add them to the participant view. The main interface HTML will have a container for remote video elements.

[a] HTML

Update your main communication interface HTML file to include a container for remote videos:

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>FreeSwitch WebRTC Main Interface</title>
7    <link rel="stylesheet" href="styles.css">
8</head>
9<body>
10    <div class="main-interface">
11        <div id="local-video-container">
12            <video id="local-video" autoplay muted></video>
13        </div>
14        <div id="remote-video-container">
15            <!-- Remote video elements will be added here dynamically -->
16        </div>
17        <div class="control-panel">
18            <button id="mute-audio">Mute Audio</button>
19            <button id="toggle-video">Stop Video</button>
20            <button id="switch-camera">Switch Camera</button>
21            <button id="end-call">End Call</button>
22        </div>
23    </div>
24    <script src="webrtc.js"></script>
25</body>
26</html>

[b] CSS

Add styling for the remote video container to your styles.css file:

CSS

1#remote-video-container {
2    display: flex;
3    flex-wrap: wrap;
4    justify-content: center;
5    gap: 10px;
6}
7
8.remote-video {
9    width: 300px;
10    border-radius: 8px;
11    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
12}

Managing Participant Streams and Layouts

We will now update the JavaScript to handle multiple participant streams and manage their layout dynamically.

[c] JavaScript (webrtc.js)

JavaScript

1document.addEventListener("DOMContentLoaded", function () {
2    const muteAudioButton = document.getElementById("mute-audio");
3    const toggleVideoButton = document.getElementById("toggle-video");
4    const switchCameraButton = document.getElementById("switch-camera");
5    const endCallButton = document.getElementById("end-call");
6
7    let localStream;
8    let remoteStreams = {};
9    let isAudioMuted = false;
10    let isVideoStopped = false;
11
12    // Get user media
13    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
14        .then(stream => {
15            localStream = stream;
16            document.getElementById("local-video").srcObject = stream;
17            // Initialize connection to signaling server
18            initializeWebSocket();
19        })
20        .catch(error => {
21            console.error("Error accessing media devices.", error);
22        });
23
24    // WebSocket initialization and handling
25    function initializeWebSocket() {
26        // Replace with your actual WebSocket signaling server URL
27        const signalingServerUrl = "wss://your-signaling-server-url";
28        const ws = new WebSocket(signalingServerUrl);
29
30        ws.onopen = function () {
31            console.log("Connected to the signaling server");
32
33            // Send join message
34            const joinMessage = {
35                type: "join",
36                username: "user",
37                roomId: "room"
38            };
39            ws.send(JSON.stringify(joinMessage));
40        };
41
42        ws.onmessage = function (message) {
43            console.log("Received message from server:", message.data);
44
45            const data = JSON.parse(message.data);
46            switch (data.type) {
47                case "joined":
48                    console.log(`${data.username} joined room ${data.roomId}`);
49                    break;
50                case "new-participant":
51                    handleNewParticipant(data.participantId);
52                    break;
53                case "participant-left":
54                    handleParticipantLeft(data.participantId);
55                    break;
56                case "offer":
57                    handleOffer(data.offer, data.participantId);
58                    break;
59                case "answer":
60                    handleAnswer(data.answer, data.participantId);
61                    break;
62                case "ice-candidate":
63                    handleIceCandidate(data.candidate, data.participantId);
64                    break;
65                // Handle other message types as needed
66            }
67        };
68
69        ws.onerror = function (error) {
70            console.error("WebSocket error:", error);
71        };
72
73        ws.onclose = function () {
74            console.log("Disconnected from the signaling server");
75        };
76    }
77
78    function handleNewParticipant(participantId) {
79        // Create new video element for the participant
80        const remoteVideo = document.createElement("video");
81        remoteVideo.id = `remote-video-${participantId}`;
82        remoteVideo.classList.add("remote-video");
83        remoteVideo.autoplay = true;
84        document.getElementById("remote-video-container").appendChild(remoteVideo);
85
86        // Initialize a new RTCPeerConnection
87        const peerConnection = new RTCPeerConnection();
88
89        // Add local stream tracks to the peer connection
90        localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
91
92        // Handle incoming tracks
93        peerConnection.ontrack = function (event) {
94            if (remoteVideo.srcObject !== event.streams[0]) {
95                remoteVideo.srcObject = event.streams[0];
96                console.log("Received remote stream");
97            }
98        };
99
100        // Handle ICE candidates
101        peerConnection.onicecandidate = function (event) {
102            if (event.candidate) {
103                const candidateMessage = {
104                    type: "ice-candidate",
105                    candidate: event.candidate,
106                    participantId: participantId
107                };
108                ws.send(JSON.stringify(candidateMessage));
109            }
110        };
111
112        remoteStreams[participantId] = peerConnection;
113
114        // Create and send an offer
115        peerConnection.createOffer()
116            .then(offer => {
117                return peerConnection.setLocalDescription(offer);
118            })
119            .then(() => {
120                const offerMessage = {
121                    type: "offer",
122                    offer: peerConnection.localDescription,
123                    participantId: participantId
124                };
125                ws.send(JSON.stringify(offerMessage));
126            })
127            .catch(error => {
128                console.error("Error creating offer:", error);
129            });
130    }
131
132    function handleParticipantLeft(participantId) {
133        // Remove the video element for the participant
134        const remoteVideo = document.getElementById(`remote-video-${participantId}`);
135        if (remoteVideo) {
136            remoteVideo.remove();
137        }
138
139        // Close the peer connection
140        if (remoteStreams[participantId]) {
141            remoteStreams[participantId].close();
142            delete remoteStreams[participantId];
143        }
144    }
145
146    function handleOffer(offer, participantId) {
147        const peerConnection = new RTCPeerConnection();
148
149        // Add local stream tracks to the peer connection
150        localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
151
152        // Handle incoming tracks
153        peerConnection.ontrack = function (event) {
154            const remoteVideo = document.getElementById(`remote-video-${participantId}`);
155            if (remoteVideo.srcObject !== event.streams[0]) {
156                remoteVideo.srcObject = event.streams[0];
157                console.log("Received remote stream");
158            }
159        };
160
161        // Handle ICE candidates
162        peerConnection.onicecandidate = function (event) {
163            if (event.candidate) {
164                const candidateMessage = {
165                    type: "ice-candidate",
166                    candidate: event.candidate,
167                    participantId: participantId
168                };
169                ws.send(JSON.stringify(candidateMessage));
170            }
171        };
172
173        peerConnection.setRemoteDescription(new RTCSessionDescription(offer))
174            .then(() => {
175                return peerConnection.createAnswer();
176            })
177            .then(answer => {
178                return peerConnection.setLocalDescription(answer);
179            })
180            .then(() => {
181                const answerMessage = {
182                    type: "answer",
183                    answer: peerConnection.localDescription,
184                    participantId: participantId
185                };
186                ws.send(JSON.stringify(answerMessage));
187            })
188            .catch(error => {
189                console.error("Error handling offer:", error);
190            });
191
192        remoteStreams[participantId] = peerConnection;
193    }
194
195    function handleAnswer(answer, participantId) {
196        const peerConnection = remoteStreams[participantId];
197        peerConnection.setRemoteDescription(new RTCSessionDescription(answer))
198            .catch(error => {
199                console.error("Error setting remote description:", error);
200            });
201    }
202
203    function handleIceCandidate(candidate, participantId) {
204        const peerConnection = remoteStreams[participantId];
205        peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
206            .catch(error => {
207                console.error("Error adding ICE candidate:", error);
208            });
209    }
210
211    // Mute/Unmute Audio
212    muteAudioButton.addEventListener("click", function () {
213        isAudioMuted = !isAudioMuted;
214        localStream.getAudioTracks()[0].enabled = !isAudioMuted;
215        muteAudioButton.textContent = isAudioMuted ? "Unmute Audio" : "Mute Audio";
216    });
217
218    // Start/Stop Video
219    toggleVideoButton.addEventListener("click", function () {
220        isVideoStopped = !isVideoStopped;
221        localStream.getVideoTracks()[0].enabled = !isVideoStopped;
222        toggleVideoButton.textContent = isVideoStopped ? "Start Video" : "Stop Video";
223    });
224
225    // Switch Camera
226    switchCameraButton.addEventListener("click", async function () {
227        if (localStream) {
228            const videoTrack =
229
230 localStream.getVideoTracks()[0];
231            const currentConstraints = videoTrack.getConstraints();
232
233            const devices = await navigator.mediaDevices.enumerateDevices();
234            const videoDevices = devices.filter(device => device.kind === 'videoinput');
235
236            if (videoDevices.length > 1) {
237                const currentDeviceId = videoTrack.getSettings().deviceId;
238                const newDeviceId = videoDevices.find(device => device.deviceId !== currentDeviceId).deviceId;
239
240                const newConstraints = {
241                    video: {
242                        deviceId: { exact: newDeviceId }
243                    }
244                };
245
246                const newStream = await navigator.mediaDevices.getUserMedia(newConstraints);
247                const newVideoTrack = newStream.getVideoTracks()[0];
248                localStream.removeTrack(videoTrack);
249                localStream.addTrack(newVideoTrack);
250
251                document.getElementById("local-video").srcObject = localStream;
252            }
253        }
254    });
255
256    // End Call
257    endCallButton.addEventListener("click", function () {
258        if (localStream) {
259            localStream.getTracks().forEach(track => track.stop());
260        }
261        // Implement additional logic to handle call termination
262        window.location.href = "join.html";
263    });
264});

Explanation of the JavaScript Code

WebSocket Initialization and Handling

  • Establishes a WebSocket connection to the signaling server and handles incoming messages.
  • Manages participant join, leave, offer, answer, and ICE candidate messages.

Handling New Participants

  • Creates a new video element for each participant.
  • Initializes a new RTCPeerConnection for each participant.
  • Adds local stream tracks to the peer connection.
  • Handles incoming tracks and ICE candidates.
  • Creates and sends an offer to the new participant.

Handling Participant Left

  • Removes the video element for the participant who left.
  • Closes the peer connection and removes it from the remoteStreams object.

Handling Offers and Answers

  • Sets up the RTCPeerConnection for incoming offers and answers.
  • Manages remote descriptions and ICE candidates.

Implementing Control Features

  • Mute/unmute audio.
  • Start/stop video.
  • Switch camera.
  • End call and redirect to the join screen.
This completes the implementation of the participant view for your FreeSwitch WebRTC application. Users can now see and interact with multiple participants in a session. In the next part, we will cover how to run your code and test the application to ensure everything works as expected.

Step 6: Run Your Code Now

Running the Application

Now that you have implemented all the core features of your FreeSwitch WebRTC application, it’s time to run and test your code. This section will guide you through the steps to ensure your application is up and running smoothly.

Steps to Run the FreeSwitch WebRTC App

Start FreeSwitch

  • Ensure FreeSwitch is properly configured and running.
  • Start FreeSwitch by running:

sh

1     sudo freeswitch -nc

Launch the Web Server

  • Serve your HTML, CSS, and JavaScript files using a web server. You can use a simple HTTP server like http-server for this purpose.
  • Install http-server if you haven't already:

sh

1     npm install -g http-server
  • Navigate to the directory containing your index.html file and start the server:

    sh

    1http-server
  • The server will start and provide you with a URL (e.g., http://localhost:8080).

Access the Application

  • Open a web browser and navigate to the URL provided by your HTTP server (e.g., http://localhost:8080).
  • You should see the join screen of your FreeSwitch WebRTC application.

Testing and Debugging Tips

Test the Join Screen

  • Enter a username and room ID, then click the "Join" button.
  • Ensure the WebSocket connection to the signaling server is established and that you can join the session without errors.

Test Audio and Video Controls

  • Verify that you can mute/unmute audio and start/stop video using the respective buttons.
  • Check that switching the camera works if you have multiple video input devices.

Test Participant View

  • Open multiple browser tabs or windows and join the same room from different tabs.
  • Ensure that video streams from all participants are displayed correctly.
  • Test the handling of participants joining and leaving the session.

Check Console Logs

  • Open the browser's developer console (usually accessible with F12 or right-clicking the page and selecting "Inspect").
  • Look for any error messages or warnings in the console log that might indicate issues with your WebRTC implementation.

Network Debugging

  • Use the network tab in the browser's developer tools to monitor WebSocket communication and network traffic.
  • Ensure that signaling messages (offer, answer, ICE candidates) are being sent and received correctly.

Troubleshooting Common Issues

WebSocket Connection Issues

  • Ensure that the signaling server URL is correct and that the server is running.
  • Check network connectivity and firewall settings that might block WebSocket connections.

Media Device Access Errors

  • Verify that your browser has permission to access the microphone and camera.
  • Check that the correct media devices are selected and available.

ICE Candidate Gathering Issues

  • Ensure that ICE candidates are being gathered and sent correctly.
  • Verify that STUN/TURN servers are configured if necessary.

Video/Audio Stream Issues

  • Ensure that video and audio tracks are being added to the RTCPeerConnection.
    • Check that the correct video and audio elements are being updated with the media streams.

Example Scenario: Testing a Two-Participant Session

Participant 1

  • Open a browser window and navigate to http://localhost:8080.
  • Enter a username (e.g., "User1") and a room ID (e.g., "TestRoom").
  • Click "Join" to enter the session.

Participant 2

  • Open another browser window or tab and navigate to http://localhost:8080.
  • Enter a different username (e.g., "User2") and the same room ID (e.g., "TestRoom").
  • Click "Join" to enter the session.

Verify Interaction

  • Ensure that both participants can see each other's video streams.
  • Test the audio and video controls for both participants.
  • Verify that messages and streams are handled correctly by the signaling server.

Conclusion

Congratulations! You have successfully built a FreeSwitch WebRTC application that allows users to join sessions, control their audio and video, and interact with multiple participants. This guide covered the essential steps, from setting up FreeSwitch and configuring WebRTC to implementing the user interface and testing the application.

Want to level-up your learning? Subscribe now

Subscribe to our newsletter for more tech based insights

FAQ