Back to Blog

Build a Scalable Video Chat App with Agora in Django

Build a Scalable Video Chat App with Agora in Django

Author: Kofi Ocran. I am a software engineer who builds products and services with web technologies. I occupy the sweet spot between software engineering and data science and learn more by sharing my knowledge through technical articles.


Introduction

Django is a high-level Python web framework that takes care of much of the hassle of Web development so that you can focus on writing your app without needing to reinvent the wheel. For its part, Agora takes away the hassle of building a video chat application from scratch.

Previously, I built a video chat app with WebRTC and Laravel and wrote about it here: Adding Video Chat To Your Laravel App. WebRTC is one of the ways through which you can implement video chat features. Companies like Agora provide a fully packaged video chat SDK to provide a high-quality Real-Time Engagement video chat experience. As someone who has WebRTC development experience, I can tell you there are some limitations with WebRTC, such as:

  • Quality of experience: Since WebRTC is transmitted over the Internet, which is a public domain, the quality of experience is hard to guarantee.
  • Scalability: Scalability is fairly limited on group video calls due to the peer-to-peer nature of WebRTC.

After I was introduced to the Agora platform, I was impressed that setting up the same video call feature is easier with the Agora SDK than with WebRTC. I went ahead to build a video chat application with Agora and Laravel. In this article, however, I don’t want Django developers to be left out, so we are going to implement a video chat application with Django and Agora.

Why Agora Is the Preferred Solution

After building a video chat app with Agora, I want to highlight some of the advantages:

  • There’s one SDK for everything — voice, video, live streaming, screen sharing, and so on.
  • I didn’t have to set up a turn server with coturn on Amazon EC2 as I did in the other implementation to relay traffic between peers on different networks.
  • You get 10,000 minutes every month free, and this gives you the flexibility to develop your solution prototype for free.
  • You don’t have the challenge of managing the underlying infrastructure supporting the video call functionality.
  • Intuitive API documentation is available.

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 your Django project directory. We will use mysite as the project name for this tutorial.
  3. Create a new app called agora. Run the following from your terminal:
python manage.py startapp agora

4. Install the necessary packages from your terminal or command prompt:

pip install pusher python-dotenv

5. In the project directory (mysite), run your migrations and create new superusers by running the following command from your terminal:

python manage.py migrate
// run the next command multiple times to create more users
python manage.py createsuperuser

6. Download the AgoraDynamicKey Python 3 code from the Agora repository: AgoraDynamicKey.

Keep the downloaded folder in a location outside the project folder. Some of the files in the folder will be copied into our project when we’re configuring the back end.

Configuring the Back end

We will create the views and classes with the methods needed to generate the Agora token to establish a call. We will set up Pusher at the server-side as well.

1. Add agora to the installed apps in mysite/settings.py

// At the top of the file
import dotenv
dotenv.load_dotenv()
// in the INSTALLED_APPS list
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Your agora app comes here
'agora'
]

2. Add application routes

Create a file named urls.py in the agora directory and add the following code.

From your terminal or command prompt:

touch agora/urls.py

Add the following to agora/urls.py:

from django.urls import path
from . import views
urlpatterns = [
path(' ', views.index, name='agora-index'),
path('pusher/auth/', views.pusher_auth, name='agora-pusher-auth'),
path('token/', views.generate_agora_token, name='agora-token'),
path('call-user/', views.call_user, name='agora-call-user'),
]
view raw urls.py hosted with ❤ by GitHub

Register the Agora app routes at the project level. Add the following code to mysite/urls.py:

from django.contrib import admin
from django.urls import path, include
urlpatterns = [
# Agora Route
path('', include('agora.urls')),
path('admin/', admin.site.urls),
]
view raw urls.py hosted with ❤ by GitHub

3. Add the downloaded AgoraDynamicKey generator files

  • Open your command prompt, and in the agora directory, create a subdirectory named agora_key:
cd agora
mkdir agora_key
  • Copy AccessToken.py and RtcTokenBuilder.py from the src directory in the downloaded files and add them to the agora_key directory:

4. Create the views for the Agora app in agora/views.py

Add the following block of code to the agora/views.py file

