Back to Blog

Video Call Invitations with Agora RTM and RTC Using Vue JS and Flask

Video Call Invitations with Agora RTM and RTC Using Vue JS and Flask

If you have ever wondered how to include video calling features in your web applications, you’ve found a well-structured and well-written article.
In this article, we will build an application that has video calling capabilities similar to those in WhatsApp Web, using the Agora Web SDK.

Features to Expect in the Article

  • Inviting a user to a video call with an incoming call notification
  • Returning the appropriate feedback if the user is offline
  • Terminating calls before they’re answered
  • Declining/rejecting calls
  • Accepting/receiving calls

Prerequisites

Project Setup

  1. Create and activate a Python 3 virtual environment for this project.
  2. Open your terminal or command prompt and navigate to the starter project that you downloaded as part of the prerequisites. The folder is named agora-flask-starter.
  3. Follow the instructions in the README.md file in the agora-flask starter to set up the application.
  4. Install the agora_token_builder package:
    pip install agora-token-builder
  5. Download the latest version of the Agora RTM SDK. Agora RTM SDK

Configuring the Back End

We will create a new app named agora_rtm, register its blueprint, and create the needed static templates and views

1. Create the folders needed for the app:

mkdir app/agora_rtm
mkdir app/static/agora_rtm
mkdir app/templates/agora_rtm

2. Create the Agora view:

  • Create views.py and __init__.py in the app/agora_rtm directory from your terminal:
touch app/agora_rtm/views.py
touch app/agora_rtm/__init__.py
  • Add the following to __init__.py:
from flask import Blueprint
agora_rtm = Blueprint('agora_rtm', '__init__')
from . import views # isort:skip
view raw __init__.py hosted with ❤ by GitHub

3. Register the agora_rtm blueprint.
Import the agora_rtm app and register it as a blueprint in app/__init__.py:

from .agora_rtm import agora_rtm as agora_rtm_blueprint
app.register_blueprint(agora_rtm_blueprint)

Place the above code before the return app statement.

Breakdown of Methods in agora_rtm/views.py

index: To view the video call page. Only authenticated users can view the page. Non-authenticated users are redirected to the login page. We return a list of all the registered users.

fetch_users: Returns all the registered users as a JSON response.

generate_agora_token: Returns the token used for the RTM and RTC connections.
* token: The RTC token
* rtm_token: The RTM token

Note that we have set the token’s expiration time to 3600s (1 hr). You can modify the endpoint to use the expiration time that you want.

Configuring the Front End

We will create the user interface for making and receiving the video call with the ability to toggle the on and off states of the camera and the microphone.

We show incoming call notifications where the recipient can accept or reject the call.

1. Add the Downloaded RTM SDK to the project.

  • Unzip the file we downloaded in step 5 in “Project Setup”.
  • Navigate to the libs folder and copy the agora-rtm-sdk-1.4.4.js file into static/agora_rtm.
  • Rename the copied file to agora-rtm.js

2. Add the HTML file for the index view.

The HTML file will contain the links to the CDN for the Agora RTC SDK, Agora RTM SDK, Vue.js, Axios, Bootstrap for styling, and our custom CSS and JavaScript.

