Back to Blog

Highlighting The Active Speakers During A Group Video Call

Highlighting The Active Speakers During A Group Video Call

This blog was written by Akshat Gupta an Agora Developer Evangelist Intern who had started his journey in Agora as a Superstar. The Agora Superstar program empowers developers around the world to share their passion and technical expertise, and create innovative real-time communications apps and projects using Agora’s customizable SDKs.


We all want our remote video conference attendees to feel like they can participate and are truly a part of the meeting. Interruptions and being talked over are two of the biggest meeting challenges for both remote and on-site workers.

Visually highlighting or differentiating the active speaker creates an organic, conversational atmosphere that engages remote participants far more than on a normal video call. The attention of the participants is shifted to the right person, so they know where to look on the screen.In this tutorial, we will develop a web application that highlights active speakers in a group video calling application using the Agora Web SDK.

Highlighting The Active Speakers During A Group Video Call screenshot 1
Screenshot of the video calling application we will be developing.

Prerequisites

Project Setup

Let’s start by laying out our basic HTML structure. A few UI elements are necessary, such as the local audio stream uid, the remote audio streams’ uids, and buttons for joining, leaving, and muting and unmuting. I’ve also imported the necessary CDNs and linked the custom CSS and JS files.

<!DOCTYPE html>
<html lang="en">
<head>
<!-- Meta Tags For SEO -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Agora Find Active Speaker</title>
<link rel="icon" href="./favicon.png" type="image/png">
<link rel="apple-touch-icon" href="./apple-touch-icon.png" type="image/png">
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
<link rel="stylesheet" href="./index.css">
</head>
<body>
<!-- Container -->
<div class="container-fluid banner">
<p class="banner-text">Agora Find Active Speaker</p>
</div>
<div class="container mt-5 mb-5">
<!-- Form for joining and leaving -->
<form id="join-form">
<!-- Inputs -->
<div class="row">
<div class="col-sm">
<p class="join-info-text">AppID</p>
<input id="appid" type="text" placeholder="Enter AppID" required class="form-control">
</div>
<div class="col-sm">
<p class="join-info-text">Channel</p>
<input id="channel" type="text" placeholder="Enter Channel Name" required class="form-control">
</div>
</div>
<!-- Button controls -->
<div class="button-group mt-2">
<button id="join" type="submit" class="btn btn-sm">Join</button>
<button id="mic-btn" type="button" class="btn btn-sm" disabled>
Mute Audio
</button>
<button id="video-btn" type="button" class="btn btn-sm" disabled>
Mute Video
</button>
<button id="leave" type="button" class="btn btn-sm" disabled>Leave</button>
</div>
</form>
<div class="row">
<div class="col">
<div class="row video-group">
<!-- Local Video -->
<div class="col">
<p id="local-player-name" class="player-name"></p>
<div id="local-player" class="player"></div>
</div>
<div class="w-100"></div>
<!-- Remote Players -->
<div id="remote-playerlist" class="row justify-content-center"></div>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous">
</script>
<script src="https://download.agora.io/sdk/release/AgoraRTC_N.js"></script>
<script src="./index.js"></script>
</body>
</html>
Highlighting The Active Speakers During A Group Video Call screenshot 2
Screenshot of our site with the above code.

Adding Color

Now that our basic layout is ready, it’s time to add some CSS.I’ve already added basic Bootstrap classes to the HTML to make the site presentable, but we’ll use custom CSS to match the site with a blue Agora-based theme.

.banner {
padding: 10px;
background-color: #1A0374;
color: white;
}
.banner-text {
padding: 8px 20px;
margin: 0;
font-size: 18px;
}
#join-form {
margin-top: 10px;
}
.join-info-text {
margin-bottom: 2px;
}
input {
width: 100%;
margin-bottom: 2px;
}
.player {
width: 480px;
height: 320px;
}
.player-name {
margin: 8px 0;
}
@media (max-width: 640px) {
.player {
width: 320px;
height: 240px;
}
}
#join,
#leave,
#mic-btn,
#video-btn {
background-color: #0C9DFD;
color: white;
border-radius: 5px;
}
#join:hover,
#leave:hover,
#mic-btn:hover,
#video-btn:hover {
background-color: rgba(2, 133, 221, 0.911);
}
.all-users-text {
background: rgba(12, 157, 253, 0.897);
padding: 30px;
color: white;
border-radius: 15px;
max-height: 500px;
overflow: scroll;
}
.btn-control {
outline: none;
border: 1px solid white;
box-shadow: none;
}
.btn-control:hover,
.btn-control:focus {
outline: none;
box-shadow: none;
border: 1px solid white;
background: rgb(12, 157, 253);
}
.remoteMicrophone,
.remoteCamera {
cursor: pointer;
}
Highlighting The Active Speakers During A Group Video Call screenshot 3
The magic of CSS.