import os
import time
import json
from django.http.response import JsonResponse
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from .agora_key.RtcTokenBuilder import RtcTokenBuilder, Role_Attendee
from pusher import Pusher
# Instantiate a Pusher Client
pusher_client = Pusher(app_id=os.environ.get('PUSHER_APP_ID'),
key=os.environ.get('PUSHER_KEY'),
secret=os.environ.get('PUSHER_SECRET'),
ssl=True,
cluster=os.environ.get('PUSHER_CLUSTER')
)
@login_required(login_url='/admin/')
def index(request):
User = get_user_model()
all_users = User.objects.exclude(id=request.user.id).only('id', 'username')
return render(request, 'agora/index.html', {'allUsers': all_users})
def pusher_auth(request):
payload = pusher_client.authenticate(
channel=request.POST['channel_name'],
socket_id=request.POST['socket_id'],
custom_data={
'user_id': request.user.id,
'user_info': {
'id': request.user.id,
'name': request.user.username
}
})
return JsonResponse(payload)
def generate_agora_token(request):
appID = os.environ.get('AGORA_APP_ID')
appCertificate = os.environ.get('AGORA_APP_CERTIFICATE')
channelName = json.loads(request.body.decode(
'utf-8'))['channelName']
userAccount = request.user.username
expireTimeInSeconds = 3600
currentTimestamp = int(time.time())
privilegeExpiredTs = currentTimestamp + expireTimeInSeconds
token = RtcTokenBuilder.buildTokenWithAccount(
appID, appCertificate, channelName, userAccount, Role_Attendee, privilegeExpiredTs)
return JsonResponse({'token': token, 'appID': appID})
def call_user(request):
body = json.loads(request.body.decode('utf-8'))
user_to_call = body['user_to_call']
channel_name = body['channel_name']
caller = request.user.id
pusher_client.trigger(
'presence-online-channel',
'make-agora-call',
{
'userToCall': user_to_call,
'channelName': channel_name,
'from': caller
}
)
return JsonResponse({'message': 'call has been placed'})
view raw views.py hosted with ❤ by GitHub

Breakdown of functions in agora/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 users apart from the currently authenticated user to be rendered on the front end.
  • pusher_auth: It serves as the endpoint for authenticating the logged-in user as they join the Pusher presence channel. The ID and name of the user are returned after successful authentication with the pusher.
  • generate_agora_token: To generate the Agora dynamic token. The token is used to authenticate app users when they join the Agora channel to establish a call.
  • call_user: This triggers a make-agora-call event on the presence-online-channel to which all logged-in users are subscribed.

The data broadcast with the make-agora-call event across the presence-online-channel contains the following:

  • userToCall: This is the ID of the user who is supposed to receive a call from a caller.
  • channelName: This is the call channel that the caller has already joined on the front end. This is a channel created with the Agora SDK on the client side. It is the room the caller has already joined, waiting for the callee to also join to establish a call connection.
  • from: The ID of the caller.

From the make-agora-call event, a user can determine whether they are being called if the userToCall value matches their ID. We show an incoming call notification with a button to accept the call. They know who the caller is by the value of from.

Configuring the Front End

We are going to 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.

Create the HTML file for the index view.

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

In your terminal, navigate to the agora directory and create a templates directory and a agora subdirectory within it.

Create your index.html file in the agora subdirectory:

cd agora
mkdir -p templates/agora
touch templates/agora/index.html

Add the following to the index.htmlfile.

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Build A Scalable Video Chat Application With Agora"
/>
<meta
name="keywords"
content="Video Call, Agora, Django, Real Time Engagement"
/>
<meta name="author" content="Kofi Obrasi Ocran" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
type="text/css"
href="{% static 'agora/index.css' %}"
/>
<script src="https://cdn.agora.io/sdk/release/AgoraRTCSDK-3.3.1.js"></script>
<title>Agora Video Chat Django</title>
</head>
<body>
<main id="app">
<main>
<div class="container">
<div class="row">
<div class="col-12 text-center">
<img
src="{% static 'agora/agora-logo.png' %}"
alt="Agora Logo"
class="block img-fuild"
/>
</div>
</div>
</div>
<div class="container my-5">
<div class="row">
<div class="col">
<div class="btn-group" role="group">
{% for singleUser in allUsers%}
<button
type="button"
class="btn btn-primary mr-2"
@click="placeCall('{{singleUser.id}}','{{singleUser}}')"
>
Call {{ singleUser }}
<span class="badge badge-light"
>${ getUserOnlineStatus({{singleUser.id}})}</span
>
</button>
{% endfor %}
</div>
</div>
</div>
<!-- Incoming Call -->
<div class="row my-5" v-if="incomingCall">
<div class="col-12">
<p>Incoming Call From <strong>${ incomingCaller }</strong></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"></div>
<div id="remote-video"></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>
</main>
</main>
<!-- Add Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/7.0.3/pusher.min.js"></script>
<script>
window.pusher = new Pusher("420e941c25574fda6378", {
authEndpoint: "{% url 'agora-pusher-auth' %}",
auth: {
headers: {
"X-CSRFToken": "{{ csrf_token }}",
},
},
});
const AUTH_USER = "{{user}}"
const AUTH_USER_ID = "{{request.user.id}}"
const CSRF_TOKEN = "{{ csrf_token }}"
</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="{% static 'agora/index.js' %}"></script>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