The index.html file will also inherit a base template, which is used to render the view:

  • Create an index.html file in templates/agora_rtm:
    touch app/templates/agora_rtm/index.html
  • Add the following code to the index.html file:
{% extends "base.html" %} {% block head_scripts %}
<link
rel="stylesheet"
type="text/css"
href="{{ url_for('static', filename='agora/index.css') }}"
/>
<script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.7.0.js"></script>
<script src="{{ url_for('static', filename='agora_rtm/agora-rtm.js') }}"></script>
{% endblock head_scripts %} {% block content%}
<div id="app">
<div class="container my-5">
<div class="row">
<div class="col" v-if="isLoggedIn">
<div class="btn-group" role="group" id="btnGroup">
{% for singleUser in allUsers%} {% if singleUser['id'] !=
current_user['id'] %} {% set username = singleUser['username']%}
<button
type="button"
class="btn btn-primary mr-2 my-2"
@click="placeCall('{{username}}')"
>
Call {{ username}}
<span class="badge badge-light"
>${updatedOnlineStatus?.["{{username}}"]?.toLowerCase() ||
'offline'}</span
>
</button>
{% endif %} {% endfor %}
</div>
</div>
</div>
<div class="row my-5" v-if="isCallingUser">
<div class="col-12">
<p>${callingUserNotification}</p>
<button type="button" class="btn btn-danger" @click="cancelCall">
Cancel Call
</button>
</div>
</div>
<!-- Incoming Call -->
<div class="row my-5" v-if="incomingCall">
<div class="col-12">
<!-- <p>Incoming Call From <strong>${ incomingCaller }</strong></p> -->
<p>${incomingCallNotification}</p>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-danger"
data-dismiss="modal"
@click="declineCall"
>
Decline
</button>
<button
type="button"
class="btn btn-success ml-5"
@click="acceptCall"
>
Accept
</button>
</div>
</div>
</div>
<!-- End of Incoming Call -->
</div>
<section id="video-container" v-if="callPlaced">
<div id="local-video" ref="localVideo"></div>
<div id="remote-video" ref="remoteVideo"></div>
<div class="action-btns">
<button type="button" class="btn btn-info" @click="handleAudioToggle">
${ mutedAudio ? "Unmute" : "Mute" }
</button>
<button
type="button"
class="btn btn-primary mx-4"
@click="handleVideoToggle"
>
${ mutedVideo ? "ShowVideo" : "HideVideo" }
</button>
<button type="button" class="btn btn-danger" @click="endCall">
EndCall
</button>
</div>
</section>
</div>
{% endblock content %}
<!-- Add Scripts -->
{% block bottom_scripts%}
<script>
const AUTH_USER = "{{current_user['username']}}";
const AUTH_USER_ID = "{{current_user['id']}}";
const CSRF_TOKEN = "{{ csrf_token }}";
const AGORA_APP_ID = "{{agoraAppID}}";
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="{{ url_for('static', filename='agora_rtm/index.js') }}"></script>
{% endblock bottom_scripts %}
view raw index.html hosted with ❤ by GitHub

We use Flask’s templating language to reuse some code. As indicated earlier, we inherit a base template named base.html. It has the following blocks:

  • head_scripts: This is the block where we place the link to the Agora RTC and RTM SDKs and our index.css for styling the video call page.
  • content: The content block contains the user interface for rendering the video stream with its control buttons.
  • bottom_scripts: This block contains the CDN links to Axios for sending AJAX requests and Vue.js for writing the client-side logic for our video chat application. We also have index.js for our custom JavaScript code.

3. Create the static files

We have index.css for custom styling and index.js, which is our script for handling the call logic.
Run the following command to create the files from your terminal or command prompt:

touch app/static/agora_rtm/index.js
touch app/static/agora_rtm/index.css

Add the following to index.js:

