STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

electron+hooks+ts实现互动直播大班课(一)

最近项目在做直播老师端客户端需求,第一次接触直播的技术,还有比较前言的react技术,值得好好学习学习。
公司用的是声网sdk来实时音视频互动的。

本篇文章概要:

  • 功能介绍
  • 技术方案
  • 实时消息RTM
  • 实时音视频RTC
  • 白板Netless
  • 三个SDK使用及成本
  • 三个SDK初始化流程

1.功能介绍

inclass

大班课场景描述:一名老师在课堂上进行教学,成千上万的学生通过网络实时观看和收听;同时,学生可以举手请求发言,与老师进行实时音视频互动。
该场景在大型网络公开课中应用尤为广泛。

功能点剖析:

  • 实时音视频
  • 实时消息
  • 白板
  • 录制
  • 课堂管理
  • 设备及网络检测
  • 屏幕共享

实时音视频:
教师对学生讲课,学生能实时接收老师的音频和视频。
教学过程中,学生可以举手请求发言,与老师进行互动。
所有学生都可以看到和听到互动学生和老师的画面及声音。

实时消息:
学生和教师在课堂中发送实时文字消息进行互动。

白板:
教师在白板上涂鸦、上传文件(PPT、Word 和 PDF)或播放视频,
有助于提炼教学重点,帮助学生理解或记忆。

录制:
教师将课堂内容录制下来,并即时生成回放链接,方便学生课后复习,
和学校评估教学质量。

课堂管理:
教师控制课堂的开始或结束,并管理学生在上课过程中发送音、视频
和实时消息的权限。

设备及网络检测:
正式上课前,教师可以检测麦克风、摄像头等音视频设备能否正常工作,
同时整个上课过程中,学生和教师都可以实时检测网络质量,确保课堂顺利进行。

屏幕共享:
教师将自己屏幕的内容分享给学生观看,提高教学效果。


2.技术方案

mlive

明确使用的SDK有:
实时消息RTM(agora-rtm-sdk)
实时音视频RTC
Web RTC (agora-rtc-sdk)
Electron RTC (agora-electron-sdk)
互动白板Netless
White Board(white-web-sdk)


RTM 使用流程:
createInstance:创建并返回一个 RtmClient 实例
login:登录 Agora RTM 系统
createChannel:创建 Agora RTM 频道,一个 RtcClient 可以创建多个频道
join:加入 Agora RTM 频道
sendMessage:发送频道消息,成功发送后,频道内所有用户都能收到。
leave:离开 RTM 频道


RTC 使用流程:
createClient:创建客户端
Client.init:初始化客户端对象
Client.setClientRole:设置直播场景下的用户角色,互动直播大班课场景中,我们将老师的用户角色设为主播。
createStream:创建并返回音视频流对象
Stream.init:初始化音视频对象
Client.join:加入 Agora RTC 频道
Client.publish:发布本地音视频流至 SD-RTN
Client.on(“stream-added”):远端音视频已添加
Client.subscribe:订阅远端音视频流
Stream.play:播放音、视频流
Client.leave:离开 RTC 频道


Netless 使用流程:
new WhiteWebSdk:创建白板房间whiteWebSdk实例
joinRoom:调用joinRoom,获得room对象
WhiteWebSdk.on(“onPhaseChanged”):白板房间连接状态发生改变
WhiteWebSdk.on(“onRoomStateChanged”):白板房间状态发生改变
disconnect:离开白板房间

Netless 白板流程图:
netless

想用好SDK,必须对其常用API进行学习和使用,哪些API在哪些场景,哪个生命周期中使用,是必要的。本篇着重解释三个SDK其涉及的方法~