2. Create the static files

We have index.css for custom styling and index.js, which is our script for handling the call logic.

Add the following to index.js:

const app = new Vue({
el: "#app",
delimiters: ["${", "}"],
data: {
callPlaced: false,
client: null,
localStream: null,
mutedAudio: false,
mutedVideo: false,
userOnlineChannel: null,
onlineUsers: [],
incomingCall: false,
incomingCaller: "",
agoraChannel: null,
},
mounted() {
this.initUserOnlineChannel();
},
methods: {
initUserOnlineChannel() {
const userOnlineChannel = pusher.subscribe("presence-online-channel");
// Start Pusher Presence Channel Event Listeners
userOnlineChannel.bind("pusher:subscription_succeeded", (data) => {
// From Laravel Echo, wrapper for Pusher Js Client
let members = Object.keys(data.members).map((k) => data.members[k]);
this.onlineUsers = members;
});
userOnlineChannel.bind("pusher:member_added", (data) => {
let user = data.info;
// check user availability
const joiningUserIndex = this.onlineUsers.findIndex(
(data) => data.id === user.id
);
if (joiningUserIndex < 0) {
this.onlineUsers.push(user);
}
});
userOnlineChannel.bind("pusher:member_removed", (data) => {
let user = data.info;
const leavingUserIndex = this.onlineUsers.findIndex(
(data) => data.id === user.id
);
this.onlineUsers.splice(leavingUserIndex, 1);
});
userOnlineChannel.bind("pusher:subscription_error", (err) => {
console.log("Subscription Error", err);
});
userOnlineChannel.bind("an_event", (data) => {
console.log("a_channel: ", data);
});
userOnlineChannel.bind("make-agora-call", (data) => {
// Listen to incoming call. This can be replaced with a private channel
if (parseInt(data.userToCall) === parseInt(AUTH_USER_ID)) {
const callerIndex = this.onlineUsers.findIndex(
(user) => user.id === data.from
);
this.incomingCaller = this.onlineUsers[callerIndex]["name"];
this.incomingCall = true;
// the channel that was sent over to the user being called is what
// the receiver will use to join the call when accepting the call.
this.agoraChannel = data.channelName;
}
});
},
getUserOnlineStatus(id) {
const onlineUserIndex = this.onlineUsers.findIndex(
(data) => data.id === id
);
if (onlineUserIndex < 0) {
return "Offline";
}
return "Online";
},
async placeCall(id, calleeName) {
try {
// channelName = the caller's and the callee's id. you can use anything. tho.
const channelName = `${AUTH_USER}_${calleeName}`;
const tokenRes = await this.generateToken(channelName);
// // Broadcasts a call event to the callee and also gets back the token
let placeCallRes = await axios.post(
"/call-user/",
{
user_to_call: id,
channel_name: channelName,
},
{
headers: {
"Content-Type": "application/json",
"X-CSRFToken": CSRF_TOKEN,
},
}
);
this.initializeAgora(tokenRes.data.appID);
this.joinRoom(tokenRes.data.token, channelName);
} catch (error) {
console.log(error);
}
},
async acceptCall() {
const tokenRes = await this.generateToken(this.agoraChannel);
this.initializeAgora(tokenRes.data.appID);
this.joinRoom(tokenRes.data.token, this.agoraChannel);
this.incomingCall = false;
this.callPlaced = true;
},
declineCall() {
// You can send a request to the caller to
// alert them of rejected call
this.incomingCall = false;
},
generateToken(channelName) {
return axios.post(
"/token/",
{
channelName,
},
{
headers: {
"Content-Type": "application/json",
"X-CSRFToken": CSRF_TOKEN,
},
}
);
},
/**
* Agora Events and Listeners
*/
initializeAgora(agora_app_id) {
this.client = AgoraRTC.createClient({ mode: "rtc", codec: "h264" });
this.client.init(
agora_app_id,
() => {
console.log("AgoraRTC client initialized");
},
(err) => {
console.log("AgoraRTC client init failed", err);
}
);
},
async joinRoom(token, channel) {
this.client.join(
token,
channel,
AUTH_USER,
(uid) => {
console.log("User " + uid + " join channel successfully");
this.callPlaced = true;
this.createLocalStream();
this.initializedAgoraListeners();
},
(err) => {
console.log("Join channel failed", err);
}
);
},
initializedAgoraListeners() {
// Register event listeners
this.client.on("stream-published", function (evt) {
console.log("Publish local stream successfully");
console.log(evt);
});
//subscribe remote stream
this.client.on("stream-added", ({ stream }) => {
console.log("New stream added: " + stream.getId());
this.client.subscribe(stream, function (err) {
console.log("Subscribe stream failed", err);
});
});
this.client.on("stream-subscribed", (evt) => {
// Attach remote stream to the remote-video div
console.log("incoming remote stream event: ", evt);
evt.stream.play("remote-video");
this.client.publish(evt.stream);
});
this.client.on("stream-removed", ({ stream }) => {
console.log(String(stream.getId()));
stream.close();
});
this.client.on("peer-online", (evt) => {
console.log("peer-online", evt.uid);
});
this.client.on("peer-leave", (evt) => {
var uid = evt.uid;
var reason = evt.reason;
console.log("remote user left ", uid, "reason: ", reason);
});
this.client.on("stream-unpublished", (evt) => {
console.log(evt);
});
},
createLocalStream() {
this.localStream = AgoraRTC.createStream({
audio: true,
video: true,
});
// Initialize the local stream
this.localStream.init(
() => {
// Play the local stream
this.localStream.play("local-video");
// Publish the local stream
this.client.publish(this.localStream, (err) => {
console.log("publish local stream", err);
});
},
(err) => {
console.log(err);
}
);
},
endCall() {
this.localStream.close();
this.client.leave(
() => {
console.log("Leave channel successfully");
this.callPlaced = false;
},
(err) => {
console.log("Leave channel failed");
}
);
window.pusher.unsubscribe();
},
handleAudioToggle() {
if (this.mutedAudio) {
this.localStream.unmuteAudio();
this.mutedAudio = false;
} else {
this.localStream.muteAudio();
this.mutedAudio = true;
}
},
handleVideoToggle() {
if (this.mutedVideo) {
this.localStream.unmuteVideo();
this.mutedVideo = false;
} else {
this.localStream.muteVideo();
this.mutedVideo = true;
}
},
},
});
view raw index.js hosted with ❤ by GitHub