const app = new Vue({
el: "#app",
delimiters: ["${", "}"],
data: {
callPlaced: false,
localStream: null,
mutedAudio: false,
mutedVideo: false,
onlineUsers: [],
isLoggedIn: false,
incomingCall: false,
incomingCaller: "",
incomingCallNotification: "",
rtmClient: null,
rtmChannelInstance: null,
rtcClient: null,
users: [],
updatedOnlineStatus: {},
rtmChannelName: null,
isCallingUser: false,
callingUserNotification: "",
localAudioTrack: null,
localVideoTrack: null,
remoteVideoTrack: null,
remoteAudioTrack: null,
},
mounted() {
this.fetchUsers();
this.initRtmInstance();
},
created() {
window.addEventListener("beforeunload", this.logoutUser);
},
beforeDestroy() {
this.endCall();
this.logoutUser();
},
methods: {
async fetchUsers() {
const { data } = await axios.get("/users");
this.users = data;
},
async logoutUser() {
console.log("destroyed!!!");
this.rtmChannelInstance.leave(AUTH_USER);
await this.rtmClient.logout();
},
async initRtmInstance() {
// initialize an Agora RTM instance
this.rtmClient = AgoraRTM.createInstance(AGORA_APP_ID, {
enableLogUpload: false,
});
// RTM Channel to be used
this.rtmChannelName = "videoCallChannel";
// Generate the RTM token
const { data } = await this.generateToken(this.rtmChannelName);
// Login when it mounts
await this.rtmClient.login({
uid: AUTH_USER,
token: data.rtm_token,
});
this.isLoggedIn = true;
// RTM Message Listeners
this.rtmClient.on("MessageFromPeer", (message, peerId) => {
console.log("MessageFromPeer");
console.log("message: ", message);
console.log("peerId: ", peerId);
});
// Display connection state changes
this.rtmClient.on("ConnectionStateChanged", (state, reason) => {
console.log("ConnectionStateChanged");
console.log("state: ", state);
console.log("reason: ", reason);
});
// Emitted when a Call Invitation is sent from Remote User
this.rtmClient.on("RemoteInvitationReceived", (data) => {
this.remoteInvitation = data;
this.incomingCall = true;
this.incomingCaller = data.callerId;
this.incomingCallNotification = `Incoming Call From ${data.callerId}`;
data.on("RemoteInvitationCanceled", () => {
console.log("RemoteInvitationCanceled: ");
this.incomingCallNotification = "Call has been cancelled";
setTimeout(() => {
this.incomingCall = false;
}, 5000);
});
data.on("RemoteInvitationAccepted", (data) => {
console.log("REMOTE INVITATION ACCEPTED: ", data);
});
data.on("RemoteInvitationRefused", (data) => {
console.log("REMOTE INVITATION REFUSED: ", data);
});
data.on("RemoteInvitationFailure", (data) => {
console.log("REMOTE INVITATION FAILURE: ", data);
});
});
// Subscribes to the online statuses of all users apart from
// the currently authenticated user
this.rtmClient.subscribePeersOnlineStatus(
this.users
.map((user) => user.username)
.filter((user) => user !== AUTH_USER)
);
this.rtmClient.on("PeersOnlineStatusChanged", (data) => {
this.updatedOnlineStatus = data;
});
// Create a channel and listen to messages
this.rtmChannelInstance = this.rtmClient.createChannel(
this.rtmChannelName
);
// Join the RTM Channel
this.rtmChannelInstance.join();
this.rtmChannelInstance.on("ChannelMessage", (message, memberId) => {
console.log("ChannelMessage");
console.log("message: ", message);
console.log("memberId: ", memberId);
});
this.rtmChannelInstance.on("MemberJoined", (memberId) => {
console.log("MemberJoined");
// check whether user exists before you add them to the online user list
const joiningUserIndex = this.onlineUsers.findIndex(
(member) => member === memberId
);
if (joiningUserIndex < 0) {
this.onlineUsers.push(memberId);
}
});
this.rtmChannelInstance.on("MemberLeft", (memberId) => {
console.log("MemberLeft");
console.log("memberId: ", memberId);
const leavingUserIndex = this.onlineUsers.findIndex(
(member) => member === memberId
);
this.onlineUsers.splice(leavingUserIndex, 1);
});
this.rtmChannelInstance.on("MemberCountUpdated", (data) => {
console.log("MemberCountUpdated");
});
},
async placeCall(calleeName) {
// Get the online status of the user.
// For our use case, if the user is not online we cannot place a call.
// We send a notification to the caller accordingly.
this.isCallingUser = true;
this.callingUserNotification = `Calling ${calleeName}...`;
const onlineStatus = await this.rtmClient.queryPeersOnlineStatus([
calleeName,
]);
if (!onlineStatus[calleeName]) {
setTimeout(() => {
this.callingUserNotification = `${calleeName} could not be reached`;
setTimeout(() => {
this.isCallingUser = false;
}, 5000);
}, 5000);
} else {
// Create a channel/room name for the video call
const videoChannelName = `${AUTH_USER}_${calleeName}`;
// Create LocalInvitation
this.localInvitation = this.rtmClient.createLocalInvitation(calleeName);
this.localInvitation.on(
"LocalInvitationAccepted",
async (invitationData) => {
console.log("LOCAL INVITATION ACCEPTED: ", invitationData);
// Generate an RTC token using the channel/room name
const { data } = await this.generateToken(videoChannelName);
// Initialize the agora RTC Client
this.initializeRTCClient();
// Join a room using the channel name. The callee will also join the room then accept the call
await this.joinRoom(AGORA_APP_ID, data.token, videoChannelName);
this.isCallingUser = false;
this.callingUserNotification = "";
}
);
this.localInvitation.on("LocalInvitationCanceled", (data) => {
console.log("LOCAL INVITATION CANCELED: ", data);
this.callingUserNotification = `${calleeName} cancelled the call`;
setTimeout(() => {
this.isCallingUser = false;
}, 5000);
});
this.localInvitation.on("LocalInvitationRefused", (data) => {
console.log("LOCAL INVITATION REFUSED: ", data);
this.callingUserNotification = `${calleeName} refused the call`;
setTimeout(() => {
this.isCallingUser = false;
}, 5000);
});
this.localInvitation.on("LocalInvitationReceivedByPeer", (data) => {
console.log("LOCAL INVITATION RECEIVED BY PEER: ", data);
});
this.localInvitation.on("LocalInvitationFailure", (data) => {
console.log("LOCAL INVITATION FAILURE: ", data);
this.callingUserNotification = "Call failed. Try Again";
});
// set the channelId
this.localInvitation.channelId = videoChannelName;
// Send call invitation
this.localInvitation.send();
}
},
async cancelCall() {
await this.localInvitation.cancel();
this.isCallingUser = false;
},
async acceptCall() {
// Generate RTC token using the channelId of the caller
const { data } = await this.generateToken(
this.remoteInvitation.channelId
);
// Initialize AgoraRTC Client
this.initializeRTCClient();
// Join the room created by the caller
await this.joinRoom(
AGORA_APP_ID,
data.token,
this.remoteInvitation.channelId
);
// Accept Call Invitation
this.remoteInvitation.accept();
this.incomingCall = false;
this.callPlaced = true;
},
declineCall() {
this.remoteInvitation.refuse();
this.incomingCall = false;
},
async generateToken(channelName) {
return await axios.post(
"/agora-rtm/token",
{
channelName,
},
{
headers: {
"Content-Type": "application/json",
"X-CSRFToken": CSRF_TOKEN,
},
}
);
},
/**
* Agora Events and Listeners
*/
initializeRTCClient() {
this.rtcClient = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
},
async joinRoom(appID, token, channel) {
try {
await this.rtcClient.join(appID, channel, token, AUTH_USER);
this.callPlaced = true;
this.createLocalStream();
this.initializeRTCListeners();
} catch (error) {
console.log(error);
}
},
initializeRTCListeners() {
// Register event listeners
this.rtcClient.on("user-published", async (user, mediaType) => {
await this.rtcClient.subscribe(user, mediaType);
// If the remote user publishes a video track.
if (mediaType === "video") {
// Get the RemoteVideoTrack object in the AgoraRTCRemoteUser object.
this.remoteVideoTrack = user.videoTrack;
this.remoteVideoTrack.play("remote-video");
}
// If the remote user publishes an audio track.
if (mediaType === "audio") {
// Get the RemoteAudioTrack object in the AgoraRTCRemoteUser object.term
this.remoteAudioTrack = user.audioTrack;
// Play the remote audio track. No need to pass any DOM element.
this.remoteAudioTrack.play();
}
});
this.rtcClient.on("user-unpublished", (data) => {
console.log("USER UNPUBLISHED: ", data);
// await this.endCall();
});
},
async createLocalStream() {
const [microphoneTrack, cameraTrack] =
await AgoraRTC.createMicrophoneAndCameraTracks();
await this.rtcClient.publish([microphoneTrack, cameraTrack]);
cameraTrack.play("local-video");
this.localAudioTrack = microphoneTrack;
this.localVideoTrack = cameraTrack;
},
async endCall() {
this.localAudioTrack.close();
this.localVideoTrack.close();
this.localAudioTrack.removeAllListeners();
this.localVideoTrack.removeAllListeners();
await this.rtcClient.unpublish();
await this.rtcClient.leave();
this.callPlaced = false;
},
async handleAudioToggle() {
if (this.mutedAudio) {
await this.localAudioTrack.setMuted(!this.mutedAudio);
this.mutedAudio = false;
} else {
await this.localAudioTrack.setMuted(!this.mutedAudio);
this.mutedAudio = true;
}
},
async handleVideoToggle() {
if (this.mutedVideo) {
await this.localVideoTrack.setMuted(!this.mutedVideo);
this.mutedVideo = false;
} else {
await this.localVideoTrack.setMuted(!this.mutedVideo);
this.mutedVideo = true;
}
},
},
});
view raw index.js hosted with ❤ by GitHub