3.实时消息RTM(Real-time Messaging)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 创建rtm实例
const rtmClient = AgoraRTM.createInstance(appID, { enableLogUpload: ENABLE_LOG, logFilter });
// 登录
rtmClient.login({uid, token});
// 创建频道
rtmClient.createChannel(channel);
// 加入频道
rtmClient.join();
// 发送频道消息
rtmClient.sendMessage({text: body}, {enableHistoricalMessaging});
// 离开频道频道
rtmClient.leave()
// 登出
rtmClient.logout();
// 移除所有监听函数
rtmClient.removeAllListeners();
// 查询某指定频道的全部属性
rtmClient.getChannelAttributes(this._currentChannelName)
// 查询单个或多个频道的成员人数
rtmClient.getChannelMemberCount(ids)
// 查询指定用户的在线状态
rtmClient.queryPeersOnlineStatus(ids)
// 添加或更新某指定频道的属性
rtmClient.addOrUpdateChannelAttributes(
this._currentChannelName,
channelAttributes,
{enableNotificationToChannelMembers: true}
);
// 删除某指定频道的指定属性
rtmClient.deleteChannelAttributesByKeys(
this._currentChannelName,
[this._channelAttrsKey],
{enableNotificationToChannelMembers: true}
);

// 连接状态改变的处理逻辑
rtmClient.on("ConnectionStateChanged", (newState: string, reason: string) => {
this._bus.emit("ConnectionStateChanged", {newState, reason});
});
// 收到点对点消息的处理逻辑
rtmClient.on("MessageFromPeer", (message: any, peerId: string, props: any) => {
this._bus.emit("MessageFromPeer", {message, peerId, props});
});

// 收到频道消息的处理逻辑
rtmClient.on('ChannelMessage', (message: string, memberId: string) => {
this._bus.emit('ChannelMessage', {message, memberId});
});

// 收到频道成员加入的处理逻辑
rtmClient.on('MemberJoined', (memberId: string) => {
this._bus.emit('MemberJoined', memberId);
});

// 收到频道成员离开的处理逻辑
rtmClient.on('MemberLeft', (memberId: string) => {
this._bus.emit('MemberLeft', memberId);
});

// 收到频道成员数量更新的逻辑
rtmClient.on('MemberCountUpdated', (count: number) => {
this._bus.emit('MemberCountUpdated', count);
})

// 收到频道属性更新的处理逻辑
rtmClient.on('AttributesUpdated', (attributes: any) => {
this._bus.emit('AttributesUpdated', attributes);
});

想看更多关于RTM的 API,可参考声网官方文档:
Agora RTM JavaScript SDK API 参考:
https://docs.agora.io/cn/Real-time-Messaging/API%20Reference/RTM_web/index.html


4.实时音视频RTC(Real-Time Communication)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const AgoraRtcEngine = require('agora-electron-sdk').default;
const rtcEngine = new AgoraRtcEngine();
// 初始化
rtcEngine.initialize(APP_ID);
// 设置频道场景
rtcEngine.setChannelProfile(1);
// 启用视频模块
rtcEngine.enableVideo();
// 启用音频模块
rtcEngine.enableAudio();
// 桌面端开启与 Web SDK 的互通
rtcEngine.enableWebSdkInteroperability(true);
// 设置本地流的视频编码属性
// 分辨率 640 * 480,帧率 30 fps,码率 750 Kbps
rtcEngine.setVideoProfile(43, false);

// 设置日志文件
rtcEngine.setLogFile(logPath)

// set for preview
// 设置本地视图和渲染器
rtcEngine.setupLocalVideo(dom);
// 设置视窗内容显示模式 哪个用户的流/视频尺寸等比缩放
rtcEngine.setupViewContentMode(streamID, fillContentMode);
// 设置直播场景下的用户角色 1主播
rtcEngine.setClientRole(1);
// 开启视频预览
rtcEngine.startPreview();

// 停止/恢复发送本地视频流
rtcEngine.muteLocalVideoStream(nativeClient.published);
// 停止/恢复发送本地音频流
rtcEngine.muteLocalAudioStream(nativeClient.published);

// 停止视频预览
rtcEngine.stopPreview();
// 设置直播场景下的用户角色 2观众
rtcEngine.setClientRole(2);

// 设置 videoSource 的渲染器
rtcEngine.setupLocalVideoSource(dom);
// 销毁渲染视图
rtcEngine.destroyRenderView(streamID, dom, (err: any) => { console.warn(err.message) });

// 是否启动摄像头采集并创建本地视频流
rtcEngine.enableLocalVideo(false);

