最近项目在做直播老师端客户端需求,第一次接触直播的技术,还有比较前言的react技术,值得好好学习学习。
公司用的是声网sdk
来实时音视频互动的。
本篇文章概要:
- 功能介绍
- 技术方案
- 实时消息RTM
- 实时音视频RTC
- 白板Netless
- 三个SDK使用及成本
- 三个SDK初始化流程
1.功能介绍
大班课场景描述:一名老师在课堂上进行教学,成千上万的学生通过网络实时观看和收听;同时,学生可以举手请求发言,与老师进行实时音视频互动。
该场景在大型网络公开课中应用尤为广泛。
功能点剖析:
实时音视频
实时消息
白板
录制
课堂管理
设备及网络检测
屏幕共享
实时音视频:
教师对学生讲课,学生能实时接收老师的音频和视频。
教学过程中,学生可以举手请求发言,与老师进行互动。
所有学生都可以看到和听到互动学生和老师的画面及声音。
实时消息:
学生和教师在课堂中发送实时文字消息进行互动。
白板:
教师在白板上涂鸦、上传文件(PPT、Word 和 PDF)或播放视频,
有助于提炼教学重点,帮助学生理解或记忆。
录制:
教师将课堂内容录制下来,并即时生成回放链接,方便学生课后复习,
和学校评估教学质量。
课堂管理:
教师控制课堂的开始或结束,并管理学生在上课过程中发送音、视频
和实时消息的权限。
设备及网络检测:
正式上课前,教师可以检测麦克风、摄像头等音视频设备能否正常工作,
同时整个上课过程中,学生和教师都可以实时检测网络质量,确保课堂顺利进行。
屏幕共享:
教师将自己屏幕的内容分享给学生观看,提高教学效果。
2.技术方案
明确使用的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 白板流程图:
想用好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
| 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();
rtcEngine.enableWebSdkInteroperability(true);
rtcEngine.setVideoProfile(43, false);
rtcEngine.setLogFile(logPath)
rtcEngine.setupLocalVideo(dom);
rtcEngine.setupViewContentMode(streamID, fillContentMode);
rtcEngine.setClientRole(1);
rtcEngine.startPreview();
rtcEngine.muteLocalVideoStream(nativeClient.published);
rtcEngine.muteLocalAudioStream(nativeClient.published);
rtcEngine.stopPreview();
rtcEngine.setClientRole(2);
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
|
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 fromUserId: string toUserId?: string }
|
业务场景,目前定义的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的时候,就得注意了。
实时音视频RTC成本
:
每月1w分钟的免费时长
,超过1w分钟,按照音频每1000分钟7元,视频每1000分钟28元/105元收费,小程序的话更贵。
付费成本算最高的了,每月只有1w分钟可以免费用,超过就开始收费了。
白板Netless的成本
:
付费成本还算合理,用多少付多少,价格也便宜。
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,尽情期待。