Add the following to index.css:

#video-container {
width: 700px;
height: 500px;
max-width: 90vw;
max-height: 50vh;
margin: 0 auto;
border: 1px solid #099dfd;
position: relative;
box-shadow: 1px 1px 11px #9e9e9e;
background-color: #fff;
}
#local-video {
width: 30%;
height: 30%;
position: absolute;
left: 10px;
bottom: 10px;
border: 1px solid #fff;
border-radius: 6px;
z-index: 2;
cursor: pointer;
}
#remote-video {
width: 100%;
height: 100%;
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 1;
margin: 0;
padding: 0;
cursor: pointer;
}
.action-btns {
position: absolute;
bottom: 20px;
left: 50%;
margin-left: -50px;
z-index: 3;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#btnGroup {
flex-wrap: wrap;
}
view raw index.css hosted with ❤ by GitHub

Breakdown of the Agora Call Page

On the video call page (app/templates/agora_rtm/index.html), we display buttons that bear the name of each registered user and whether they are online or offline at the moment.

Placing a Call

Click the button of the user that you want to call. You see an outgoing call interface with the ability to cancel the call:

Video Call Invitations with Agora RTM and RTC using Vue JS and Flask 1

The callee/recipient gets an incoming call notification where they can either decline or accept the call:

Video Call Invitations with Agora RTM and RTC using Vue JS and Flask 2

