基于七牛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>

 

posted @ 2020-04-24 11:41  休耕  阅读(738)  评论(0编辑  收藏  举报