想看更多关于RTC的 API,可参考声网官方文档:
Agora Electron SDK API 参考:
https://docs.agora.io/cn/Video/API%20Reference/electron/index.html


5.白板Netless

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { Room, WhiteWebSdk, DeviceType, SceneState, createPlugins, RoomPhase } from 'white-web-sdk';

//初始化 SDK
const whiteWebSdk = new WhiteWebSdk({
appIdentifier: "{{appIdentifier}}"
preloadDynamicPPT: false, // 可选,是否预先加载动态 PPT 中的图片,会显著提升用户体验,降低翻页的图片加载时长
deviceType: "touch", // 可选, touch or desktop , 默认会根据运行环境进行推断
plugins,
loggerOptions: {
disableReportLog: ENABLE_LOG ? false : true,
reportLevelMask: "debug",
printLevelMask: "debug",
}
// ...更多可选参数配置
});

// 引入plugins的地方,集成视频、音频插件
import { videoPlugin } from '@netless/white-video-plugin';
import { audioPlugin } from '@netless/white-audio-plugin';

// createPlugins 方法可以构造出 plugins
const plugins = createPlugins({"video": videoPlugin, "audio": audioPlugin});
// setPluginContext 方法可以设置 plugin 谁可以控制
plugins.setPluginContext("video", {identity: "host"});
// 如果身份是老师填 host 是学生 guest
plugins.setPluginContext("audio", {identity: "host"});

// 初始化完成后,调用joinRoom,获得room对象
const roomParams = {
uuid,
roomToken,
disableBezier: true,
disableDeviceInputs,
disableOperations,
isWritable,
}

const room = await whiteWebSdk.joinRoom(roomParams, {
// 房间连接状态发生改变时
onPhaseChanged: (phase: RoomPhase) => {
if (phase === RoomPhase.Connected) {
this.updateLoading(false);
} else {
this.updateLoading(true);
}
console.log("[White] onPhaseChanged phase : ", phase);
},
// 房间状态发生改变时
onRoomStateChanged: state => {
console.log("onRoomStateChanged", state)
if (state.zoomScale) {
whiteboard.updateScale(state.zoomScale);
}
if (state.sceneState || state.globalState) {
whiteboard.updateRoomState();
}
},
onDisconnectWithError: error => {},
onKickedWithReason: reason => {},
onKeyDown: event => {},
onKeyUp: event => {},
onHandToolActive: active => {},
onPPTLoadProgress: (uuid: string, progress: number) => {},
});

onPhaseChanged:
仅当房间处于connected状态时,房间接受用户教具操作。为了用户体验,推荐对连接中状态进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
export enum RoomPhase {
//正在连接
Connecting = "connecting",
//已连接服务器
Connected = "connected",
//正在重连
Reconnecting = "reconnecting",
//正在断开连接
Disconnecting = "disconnecting",
//连接中断
Disconnected = "disconnected",
}

onRoomStateChanged:
房间状态发生改变时,会返回RoomState发生变化的房间状态字段。
RoomState 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
///Room.d.ts

type RoomState = {
// 全局状态,所有人可读
readonly globalState: GlobalState;
// 房间成员列表
readonly roomMembers: ReadonlyArray<RoomMember>;
// 获取场景状态 [页面(场景)管理]
readonly sceneState: SceneState;
// 用户的教具状态 [教具使用]
readonly memberState: MemberState;
// 主播用户信息 [视角操作]
readonly broadcastState: Readonly<BroadcastState>;
// 切换主播,观众,自由视角模式 [视角操作]
readonly zoomScale: number;
};

想看更多关于Netless的 API,可参考Netless官方文档:
Agora Electron SDK API 参考:
https://developer.herewhite.com/docs/javascript/parameters/js-sdk/


6.三个SDK使用及成本

在一个项目里,同时集成这么多SDK的时候,需要清楚的明白,为什么使用该SDK,每个SDK的使用场景是什么,在什么时候使用每个SDK的成本

6.1 使用场景

RTM:使用它实现实时消息服务,场景是发送频道消息,比如:加入/离开房间、消息聊天、学生举手请求发言、老师操作xxx,影响学生端等功能;
定义一个Message Model:

