This blog was written by Akshat Gupta an Agora 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.
Introduction
In today’s era, every person and company has gone digital to move ahead with the trends.
Live streaming has become a vital feature in social media apps as more users look to interact and share real-life moments with their family and friends in real-time, grow their followings, or even establish themselves as creators.
In this tutorial, we will develop a web application that supports live streaming supporting multiple hosts as well as multiple audience members using Agora’s SDK.

Prerequisites:
- Basic knowledge of how to work with JavaScript, JQuery, Bootstrap, Font Awesome
- An Agora developer account (see How to Get Started with Agora)
- The Agora Web SDK.
Project Setup
Let’s start by laying out our basic html structure. There are a few UI elements we must have, such as the local video stream, the remote video streams, a toolbar that will contain buttons for toggling audio/video streams, and lastly a way to leave the chat. I’ve also imported the Bootstrap, JQuery, Font Awesome and Agora SDK CDNs and linked the custom CSS and JS files.
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<!-- Meta Tags For SEO --> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
<meta name="description" content="Many to many, live video streaming using the Agora Web NG SDK."> | |
<title>Many to Many Live Streaming || Agora Web NG SDK</title> | |
<!-- CSS only --> | |
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" | |
integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> | |
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" | |
integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous"> | |
<link rel="stylesheet" href="assets/css/m2m-live.css"> | |
<link rel="icon" href="assets/img/favicon.png" type="image/png"> | |
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon.png" type="image/png"> | |
</head> | |
<body> | |
<!-- Title --> | |
<div class="container-fluid banner"> | |
<p class="banner-text">Live Broadcast</p> | |
</div> | |
<div class="container"> | |
<form id="join-form" name="join-form" class="mt-4"> | |
<!-- Input Field --> | |
<div class="row join-info-group"> | |
<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> | |
<!-- UI Controls --> | |
<div class="button-group mt-3"> | |
<button id="host-join" type="submit" class="btn btn-live btn-sm">Join as Host</button> | |
<button id="audience-join" type="submit" class="btn btn-live btn-sm">Join as Audience</button> | |
<button id="mic-btn" type="button" class="btn btn-live btn-sm"> | |
<i id="mic-icon" class="fas fa-microphone"></i> | |
</button> | |
<button id="video-btn" type="button" class="btn btn-live btn-sm"> | |
<i id="video-icon" class="fas fa-video"></i> | |
</button> | |
<button id="leave" type="button" class="btn btn-live btn-sm" disabled>Leave</button> | |
</div> | |
</form> | |
<!-- Streams --> | |
<div class="row video-group"> | |
<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> | |
<div class="col"> | |
<div id="remote-playerlist"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Scripts --> | |
<script src="https://code.jquery.com/jquery-3.5.1.min.js" | |
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> | |
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" | |
integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" | |
crossorigin="anonymous"></script> | |
<script src="https://download.agora.io/sdk/release/AgoraRTC_N.js"></script> | |
<script src="assets/js/o2o-voice.js"></script> | |
</body> | |
</html> |

Adding CSS
Now that our base has been laid out, 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: #2F3FB0; | |
color: white; | |
} | |
.banner-text { | |
padding: 8px 20px; | |
margin: 0; | |
} | |
#join-form { | |
margin-top: 10px; | |
} | |
.tips { | |
font-size: 12px; | |
margin-bottom: 2px; | |
color: gray; | |
} | |
.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; | |
} | |
} | |
.btn-live { | |
background-color: #2F3FB0; | |
color: white; | |
border: 1px solid #2F3FB0; | |
} | |
.btn-live:hover { | |
color: #2F3FB0; | |
background-color: white; | |
border: 1px solid #2F3FB0; | |
} |