Core Functionality (JS)

Now that we have the HTML/DOM structure laid out, we can add the JS, which uses the Agora Web SDK. It may look intimidating at first, but if you follow Agora’s official docs and demos and put in a little practice, it’ll be a piece of cake.

In the following snippet we first create a client and then create a microphone audio and camera video track.

Tokens are used for incorporating additional security into our application. If you don’t use tokens, specify the tokens as null.

When a user joins a channel by clicking the button, you begin playing the tracks specified while creating the client. The user’s stream is then published and subscribed, which can be toggled using the UI controls.

When a user leaves, remove the uid from the user’s screen.

Finally, we give the user the option to end the call and leave the channel.

// create Agora client
var client = AgoraRTC.createClient({
mode: "rtc",
codec: "vp8"
});
var localTracks = {
videoTrack: null,
audioTrack: null
};
var localTrackState = {
videoTrackEnabled: true,
audioTrackEnabled: true
}
var remoteUsers = {};
// Agora client options
var options = {
appid: null,
channel: null,
uid: null,
token: null
};
$("#join-form").submit(async function (e) {
e.preventDefault();
$("#join").attr("disabled", true);
try {
options.appid = $("#appid").val();
options.channel = $("#channel").val();
await join();
} catch (error) {
console.error(error);
} finally {
$("#leave").attr("disabled", false);
}
});
$("#leave").click(function (e) {
leave();
});
$("#mic-btn").click(function (e) {
if (localTrackState.audioTrackEnabled) {
muteAudio();
} else {
unmuteAudio();
}
});
$("#video-btn").click(function (e) {
if (localTrackState.videoTrackEnabled) {
muteVideo();
} else {
unmuteVideo();
}
})
async function join() {
$("#mic-btn").prop("disabled", false);
$("#video-btn").prop("disabled", false);
// add event listener to play remote tracks when remote users join, publish and leave.
client.on("user-published", handleUserPublished);
client.on("user-joined", handleUserJoined);
client.on("user-left", handleUserLeft);
// join a channel and create local tracks, we can use Promise.all to run them concurrently
[options.uid, localTracks.audioTrack, localTracks.videoTrack] = await Promise.all([
// join the channel
client.join(options.appid, options.channel, options.token || null),
// create local tracks, using microphone and camera
AgoraRTC.createMicrophoneAudioTrack(),
AgoraRTC.createCameraVideoTrack()
]);
showMuteButton();
// play local video track
localTracks.videoTrack.play("local-player");
$("#local-player-name").text(`localVideo(${options.uid})`);
// publish local tracks to channel
await client.publish(Object.values(localTracks));
console.log("publish success");
}
async function leave() {
for (trackName in localTracks) {
var track = localTracks[trackName];
if (track) {
track.stop();
track.close();
$('#mic-btn').prop('disabled', true);
$('#video-btn').prop('disabled', true);
localTracks[trackName] = undefined;
}
}
// remove remote users and player views
remoteUsers = {};
$("#remote-playerlist").html("");
// leave the channel
await client.leave();
$("#local-player-name").text("");
$("#join").attr("disabled", false);
$("#leave").attr("disabled", true);
hideMuteButton();
console.log("client leaves channel success");
}
async function subscribe(user, mediaType) {
const uid = user.uid;
// subscribe to a remote user
await client.subscribe(user, mediaType);
console.log("subscribe success");
// if the video wrapper element is not exist, create it.
if (mediaType === 'video') {
if ($(`#player-wrapper-${uid}`).length === 0) {
const player = $(`
<div id="player-wrapper-${uid}" class="col col-xl-6">
<p class="player-name">remoteUser(${uid})</p>
<div id="player-${uid}" class="player"></div>
</div>
`);
$("#remote-playerlist").append(player);
}
// play the remote video.
user.videoTrack.play(`player-${uid}`);
}
if (mediaType === 'audio') {
user.audioTrack.play();
}
}
// Handle user join
function handleUserJoined(user) {
const id = user.uid;
remoteUsers[id] = user;
}
// Handle user leave
function handleUserLeft(user) {
const id = user.uid;
delete remoteUsers[id];
$(`#player-wrapper-${id}`).remove();
}
// Handle user published
function handleUserPublished(user, mediaType) {
subscribe(user, mediaType);
}
// Hide mute button
function hideMuteButton() {
$("#video-btn").css("display", "none");
$("#mic-btn").css("display", "none");
}
// Display mute button
function showMuteButton() {
$("#video-btn").css("display", "inline-block");
$("#mic-btn").css("display", "inline-block");
}
// Mute audio function
async function muteAudio() {
if (!localTracks.audioTrack) return;
await localTracks.audioTrack.setEnabled(false);
localTrackState.audioTrackEnabled = false;
$("#mic-btn").text("Unmute Audio");
$("#local-player").css({
"box-shadow": "none"
});
}
// Mute video function
async function muteVideo() {
if (!localTracks.videoTrack) return;
await localTracks.videoTrack.setEnabled(false);
localTrackState.videoTrackEnabled = false;
$("#video-btn").text("Unmute Video");
}
// Unmute audio function
async function unmuteAudio() {
if (!localTracks.audioTrack) return;
await localTracks.audioTrack.setEnabled(true);
localTrackState.audioTrackEnabled = true;
$("#mic-btn").text("Mute Audio");
}
// Unmute video function
async function unmuteVideo() {
if (!localTracks.videoTrack) return;
await localTracks.videoTrack.setEnabled(true);
localTrackState.videoTrackEnabled = true;
$("#video-btn").text("Mute Video");
}

