We're Live onShow your support by spreading the word on

How to Build Ejabberd WebRTC App with Erlang?

Learn how to set up and integrate Ejabberd with WebRTC for real-time communication using Erlang/OTP.

Introduction to Ejabberd WebRTC

In today's digital age, real-time communication has become a crucial component of many applications, enabling seamless interaction through text, voice, and video. One of the technologies at the forefront of this revolution is WebRTC (Web Real-Time Communication), an open-source project that facilitates peer-to-peer communication via web browsers and mobile applications. Integrating WebRTC with powerful messaging platforms like Ejabberd can significantly enhance the capabilities of your communication infrastructure.

What is Ejabberd WebRTC?

Ejabberd is a robust, ubiquitous, and massively scalable messaging platform written in Erlang/OTP. It supports a variety of protocols including XMPP, MQTT, and SIP, making it a versatile solution for real-time messaging needs. Ejabberd stands out for its reliability and ability to handle thousands of concurrent connections, making it an ideal choice for large-scale applications.
WebRTC, on the other hand, is designed to enable real-time communication directly between browsers and mobile apps. It handles audio, video, and data sharing, making it a comprehensive solution for modern communication requirements. The integration of Ejabberd with WebRTC leverages the strengths of both technologies, providing a powerful platform for real-time messaging and communication.
By combining Ejabberd’s robust messaging capabilities with the real-time communication features of WebRTC, developers can build applications that support video conferencing, live chat, and other interactive features without the need for additional plugins or third-party services. This integration not only simplifies the development process but also ensures a high level of performance and scalability, crucial for handling the demands of modern communication apps.
In the following sections, we will delve into the technical aspects of setting up and integrating Ejabberd with WebRTC, providing detailed code snippets and step-by-step instructions to help you build your own real-time communication application. Whether you're developing a simple chat app or a complex video conferencing tool, this guide will provide the foundational knowledge and practical insights you need to get started.

Getting Started with the Code!

In this section, we will walk through the initial setup and configuration required to create an Ejabberd WebRTC application. This includes setting up a new project, installing necessary dependencies, structuring the project directory, and understanding the app architecture. By following these steps, you will lay a solid foundation for building a robust real-time communication application.

Create a New Ejabberd WebRTC App

To begin, you need to set up a new project directory for your Ejabberd WebRTC application. Open your terminal and execute the following commands:

bash

1mkdir ejabberd-webrtc-app
2cd ejabberd-webrtc-app
This will create a new directory named ejabberd-webrtc-app and navigate into it.

[a] Install Dependencies

Next, you need to install the necessary dependencies for your project. Ejabberd and WebRTC require specific packages to function correctly. Here's a list of essential dependencies and how to install them:

[b] Erlang/OTP

Ensure that Erlang/OTP is installed on your system. You can download and install it from the

official Erlang website

.

[c] Ejabberd

Download and install Ejabberd from the

official Ejabberd website

. You can follow the instructions provided to install Ejabberd on your operating system.

[d] Rebar3

Rebar3 is a build tool for Erlang projects. Install Rebar3 by running the following commands:

bash

1    wget https://s3.amazonaws.com/rebar3/rebar3
2    chmod +x rebar3
3    ./rebar3 local install

[e] WebRTC Libraries

Depending on your platform, you might need to install WebRTC libraries. Refer to the

WebRTC official documentation

for detailed instructions.

Structure of the Project

Once the dependencies are installed, it's essential to organize your project directory properly. Here is a suggested structure:
1ejabberd-webrtc-app/
2├── _build/
3├── config/
4│   └── ejabberd.yml
5├── deps/
6├── src/
7│   └── main.erl
8├── rebar.config
9└── README.md
  • _build/: Directory where compiled files will be stored.
  • config/: Contains configuration files, such as ejabberd.yml.
  • deps/: Directory for project dependencies.
  • src/: Contains source files, such as main.erl.
  • rebar.config: Configuration file for Rebar3.
  • README.md: Documentation for your project.

App Architecture

Ejabberd-webrtc
Understanding the architecture of your Ejabberd WebRTC app is crucial for effective development. The core components include:
  1. Ejabberd Server: Handles XMPP communication and serves as the messaging backbone.
  2. WebRTC Signaling Server: Manages signaling for WebRTC connections, typically integrated within the Ejabberd server.
  3. Client Applications: Web or mobile clients that utilize WebRTC for peer-to-peer communication.
The interaction flow is as follows:
  • Clients connect to the Ejabberd server using XMPP.
  • When a user initiates a call, the signaling data is exchanged via the Ejabberd server.
  • WebRTC establishes a peer-to-peer connection for media (audio/video) transmission.
By structuring your project and understanding the architecture, you are now ready to dive into the code and start building your Ejabberd WebRTC application. In the next sections, we will provide detailed step-by-step instructions and code snippets to guide you through the development process.

Step 1: Get Started with "main.erl"

In this step, we'll create and configure the main Erlang file for our Ejabberd WebRTC application. This file will be the entry point of our application, handling the initial setup and integration with Ejabberd and WebRTC.

Creating and Setting Up main.erl

First, navigate to the src directory within your project:

bash

1cd src
Create a new file named main.erl:

bash

1touch main.erl
Open main.erl in your preferred text editor and start by defining the module and including necessary headers:

Erlang

1-module(main).
2-author("Your Name").
3
4%% Import necessary libraries
5-include_lib("ejabberd/include/ejabberd.hrl").
6-include_lib("stdlib/include/erl_opts.hrl").
7
8%% Export functions
9-export([start/0, init/0]).

Configuring Ejabberd to Support WebRTC

Next, we need to set up the initialization function and configure Ejabberd to support WebRTC signaling. Add the following code to your main.erl file:

Erlang

1%% Initialization function
2init() ->
3    application:start(crypto),
4    application:start(sasl),
5    application:start(ejabberd).
6
7%% Start function
8start() ->
9    init(),
10    %% Setup Ejabberd configurations
11    ejabberd:load_config("config/ejabberd.yml"),
12    ejabberd:start().
In this setup:
  • The init/0 function initializes necessary applications such as crypto and sasl along with Ejabberd.
  • The start/0 function loads the Ejabberd configuration file and starts the Ejabberd server.

Ejabberd Configuration

Next, ensure your Ejabberd configuration (config/ejabberd.yml) is set up to support WebRTC signaling. Open ejabberd.yml and add or modify the following sections:

YAML

1hosts:
2  - "localhost"
3
4listen:
5  -
6    port: 5222
7    module: ejabberd_c2s
8    certfile: "/etc/ejabberd/ejabberd.pem"
9    starttls: true
10    starttls_required: false
11
12  -
13    port: 5280
14    module: ejabberd_http
15    request_handlers:
16      "/websocket": ejabberd_http_ws
17
18modules:
19  mod_admin_extra: {}
20  mod_announce:  # recommends mod_announce in mod_muc
21    access: announce
22  mod_blocking: {}
23  mod_bosh: {}
24  mod_caps: {}
25  mod_carboncopy: {}
26  mod_client_state: {}
27  mod_configure: {}
28  mod_disco: {}
29  mod_http_api: {}
30  mod_http_upload:
31    put_url: "https://@HOST@:5443/upload"
32  mod_last: {}
33  mod_mam:
34    request_archive: true
35  mod_muc:
36    access:
37      - max_users: 1000000
38    default_room_options:
39      mam: true
40  mod_muc_admin: {}
41  mod_offline: {}
42  mod_ping: {}
43  mod_privacy: {}
44  mod_private: {}
45  mod_pubsub:
46    access_createnode: pubsub_createnode
47    plugins:
48      - "flat"
49      - "hometree"
50      - "pep"  # XEP-0163 PEP
51  mod_push: {}
52  mod_push_keepalive: {}
53  mod_register:
54    welcome_message:
55      subject: "Welcome!"
56      body: "Hi."
57    access: register
58  mod_roster: {}
59  mod_shared_roster: {}
60  mod_stream_mgmt:
61    resend_on_timeout: if_offline
62  mod_vcard: {}
63  mod_version: {}
This configuration sets up Ejabberd to handle WebRTC signaling via WebSocket and BOSH, crucial for real-time communication.
By the end of this step, you have created the main Erlang file for your Ejabberd WebRTC application and configured Ejabberd to support WebRTC. In the next step, we will design the wireframe for the application, defining the UI components required for WebRTC integration.

Step 2: Wireframe All the Components

In this step, we'll design the wireframe for our Ejabberd WebRTC application, focusing on the essential UI components required for a functional real-time communication interface. This includes the join screen, call controls, and participant view. By setting up a clear wireframe, we can ensure a cohesive and user-friendly design that facilitates seamless interaction.

Designing the Wireframe

A well-thought-out wireframe helps visualize the layout and functionality of the application before diving into the actual code. Here, we'll outline the main components and their purposes.

[a] HTML for Join Screen

The join screen is where users will enter their credentials or room information to join a video call. This screen should be simple and intuitive.

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>Ejabberd WebRTC - Join</title>
7    <link rel="stylesheet" href="styles.css">
8</head>
9<body>
10    <div class="join-container">
11        <h1>Join a Room</h1>
12        <form id="join-form">
13            <input type="text" id="username" placeholder="Enter your username" required>
14            <input type="text" id="room" placeholder="Enter room name" required>
15            <button type="submit">Join</button>
16        </form>
17    </div>
18    <script src="main.js"></script>
19</body>
20</html>

[b] CSS for Join Screen

CSS

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

Call Controls

The call controls should allow users to manage their video and audio streams effectively. This includes buttons for muting/unmuting the microphone, starting/stopping the video, and ending the call.

[a] HTML for Call Controls

HTML

1<div class="controls">
2    <button id="mute-btn">Mute</button>
3    <button id="video-btn">Stop Video</button>
4    <button id="end-call-btn">End Call</button>
5</div>

[b] CSS for Call Controls

CSS

1.controls {
2    position: fixed;
3    bottom: 20px;
4    left: 50%;
5    transform: translateX(-50%);
6    display: flex;
7    gap: 10px;
8}
9
10.controls button {
11    padding: 10px;
12    font-size: 14px;
13    background-color: #007bff;
14    color: white;
15    border: none;
16    border-radius: 4px;
17    cursor: pointer;
18}
19
20.controls button:hover {
21    background-color: #0056b3;
22}

Participant View

The participant view displays the video streams of all users in the call. Each participant should have their own video element, arranged in a grid or flex layout.

[a] HTML for Participant View

HTML

1<div class="participant-view">
2    <video id="local-video" autoplay muted></video>
3    <div id="remote-videos"></div>
4</div>

[b] CSS for Participant View

CSS

1.participant-view {
2    display: flex;
3    flex-wrap: wrap;
4    justify-content: center;
5    align-items: center;
6    padding: 20px;
7}
8
9#local-video {
10    width: 200px;
11    height: 150px;
12    margin: 10px;
13    border: 2px solid #007bff;
14    border-radius: 8px;
15}
16
17#remote-videos video {
18    width: 200px;
19    height: 150px;
20    margin: 10px;
21    border: 2px solid #007bff;
22    border-radius: 8px;
23}

Setting Up HTML/CSS for the Join Screen, Controls, and Participant View

By defining the HTML and CSS for the join screen, call controls, and participant view, we establish the basic structure and style of our application. This setup ensures that users can join calls, control their media streams, and view other participants efficiently.
In the next step, we will implement the join screen's functionality, handling user input and initializing WebRTC sessions. This will involve writing JavaScript to manage the user interactions and integrate with the Ejabberd server for signaling.

Step 3: Implement Join Screen

In this step, we will implement the functionality of the join screen, allowing users to enter their username and room name to join a video call. We will handle user input and initialize WebRTC sessions. This involves writing JavaScript to manage user interactions and integrating with the Ejabberd server for signaling.