Muting and Unmuting the Video and Audio
Let’s add some functionality to our beautiful website. We will begin with the UI controls (muting and unmuting the video as well as audio for the hosts). A little JS here and a little JS there does the job:
// Mute audio click | |
$("#mic-btn").click(function (e) { | |
if (localTrackState.audioTrackEnabled) { | |
muteAudio(); | |
} else { | |
unmuteAudio(); | |
} | |
}); | |
// Mute video click | |
$("#video-btn").click(function (e) { | |
if (localTrackState.videoTrackEnabled) { | |
muteVideo(); | |
} else { | |
unmuteVideo(); | |
} | |
}) | |
// Hide mute buttons | |
function hideMuteButton() { | |
$("#video-btn").css("display", "none"); | |
$("#mic-btn").css("display", "none"); | |
} | |
// Show mute buttons | |
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"); | |
} | |
// 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"); | |
} |
So far so good?
Core Structure (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 & demos and put in a little bit of practice, it’ll be a piece of cake.
We first create a client and specify the audio as well as video tracks. You can use a .env
file or directly hardcode the App ID into the application and take in the channel name and token (optional) from the frontend.
If you don’t use tokens, specify the tokens as null
.
When a host or audience joins a channel by clicking the buttons, you can set the user’s role and begin playing the tracks specified while creating the client. The user’s stream is then subscribed and published which can be toggled using the UI controls we wrote above.
Finally, we give the user an option to end the stream and leave the channel.
// create Agora client | |
var client = AgoraRTC.createClient({ | |
mode: "live", | |
codec: "vp8" | |
}); | |
var localTracks = { | |
videoTrack: null, | |
audioTrack: null | |
}; | |
var localTrackState = { | |
videoTrackEnabled: true, | |
audioTrackEnabled: true | |
} | |
var remoteUsers = {}; | |
// Agora client options | |
var options = { | |
appid: <>, | |
channel: null, | |
uid: null, | |
token: null, | |
role: "audience" // host or audience | |
}; | |
$("#host-join").click(function (e) { | |
options.role = "host"; | |
}) | |
$("#audience-join").click(function (e) { | |
options.role = "audience"; | |
}) | |
$("#join-form").submit(async function (e) { | |
e.preventDefault(); | |
$("#host-join").attr("disabled", true); | |
$("#audience-join").attr("disabled", true); | |
try { | |
options.appid = <>; | |
options.channel = $("#channel").val(); | |
await join(); | |
} catch (error) { | |
console.error(error); | |
} finally { | |
$("#leave").attr("disabled", false); | |
} | |
}) | |
$("#leave").click(function (e) { | |
leave(); | |
}) | |
async function join() { | |
// create Agora client | |
client.setClientRole(options.role); | |
$("#mic-btn").prop("disabled", false); | |
$("#video-btn").prop("disabled", false); | |
if (options.role === "audience") { | |
$("#mic-btn").prop("disabled", true); | |
$("#video-btn").prop("disabled", true); | |
// add event listener to play remote tracks when remote user publishs. | |
client.on("user-published", handleUserPublished); | |
client.on("user-joined", handleUserJoined); | |
client.on("user-left", handleUserLeft); | |
} | |
// join the channel | |
options.uid = await client.join(options.appid, options.channel, options.token || null); | |
if (options.role === "host") { | |
$('#mic-btn').prop('disabled', false); | |
$('#video-btn').prop('disabled', false); | |
client.on("user-published", handleUserPublished); | |
client.on("user-joined", handleUserJoined); | |
client.on("user-left", handleUserLeft); | |
// create local audio and video tracks | |
localTracks.audioTrack = await AgoraRTC.createMicrophoneAudioTrack(); | |
localTracks.videoTrack = await AgoraRTC.createCameraVideoTrack(); | |
showMuteButton(); | |
// play local video track | |
localTracks.videoTrack.play("local-player"); | |
$("#local-player-name").text(`localTrack(${options.uid})`); | |
// publish local tracks to channel | |
await client.publish(Object.values(localTracks)); | |
console.log("Successfully published."); | |
} | |
} | |
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(""); | |
$("#host-join").attr("disabled", false); | |
$("#audience-join").attr("disabled", false); | |
$("#leave").attr("disabled", true); | |
hideMuteButton(); | |
console.log("Client successfully left channel."); | |
} | |
async function subscribe(user, mediaType) { | |
const uid = user.uid; | |
// subscribe to a remote user | |
await client.subscribe(user, mediaType); | |
console.log("Successfully subscribed."); | |
if (mediaType === 'video') { | |
const player = $(` | |
<div id="player-wrapper-${uid}"> | |
<p class="player-name">remoteUser(${uid})</p> | |
<div id="player-${uid}" class="player"></div> | |
</div> | |
`); | |
$("#remote-playerlist").append(player); | |
user.videoTrack.play(`player-${uid}`); | |
} | |
if (mediaType === 'audio') { | |
user.audioTrack.play(); | |
} | |
} | |
// Handle user published | |
function handleUserPublished(user, mediaType) { | |
const id = user.uid; | |
remoteUsers[id] = user; | |
subscribe(user, mediaType); | |
} | |
// Handle user joined | |
function handleUserJoined(user, mediaType) { | |
const id = user.uid; | |
remoteUsers[id] = user; | |
subscribe(user, mediaType); | |
} | |
// Handle user left | |
function handleUserLeft(user) { | |
const id = user.uid; | |
delete remoteUsers[id]; | |
$(`#player-wrapper-${id}`).remove(); | |
} |
Note: You need to enter your own App ID in the JS code above. I replaced my App ID by <> to avoid unnecessary charges.
You can now run and test the application.
Note: For testing, you can use two (or more) browser tabs to simulate single/multiple host(s) and single/multiple remote audience.
Conclusion
You did it!
We have successfully made our very own live streaming web application. In-case you weren’t coding along or want to see the finished product all together, I have uploaded all the code to GitHub:

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

You can also learn how to combine video streams using Agora's Web SDK. Thanks for taking the time to read my tutorial and if you have any questions please let me know with a comment. If you see any room for improvement feel free to fork the repo and make a pull request!
Other Resources:
To learn more about Agora’s Web NG SDK and other use cases you can refer to the developer guide given over here:
You can also have a look at the complete documentation for the functions discussed above and many more over here.
You can also join the Agora Developer Slack Community: