In this tutorial we will be a highly scalable group video conferencing app using WebRTC, PHP Laravel and Javascript
This video calling application will be able to handle hundreds of participants in a group video call.
The source code this application is available on Github.
Overview
We will discuss the technologies required to build the group video calling, learn about WebRTC and it's limitations in developing a group video call with large number of participants and how we can bypass the limitations.
Then we will build a highly scalable group video conferencing application with WebRTC PHP+Laravel backend and Livewire.
Why WebRTC?
WebRTC is a collection of protocols and technologies that allows real-time communication.
It providers a way to have plugin-free real time audio and video communication in very low latency right from the browser.
Hence we will use WebRTC to create a seamless plugin free video calling experience for the user, and allow the user to have a video call right from their browser without installing any software or plugins.
WebRTC was designed to have peer-to-peer communication between participants in a WebRTC call.
But WebRTC calls are not entirely peer-to-peer, centralized server is required for the peers to discover each other, and if the peers are behind a NAT, then STUN/TURN servers are required for NAT Traversal.
In the next section we will briefly touch over these technologies.
Building blocks of WebRTC
WebRTC has many components, and many of those components are handled automatically by the underlying browser.
But we will discuss about the two components that require a centralized server to operate, and they are:
- WebRTC Signaling Server
- STUN/TURN Server
WebRTC Signaling Server
A WebRTC Signaling server is required for the peers to discover each other, and exchange the SDP information and ICE Candidates to establish a peer to peer session.
It is left to the user to build their own signaling server. The signaling server is built typically using WebSockets.
STUN/TURN Server
The STUN/TURN Server is required for NAT Traversal. Most consumer devices these days, be it your laptop or smartphone is typically behind a NAT.
To connect to the internet a public IP address is required, the public IP address is assigned to you by your internet service provider.
For each device you own, you would require a public IP address, but there are more devices currently connected to the internet than the available IPv4 addresses.
To solve this problem NAT was invented, NAT allows multiple devices to share one public IP address.
NAT or Network Address translation is typically done by your router or modem provided by your ISP.
In WebRTC we establish a direct connection between the two devices, but both the devices are typically behind a NAT, and we need a mechanism to traverse the NAT and connect directly to those devices.
For this we use the STUN and TURN servers.
If you are looking for STUN and TURN servers then you can consider Metered TURN server
Here are some of the features of Metered TURN servers
Metered Global TURN servers
- Global Geo-Location targeting: Automatically directs traffic to the nearest servers, for lowest possible latency and highest quality performance.
- Servers in 12 Regions of the world: Metered TURN has servers in 12 regions of the world including: Toronto, Miami, San Francisco, Amsterdam, London, Frankfurt, Bangalore, Singapore,Sydney
- Low Latency: less than 50 ms latency, anywhere across the world.
- Cost-Effective: pay-as-you-go pricing with bandwidth and volume discounts available.
- Easy Administration: Get usage logs, emails when accounts reach threshold limits, billing records and email and phone support.
- Standards Compliant: Conforms to RFCs 5389, 5769, 5780, 5766, 6062, 6156, 5245, 5768, 6336, 6544, 5928 over UDP, TCP, TLS, and DTLS.
- Multi‑Tenancy: Create multiple credentials and separate the usage by customer, or different apps. Get Usage logs, billing records and threshold alerts.
- Reliability: 99.999% Uptime with SLA.
- Enterprise Scale: : With no limit on concurrent traffic or total traffic. Metered TURN Servers provide Enterprise Scalability
- 50 GB/mo Free: Get 50 GB every month free TURN server usage with the Free Plan
You can sign up here: Metered TURN server Sign Up
Limitations and Scaling Considerations
As we have discussed WebRTC works in a peer to peer manner, where each participant in a WebRTC meeting connects to every other peer in the meeting.
This topology is good for one to one calls, or even for calls with 3 participants, but it results is too much load on upload bandwidth and CPU for calls with more than 3 participants.
Because, if there are for e.g. 10 people in a call, then each participant would have to upload their video+audio to 9 other participants simultaneously, which puts tremendous load on the CPU and bandwidth.
To scale a WebRTC call, you require a WebRTC Server, what WebRTC Server does, is aggregates the audio+video stream from all the participants and distributes it to others in the call.
So if we consider our previous example with 10 participants if we use a WebRTC server, then the user would have to upload their audio+video to the WebRTC Server.
Instead of uploading the video to 9 other participants there will be only one upload, which is to the WebRTC Server and it reduces the load on CPU and bandwidth significantly.
This makes it possible to easily scale the WebRTC call to hundreds of participants.
In our tutorial we will use the WebRTC Server from Metered Video.
Building Video Chat Application
Let's start building a highly scalable group video calling application with WebRTC and PHP Laravel.
We will first start with building the backend server of the application, the backend server will have API to create a meeting room, where other participants can join.
We will also create an API to validate the meeting room, so when a user wants to join a meeting room, we can check if the room exists or not.
In the PHP backend we will call the Metered Video SDK to create a meeting and also to validate the meeting room.
Pre-requisite
You will need basic knowledge of PHP, HTML and JavaScript to develop this application.
You will also need a Metered Video account, if you don't have an account then signup for a free account at metered.ca and click "Signup and Start Building"
After you have created your account, come back here and follow along.
Metered Domain and Secret Key
Let's obtain the Metered Domain and Secret Key we will need them later in the project.
To get your Metered Domain and Secret Key login to your Metered Dashboard, and the click on "Developers" in the sidebar.
Note down the Metered Domain and Secret Key, we will use them later in the tutorial.
Scaffolding the project
We will scaffold the php Laravel project by running the command
laravel new webrtc-php-laravel
And start the server using
php artisan serve
Once we have scaffolded the project, let start building our video chat application.
Building the Join Meeting Page
We will update the home page view to create the interface to join or create a new meeting.
Let update the homepage view under resources/views/welcome.blade.php
and build the UI to create a new meeting or join an existing meeting.
Let's scaffold the basic UI, we will use tailwindcss
for our styling, here is the basic UI
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Nunito', sans-serif;
}
</style>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="antialiased">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="py-4">
<h1 class="text-2xl">Video Chat Application</h1>
</div>
<div class="max-w-2xl">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<div class="mt-1">
<input type="text" name="name" id="name" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="John Smith">
</div>
</div>
</div>
<div class="max-w-2xl">
<div class="grid md:grid-cols-3 grid-cols-1 mt-4">
<div class="col-span-2">
<div class="mt-1 flex rounded-md shadow-sm">
<div class="relative flex items-stretch flex-grow focus-within:z-10">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<!-- Heroicon name: solid/users -->
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
</div>
<input type="text" name="meetingId" id="meetingId" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300" placeholder="Meeting ID">
</div>
<button type="button" class="-ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
<span>Join Meeting</span>
</button>
</div>
</div>
<div>
<span class="text-xs uppercase font-bold text-gray-400 px-1">OR</span>
<button type="button" class="mt-1 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Create New Meeting</button>
</div>
</div>
</div>
</div>
</body>
</html>
Now let's build the logic to handle the Join Meeting page.
We will create two routes, one to handle Join Meeting and the second to handle Create New Meeting.
Under the Join Meeting route we will call the Metered Video SDK, to validate the meetingID
provided by the user, if the meetingID
is valid then we will redirect the user to the Meeting Area, otherwise we will show an error.
For Create Meeting route we will call the Metered Video SDK's Create Room API, and then redirect the user to the Meeting Area.
Create Meeting
Let's wrap the "Create Meeting" button in a form tag, and with action route to createMeeting
<div>
<span class="text-xs uppercase font-bold text-gray-400 px-1">OR</span>
<form method="post" action="{{ route('createMeeting') }}">
{{ csrf_field() }}
<button type="submit" class="mt-1 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Create New Meeting</button>
</form>
</div>
Let's create a controller to handle all our meeting related methods.
php artisan make:controller MeetingController
We will update our web.php
file as well and include the createMeeting
route and call the createMeeting
method of our MeetingController
in the route.
Remember earlier in this tutorial we have noted down the Metered Domain and the Metered Secret Key, now we will be using them.
In the .env
file add the METERED_DOMAIN
and the METERED_SECRET_KEY
METERED_DOMAIN="yourappname.metered.live"
METERED_SECRET_KEY="hoHqpIkn8MqZvwHReHt8tm_6K0SRMgg6vHwPrBoKkUhz"
Now, let's get back to our MeetingController.php
and call the Create Room API
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class MeetingController extends Controller
{
//
public function createMeeting(Request $request) {
$METERED_DOMAIN = env('METERED_DOMAIN');
$METERED_SECRET_KEY = env('METERED_SECRET_KEY');
Log::info("https://{$METERED_DOMAIN}/api/v1/room?secretKey={$METERED_SECRET_KEY}");
// Contain the logic to create a new meeting
$response = Http::post("https://{$METERED_DOMAIN}/api/v1/room?secretKey={$METERED_SECRET_KEY}", [
'autoJoin' => true
]);
$roomName = $response->json("roomName");
return redirect('/'); // We will update this soon.
}
}
In the above code we are calling the "Create Room" REST API of Metered Video SDK to create a new meeting.
Join Existing Meeting
Now let handle the logic to Join Meeting button. Here the user will enter the Meeting ID, and we will validate the Meeting ID.
If the Meeting ID is valid then we will redirect the user to the Meeting page, if the Meeting ID is invalid then we will show an error.
Open the MeetingController.php
file and create a method called validateMeeting
, this method will call the Get Room API of the Metered Video SDK, and check if a Meeting Room specified by the user exists or not.
public function validateMeeting(Request $request) {
$METERED_DOMAIN = env('METERED_DOMAIN');
$METERED_SECRET_KEY = env('METERED_SECRET_KEY');
$meetingId = $request->input('meetingId');
// Contains logic to validate existing meeting
$response = Http::get("https://{$METERED_DOMAIN}/api/v1/room/{$meetingId}?secretKey={$METERED_SECRET_KEY}");
if ($response->status() === 200) {
return redirect("/"); // We will update this soon
} else {
return redirect("/?error=Invalid Meeting ID");
}
}
Open web.php
and create a /validateMeeting
route, and call the validateMeeting
method of the MeetingController
.
Route::post("/validateMeeting", [MeetingController::class, 'validateMeeting'])->name("validateMeeting");
Now open the welcome.blade.php
and wrap the Join Meeting button and input tag into a form field and call the validateMeeting
route.
<form method="post" action="{{ route('validateMeeting') }}">
{{ csrf_field() }}
<div class="mt-1 flex rounded-md shadow-sm">
<div class="relative flex items-stretch flex-grow focus-within:z-10">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
</div>
<input type="text" name="meetingId" id="meetingId" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300" placeholder="Meeting ID">
</div>
<button type="submit" class="-ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
<span>Join Meeting</span>
</button>
</div>
</form>
Building the Meeting Page
Then we will redirect the user to the Meeting Page. In the Meeting page we will first show the user Meeting Lobby.
In the Meeting Lobby the user can adjust their camera, speaker and microphone and join the meeting.
We will create a view called as meeting.blade.php
in this view we will create the UI for the Meeting
Go to resources/views
and create a file called meeting.blade.php
for and add the following contents to the file
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Nunito', sans-serif;
}
</style>
<script src="https://cdn.metered.ca/sdk/video/1.4.5/sdk.min.js"></script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="antialiased">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="py-4">
<h1 class="text-2xl">Meeting Lobby</h1>
</div>
<div class="max-w-2xl">
</div>
</div>
</body>
</html>
We have included the Metered Video SDK in the head of the file
<script src="https://cdn.metered.ca/sdk/video/1.4.5/sdk.min.js"></script>
Now open the file routes/web.php
and create the route for the lobby.
Route::get("/meeting/{meetingId}", function() {
return view('meeting');
});
Our web.php
file looks like this
Let open the MeetingController.php
file and redirect the user to /meeting/{meetingId}
after successfully creating or joining a meeting.
Let's update the meeting.blade.php
file to build the basic UI of the lobby
Building the JavaScript Code
We will use vanilla javascript with some jquery to build the front-end of our application.
Initializing the Metered Meeting Object
Open resources/js/app.js
and create the meeting object
const meeting = new Metered.Meeting();
Populating available Cameras and Microphone
We will call the listVideoInputDevices method to get a list of available cameras and listAudioInputDevices method to get the list of available microphones in the system.
We will call these methods and populate the select box
Handling Waiting Area Share Camera/Microphone
We have created two buttons, one for camera and another one for microphone. When the user clicks on these buttons we will share the camera and when then user clicks on the microphone button we will share the microphone, after the user joins the meeting.
To build this logic we will add click event listener to these buttons, and store the state in a variable, for camera we will create a variable called as cameraOn
and for microphone we will create a variable called as micOn
In case of camera we also want to show the camera video preview to the user, for that we will store the video stream is localVideoStream
variable and display it in a video tag.
/**
* Mute/Unmute Camera and Microphone
*/
let micOn = false;
jquery("#waitingAreaToggleMicrophone").on("click", function() {
if (micOn) {
micOn = false;
jquery("#waitingAreaToggleMicrophone").removeClass("bg-gray-500");
jquery("#waitingAreaToggleMicrophone").addClass("bg-gray-400");
} else {
micOn = true;
jquery("#waitingAreaToggleMicrophone").removeClass("bg-gray-400");
jquery("#waitingAreaToggleMicrophone").addClass("bg-gray-500");
}
});
let cameraOn = false;
let localVideoStream = null;
jquery("#waitingAreaToggleCamera").on("click", async function() {
if (cameraOn) {
cameraOn = false;
jquery("#waitingAreaToggleCamera").removeClass("bg-gray-500");
jquery("#waitingAreaToggleCamera").addClass("bg-gray-400");
const tracks = localVideoStream.getTracks();
tracks.forEach(function (track) {
track.stop();
});
localVideoStream = null;
jquery("#waitingAreaLocalVideo")[0].srcObject = null;
} else {
cameraOn = true;
jquery("#waitingAreaToggleCamera").removeClass("bg-gray-400");
jquery("#waitingAreaToggleCamera").addClass("bg-gray-500");
localVideoStream = await meeting.getLocalVideoStream();
jquery("#waitingAreaLocalVideo")[0].srcObject = localVideoStream;
cameraOn = true;
}
});
After the user joins the meeting we will check the state of these variables. If the cameraOn
variable is set to true then we will share the user's camera and if micOn
variable is set to true then we will share user's microphone.
Handling Device Change
Change the Microphone or Camera selected by the user, we will add a onChange
event listener on the camera and microphone select boxes that we had populated and call the chooseVideoInputDevice(deviceId)
if camera was changed and chooseAudioInputDevice(deviceId)
method if microphone was changed.
/**
* Adding Event Handlers
*/
jquery("#cameraSelectBox").on("change", async function() {
const deviceId = jquery("#cameraSelectBox").val();
await meeting.chooseVideoInputDevice(deviceId);
if (cameraOn) {
localVideoStream = await meeting.getLocalVideoStream();
jquery("#waitingAreaLocalVideo")[0].srcObject = localVideoStream;
}
});
jquery("#microphoneSelectBox").on("change", async function() {
const deviceId = jquery("#microphoneSelectBox").val();
await meeting.chooseAudioInputDevice(deviceId);
});
If camera is changed we also need to update the localVideoStream
variable to update the preview of the currently shared camera.
Implementing Join Meeting
We will add an click event listener to the "Join Meeting" button, when the button is clicked, we will call the join(options)
method of the Metered Video SDK.
After the Join is successful we will hide the "Waiting Area" and Show the Meeting View.
We will also check if the camera button and microphone button is clicked on the waiting area, if yes then after joining the meeting we will share the camera and microphone.
let meetingInfo = {};
jquery("#joinMeetingBtn").on("click", async function () {
var username = jquery("#username").val();
if (!username) {
return alert("Please enter a username");
}
try {
meetingInfo = await meeting.join({
roomURL: `${window.METERED_DOMAIN}/${window.MEETING_ID}`,
name: username,
});
console.log("Meeting joined", meetingInfo);
jquery("#waitingArea").addClass("hidden");
jquery("#meetingView").removeClass("hidden");
jquery("#meetingAreaUsername").text(username);
/**
* If camera button is clicked on the meeting view
* then sharing the camera after joining the meeting.
*/
if (cameraOn) {
await meeting.startVideo();
}
/**
* Microphone button is clicked on the meeting view then
* sharing the microphone after joining the meeting
*/
if (microphoneOn) {
await meeting.startAudio();
}
} catch (ex) {
console.log("Error occurred when joining the meeting", ex);
}
});
Scaffolding the Meeting View
Now we will build the meeting view, the meeting view will show other participants video and audio.
We will also show preview of our own video stream if we are sharing, and will have controls to share camera, microphone and screen.
<div id='meetingView' class="hidden flex w-screen h-screen space-x-4 p-10">
<div id="activeSpeakerContainer" class=" bg-gray-900 rounded-3xl flex-1 flex relative">
<video id="activeSpeakerVideo" src="" autoplay class=" object-contain w-full rounded-t-3xl"></video>
<div id="activeSpeakerUsername" class="hidden absolute h-8 w-full bg-gray-700 rounded-b-3xl bottom-0 text-white text-center font-bold pt-1">
</div>
</div>
<div id="remoteParticipantContainer" class="flex flex-col space-y-4">
<div id="localParticiapntContainer" class="w-48 h-48 rounded-3xl bg-gray-900 relative">
<video id="localVideoTag" src="" autoplay class="object-contain w-full rounded-t-3xl"></video>
<div id="localUsername" class="absolute h-8 w-full bg-gray-700 rounded-b-3xl bottom-0 text-white text-center font-bold pt-1">
Me
</div>
</div>
</div>
<div class="flex flex-col space-y-2">
<button id='toggleMicrophone' class="bg-gray-400 w-10 h-10 rounded-md p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path></svg>
</button>
<button id='toggleCamera' class="bg-gray-400 w-10 h-10 rounded-md p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
</button>
<button id='toggleScreen' class="bg-gray-400 w-10 h-10 rounded-md p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
</button>
<button id='leaveMeeting' class="bg-red-400 text-white w-10 h-10 rounded-md p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
</button>
</div>
</div>
In the Implementing the Join Meeting section we are removing the hidden
class from the #meetingView
div.
In the above code snippet we have created the #meetingView
div, in this container we will build the UI for the meeting.
Lets go through each part of the #meetingView
container and explain its function:
-
#activeSpeakerContainer
- Here we will show the active speaker if there are multiple people in the meeting, and if it is just a 2 person meeting then we will show the remote participant here#activeSpeakerVideo
- This is the video tag that will show the video of the active speaker#activeSpeakerUsername
- This will contain the username of the active speaker
-
#remoteParticipantContainer
- This will contain the video tiles of the remote participants#localParticiapntContainer
- We will create default first tile to show the user his/her own video#localVideoTag
- Video tag to show the local video that is beign shared by the current user#localUsername
- Username of the current user
-
#toggleMicrophone
- Button to mute/unmute microphone -
#toggleCamera
- Button to mute/unmute camera -
#toggleScreen
- Button to share screen -
#leaveMeeting
- Button to exit the meeting -
#leaveMeetingView
- When the user leaves the meeting we will display this view and hide the#meetingView
Implementing Meeting View Logic
Now lets create the logic to wire up the Meeting View User Interface.
Handle onlineParticipants event
The onlineParticipants
event is triggered multiple times during the meeting lifecycle.
It is contains the array of participants currently present in the meeting.
We will use the onlineParticipants
event to show the list of online participants in the meeting.
meeting.on("onlineParticipants", function(participants) {
for (let participantInfo of participants) {
// Checking if a div to hold the participant already exists or not.
// If it exisits then skipping.
// Also checking if the participant is not the current participant.
if (!jquery(`#participant-${participantInfo._id}`)[0] && participantInfo._id !== meeting.participantInfo._id) {
jquery("#remoteParticipantContainer").append(
`
<div id="participant-${participantInfo._id}" class="w-48 h-48 rounded-3xl bg-gray-900 relative">
<video id="video-${participantInfo._id}" src="" autoplay class="object-contain w-full rounded-t-3xl"></video>
<video id="audio-${participantInfo._id}" src="" autoplay class="hidden"></video>
<div class="absolute h-8 w-full bg-gray-700 rounded-b-3xl bottom-0 text-white text-center font-bold pt-1">
${participantInfo.name}
</div>
</div>
`
);
}
}
});
We are populating the remoteParticipants
container with the list of participants and also creating video
and audio
tag for each participants.
Currently the video and audio tag do not contain any remote stream, but we will add the remote stream to these tags in remoteTrackStarted
event handler.
For now we are just looping through all the participants, checking if the tag for the participant already exists in the remoteParticipants
container or not.
If the participant does not exists then we are creating a container to hold the participant's video tag to display the video, audio tag for the participants audio and a field to hold the username.
We specify the id of the container as participant-<participant_id>
.
Handling participantLeft event
The participantLeft event is triggered when the participant leaves the meeting.
meeting.on("participantLeft", function(participantInfo) {
jquery("#participant-" + participantInfo._id).remove();
if (participantInfo._id === activeSpeakerId) {
jquery("#activeSpeakerUsername").text("");
jquery("#activeSpeakerUsername").addClass("hidden");
}
});
We will remove the participant div.
Handling remoteTrackStarted event
The remoteTrackStarted
event is triggered when the participants in the meeting share their camera or microphone. It is also triggered when user joins an existing meeting where participants are already sharing their camera or microphone.
meeting.on("remoteTrackStarted", function(remoteTrackItem) {
jquery("#activeSpeakerUsername").removeClass("hidden");
if (remoteTrackItem.type === "video") {
let mediaStream = new MediaStream();
mediaStream.addTrack(remoteTrackItem.track);
if (jquery("#video-" + remoteTrackItem.participantSessionId)[0]) {
jquery("#video-" + remoteTrackItem.participantSessionId)[0].srcObject = mediaStream;
jquery("#video-" + remoteTrackItem.participantSessionId)[0].play();
}
}
if (remoteTrackItem.type === "audio") {
let mediaStream = new MediaStream();
mediaStream.addTrack(remoteTrackItem.track);
if ( jquery("#video-" + remoteTrackItem.participantSessionId)[0]) {
jquery("#audio-" + remoteTrackItem.participantSessionId)[0].srcObject = mediaStream;
jquery("#audio-" + remoteTrackItem.participantSessionId)[0].play();
}
}
setActiveSpeaker(remoteTrackItem);
});
In the event handler we will find the div of the remoteParticipant
and setting the srcObject
property of the video or audio tag that we had already created in the onlineParticipants
event handler depending upon the type of remote track.
In the end of the event handler we are calling the setActiveSpeaker
method. This method will set the remote participant as activeSpeaker.
function setActiveSpeaker(activeSpeaker) {
if (activeSpeakerId != activeSpeaker.participantSessionId) {
jquery(`#participant-${activeSpeakerId}`).show();
}
activeSpeakerId = activeSpeaker.participantSessionId;
jquery(`#participant-${activeSpeakerId}`).hide();
jquery("#activeSpeakerUsername").text(activeSpeaker.name || activeSpeaker.participant.name);
if (jquery(`#video-${activeSpeaker.participantSessionId}`)[0]) {
let stream = jquery(
`#video-${activeSpeaker.participantSessionId}`
)[0].srcObject;
jquery("#activeSpeakerVideo")[0].srcObject = stream.clone();
}
if (activeSpeaker.participantSessionId === meeting.participantSessionId) {
let stream = jquery(`#localVideoTag`)[0].srcObject;
if (stream) {
jquery("#localVideoTag")[0].srcObject = stream.clone();
}
}
}
In this method we will hide the user from the remoteParticipantsContainer
and move it to the center activeSpeaker
block.
Handling remoteTrackStopped event
When the remote participant stop sharing their camera, screen or microphone the remoteTrackStopped event is triggered.
meeting.on("remoteTrackStopped", function(remoteTrackItem) {
if (remoteTrackItem.type === "video") {
if ( jquery("#video-" + remoteTrackItem.participantSessionId)[0]) {
jquery("#video-" + remoteTrackItem.participantSessionId)[0].srcObject = null;
jquery("#video-" + remoteTrackItem.participantSessionId)[0].pause();
}
if (remoteTrackItem.participantSessionId === activeSpeakerId) {
jquery("#activeSpeakerVideo")[0].srcObject = null;
jquery("#activeSpeakerVideo")[0].pause();
}
}
if (remoteTrackItem.type === "audio") {
if (jquery("#audio-" + remoteTrackItem.participantSessionId)[0]) {
jquery("#audio-" + remoteTrackItem.participantSessionId)[0].srcObject = null;
jquery("#audio-" + remoteTrackItem.participantSessionId)[0].pause();
}
}
});
In this event handler we are setting the srcObject
property to null of the remote participant.
Handing activeSpeaker event
The activeSpeaker event is triggered to indicate which participant is currently speaking.
We will call our setActiveSpeaker
method that we have previously created in this event handler.
meeting.on("activeSpeaker", function(activeSpeaker) {
setActiveSpeaker(activeSpeaker);
});
Handling Microphone toggle
We have created a button with id #toggleMicrophone
when this button is pressed we will mute/unmute the microphone.
To mute the microphone we will call the stopAudio() method of the Metered Video SDK.
And to un-mute or share the microphone we will call the startAudio() method of the Metered Video SDK.
We will use a global variable called as micOn
to store the state of the microphone, whether it is currently begin shared or not.
jquery("#toggleMicrophone").on("click", async function() {
if (micOn) {
jquery("#toggleMicrophone").removeClass("bg-gray-500");
jquery("#toggleMicrophone").addClass("bg-gray-400");
micOn = false;
await meeting.stopAudio();
} else {
jquery("#toggleMicrophone").removeClass("bg-gray-400");
jquery("#toggleMicrophone").addClass("bg-gray-500");
micOn = true;
await meeting.startAudio();
}
});
Handling camera toggle
We have created a button with id #toggleCamera
when this button is pressed we will share/un-share the camera.
To share the camera we will call the startVideo() method of Metered Video SDK.
To stop sharing the camera we will call the stopVideo() method.
In this method we will also show the preview of the video that the user is currently sharing.
To get the local video stream we will call the getLocalVideoStream() method to fetch the video stream currently shared by the user.
We will then display the video stream in the #localVideoTag
jquery("#toggleCamera").on("click", async function() {
if (cameraOn) {
jquery("#toggleCamera").removeClass("bg-gray-500");
jquery("#toggleCamera").addClass("bg-gray-400");
jquery("#toggleScreen").removeClass("bg-gray-500");
jquery("#toggleScreen").addClass("bg-gray-400");
cameraOn = false;
await meeting.stopVideo();
const tracks = localVideoStream.getTracks();
tracks.forEach(function (track) {
track.stop();
});
localVideoStream = null;
jquery("#localVideoTag")[0].srcObject = null;
} else {
jquery("#toggleCamera").removeClass("bg-gray-400");
jquery("#toggleCamera").addClass("bg-gray-500");
cameraOn = true;
await meeting.startVideo();
localVideoStream = await meeting.getLocalVideoStream();
jquery("#localVideoTag")[0].srcObject = localVideoStream;
}
});
When the user decides to stop sharing the video, we will cleanup the localVideoStream
variable by stopping the video tracks and also call the stopVideo() method of the Metered Video SDK.
Handling Screen Sharing
We have created a with id #toggleScreen
when this button is pressed we will share the screen.
To share the screen we will call the startScreenShare() method of the Metered Video SDK.
This method returns the video stream of the screen that is currently begin shared, and we will set the video stream to localVideoTag
to show the user preview of their screen which is currently being shared.
jquery("#toggleScreen").on("click", async function() {
if (screenSharingOn) {
jquery("#toggleScreen").removeClass("bg-gray-500");
jquery("#toggleScreen").addClass("bg-gray-400");
screenSharingOn = false;
await meeting.stopVideo();
const tracks = localVideoStream.getTracks();
tracks.forEach(function (track) {
track.stop();
});
localVideoStream = null;
jquery("#localVideoTag")[0].srcObject = null;
} else {
jquery("#toggleScreen").removeClass("bg-gray-400");
jquery("#toggleScreen").addClass("bg-gray-500");
jquery("#toggleCamera").removeClass("bg-gray-500");
jquery("#toggleCamera").addClass("bg-gray-400");
screenSharingOn = true;
localVideoStream = await meeting.startScreenShare();
jquery("#localVideoTag")[0].srcObject = localVideoStream;
}
});
To stop the screen sharing we will call the meeting.stopVideo()
method and it will stop the screensharing.
Handling Leave Meeting
To implement leave meeting we will call the leaveMeeting() method of the Metered Video SDK.
And we will hide the meeting view and show the leaveMeeting View.
jquery("#leaveMeeting").on("click", async function() {
await meeting.leaveMeeting();
jquery("#meetingView").addClass("hidden");
jquery("#leaveMeetingView").removeClass("hidden");
});
<div id="leaveMeetingView" class="hidden">
<h1 class="text-center text-3xl mt-10 font-bold">
You have left the meeting
</h1>
</div>
Putting it all together
Here is final code of our meeting.blade.php
which contains the complete Meeting UI
The final code for our app.js
file that contains the logic for the meeting UI
Our welcome.blade.php
file that contains the controls to join an existing meeting or create a new meeting
And our web.php
file and MeetingContoller.php
MeetingController.php
Github
You can find the Github repo for this project here: Group Video Chat App with PHP Laravel and JavaScript
Conclusion
So we have built the complete video calling application in PHP Laravel and WebRTC using the Metered Video SDK.