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.
I conduct regular workshops with my friends and have explored several live streaming platforms. My favourite platform for conducting online workshops is Airmeet.
The feature that caught my eye and impressed me the most was that Airmeet dynamically allowed the organisers to call people onto stage by changing their roles from audience to host and vice versa.
This is a useful service when the session needs to be interactive. A user can simply be called onto stage as a host to express doubts, talk to the speakers, and make the session more interesting. Once the user is done interacting, their role can be changed back to audience.In this tutorial, we will develop a web application that supports changing the roles of remote users in a live streaming application using the Agora Web SDK and the Agora RTM SDK.
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, a list of remote users, and buttons for joining and leaving. 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 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"> | |
<title>Change remote user role | Agora</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-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="index.css"> | |
</head> | |
<body> | |
<!-- Title --> | |
<div class="container-fluid banner"> | |
<p class="banner-text">Live Streaming</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">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 class="col-sm"> | |
<p class="join-info-text">Name</p> | |
<input id="accountName" type="text" placeholder="Enter Your 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="leave" type="button" class="btn btn-live btn-sm" disabled>Leave</button> | |
</div> | |
</form> | |
<div class="row"> | |
<!-- Streams --> | |
<div class="col"> | |
<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> | |
<div class="col"> | |
<!-- All Users List with Controls --> | |
<div class="all-users-text my-3"> | |
<h5 class="text-decoration-underline">All Users:</h5> | |
<div class="all-users" id="all-users"> | |
<ul id="insert-all-users"> | |
</ul> | |
</div> | |
</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="https://cdn.jsdelivr.net/npm/agora-rtm-sdk@1.3.1/index.js"></script> | |
<script src="index.js"></script> | |
</body> | |
</html> |
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: #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; | |
} | |
} | |
.all-users-text { | |
background: rgba(12, 157, 253, 0.897); | |
padding: 30px; | |
color: white; | |
border-radius: 15px; | |
max-height: 500px; | |
overflow: scroll; | |
} | |
#host-join, | |
#audience-join, | |
#leave { | |
background-color: #0C9DFD; | |
color: white; | |
border-radius: 5px; | |
} | |
#host-join:hover, | |
#leave:hover, | |
#audience-join:hover { | |
background-color: rgba(2, 133, 221, 0.911); | |
} | |
.btn-live { | |
outline: none; | |
border: 1px solid white; | |
box-shadow: none; | |
} | |
.btn-live:hover, | |
.btn-live:focus { | |
outline: none; | |
box-shadow: none; | |
border: 1px solid white; | |
background: rgb(12, 157, 253); | |
} |
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.
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.
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" | |
}); | |
// RTM Global Vars | |
var isLoggedIn = false; | |
var localTracks = { | |
videoTrack: null, | |
audioTrack: null | |
}; | |
var remoteUsers = {}; | |
// Agora client options | |
var options = { | |
appid: $("#appid").val(), | |
channel: null, | |
uid: null, | |
token: null, | |
accountName: null, | |
role: "audience" | |
}; | |
// Host join | |
$("#host-join").click(function (e) { | |
RTMJoin(); | |
options.role = "host"; | |
}) | |
// Audience join | |
$("#audience-join").click(function (e) { | |
RTMJoin(); | |
options.role = "audience"; | |
}) | |
// Join form submission | |
$("#join-form").submit(async function (e) { | |
e.preventDefault(); | |
$("#host-join").attr("disabled", true); | |
$("#audience-join").attr("disabled", true); | |
try { | |
options.appid = $("#appid").val(); | |
options.token = $("#token").val(); | |
options.channel = $("#channel").val(); | |
options.accountName = $('#accountName').val(); | |
await join(); | |
} catch (error) { | |
console.error(error); | |
} finally { | |
$("#leave").attr("disabled", false); | |
} | |
}) | |
// Leave click | |
$("#leave").click(function (e) { | |
leave(); | |
}) | |
async function join() { | |
// create Agora client | |
client.setClientRole(options.role); | |
if (options.role === "audience") { | |
// 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") { | |
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(); | |
// 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."); | |
} | |
} | |
// Leave | |
async function leave() { | |
for (trackName in localTracks) { | |
var track = localTracks[trackName]; | |
if (track) { | |
track.stop(); | |
track.close(); | |
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); | |
console.log("Client successfully left channel."); | |
} | |
// Subscribe to a remote user | |
async function subscribe(user, mediaType) { | |
const uid = user.uid; | |
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(); | |
} |
Now that our application supports live streaming, it’s time to incorporate the main functionality: to change a remote user’s role using the Agora RTM SDK.
We call the RTMJoin() function to create a RTM client. Following the RTM Docs, we log in to RTM (line 8) and join a channel (line 16) to get started with the RTM SDK.
We use channel.getMembers() (line 19) to get a list of all users in the RTM channel. We’ll use their usernames, which we received from the front end to each button using a custom id.
The getMembers function is called only once, so the list isn’t updated when a user leaves or joins the channel. To prevent this data inconsistency, we use the MemberLeft (line 120) and MemberJoined (line 98) callbacks provided by the RTM SDK.
We then check for clicks on the host and audience role-change buttons (line 39). When a user clicks the button, we send a peer-to-peer message using RTM to tell the remote user what kind of a role change it is (host or audience).
When the peer receives the message (line 75), the user’s role is changed for all users in the RTC channel. If the role change is from audience to host, a track is published. If a role is changed from host to audience, all local tracks are stopped.
Finally, we add a logout function (line 148) because, unlike the Web SDK, the RTM SDK requires the user to log out in addition to leaving the channel.
async function RTMJoin() { | |
// Create Agora RTM client | |
const clientRTM = AgoraRTM.createInstance($("#appid").val(), { | |
enableLogUpload: false | |
}); | |
var accountName = $('#accountName').val(); | |
// Login | |
clientRTM.login({ | |
uid: accountName | |
}).then(() => { | |
console.log('AgoraRTM client login success. Username: ' + accountName); | |
isLoggedIn = true; | |
// RTM Channel Join | |
var channelName = $('#channel').val(); | |
channel = clientRTM.createChannel(channelName); | |
channel.join().then(() => { | |
console.log('AgoraRTM client channel join success.'); | |
// Get all members in RTM Channel | |
channel.getMembers().then((memberNames) => { | |
console.log("------------------------------"); | |
console.log("All members in the channel are as follows: "); | |
console.log(memberNames); | |
var newHTML = $.map(memberNames, function (singleMember) { | |
if (singleMember != accountName) { | |
return (`<li class="mt-2"> | |
<div class="row"> | |
<p>${singleMember}</p> | |
</div> | |
<div class="mb-4"> | |
<button class="text-white btn btn-live mx-3 remoteHost hostOn" id="remoteAudio-${singleMember}">Make Host</button> | |
<button class="text-white btn btn-live remoteAudience audienceOn" id="remoteVideo-${singleMember}">Make Audience</button> | |
</div> | |
</li>`); | |
} | |
}); | |
$("#insert-all-users").html(newHTML.join("")); | |
}); | |
// Send peer-to-peer message for changing role to host | |
$(document).on('click', '.remoteHost', function () { | |
fullDivId = $(this).attr('id'); | |
peerId = fullDivId.substring(fullDivId.indexOf("-") + 1); | |
console.log("Remote host button pressed."); | |
let peerMessage = "host"; | |
clientRTM.sendMessageToPeer({ | |
text: peerMessage | |
}, | |
peerId, | |
).then(sendResult => { | |
if (sendResult.hasPeerReceived) { | |
console.log("Message has been received by: " + peerId + " Message: " + peerMessage); | |
} else { | |
console.log("Message sent to: " + peerId + " Message: " + peerMessage); | |
} | |
}) | |
}); | |
// Send peer-to-peer message for changing role to audience | |
$(document).on('click', '.remoteAudience', function () { | |
fullDivId = $(this).attr('id'); | |
peerId = fullDivId.substring(fullDivId.indexOf("-") + 1); | |
console.log("Remote audience button pressed."); | |
let peerMessage = "audience"; | |
clientRTM.sendMessageToPeer({ | |
text: peerMessage | |
}, | |
peerId, | |
).then(sendResult => { | |
if (sendResult.hasPeerReceived) { | |
console.log("Message has been received by: " + peerId + " Message: " + peerMessage); | |
} else { | |
console.log("Message sent to: " + peerId + " Message: " + peerMessage); | |
} | |
}) | |
}); | |
// Display messages from peer | |
clientRTM.on('MessageFromPeer', function ({ | |
text | |
}, peerId) { | |
console.log(peerId + " changed your role to " + text); | |
if (text == "host") { | |
leave(); | |
options.role = "host"; | |
console.log("Role changed to host."); | |
client.setClientRole("host"); | |
join(); | |
$("#host-join").attr("disabled", true); | |
$("#audience-join").attr("disabled", true); | |
} else if (text == "audience") { | |
leave(); | |
options.role = "audience"; | |
console.log("Role changed to audience."); | |
client.setClientRole("audience"); | |
join(); | |
$("#host-join").attr("disabled", true); | |
$("#audience-join").attr("disabled", true); | |
} | |
}) | |
// Display channel member joined updated users | |
channel.on('MemberJoined', function () { | |
// Get all members in RTM Channel | |
channel.getMembers().then((memberNames) => { | |
console.log("New member joined so updated list is: "); | |
console.log(memberNames); | |
var newHTML = $.map(memberNames, function (singleMember) { | |
if (singleMember != accountName) { | |
return (`<li class="mt-2"> | |
<div class="row"> | |
<p>${singleMember}</p> | |
</div> | |
<div class="mb-4"> | |
<button class="text-white btn btn-live mx-3 remoteHost hostOn" id="remoteAudio-${singleMember}">Make Host</button> | |
<button class="text-white btn btn-live remoteAudience audienceOn" id="remoteVideo-${singleMember}">Make Audience</button> | |
</div> | |
</li>`); | |
} | |
}); | |
$("#insert-all-users").html(newHTML.join("")); | |
}); | |
}) | |
// Display channel member left updated users | |
channel.on('MemberLeft', function () { | |
// Get all members in RTM Channel | |
channel.getMembers().then((memberNames) => { | |
console.log("A member left so updated list is: "); | |
console.log(memberNames); | |
var newHTML = $.map(memberNames, function (singleMember) { | |
if (singleMember != accountName) { | |
return (`<li class="mt-2"> | |
<div class="row"> | |
<p>${singleMember}</p> | |
</div> | |
<div class="mb-4"> | |
<button class="text-white btn btn-live mx-3 remoteHost hostOn" id="remoteAudio-${singleMember}">Make Host</button> | |
<button class="text-white btn btn-live remoteAudience audienceOn" id="remoteVideo-${singleMember}">Make Audience</button> | |
</div> | |
</li>`); | |
} | |
}); | |
$("#insert-all-users").html(newHTML.join("")); | |
}); | |
}); | |
}).catch(error => { | |
console.log('AgoraRTM client channel join failed: ', error); | |
}).catch(err => { | |
console.log('AgoraRTM client login failure: ', err); | |
}); | |
}); | |
// Logout | |
document.getElementById("leave").onclick = async function () { | |
console.log("Client logged out of RTM."); | |
await clientRTM.logout(); | |
} | |
} |
You can now run and test the application.
Note: For testing, you can use multiple browser tabs to simulate multiple users on the call.
Demo of how the application works.
We have successfully made our own live streaming application with a remote role-changing service. 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:
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!
To learn more about the Agora Web SDK and other use cases, you can refer to the developer guide here.