Developing the Join Screen

We have already created the HTML and CSS for the join screen. Now, let's add the JavaScript functionality.

[a] Setup Event Listeners and Form Handling

First, we need to set up event listeners for the form submission. This will capture the user input and use it to join a room.
JavaScript for Join Screen (main.js):

JavaScript

1document.addEventListener('DOMContentLoaded', () => {
2    const joinForm = document.getElementById('join-form');
3
4    joinForm.addEventListener('submit', async (event) => {
5        event.preventDefault();
6
7        const username = document.getElementById('username').value;
8        const room = document.getElementById('room').value;
9
10        if (username && room) {
11            await joinRoom(username, room);
12        } else {
13            alert('Please enter both username and room name.');
14        }
15    });
16});

[b] Initialize WebRTC and Ejabberd Connection

Next, we need to define the joinRoom function. This function will handle the initialization of the WebRTC session and connect to the Ejabberd server.
JavaScript for Join Room (main.js):

JavaScript

1async function joinRoom(username, room) {
2    try {
3        // Initialize Ejabberd connection
4        const connection = new Strophe.Connection('wss://your-ejabberd-server.com:5280/websocket');
5        
6        connection.connect(username, 'password', (status) => {
7            if (status === Strophe.Status.CONNECTED) {
8                console.log('Connected to Ejabberd');
9                
10                // Join the room
11                connection.send($pres().c('x', { xmlns: 'http://jabber.org/protocol/muc' }).c('history', { maxstanzas: 0 }));
12                
13                // Initialize WebRTC
14                startWebRTC(connection, room);
15            } else {
16                console.error('Failed to connect to Ejabberd');
17            }
18        });
19    } catch (error) {
20        console.error('Error joining room:', error);
21    }
22}

[c] Implement WebRTC Initialization

Now, let's implement the startWebRTC function. This function will handle setting up the WebRTC peer connection, managing local and remote streams, and handling signaling via Ejabberd.
JavaScript for WebRTC Initialization (main.js):

JavaScript

1async function startWebRTC(connection, room) {
2    const localVideo = document.getElementById('local-video');
3    const remoteVideos = document.getElementById('remote-videos');
4
5    // Get user media
6    const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
7    localVideo.srcObject = localStream;
8
9    const peerConnections = {};
10
11    connection.addHandler((msg) => {
12        const from = msg.getAttribute('from');
13        const type = msg.getAttribute('type');
14        
15        if (type === 'offer') {
16            const offer = JSON.parse(msg.getElementsByTagName('body')[0].textContent);
17            handleOffer(connection, from, offer, localStream, peerConnections);
18        } else if (type === 'answer') {
19            const answer = JSON.parse(msg.getElementsByTagName('body')[0].textContent);
20            handleAnswer(from, answer, peerConnections);
21        } else if (type === 'candidate') {
22            const candidate = JSON.parse(msg.getElementsByTagName('body')[0].textContent);
23            handleCandidate(from, candidate, peerConnections);
24        }
25        
26        return true;
27    }, null, 'message', 'chat');
28
29    connection.send($pres().c('x', { xmlns: 'http://jabber.org/protocol/muc' }).c('history', { maxstanzas: 0 }));
30
31    // Send offer to new participants
32    localStream.getTracks().forEach(track => {
33        for (const peerId in peerConnections) {
34            peerConnections[peerId].addTrack(track, localStream);
35        }
36    });
37
38    // Create offer
39    for (const peerId in peerConnections) {
40        const peerConnection = peerConnections[peerId];
41        const offer = await peerConnection.createOffer();
42        await peerConnection.setLocalDescription(offer);
43
44        connection.send($msg({ to: peerId, type: 'offer' }).c('body').t(JSON.stringify(offer)));
45    }
46}
47
48function handleOffer(connection, from, offer, localStream, peerConnections) {
49    const peerConnection = new RTCPeerConnection();
50    peerConnections[from] = peerConnection;
51
52    peerConnection.ontrack = (event) => {
53        const remoteVideo = document.createElement('video');
54        remoteVideo.srcObject = event.streams[0];
55        remoteVideo.autoplay = true;
56        document.getElementById('remote-videos').appendChild(remoteVideo);
57    };
58
59    localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
60
61    peerConnection.setRemoteDescription(new RTCSessionDescription(offer)).then(() => {
62        return peerConnection.createAnswer();
63    }).then((answer) => {
64        return peerConnection.setLocalDescription(answer);
65    }).then(() => {
66        connection.send($msg({ to: from, type: 'answer' }).c('body').t(JSON.stringify(peerConnection.localDescription)));
67    });
68}
69
70function handleAnswer(from, answer, peerConnections) {
71    const peerConnection = peerConnections[from];
72    peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
73}
74
75function handleCandidate(from, candidate, peerConnections) {
76    const peerConnection = peerConnections[from];
77    peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
78}
By implementing the JavaScript functionality for the join screen, we enable users to enter their credentials and join a video call. The joinRoom function establishes a connection to the Ejabberd server, and the startWebRTC function sets up the WebRTC peer connections, managing local and remote media streams.
In the next step, we will add functionality to the call controls, allowing users to manage their audio and video streams effectively during a call.