Now that our application supports group video calling, it’s time to incorporate the main functionality: highlighting any user who’s speaking through a blue box shadow around the user’s video stream.

Highlighting a Speaker

Following the Agora Docs, we enable the volume indicator. This method enables the SDK to regularly report the remote users who are speaking and their volumes.

After the volume indicator is enabled, the SDK triggers the AgoraRTCClient.on(“volume-indicator”) callback to report the volumes every two seconds, regardless of whether active speakers are in the channel.

The volume-indicator callback reports all the speaking remote users and their volumes. It is disabled by default. It’s enabled only via the enableAudioVolumeIndicator. If enabled, it reports the users’ volumes every two seconds regardless of whether users are speaking.

The volume is an integer ranging from 0 to 100. Usually, a user with a volume level above 5 is a speaking user.

// Find active speakers
client.enableAudioVolumeIndicator();
client.on("volume-indicator", volumes => {
volumes.forEach((volume) => {
console.log(`UID ${volume.uid} Level ${volume.level}`);
if (options.uid == volume.uid && volume.level > 5) {
$("#local-player").css({
"box-shadow": "0 2px 4px 0 #0C9DFD, 0 2px 5px 0 #0C9DFD"
});
} else if (options.uid == volume.uid && volume.level < 5) {
$("#local-player").css({
"box-shadow": "none"
});
}
if (options.uid != volume.uid && volume.level > 5) {
$("#player-" + volume.uid).css({
"box-shadow": "0 2px 4px 0 #0C9DFD, 0 2px 5px 0 #0C9DFD"
});
} else if (options.uid != volume.uid && volume.level < 5) {
$("#player-" + volume.uid).css({
"box-shadow": "none"
});
}
});
})

You can now run and test the application.

Note: For testing, you can use multiple browser tabs to simulate multiple users on the call.

Conclusion

You did it!

We have successfully made our own video calling application that highlights the active speaker. In case you weren’t coding along or want to see the finished product all together, I have uploaded all the code to GitHub:

Highlighting The Active Speakers During A Group Video Call screenshot 4

If you would like to see the demo in action, check out the demo of the code in action:

Highlighting The Active Speakers During A Group Video Call screenshot 5

Thanks for taking the time to read my tutorial. If you have questions, please let me know with a comment. If you see room for improvement, feel free to fork the repo and make a pull request!

RTE Telehealth 2023
Join us for RTE Telehealth - a virtual webinar where we’ll explore how AI and AR/VR technologies are shaping the future of healthcare delivery.

Learn more about Agora's video and voice solutions

Ready to chat through your real-time video and voice needs? We're here to help! Current Twilio customers get up to 2 months FREE.

Complete the form, and one of our experts will be in touch.

Try Agora for Free

Sign up and start building! You don’t pay until you scale.
Try for Free