1
2
3
4
5
6
export type ChatMessage = {
cmd: ChatCmdType, // 消息类型
data: string // cmd为聊天类型时,data为聊天内容,其余为json数据
fromUserId: string // 发送消息的用户id,不可缺省
toUserId?: string // 接收消息的用户id,部分消息可缺省,看具体业务
}

业务场景,目前定义的cmd类型有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export enum ChatCmdType {
chat = 1, // 群聊消息,data表示消息内容
addAnnounce = 2, // 发布公告,data表示消息内容
deleteAnnounce = 3, // 删除公告,data表示公告内容
startCourse = 4, // 开始上课
endCourse = 5, // 结束上课
roomMemberJoin = 6, // 用户进入,data表示用户数据json
roomMemberLeft = 7, // 用户离开
bannedOn = 8, // 开启禁言
bannedOff = 9, // 关闭禁言
videoMajor = 10, // 老师画面出于主区域
videoMinor = 11, // 老师画面出于副区域
studentSendApply = 12, // 学生举手连麦
teacherSendAccept = 13, // 老师同意连麦,包含强制连麦
teacherSendReject = 14, // 老师拒绝连麦
teacherSendStop = 15, // 断开连麦
muteVideo = 16, // 禁用学生视频
unmuteVideo = 17, // 开启学生视频
muteAudio = 18, // 禁用学生音频
unmuteAudio = 19, // 开启学生音频
lockBoard = 20, // 老师锁定白板
unlockBoard = 21, // 老师解锁白板
studentCancelApply = 22, // 学生取消举手连麦
muteAllChat = 23, // 全员禁言
unmuteAllChat = 24, // 取消全员禁言
}

RTC:使用它实现实时音视频互动,场景是传输音视频,比如:老师开始上课,老师和学生进行连麦,学生能看到老师,听到老师的声音等。

NetLess:使用它实现电子上课黑板,场景是老师上课,比如:老师上传的课件资源(PPT、EXCEL、图片、视频、音频等)。


6.2 成本

实时消息RTM成本
每月最高日活跃用户2w以下免费,超过日活2w以上,每多1000人,收100元。
付费成本最低,RTM相当于免费用,只是日活多2w的时候,就得注意了。

rtm-price


实时音视频RTC成本
每月1w分钟的免费时长,超过1w分钟,按照音频每1000分钟7元,视频每1000分钟28元/105元收费,小程序的话更贵。
付费成本算最高的了,每月只有1w分钟可以免费用,超过就开始收费了。

rtc-price.


白板Netless的成本
付费成本还算合理,用多少付多少,价格也便宜。

netless-price


7.三个SDK初始化流程:

老师端:
老师进入房间:
初始化RTM,加入RTM频道;
初始化RTC,不加入RTC频道,点击上课,才开始加入RTC频道;
初始化NetLess,加入NetLess频道。

老师开始上课:
发RTM消息,通知学生上课;
加入RTC频道,开始推送视频流给学生。

老师结束上课:
发RTM消息,通知学生下课;
离开RTC频道。

老师离开房间:
离开RTM频道,离开NetLess频道。


学生端:
学生进入房间:
初始化RTM,加入RTM频道;
初始化RTC,不加入RTC频道,等收到老师发的RTM上课消息,才开始加入RTC频道;
初始化NetLess,不加入NetLess频道,等收到老师发的RTM上课消息,才开始加入NetLess频道。

学生开始上课:
收到老师开始上课的RTM消息,加入RTC频道,开始接收老师视频流。

学生结束上课:
收到老师结束上课的RTM消息,离开RTC频道,断开老师视频流;
收到老师离开RTC频道的消息,离开RTC频道,断开老师视频流。

学生离开房间:
正在上课离开:离开RTM频道、离开RTC频道,离开NetLess频道。
结束上课离开:离开RTM频道、离开NetLess频道。


熟悉完这三个SDK后,一个直播间,其实是由三个小房间组成。完成它们正常的调用逻辑,相当于完成整个项目的30%了。下一篇着重从项目入手,介绍技术栈electron、react hooks、及ts,尽情期待。