• 基于七牛RTN实现多人在线会议或课堂(一)


    一、 七牛实时音视频云介绍

    1、产品架构

      

      客户端SDK:主要负责客户端的音视频采集、渲染、滤镜处理、编解码、传输等工作,客户可以快速集成到自己 App 中,让自己的应用具备音视频通话的能力。 支持 Android、iOS、Web ,集成 SDK 就可实现音视频的采集、编解码、渲染播放等工作。

      服务端REST API和SDK:主要提供房间管理、状态回调等基本的业务功能,另外还提供鉴黄鉴暴、质量分析等配套功能 只需要集成对应语言的服务端 SDK 即可以管理实时音视频互动房间、调用配套数据处理服务、向客户端通知音视频流和数据处理的状态。

    2、信令传输

      实时通话交互流程如下:

      

      因此,服务端需要开发的主要工作如下:

      1.为用户创建通话房间,并将通话房间和对应主播的Id关联起来;

      2.计算加入房间的roomToken并提供给App,该roomToken是结合uerId、roomName等信息使用七牛的AccessKey和SecretKey按照一定的规则生成;

      3.提供通话的业务逻辑,如:通话请求/应答业务逻辑、服务端房间管理和踢人等;

      4.关于roomToken的计算方法及RTC Server API的说明可查阅《七牛实时音视频云服务端 API 接口规范》。

    3、产品相关资料

      RTN Demo体验:https://doc.qnsdk.com/rtn/docs/demo

      七牛实时音视频云web SDK官方地址:https://doc.qnsdk.com/rtn/web

      音视频云接入流程:https://doc.qnsdk.com/rtn/docs/rtn_startup

      web SDK接入文档:https://doc.qnsdk.com/rtn/web/docs/sdk_overview.html

    二、Vue前端项目中开发准备

    1、引入SDK

      这里通过npm引入SDK:

    npm install --save pili-rtc-web

      如果想更新到最新版本或指定版本,运行如下命令:

    npm install --save pili-rtc-web@latest  # 最新版本
    npm install --save pili-rtc-web@2.0.0   # 指定版本 

    2、在单页面应用中引入

    <template>
    </template>
    
    <script>
        import * as QNRTC from "pili-rtc-web";
    
        export default {
            name: 'VideoConference',
        },
        mounted() {
            console.log("current version is", QNRTC.version);
        }
    </script>

      访问对应页面地址,看到打印的 current version 表示引入成功。

    3、关于async/await

      SDK 在对外 API 以及之后的示例中都会默认使用 async/await 的异步方案。

      这里以最常用的异步操作 setTimeout 为例,分别介绍 Promise/Async/Await 这三种异步方案。

      这里编写了 2 个函数,功能都是调用 setTimeout 然后 1s 后执行相关代码。前者是最常用的 Callback 模式,后者是一个 Promise:

    const setTimeoutCallback = (callback) => {
      setTimeout(callback, 1000);
    }
    
    const setTimeoutPromise = () => new Promise(resolve => {
      setTimeout(resolve, 1000);
    })

      然后利用上面的函数分别用三种异步方案实现一个需求——每隔一秒钟打印一行字,重复 3 次。

    (1)callback方案

    setTimeoutCallback(() => {
      console.log("text!");
      setTimeoutCallback(() => {
        console.log("text!");
        setTimeoutCallback(() => {
          console.log("text!");
        });
      });
    });

    (2)promise方案

    // promise
    setTimeoutPromise()
      .then(() => {
        console.log("text!");
        return setTimeoutPromise();
      }).then(() => {
        console.log("text!");
        return setTimeoutPromise();
      }).then(() => {
        console.log("text!");
      });

    (3)async/await方案

    // async/await
    (async () => {
      await setTimeoutPromise();
      console.log("text!");
      await setTimeoutPromise();
      console.log("text!");
      await setTimeoutPromise();
      console.log("text!");
    })();

      在这种场景下 async/await 的写法会更加简洁优雅。在七牛的 SDK 中,大部分都是这种串行异步的场景,也推荐使用 async/await 的写法。

    4、Track模式和Stream模式对比

      Web 端选择封装了两套模式的 API 供用户选择。一套为 Stream 模式,也就是用户和流一对一关系的模式。一套为 Track 模式,也就是用户和 Track 一对多关系的模式。

      需要注意的是,这 2 个模式只有在 Web SDK 下 的 API 中有区别,在实际实现上,都是通过以 Track 为单位操作流实现的。只是在 Stream 模式下,只允许用户发布至多一个音频 Track 和至多一个视频 Track。

    (1)什么情况使用Track模式

      需求场景满足以下情况时,优先使用Track模式:

    • 不清楚需求边界的情况
    • 需要一个用户同时发布多路视频画面或多路音频画面
    • 用户订阅时需要有动态订阅逻辑,即纯音频订阅、纯视频订阅等
    • 虽同时刻只有一个视频画面,但有画面切换的需求

    (2)什么情况使用Stream模式

      需求场景满足以下情况时,优先使用Stream模式:

    • 明确需求中一个用户最多只发布一路视频和一路音频
    • 没有动态订阅需求,远端发布什么就订阅什么
    • 老版本用户希望升级v2

    三、房间管理

      由于我需要实现多人在线会议场景,因此必须选用Track模式实现。

    1、从后端获取token

      SDK通过传入RoomToken来完成加入房间的。这个RoomToken是包含连麦所需要的主要信息:七牛的账户标识、连麦的应用 ID(appId)、连麦的房间号 (roomName)、连麦的用户名(userId)、有效期等。

    <template>
    </template>
    
    <script>
      import * as QNRTC from "pili-rtc-web";
    
      export default {
        name: 'VideoConference',
        data() {
          return {
    
          }
        },
        methods: {
          httpGetList: function () {
            var self = this;
            this.$httpGet(this.$http, "users/teachingprocController/getClassroomInfo", this.$trimJson(self.queryInfo), function (ret) {
              if (ret.currentTimetableInfo != undefined) {
                self.queryInfo = ret;
                self.queryInfo.currentStudentList = [];
                var timenow = Date.parse(new Date());
                if (timenow >= ret.currentTimetableInfo.endTime || ret.currentTimetableInfo.status == 3) {
                  self.classroomType = 2;  //2 表示直播已结束
                } else if (timenow > ret.currentTimetableInfo.beginTime && timenow < ret.currentTimetableInfo.endTime) {
                  self.classroomType = 0;  //0 表示正在直播
                  self.token = ret.currentTimetableInfo.realParam;
                  self.joinRoom(self.token);
                } else if (timenow < ret.currentTimetableInfo.beginTime) {
                  self.classroomType = 1;  //1 表示尚未开始直播
                  //为尚未开始直播时,开启倒计时定时器
                  if (!!self.counterTimer) {
                    clearInterval(self.counterTimer);
                    self.counterTimer = setInterval(self.handleCounter, 1000);
                  } else {
                    self.counterTimer = setInterval(self.handleCounter, 1000);
                  }
                }
              } else {
                self.classroomType = 3;  // 3 表示点播课堂
              }
            });
          },
        created() {
          let self = this;
          this.currentUser = this.$sessionUser.fetch();// 获取url中携带的值
          if (Object.keys(this.$route.query).length > 0) {
            if (this.$route.query.classroomno != undefined) {
              this.queryInfo.classroomno = this.$route.query.classroomno;
            }
            if (this.$route.query.timetableno != undefined) {
              this.queryInfo.timetableno = this.$route.query.timetableno;
            }
            // 获取token
            this.httpGetList();
          }
        },
      }
    </script>

      在用户跳转到该页面时,created在实例创建完成后被立即调用,根据url携带的值触发httpGetList函数,根据课程类型执行joinRoom方法。

    2、实例化全局房间session对象

      加入房间之前,需要实例化一个全局 Session 对象。之后所有和房间相关的操作都会通过调用这个对象的方法来实现。

    import * as QNRTC from "pili-rtc-web";
    
    const myRoom = new QNRTC.TrackModeSession();

    3、加入房间

      通过前面获取的token加入房间。

        methods: {
          // 创建房间
          async joinRoom(token) {
            // 初始化一个房间Session对象,这里使用Track模式
            const myRoom = new QNRTC.TrackModeSession();
            this.myRoom = myRoom;
            // 使用 RoomToken加入房间
            await myRoom.joinRoomWithToken(token);
            // 自动加载视频
            this.publish();
            this.autoSubscribe(myRoom);
          },
        }

    4、离开房间

      离开房间后 SDK 会自动和房间断开连接并销毁所有订阅音视频对象,但不会清理采集到的音视频对象。

      如果离开房间后想再次加入房间,重新调用加入房间的方法。

    <script>
      import * as QNRTC from "pili-rtc-web";
    
      export default {
        name: 'VideoConference',
        //
        beforeDestroy() {
          // 销毁释放本地track
          for (let localTrack of this.localTracks) {
            localTrack.release();
          }
          // 离开房间
          this.myRoom.leaveRoom();
        }
      }
    </script>

      监听窗口关闭事件,在每次页面即将被关闭或刷新时自动离开房间。

      配合浏览器的 onbeforeunload 事件在每次页面即将被关闭或者刷新时自动离开房间。

    <script>
      import * as QNRTC from "pili-rtc-web";
    
      export default {
        name: 'VideoConference',
        methods: {
          beforeunloadHandler(){
            this._beforeUnload_time=new Date().getTime();
            // 销毁释放本地track
            for (let localTrack of this.localTracks) {
              localTrack.release();
            }
            // 离开房间
            this.myRoom.leaveRoom();
          }
        },
        mounted() {
          //
          // 监听窗口关闭事件
          window.addEventListener('beforeunload', e => this.beforeunloadHandler(e))
        },
      }
    </script>

    5、踢人

      以管理员身份签发的RoomToken加入,可以通过如下方法强制将其他用户踢出房间。

    await myRoom.kickoutUser("USERID");   // 暂时没用到

    四、页面模板设计

    1、页面设计目标

      

    2、模板结构

    <template>
      <div class="meeting">
        <div class="meeting-wraper">
          <div class="meeting-header">
            <h4>多人在线视频直播</h4>
          </div>
          <div class="meeting-content">
            <div class="main-meeting">
              <div id="maintracks" class="maintracks" v-show="!whitePadStatus"></div>
              </div>
              <div class="meeting-member">
                <!-- 其他人的头像 -->
                <div class="meeting-item" v-for="(remoteUser, index) in remoteUserList" :key="index"
                     v-show="remoteUser.isLive">
                  <div :id="remoteUser.userId"></div>
                  <div :id="remoteUser.userId + '_vol'" style="visibility:hidden;"></div>
                  <div class="btn-box" v-if="currentUser.userType == 1">
                    <button @click="setSpeaker(remoteUser.userId)">
                      <span v-if="remoteUser.screenStatus" key="1" class="iconfont iconjinzhitouping"></span>
                      <span v-else key="2" class="iconfont iconzhibo"></span>
                    </button>
                    <button @click="setAudio(remoteUser.userId)">
                      <!-- 有声音,显示静音图标 -->
                      <span v-if="!remoteUser.audioStatus" key="1" class="iconfont iconjingyin"></span>
                      <!-- 无声音,显示声音图标 -->
                      <span v-else key="2" class="iconfont iconyingliang"></span>
                    </button>
                    <button @click="setWhitePad(remoteUser.userId)">
                      <!-- 禁止使用画板绘画 -->
                      <span v-if="!remoteUser.drawStatus" key="1" class="iconfont iconsousuo"></span>
                      <!-- 默认不能使用画板绘画,授权可以使用画板绘画 -->
                      <span v-else key="2" class="iconfont iconzu1"></span>
                    </button>
                    <button @click="exitMeeting(remoteUser.userId)">
                      <span class="iconfont icontuichu"></span>
                    </button>
                  </div>
                </div>
              </div>
            </div>
            <div class="meeting-list">
              <!-- 自己的头像 -->
              <div class="self_wraper">
                <div id="localtracks" class="self_camera"></div>
                <div id="local_vol_tracks" style="visibility:hidden;"></div>
                <div class="btn-box">
                  <button @click="setSpeaker('localtracks')" :disabled="localScreenDisable">
                    <span v-if="localScreenStatus" key="1" class="iconfont iconjinzhitouping"></span>
                    <span v-else class="iconfont iconzhibo"></span>
                  </button>
                  <button @click="setAudio('localtracks')" :disabled="localAudioDisable">
                    <!-- 有声音,显示静音图标 -->
                    <span v-if="!localAudioStatus" key="1" class="iconfont iconjingyin"></span>
                    <!-- 无声音,显示声音图标 -->
                    <span v-else key="2" class="iconfont iconyingliang"></span>
                  </button>
                  <button @click="setWhitePad('localtracks')">
                    <span class="iconfont iconzu1"></span>
                  </button>
                  <button @click="exitMeeting('localtracks')"><span class="iconfont icontuichu"></span></button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>
  • 相关阅读:
    【训练题】最优比率生成树 P1696
    2019/9/15 校内模拟赛 考试报告
    b 解题报告
    HDU4714 Tree2cycle 解题报告
    2019/9/2 校内练习赛 考试报告
    2019/8/31 校内模拟赛 考试报告
    2019/8/29 校内模拟赛 考试报告
    康托展开
    洛谷P3807卢卡斯定理
    矩阵
  • 原文地址:https://www.cnblogs.com/xiugeng/p/12761607.html
Copyright © 2020-2023  润新知