Blog Logo

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

写于2020-06-17 12:49 阅读耗时16分钟 阅读量


最近项目在做直播老师端客户端需求,第一次接触直播的技术,还有比较前言的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)

// 创建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)

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

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状态时,房间接受用户教具操作。为了用户体验,推荐对连接中状态进行处理。

export enum RoomPhase {
    //正在连接
    Connecting = "connecting",
    //已连接服务器
    Connected = "connected",
    //正在重连
    Reconnecting = "reconnecting",
    //正在断开连接
    Disconnecting = "disconnecting",
    //连接中断
    Disconnected = "disconnected",
}

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

///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:

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

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

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,尽情期待。

Headshot of Maxi Ferreira

怀着敬畏之心,做好每一件事。