Add the following to index.css:

main {
margin-top: 50px;
}
#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;
}
view raw index.css hosted with ❤ by GitHub

Breakdown of the Agora Call Page

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

To place a call, we click the button of a user with online status. An online user is a user who is available to receive a call. For our demo, we see a list of users. The user named Bar is indicated as being online. The caller named Foo can call Bar by clicking the button:

Build a Scalable Video Chat App with Agora in Django 1
Buttons displaying the name of all users and their online status

Bar gets an incoming call notification with Accept and Decline buttons and the name of the caller:

Build a Scalable Video Chat App with Agora in Django 2
An incoming call notification

From the call notification image above, we see that the caller’s name is Foo. Bar can then accept the call for a connection to be established.

The following diagram explains the call logic in terms of the code:

Build a Scalable Video Chat App with Agora in Django 3

3. Update env variables with Pusher and Agora keys.

The .env file is located at the root of your project folder. Add the credentials you got from Agora and Pusher:

PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_CLUSTER=
AGORA_APP_ID=
AGORA_APP_CERTIFICATE=

Testing

  1. Start the Django development server from your terminal:
python manage.py runserver

2. Open two different browsers or two instances of the same browser, with one instance in incognito mode, and go to http://127.0.0.1:8000.
3. You are presented with the Django admin login page if you are not already logged in.
4. After successful login, you are taken to the Django admin dashboard. Click the VIEW SITE link at the top right to navigate to the video call page.
5. In each of the browsers you opened, the other users registered on the application are displayed.
6. In one browser, you can call an online user by clicking the button that bears their name.
7. 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: Agora + Django Video Call Demo

Conclusion

You have now implemented the video call feature in your Django application! It’s not that hard, right?

To include video calling functionality in your web app, you don’t have to build it from scratch.

Agora provides a lot of great features out of the box. It also helps businesses save development hours when implementing video chat into existing projects. The only thing a developer has to do is build a compelling front end — Agora handles the video chat back end.

Link to project repository: https://github.com/Mupati/agora-django-video-call
Online Demo link: https://fleet-server.herokuapp.com/agora/login/?next=/agora/dashboard/

Make sure the demo link or production version is served over HTTPS.

Test accounts:
foo: DY6m7feJtbnx3ud
bar: 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