Step 4: Implement Controls

In this step, we will add functionality to the call controls, allowing users to manage their audio and video streams during a call. This includes buttons for muting/unmuting the microphone, starting/stopping the video, and ending the call. We'll enhance our JavaScript to handle these interactions effectively.

Adding Functional Controls

We have already defined the HTML and CSS for the call controls. Now, let's implement the JavaScript to enable these controls.

[a] Set Up Event Listeners for Controls

First, we need to add event listeners for the control buttons. These listeners will call appropriate functions to handle user actions.
JavaScript for Control Buttons (main.js):

JavaScript

1document.addEventListener('DOMContentLoaded', () => {
2    // Existing code...
3    
4    const muteBtn = document.getElementById('mute-btn');
5    const videoBtn = document.getElementById('video-btn');
6    const endCallBtn = document.getElementById('end-call-btn');
7
8    muteBtn.addEventListener('click', toggleMute);
9    videoBtn.addEventListener('click', toggleVideo);
10    endCallBtn.addEventListener('click', endCall);
11});

[b] Implement Toggle Mute Functionality

The toggleMute function will mute or unmute the local audio track.
JavaScript for Mute Function (main.js):

JavaScript

1let isMuted = false;
2
3function toggleMute() {
4    const localStream = document.getElementById('local-video').srcObject;
5    localStream.getAudioTracks().forEach(track => track.enabled = !track.enabled);
6    
7    isMuted = !isMuted;
8    document.getElementById('mute-btn').textContent = isMuted ? 'Unmute' : 'Mute';
9}

[c] Implement Toggle Video Functionality

The toggleVideo function will start or stop the local video track.
JavaScript for Video Function (main.js):

JavaScript

1let isVideoOff = false;
2
3function toggleVideo() {
4    const localStream = document.getElementById('local-video').srcObject;
5    localStream.getVideoTracks().forEach(track => track.enabled = !track.enabled);
6    
7    isVideoOff = !isVideoOff;
8    document.getElementById('video-btn').textContent = isVideoOff ? 'Start Video' : 'Stop Video';
9}

[d] Implement End Call Functionality

The endCall function will close the peer connections and stop all media tracks.
JavaScript for End Call Function (main.js):

JavaScript

1function endCall() {
2    const localVideo = document.getElementById('local-video');
3    const remoteVideos = document.getElementById('remote-videos');
4
5    // Stop local media tracks
6    const localStream = localVideo.srcObject;
7    localStream.getTracks().forEach(track => track.stop());
8
9    // Close peer connections
10    for (const peerId in peerConnections) {
11        peerConnections[peerId].close();
12        delete peerConnections[peerId];
13    }
14
15    // Clear video elements
16    localVideo.srcObject = null;
17    while (remoteVideos.firstChild) {
18        remoteVideos.removeChild(remoteVideos.firstChild);
19    }
20
21    // Redirect to join screen
22    window.location.reload();
23}

[e] Update Peer Connections Management

Ensure that peerConnections is defined globally to be accessible by all functions handling peer connection operations.
JavaScript Global Definition (main.js):

JavaScript

1let peerConnections = {};

[f] Handling State Changes and Events

To ensure a smooth user experience, we should also handle state changes and events for peer connections.
JavaScript for Handling State Changes (main.js):

JavaScript

1function handleOffer(connection, from, offer, localStream, peerConnections) {
2    const peerConnection = new RTCPeerConnection();
3    peerConnections[from] = peerConnection;
4
5    peerConnection.ontrack = (event) => {
6        const remoteVideo = document.createElement('video');
7        remoteVideo.srcObject = event.streams[0];
8        remoteVideo.autoplay = true;
9        document.getElementById('remote-videos').appendChild(remoteVideo);
10    };
11
12    peerConnection.onicecandidate = (event) => {
13        if (event.candidate) {
14            connection.send($msg({ to: from, type: 'candidate' }).c('body').t(JSON.stringify(event.candidate)));
15        }
16    };
17
18    localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
19
20    peerConnection.setRemoteDescription(new RTCSessionDescription(offer)).then(() => {
21        return peerConnection.createAnswer();
22    }).then((answer) => {
23        return peerConnection.setLocalDescription(answer);
24    }).then(() => {
25        connection.send($msg({ to: from, type: 'answer' }).c('body').t(JSON.stringify(peerConnection.localDescription)));
26    });
27}
28
29function handleAnswer(from, answer, peerConnections) {
30    const peerConnection = peerConnections[from];
31    peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
32}
33
34function handleCandidate(from, candidate, peerConnections) {
35    const peerConnection = peerConnections[from];
36    peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
37}
By implementing the JavaScript functionality for the call controls, users can now manage their audio and video streams during a call. The toggleMute function allows muting and unmuting the microphone, toggleVideo handles starting and stopping the video, and endCall ends the call and cleans up the peer connections and media streams.
In the next step, we will implement the participant view, ensuring that all participants' video and audio streams are displayed correctly.

Get Free 10,000 Minutes Every Months

No credit card required to start.

Step 5: Implement Participant View

In this step, we will implement the participant view, which displays the video and audio streams of all users in the call. This involves handling multiple WebRTC peer connections and dynamically updating the user interface as participants join or leave the call.

Building the Participant View

We have already defined the HTML and CSS for the participant view. Now, let's focus on the JavaScript functionality required to manage the participant video streams.

[a] Handle Remote Streams

First, we need to update the handleOffer function to properly handle remote streams and add video elements dynamically for each participant.
JavaScript for Handling Remote Streams (main.js):

JavaScript

1function handleOffer(connection, from, offer, localStream, peerConnections) {
2    const peerConnection = new RTCPeerConnection();
3    peerConnections[from] = peerConnection;
4
5    peerConnection.ontrack = (event) => {
6        const remoteVideo = document.createElement('video');
7        remoteVideo.srcObject = event.streams[0];
8        remoteVideo.autoplay = true;
9        remoteVideo.id = `remote-video-${from}`;
10        document.getElementById('remote-videos').appendChild(remoteVideo);
11    };
12
13    peerConnection.onicecandidate = (event) => {
14        if (event.candidate) {
15            connection.send($msg({ to: from, type: 'candidate' }).c('body').t(JSON.stringify(event.candidate)));
16        }
17    };
18
19    localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
20
21    peerConnection.setRemoteDescription(new RTCSessionDescription(offer)).then(() => {
22        return peerConnection.createAnswer();
23    }).then((answer) => {
24        return peerConnection.setLocalDescription(answer);
25    }).then(() => {
26        connection.send($msg({ to: from, type: 'answer' }).c('body').t(JSON.stringify(peerConnection.localDescription)));
27    });
28}

