本书介绍
为什么要写这本书?
2021 年初 ClubHouse 的爆火,让 ClubHouse 的音频服务提供商 Agora.io 出现在了大众眼前。每个人开始打量这样的一个公司和他们的产品。作为一个开发者,我开启了一个针对自己的 Hackthon,要用 72 小时完成一个 ClubHouse 的复刻应用的开发。
72 小时过去,不负众望,我成功的开发出来了复刻版的 ClubHouse ,并将其开源出来 —— NESHouse。
在开发过程中,我自己觉得声网提供的 SDK 很好用,希望将这个产品介绍给更多的人,也就有了想法,想要为开发者写一本 Agora.io 的上手指南。
这本书写于我完成了 NESHouse 和 NESHouse Pro 的第一个版本之后。在完成了两个版本的开发后,我对于 Agora.io 的产品有了更加深入的理解,也有了更多的认识,故而敢试着去写一本电子书,让更多的人可以借此机会了解,并试着使用 Agora.io 来开发出自己的产品。
这本书包含哪些内容?
Agora.io 的产品不少,有语音通话、视频通话、互动直播、极速直播、实时消息/信令等,十分的丰富。但就这本电子书而言,自然无法完全覆盖,这本书将聚焦在我更加熟悉的两个服务 语音通话 和 实时消息/信令 两个方面,如果你希望了解其他产品的信息,则建议你去看 Agora.io 提供的开发者社区和官方文档等信息。
此外,受限于我个人的技术能力所限,这本书涉及的技术栈主要是前端领域,后续可能会补充一些我在 Flutter 上的实践。对于其他语言和技术栈的实践,还建议去到上面的三个社区/文档中了解更加专业的信息。
这本书为谁而写?
这本书为那些想要使用 Agora.io 开发一些自己的应用的前端开发者准备。如果你是其他平台的开发者,也可以参考这本书的逻辑,对照 Agora.io 的文档,理解在自己平台中的逻辑实现。
如果给这本书定位,那么这本书应该是一本「从 0 到 1」的电子书。
如果你已经准备好了,那就点击左侧的目录,来阅读这本书吧!
LICENSE
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
备案号
关于作者
白宦成
工程师,常年以 ID @bestony 混迹于互联网。喜欢为自己打造工具、自动化、工作流。
个人博客: https://www.ixiqin.com
项目
NesHouse 是一个基于 Agora、LeanCloud 服务,使用 Alpine.js 、Bulma Css、NES.css 构建的前端项目,这个项目实现了一套基于 NES 风格的 clubhouse,你可以使用 NESHouse 来创建自己的线上直播间,也可以将其分享出去,邀请别人一起参与讨论。
Agora.io 的价格
作为开发者: 免费使用
Agora.io 为每一位开发者提供了免费的 10,000 分钟的时长,对于开发者来说,你可以大胆的使用 Agora 来完成你的产品测试阶段的开发。赠送的时长对于开发阶段来说,绰绰有余。
作为产品:付费使用,有套餐包
和我们熟悉的其他云计算产品不同,Agora.io 的产品计费是按照分钟来计费的,不同的产品的费用会有所不同,但计算方式是一致的。
Agora.io 采用的计算方式是按频道人数计时,即:如果频道中有 n 人参与通话 m 分钟,则通话总分钟数 = n * m
举例来说,
- 2 个人视频通话 10 分钟,则通话总时长为 2 * 10 = 20 分钟
- 5 个人视频通话 10 分钟,则通话总时长为 5 * 10 = 50 分钟
- 10 个人视频通话 10 分钟,则通话总时长为 10 * 10 = 100 分钟
更多的计费方式的说明,你可以参考其官网的价格页面,这里就不再赘述。
在 Web 应用中接入语音通话能力
Agora.io 在其官网文档中有一些基础的说明,但对于一些具体的细节,我认为描述的并不够清楚,因此这里给出一个 MVP 版本的接入指南。
1. 安装 SDK
根据你的开发经验不同,你可以选择使用包管理器安装 Agora 的 SDK, 或者直接加载 CDN 的 JS 文件来快速接入。
1.1 使用包管理器接入
安装 SDK
npm install agora-rtc-sdk-ng --save
# yarn add agora-rtc-sdk-ng
在项目中引入依赖文件
import AgoraRTC from "agora-rtc-sdk-ng"
1.2 使用 CDN 接入
使用 CDN 接入相对简单,你只需要将如下代码放在你的项目 HTML 中即可
<script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.3.0.js"></script>
2. 接入 Agora
如下这段代码是接入语音聊天的最简代码,你可以先大体看一下,理解一下逻辑,稍后我们详细讲解其中的一些细节。
var rtc = {
// 用来放置本地客户端。
client: null,
// 用来放置本地音频轨道对象。
localAudioTrack: null,
};
var options = {
// 替换成你自己项目的 App ID。
appId: "<YOUR APP ID>",
// 传入目标频道名。
channel: "demo_channel_name",
// 如果你的项目开启了 App 证书进行 Token 鉴权,这里填写生成的 Token 值。
token: null,
};
async function startBasicCall() {
// 初始化代码逻辑,必需
rtc.client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
const uid = await rtc.client.join(options.appId, options.channel, options.token, null);
/**
* 以下逻辑用于订阅频道中的用户音轨,从而可以收听其他用户的声音,必需
*/
rtc.client.on("user-published", async (user, mediaType) => {
// 开始订阅远端用户。
await rtc.client.subscribe(user, mediaType);
console.log("subscribe success");
// 表示本次订阅的是音频。
if (mediaType === "audio") {
// 订阅完成后,从 `user` 中获取远端音频轨道对象。
const remoteAudioTrack = user.audioTrack;
// 播放音频因为不会有画面,不需要提供 DOM 元素的信息。
remoteAudioTrack.play();
}
});
/**
* 以下部分代码用于发布自己的音轨到频道中,一般应用于发言人的逻辑,非必需,根据实际业务需求来完成
*/
// 通过麦克风采集的音频创建本地音频轨道对象。
rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
// 将这些音频轨道对象发布到频道中。
await rtc.client.publish([rtc.localAudioTrack]);
console.log("publish success!");
}
async function leaveCall() {
// 销毁本地音频轨道。
rtc.localAudioTrack.close();
// 离开频道。
await rtc.client.leave();
}
startBasicCall();
leaveCall();
在上述这段代码中,最为核心的是 startBasicCall 中的三块代码,我们按照顺序来看。
第一段代码用于初始化一个 AgoraRTC 的客户端,并使用 Appid、Channel、Token 等信息,加入到频道中,从而获得后续和频道交互的能力。这部分代码为必需的代码,无论你的应用是什么样的,都首先需要完成初始化,才能进行后续的操作。
这里的 token 根据你选择的不同的项目鉴权方式,可以选择性的传递。
rtc.client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
const uid = await rtc.client.join(options.appId, options.channel, options.token, null);
第二段代码则是用于订阅频道音频。在实际开发过程中,无论你的业务是什么样的,每一个人一定都需要先订阅频道,收听其他人的声音。因此,这部分代码也需要放在前面,且必需加入相关代码。
rtc.client.on("user-published", async (user, mediaType) => {
// 开始订阅远端用户。
await rtc.client.subscribe(user, mediaType);
console.log("subscribe success");
// 表示本次订阅的是音频。
if (mediaType === "audio") {
// 订阅完成后,从 `user` 中获取远端音频轨道对象。
const remoteAudioTrack = user.audioTrack;
// 播放音频因为不会有画面,不需要提供 DOM 元素的信息。
remoteAudioTrack.play();
}
});
第三段代码则是发布自己的声音,你可以通过下面的两行代码,实现使用麦克风采集音频,并发布到频道中,从而实现让用户说话的能力。这部分功能在实际的业务开发过程中,会根据不同的业务类型,选择性开启,因此,非必需的,这里放在了最后。
// 通过麦克风采集的音频创建本地音频轨道对象。
rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
// 将这些音频轨道对象发布到频道中。
await rtc.client.publish([rtc.localAudioTrack]);
console.log("publish success!");
3. 实现你自己的业务逻辑
当你完成了上述的代码逻辑后,你就实现了在页面中接入 Agora 的功能,接下来你要做的就是实现自己的业务逻辑即可。
实现用户说话监听
我们在开发应用的时候,会希望标记出哪个用户在发言,这个时候,我们可以通过 Agora 自己提供的 enableAudioVolumeIndicator 方法来实现该功能。
当你完成初始化后,获得了 RTCClient 的实例后, 就可以在该实例上调用 enableAudioVolumeIndicator
方法,并监听volume-indicator
事件,从而获得系统传递的事件。
client.enableAudioVolumeIndicator();
client.on("volume-indicator", volumes => {
volumes.forEach((volume, index) => {
// 此处可以拿到发言用户的 UID 和 level。
// 如果 level >= 5 ,则可以视为该用户正在说话
console.log(`${index} UID ${volume.uid} Level ${volume.level}`);
});
})
在使用时,如果你检测到用户的 Level 超过了 5 ,则可以视为该用户正在发言1
在实际使用的时候,需要注意,该事件每 2 秒传递一次数据1,如果你需要更实时的状态展示,则需要通过其他的方法来完成
相关文档见这里
实现用户静音
使用 setVolume
在实际的开发过程中,我们会希望设定管理员可以禁言/静音某些用户,这个时候我们就可以借助于 setVolume
来控制麦克风的收音音量,来实现类似于静音的效果。
// 静音用户
rtc.localAudioTrack.setVolume(0)
// 恢复正常发言
rtc.localAudioTrack.setVolume(100)
setVolume 的好处是不会重新触发 user-published 事件,相对来说,可以更加实时的表现出静音/取消静音的特质。你可以使用这个功能来完成管理员的强制禁言,或者是用户主动的闭麦。
使用 setEnabled
如果你认为 setVolume 不够安全,则可以使用 setEnabled
来实现静音的效果。setEnabled
可以将本地的轨道关闭,从而实现完全的停止收音。
// 静音用户
rtc.localAudioTrack.setEnabled(false)
// 恢复正常发言
rtc.localAudioTrack.setEnabled(true)
setEnabled 的好处是可以更加完全的关闭音轨,但坏处是当你开启后,会重新触发 user-published
事件1,存在一定的延时。
相关文档查看这里
错误码
Agora.io 的错误码放置在这个页面,你可以在这里搜索
在 Web 应用中接入实时消息能力
在 Agora.io 官方文档中,给出了一个详细的接入说明,但在我看来,对开发者依然不够友好,因此,这里给出一个 MVP 版本的代码,帮助你快速理解你需要在页面中实现的逻辑。
1. 安装 SDK
根据你的开发经验不同,你可以选择使用包管理器安装 Agora 的 SDK, 或者直接加载 CDN 的 JS 文件来快速接入。
1.1 使用包管理器接入
安装 SDK
npm install agora-rtm-sdk --save
# yarn add agora-rtm-sdk
在项目中引入依赖文件
import AgoraRTM from 'agora-rtm-sdk'
1.2 下载 SDK 并接入
不同于 RTC,Agora 并未给 RTM 提供 CDN 版本的 SDK(这里有点奇怪),因此你需要从官网下载页面下载最新版 SDK 的 ZIP 包。
解压 ZIP 包后,你可以从项目中提取出 libs/agora-rtm-sdk-x.y.z.js
文件,并将这个文件放置在你的项目中,使用 script 标签引入。
<script src="/path/to/agora-rtm-sdk-1.4.1.js"></script>
2. 接入 Agora
如下这段代码是接入实时消息的最简代码,你可以先大体看一下,理解一下逻辑,稍后我们详细讲解其中的一些细节。
/*
* 初始化客户端
*/
const client = AgoraRTM.createInstance('<APPID>');
// 监听链接状态变化
client.on('ConnectionStateChanged', (newState, reason) => {
console.log('on connection state changed to ' + newState + ' reason: ' + reason);
});
// 登陆用户,必需
client.login({ token: '<TOKEN>', uid: '<UID>' }).then(() => {
console.log('AgoraRTM client login success');
}).catch(err => {
console.log('AgoraRTM client login failure', err);
});
/*
* 发送点对点消息,非必需
*/
client.sendMessageToPeer(
{ text: 'test peer message' }, // 符合 RtmMessage 接口的参数对象
'<PEER_ID>', // 远端用户 ID
).then(sendResult => {
if (sendResult.hasPeerReceived) {
/* 远端用户收到消息的处理逻辑 */
} else {
/* 服务器已接收、但远端用户不可达的处理逻辑 */
}
}).catch(error => {
/* 发送失败的处理逻辑 */
});
client.on('MessageFromPeer', ({ text }, peerId) => { // text 为消息文本,peerId 是消息发送方 User ID
/* 收到点对点消息的处理逻辑 */
});
/*
* 发送频道消息,非必需
*/
const channel = client.createChannel('<CHANNEL_ID>'); // 此处传入频道 ID
channel.join().then(() => {
/* 加入频道成功的处理逻辑 */
}).catch(error => {
/* 加入频道失败的处理逻辑 */
});
channel.sendMessage({ text: 'test channel message' }).then(() => {
/* 频道消息发送成功的处理逻辑 */
}).catch(error => {
/* 频道消息发送失败的处理逻辑 */
});
channel.on('ChannelMessage', ({ text }, senderId) => { // text 为收到的频道消息文本,senderId 为发送方的 User ID
/* 收到频道消息的处理逻辑 */
});
channel.leave();
client.logout();
上述代码中,可以分为三块:初始化、点对点消息、频道消息。
初始化部分是整个实时消息的基础部分,你需要使用 APPID 来初始化客户端,并登陆用户。
这里的 token 根据你选择的不同的项目鉴权方式,可以选择性的传递。
const client = AgoraRTM.createInstance('<APPID>');
// 登陆用户,必需
client.login({ token: '<TOKEN>', uid: '<UID>' }).then(() => {
console.log('AgoraRTM client login success');
}).catch(err => {
console.log('AgoraRTM client login failure', err);
});
点对点消息可以根据你的实际业务需求来进行接入,如果不需要相关的功能,则可以不编写此部分的代码。
client.sendMessageToPeer(
{ text: 'test peer message' }, // 符合 RtmMessage 接口的参数对象
'<PEER_ID>', // 远端用户 ID
).then(sendResult => {
if (sendResult.hasPeerReceived) {
/* 远端用户收到消息的处理逻辑 */
} else {
/* 服务器已接收、但远端用户不可达的处理逻辑 */
}
}).catch(error => {
/* 发送失败的处理逻辑 */
});
client.on('MessageFromPeer', ({ text }, peerId) => { // text 为消息文本,peerId 是消息发送方 User ID
/* 收到点对点消息的处理逻辑 */
});
频道消息可以根据你的实际业务需求来进行接入,如果不需要相关的功能,则可以不编写此部分代码
const channel = client.createChannel('<CHANNEL_ID>'); // 此处传入频道 ID
channel.join().then(() => {
/* 加入频道成功的处理逻辑 */
}).catch(error => {
/* 加入频道失败的处理逻辑 */
});
channel.sendMessage({ text: 'test channel message' }).then(() => {
/* 频道消息发送成功的处理逻辑 */
}).catch(error => {
/* 频道消息发送失败的处理逻辑 */
});
channel.on('ChannelMessage', ({ text }, senderId) => { // text 为收到的频道消息文本,senderId 为发送方的 User ID
/* 收到频道消息的处理逻辑 */
});
channel.leave();
3. 实现你自己的业务逻辑
当你完成了上述的代码逻辑后,你就实现了在页面中接入 Agora 的功能,接下来你要做的就是实现自己的业务逻辑即可。
实现信令传递与解析
Agora.io 提供的 API 是比较基础的,直接发送文本消息或者是图片消息。对于实际作为控制系统的信令系统,显然是无法满足业务需要的,需要根据自己的实际诉求来设计信令。
根据实际的应用场景,你可以根据你的需要来设计信令,比如我一般使用的有两种。
- JSON
- 简单信令
JSON 信令
JSON 信令顾名思义,将信令转换为 JSON 格式,从而方便进行传递和解析,比如,你可以构建这样的函数用于对信令格式化。
function encode(id, cmd) {
return JSON.stringify({
id, cmd
})
}
function decode(data) {
return JSON.parse(data)
}
则在实际的发送信息和接受消息时,就可以这样处理
// 发送信息
channel.sendMessage({ text: encode(userId,"THIS_IS_COMMAND") }).then(() => {
/* 频道消息发送成功的处理逻辑 */
}).catch(error => {
/* 频道消息发送失败的处理逻辑 */
});
// 接收信息
channel.on('ChannelMessage', ({ text }, senderId) => { // text 为收到的频道消息文本,senderId 为发送方的 User ID
const cmd = decode(text)
// 判断 cmd.cmd 来进行操作。
/* 收到频道消息的处理逻辑 */
});
简单信令
JSON 信令很常用,但问题是在信令传递的过程中,会传递一些无用的数据,比如其间的引号等信息。如果希望信令进一步简化,则可以考虑使用简单信令。
简单信令则是使用不同的符号来切割信令,从而实现最大化的利用信令传递的数据空间。
比如,我们可以定义 ,
用于分隔不同的参数;|
用于分隔不同的信令,则我们可以编写这样的函数来构建信令。
function encode(id,cmd){
return cmd + "," + id
}
function decode(cmd){
return cmd.split(",")
}
则你可以使用 THIS_IS_COMMAND,1
来替代 JSON 信令中的 {"cmd":"THIS_IS_COMMAND","id":1}
,大大的减少了需要传递的信令字符,提升系统的运行效率。
实现退出时发送信令
Agora.io 提供了监听用户退出频道的能力,你只需要监听MemberLeft
事件,就可以监听到用户离开。
但MemberLeft
事件的触发需要满足以下两者中任一条件:
- 用户调用 leave 方法离开频道
- 用户由于网络原因与 Agora RTM 系统断开连接达到 30 秒都会触发此回调
在 Web 浏览器中,有些时候用户的行为我们是无法监测到的,因此,此行为可能会比较缓慢,如果你希望获得一个更高效,更快的退出显示,则可以考虑组织浏览器退出,并发送退出信令给频道。
window.addEventListener("unload", function (event) {
event.preventDefault();
event.returnValue = '';
// 发送退出的信令
});
实现超过 512 人的大型房间
Agroa 的文档中在多处标明,对于 512 人以上的房间,系统的信令将不会触发某些事件,如果你在研发的时候,依赖了该事件,则可能会出现这个问题。
如果想要避免这个问题,则可以考虑通过自建信令的机制,来避免对官方事件的依赖,你只需要在用户触发相应的事件的时候,直接发送相应的信令,来完成该能力的触发。
实现显示用户人数
Agroa.io 提供了 getChannelMemberCount 方法来获取频道中的人数。
不过,你也可以通过监听 MemberCountUpdated 事件来获得频道内的用户人数。后者在频道成员人数 ≤ 512 时,触发频率为每秒 1 次;频道成员人数超过 512 时,触发频率为每 3 秒 1 次。
一些需要注意的点
在实际开发过程中,有一些特殊的点,可能需要你注意一下,它可能和你设想的不太一样。
用户属性
用户属性只能一个个获取,你无法批量获取用户属性,因此,你无法使用它来存储用户的头像、昵称等信息。getMembers 也无法获取到这个属性,你只能获取到用户 ID 的数组。
关于项目
为什么你需要创建多个项目?
Agora.io 中的数据,是以项目为单位来进行组织的,不同的项目,会有不同的 AppID ,在进行计费的时候,也会分配到不同的项目中。
如果你需要在不同的项目中使用 Agora.io 的服务,则建议你创建多个 Project,从而可以更好的区分不同的项目的用量。
项目的不同鉴权方式影响了什么?
Agora.io 的 Project 在创建的时候,会让你选择具体的鉴权模式,你可以选择 APPID 或者 APPID + Token 的限制。前者只需要一个 APPID,就可以连接上声网的服务器,而后者则需要 APP ID 和来自服务端计算的 Token 才能连接上声网的服务器。
相比之下,后者更加安全(因为需要两个因素,且第二个因素来自访问者不可控的服务端),但相应的,后者也存在自己的实现成本问题,你需要在自己的服务端中实现相应的 Token 算法。
更多关于用户校验的说明,你可以参考这个文档
反馈
电子书勘误
关于这本书的任何勘误、补充等信息,你可以选择通过 Github 提交 PR 。
联系作者
你可以发邮件给我,我的邮箱是 bestony@linux.com