Technical Explanation Video

The following video explains the logic for the video call:

4. Set the environment variables in .flaskenv:

FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=
SQLALCHEMY_DATABASE_URI=sqlite:///db.sqlite
SQLALCHEMY_TRACK_MODIFICATIONS=False
TEMPLATES_AUTO_RELOAD=True
SEND_FILE_MAX_AGE_DEFAULT=0
ENABLE_DEBUG=True
AGORA_APP_ID=
AGORA_APP_CERTIFICATE=

Testing

1. Start the Flask development server from your terminal:

flask run

2. Open two different browsers or two instances of the same browser, with one instance in incognito mode, and navigate to the registration page: http://127.0.0.1:5000/register

3. In one of the browsers, create four users by registering four times.

4. Login with the account details that you just created on each of the browsers from the login page: http://127.0.0.1:5000/login

5. Navigate to http://127.0.0.1:5000/agora_rtm.

6. In each of the browsers you opened, the other users registered on the application are displayed.

7. In one browser, you can call an online user by clicking the button that bears their name.

8. The other user is prompted to click the Accept button to fully establish the call.

Video Demonstration of the Video Call

To confirm that your demo is functioning properly, see my demo video as an example of how the finished project should look and function:

Conclusion

The Agora RTM and RTC SDKs give you the ability to build a fully-featured video call application. You can even use the RTM SDK to implement an in-app messaging feature.

While testing, one thing that stood out for me was the reconnection of the call when the internet connectivity on either side of the call failed for a short while.

Online demo link: https://watermark-remover.herokuapp.com/auth/login?next=%2Fagora_rtm

Completed project repository:
https://github.com/Mupati/agora-call-invitation

Note: Make sure the demo link or production version is served over HTTPS and the route is /agora_rtm.

Test accounts:

foo@example.com: DY6m7feJtbnx3ud
bar@example.com: Me3tm5reQpWcn3Q

Other Resources

And I invite you to join the Agora Developer Slack Community

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