[b] Update Local Stream Handling

Ensure that the local video stream is correctly added to each new peer connection. We have already set up this functionality in the startWebRTC function, but let's make sure it covers all scenarios.
JavaScript for Local Stream Handling (main.js):

JavaScript

1async function startWebRTC(connection, room) {
2    const localVideo = document.getElementById('local-video');
3    const remoteVideos = document.getElementById('remote-videos');
4
5    // Get user media
6    const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
7    localVideo.srcObject = localStream;
8
9    connection.addHandler((msg) => {
10        const from = msg.getAttribute('from');
11        const type = msg.getAttribute('type');
12        
13        if (type === 'offer') {
14            const offer = JSON.parse(msg.getElementsByTagName('body')[0].textContent);
15            handleOffer(connection, from, offer, localStream, peerConnections);
16        } else if (type === 'answer') {
17            const answer = JSON.parse(msg.getElementsByTagName('body')[0].textContent);
18            handleAnswer(from, answer, peerConnections);
19        } else if (type === 'candidate') {
20            const candidate = JSON.parse(msg.getElementsByTagName('body')[0].textContent);
21            handleCandidate(from, candidate, peerConnections);
22        }
23        
24        return true;
25    }, null, 'message', 'chat');
26
27    connection.send($pres().c('x', { xmlns: 'http://jabber.org/protocol/muc' }).c('history', { maxstanzas: 0 }));
28
29    // Send offer to new participants
30    localStream.getTracks().forEach(track => {
31        for (const peerId in peerConnections) {
32            peerConnections[peerId].addTrack(track, localStream);
33        }
34    });
35
36    // Create offer
37    for (const peerId in peerConnections) {
38        const peerConnection = peerConnections[peerId];
39        const offer = await peerConnection.createOffer();
40        await peerConnection.setLocalDescription(offer);
41
42        connection.send($msg({ to: peerId, type: 'offer' }).c('body').t(JSON.stringify(offer)));
43    }
44}

[c] Handling Participant Disconnection

We need to manage the UI and clean up resources when participants leave the call. Update the endCall function to handle this scenario properly.
JavaScript for Handling Disconnection (main.js):

JavaScript

1function endCall() {
2    const localVideo = document.getElementById('local-video');
3    const remoteVideos = document.getElementById('remote-videos');
4
5    // Stop local media tracks
6    const localStream = localVideo.srcObject;
7    localStream.getTracks().forEach(track => track.stop());
8
9    // Close peer connections
10    for (const peerId in peerConnections) {
11        peerConnections[peerId].close();
12        delete peerConnections[peerId];
13    }
14
15    // Clear video elements
16    localVideo.srcObject = null;
17    while (remoteVideos.firstChild) {
18        remoteVideos.removeChild(remoteVideos.firstChild);
19    }
20
21    // Redirect to join screen
22    window.location.reload();
23}

[d] Handling Peer Disconnections Gracefully

To handle peers leaving the call gracefully, you should listen for ICE connection state changes and remove video elements when peers disconnect.
JavaScript for Handling Peer Disconnections (main.js):

JavaScript

