Django 中的 Agora 视频聊天应用程序
简介
Django 是一个高级 Python Web 框架,它可以处理 Web 开发中的大部分麻烦,因此你可以只需专注于编写应用程序就可以了。就其本身而言,Agora 消除了从头开始创建视频聊天应用程序的麻烦。
之前,我使用 WebRTC 和 Laravel 创建了一个视频聊天应用程序,点击此处了解相关文章:将视频聊天添加到 Laravel 应用程序。WebRTC 是实现视频聊天功能的一种方式。声网Agora等公司提供了完整的视频聊天 SDK,以提供高质量的实时互动视频聊天体验。作为有 WebRTC 开发经验的人,我可以告诉你 WebRTC 的一些局限性,例如:
- 体验质量: 由于WebRTC是通过互联网传输的,属于公有领域,体验质量难以保证。
- 可伸缩性: 由于 WebRTC 的点对点特性,组视频通话的可伸缩 性十分有限。
介绍完Agora 平台后,使我印象深刻的是,使用 Agora SDK 设置相同的视频通话功能比使用 WebRTC 更容易。我将继续使用Agora 和 Laravel创建视频聊天应用程序。然而,在本文中,我不希望将 Django 开发人员被排除在外,因此我们将使用 Django 和 Agora 开发一个视频聊天应用程序。
为什么首选 Agora
在使用 Agora 创建视频聊天应用程序后,我想强调它自身的一些优势:
- 有一个可以满足所有需求的 SDK ——语音、视频、直播、屏幕共享等等。
- 我不必在 Amazon EC2 上设置带有coturn的 turn 服务器来中继不同网络上的对等点之间的流量。
- 你每月可以免费获得10,000 分钟,这让你可以随意开发解决方案。
- 无需管理支持视频通话功能的底层基础设施。
- 提供直观的API文档。
要求
- Python 3.8.5
- 掌握创建 Django 项目和应用程序的基础知识。以下教程会对你有所帮助:编写你的第一个 Django 应用程序
- Pusher.com上的免费 Pusher 帐户。
- 了解Pusher 在线渠道和Python 服务器库。
- Agora 开发者账号 如何注册并开始使用声网Agora SDK 。
创建项目
- 为此项目创建并激活 Python 3 虚拟环境。
- 打开终端或命令提示符并导航到你的 Django 项目目录。我们将使用mysite 作为本教程的项目名称。
- 创建一个名为agora 的新应用程序。从终端运行以下命令:
python manage.py startapp agora
- 从终端或命令提示符安装必要的包:
pip install pusher python-dotenv
- 在项目目录 ( mysite ) 中,通过从终端运行以下命令来进行迁移并创建新的超级用户:
python manage.py migrate // run the next command multiple times to create more users python manage.py createsuperuser
- 从 Agora 库中下载 AgoraDynamicKey Python 3 代码:AgoraDynamicKey
将下载的文件夹保存在项目文件夹之外的位置。当我们配置后端时,文件夹中的一些文件会复制到我们的项目中。
配置后端
我们将用生成 Agora 令牌建立调用所需的方法创建视图和类。我们还将在服务器端设置 Pusher。
1.在mysite/settings.py中将agora添加到已安装的应用程序中
// 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. 添加应用路由
在agora 目录下创建一个名为urls.py 的文件,从终端或命令提示符中添加以下代码:
touch agora/urls.py
将以下内容添加到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'),
]
在项目级别上注册 Agora 应用路由。添加以下代码到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),
]
3. 添加下载号的 AgoraDynamicKey 生成器文件
- 打开命令提示符,在agora 目录中,创建一个名为agora_key 的子目录:
cd agora
mkdir agora_key
2.从下载文件的SRC 目录中复制AccessToken.py 和RtcTokenBuilder.py ,并将它们添加到agora_key 目录:
4. 在 agora/views.py 中为 Agora 应用创建视图
在agora/views.py 文件中添加以下代码块
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'})
agora/views.py 中的功能分解
- index : 查看视频通话页面。只有经过身份验证的用户才能查看该页面。未经身份验证的用户被重新导航到登录页面。除了要在前端呈现的当前经过身份验证的用户之外,我们返回所有用户的列表。
- pusher_auth :当登录用户加入 Pusher 在线频道时,它充当对登录用户进行身份验证的端点。推送器认证成功后返回用户ID和用户名。
- generate_agora_token : 生成 Agora 动态令牌。该令牌用于在应用用户加入 Agora 频道建立通话时对其进行身份验证。
- call_user :这会在所有登录用户订阅的当前在线频道 上触发一个make-agora-call 事件。
当前在线频道 的make-agora-call 事件直播的数据包含以下内容:
- userToCall :这是应该接收呼叫的用户 ID。
- channelName :这是呼叫者在前端已经加入的呼叫通道。这是客户端使用 Agora SDK 创建的频道。是呼叫者已经加入的房间,等待被接听者也加入并建立呼叫连接。
- from : 呼叫者 ID。
从make-agora-call 事件中,如果 userToCall 值与他们的 ID 匹配,用户可以确定他们是否正在被调用。我们设置一个带有按钮的来电通知来接听电话。他们通过 from 的值知道调用者的身份。
配置前端
我们将创建用于拨打和接听视频通话的用户界面,并能够切换摄像头和麦克风的开启和关闭状态。
1. 为索引视图创建 HTML 文件。
该 HTML 文件将包含指向 Agora SDK、Vue.js、Pusher、Bootstrap 样式以及我们自定义的 CSS 和 JavaScript 的 CDN 的链接。
在你的终端中,导航到agora 目录并在其中创建一个模板 目录和一个agora 子目录。
在agora 子目录中创建index.html 文件:
cd agora
mkdir -p templates/agora
touch templates/agora/index.html
将以下内容添加到index.html 文件中。
{% 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>
2. 创建静态文件
我们有自定义风格的index.css 和index.js ,这是我们处理呼叫逻辑的脚本。
将以下内容添加到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;
}
},
},
});
将以下内容添加到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;
}
Agora 通话页面分解
在视频通话页面(index.html )上,我们显示了带有每个注册用户姓名的按钮,以及他们目前的在线状态。
要想拨打电话,我们单击具有在线状态用户的按钮。在线用户是可以接听电话的用户。在演示过程中,我们会看到一个用户列表,名为Bar 的用户显示为在线,名为Foo 的调用者可以通过单击按钮来调用Bar :
显示所有用户名称及其在线状态的按钮
Bar 收到显示接受和拒绝的按钮以及来电者姓名的来电通知:
来电通知
从上面的来电通知图片中,我们看到来电者的名字是Foo。 然后Bar 可以接受呼叫以建立连接。
下图从代码的角度解释了调用逻辑:
3.使用 Pusher 和 Agora 密钥更新 env 变量。
该**.ENV** 文件位于项目文件夹的根目录。添加你从 Agora 和 Pusher 获得的凭据:
PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_CLUSTER=
AGORA_APP_ID=
AGORA_APP_CERTIFICATE=
测试
- 从终端启动 Django 开发服务器:
python manage.py runserver
-
打开两个不同的浏览器或同一浏览器的两个实例,其中一个实例处于隐身模式,然后转到http://127.0.0.1:8000。
-
如果你还没登录,你将看到 Django 管理员登录页面。
-
成功登录后,你将进入 Django 管理仪表板。单击右上角的查看站点链接以导航到视频通话页面。
-
在你打开的每个浏览器中,都会显示在应用程序上注册的其他用户。
-
在同一个浏览器中,你可以通过单击带有在线用户姓名的按钮来呼叫在线用户。
-
提示另一个用户单击接受按钮以建立呼叫。
视频通话的视频演示
要确认你的操作是否正常运行,可以参阅我的演示视频作为完成项目外观和功能的示例:Agora + Django Video Call Demo
演示视频链接 Agora Django - YouTube
结论
你现在已经在 Django 应用程序中实现了视频通话功能!没那么难,对吧?
要在你的 Web 应用程序中包含视频通话功能,大可不必从头开始创建。
Agora 提供了许多开箱即用的强大功能。它还可以帮助企业在现有项目中实施视频聊天时节省开发时间。开发人员唯一要做的就是创建一个引人注目的前端——Agora 处理视频聊天后端。
项目库的链接:https://github.com/Mupati/agora-django-video-call
在线演示链接:https://fleet-server.herokuapp.com/agora/login/?next=/agora/dashboard/
确保演示链接或生产版本由 HTTPS 提供。
测试账号:
foo: DY6m7feJtbnx3ud
bar: Me3tm5reQpWcn3Q
原文作者 Kofi Obrasi Ocran
原文链接 https://medium.com/@mupati/build-a-scalable-video-chat-app-with-agora-in-django-99dbb8a090f