1function handlePeerDisconnection(peerId) {
2    const remoteVideo = document.getElementById(`remote-video-${peerId}`);
3    if (remoteVideo) {
4        remoteVideo.srcObject = null;
5        remoteVideo.parentNode.removeChild(remoteVideo);
6    }
7    if (peerConnections[peerId]) {
8        peerConnections[peerId].close();
9        delete peerConnections[peerId];
10    }
11}
12
13function handleOffer(connection, from, offer, localStream, peerConnections) {
14    const peerConnection = new RTCPeerConnection();
15    peerConnections[from] = peerConnection;
16
17    peerConnection.ontrack = (event) => {
18        const remoteVideo = document.createElement('video');
19        remoteVideo.srcObject = event.streams[0];
20        remoteVideo.autoplay = true;
21        remoteVideo.id = `remote-video-${from}`;
22        document.getElementById('remote-videos').appendChild(remoteVideo);
23    };
24
25    peerConnection.onicecandidate = (event) => {
26        if (event.candidate) {
27            connection.send($msg({ to: from, type: 'candidate' }).c('body').t(JSON.stringify(event.candidate)));
28        }
29    };
30
31    peerConnection.oniceconnectionstatechange = () => {
32        if (peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'failed' || peerConnection.iceConnectionState === 'closed') {
33            handlePeerDisconnection(from);
34        }
35    };
36
37    localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
38
39    peerConnection.setRemoteDescription(new RTCSessionDescription(offer)).then(() => {
40        return peerConnection.createAnswer();
41    }).then((answer) => {
42        return peerConnection.setLocalDescription(answer);
43    }).then(() => {
44        connection.send($msg({ to: from, type: 'answer' }).c('body').t(JSON.stringify(peerConnection.localDescription)));
45    });
46}
By implementing the participant view functionality, we ensure that all video and audio streams are displayed correctly and dynamically updated as participants join or leave the call. The code handles remote streams, local stream integration, and disconnections gracefully.
In the next step, we will run our code and test the complete functionality of the Ejabberd WebRTC application.

Step 6: Run Your Code Now

In this final step, we will compile and run the Ejabberd WebRTC application, testing the complete functionality to ensure everything works as expected. We will also cover some basic troubleshooting steps in case you encounter any issues.

Compiling and Running the Application

[a] Start Ejabberd Server

Ensure that your Ejabberd server is running and properly configured to handle WebRTC signaling. You can start the Ejabberd server with the following command:

bash

1ejabberdctl start

[b] Compile the Erlang Code

Navigate to your project directory and compile the Erlang code using Rebar3:

bash

1cd /path/to/your/ejabberd-webrtc-app
2rebar3 compile

[c] Run the Application

After compiling, you can start the Erlang application:

bash

1erl -pa _build/default/lib/*/ebin -sname ejabberd_webrtc -s main
This command will start the Erlang shell with your application running. Ensure there are no errors during the startup process.

[d] Access the Web Application

Open a web browser and navigate to the HTML file of your application. If you are running a local server, you might access it via http://localhost/your-app-path.

Testing the Application

Join a Room

Enter a username and room name on the join screen and click the "Join" button. This should connect you to the Ejabberd server and initialize the WebRTC session.

Verify Local Video

Ensure that your local video stream appears on the screen. This confirms that your webcam is working and the media stream is being captured correctly.

Add Participants

Open the application in multiple browser tabs or devices and join the same room with different usernames. Verify that remote video streams are displayed correctly for all participants.

Test Controls

Use the mute, video, and end call buttons to test their functionality:
  • Mute/Unmute: Check that your microphone can be muted and unmuted.
  • Start/Stop Video: Verify that your video can be started and stopped.
  • End Call: Ensure that the call ends correctly and the UI resets.

Troubleshooting Common Issues

Connection Issues

If you cannot connect to the Ejabberd server, verify the following:
  • Ensure the Ejabberd server is running and reachable.
  • Check your ejabberd.yml configuration for WebSocket and BOSH settings.
  • Confirm that the Erlang/OTP environment is correctly set up.

Media Stream Issues

If you face issues with capturing or displaying media streams:
  • Ensure that your browser has permissions to access the webcam and microphone.
  • Check the console for errors related to getUserMedia or WebRTC APIs.

Signaling Issues

If WebRTC connections are not established:
  • Verify that the signaling messages are correctly exchanged between peers via Ejabberd.
  • Check for errors in the JavaScript console related to ICE candidates or SDP exchange.

Conclusion

In this guide, we walked through the steps to set up, configure, and run an Ejabberd WebRTC application. We covered the initial project setup, handling user inputs, managing media streams, and implementing essential call controls. By now, you should have a functional real-time communication application using Ejabberd and WebRTC.

Want to level-up your learning? Subscribe now

Subscribe to our newsletter for more tech based insights

FAQ