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

一、采集和发布本地的track

  采集本地音视频轨这个操作涉及到 2 个模块 —— deviceManager 和 Track

  deviceManager:SDK的媒体设备管理模块,用于监听媒体设备变化及发起采集操作。

  Track:采集方法的返回。Track模式下,所有可在页面上播放的媒体元素,都称为Track对象。

1、发起本地采集的方法

// 采集并发布本地的音视频轨
let screenLocalTracks = await QNRTC.deviceManager.getLocalTracks({
    screen: {enabled: true, tag: "screen"}   // 采集屏幕共享
});

// 采集并发布本地的音频轨
let audioLocalTracks = await QNRTC.deviceManager.getLocalTracks({
    audio: {enabled: true, tag: "audio"},    // 采集音频
});

// 采集并发布本地的视频轨
let videoLocalTracks = await QNRTC.deviceManager.getLocalTracks({
    video: {enabled: true, tag: "video"}     // 采集视频
});

  getLocalTracks 可以重复调用,即时指定的配置项完全相同,每次调用返回的Track也是各自独立的。

2、播放本地音视频轨

  通过如下操作,即可在页面指定元素内播放音频、视频。

// 获取页面上一个元素作为播放画面的父元素
let mainElement = document.getElementById("maintracks");
let localElement = document.getElementById("localtracks");


// 将 Track 列表发布到房间中
await this.myRoom.publish(screenLocalTracks);
// 遍历本地采集的屏幕对象
for (const screenTrack of screenLocalTracks) {
  this.localTracks.push(screenTrack);
  screenTrack.play(mainElement, true);
}
self.localScreenStatus = true;
console.log("发布后我的本地Track", key, this.localTracks);


// 将 Track 列表发布到房间中
await this.myRoom.publish(audioLocalTracks);
// 遍历本地采集的Track对象,不播放自己的音频
for (const audioTrack of audioLocalTracks) {
  this.localTracks.push(audioTrack);
}
self.localAudioStatus = true;
console.log("发布后我的本地Track", key, this.localTracks);


// 将 Track 列表发布到房间中
await this.myRoom.publish(videoLocalTracks);
// 遍历本地采集的Track对象并播放
for (const videoTrack of videoLocalTracks) {
  this.localTracks.push(videoTrack);
  videoTrack.play(localElement, true);
}
console.log("发布后我的本地Track", key, this.localTracks);

  SDK 会自动在 domElement 下创建 <audio> 或者 <video> 元素来播放媒体(使用 audio 还是 video 取决于 Track 本身的 kind)。

3、浏览器自动播放策略及处理

  在没有与用户任何交互的情况下调用play()方法会导致音频无法播放。

  在实际应用场景中,经常需要实现加入房间之后进行自动的发布和订阅。其中自动订阅往往伴随着自动播放。

  浏览器再没有交互操作之前不允许有声音的媒体自动播放,自动播放策略如下:

  • 始终允许静音(muted)的视频自动播放
  • 以下情况允许带声音的自动播放
    • 用户已经在访问的域名下有交互操作
    • *页面可以把autoplay权限委托给iframe,从而允许自动播放声音

  各个浏览器实现:

二、发布/取消本地的Track

  可以看到前面在播放Track前都将这些Track发布到房间*其他人订阅。

1、发布本地Track

// 采集并发布本地的音视频轨
let screenLocalTracks = await QNRTC.deviceManager.getLocalTracks({
    screen: {enabled: true, tag: "screen"}   // 采集屏幕共享
});
// 将 Track 列表发布到房间中
await this.myRoom.publish(screenLocalTracks);

  当一个Track对象经过发布操作后,有2个值的变化需要注意:

  • track.userId:将会标记为加入这个房间用户的 userId
  • track.info.trackId:因为Track发布到房间,会被分配一个这个房间相对其他Track唯一的trackId
  • trackId是房间内所有 Track 的唯一标识,需要指定 Track 的操作都会要求提供 trackId 参数

2、取消发布音视频轨

  由于我需要让用户可以轮流投屏桌面、发布或取消发布自己的声音。因此不使用 Track 的 mute 状态,统一使用取消发布Track。

<script>
  import * as QNRTC from "pili-rtc-web";

  export default {
    name: 'VideoConference',
    methods: {
      // 取消发布
      async unpublish(key) {
        var self = this;
        var tag = '';
        if (key === 'speaker') {
          tag = "screen";
          self.localScreenStatus = false;
        } else if (key === 'voice') {
          self.localAudioStatus = false;
          tag = "audio";
        } else {
          tag = "video";
        }

        for (let i = self.localTracks.length - 1; i >= 0; i--) {
          if (self.localTracks[i].info.tag === tag) {
            // 取消发布
            await self.myRoom.unpublish([self.localTracks[i].info.trackId]);

            // 释放本地资源
            self.localTracks[i].release();
            self.localTracks.splice(i, 1);

            console.log("取消发布后我的本地Track", key, this.localTracks);
          }
        }
      },
    },
  }
</script>

  遍历所有的本地track,查看info.tag信息。判断track类型:screen、voice、audio。

  如果key='speaker'则停止发布screen媒体流。如果是video则停止发布video媒体流。如果是audio则停止发布audio媒体流。

3、销毁本地Track

  由于在七牛的SDK里,退出情况房间或者取消发布并不会销毁本地Track,这些音视频轨也不会被释放。

  如果音视频轨没有及时释放,它们会一直占用摄像头/麦克风等媒体设备。需要用release()方法逐个销毁释放。

  前面取消音视频轨的代码中,就在取消的同时,销毁释放track:

for (let i = self.localTracks.length - 1; i >= 0; i--) {
  if (self.localTracks[i].info.tag === tag) {
    // 取消发布
    await self.myRoom.unpublish([self.localTracks[i].info.trackId]);

    // 释放本地资源
    self.localTracks[i].release();
    self.localTracks.splice(i, 1);

    console.log("取消发布后我的本地Track", key, this.localTracks);
  }
}

  退出房间时也会用到release销毁音视频轨:

// 退出房间
exitMeeting(id) {
  var self = this;
  if (id === 'localtracks') {
    // 销毁释放本地音视频轨
    if (self.localTracks) {
      for (let localTrack of self.localTracks) {
        localTrack.release();
      }
    }
    // 离开房间
    self.myRoom.leaveRoom();
    // 跳转到首页
    window.location.href = '/';
  } else {
    console.log(id);
    // self.kickOut(id);
    self.myRoom.sendCustomMessage('quitRoom', [id]);
  }
},

三、订阅远端的音视频轨

  这一部分官方文档非常简陋。但却是业务实现的核心。

  在前面代码中,创建房间时,首先执行publish函数自动播放自己本地视频流。随后则是启动自动订阅(autoSubscribe),随时订阅到其他加入房间的track。

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

1、由TrackInfo列表获取用户列表

  由myRoom.trackInfoList可以获取到当前房间中的 TrackInfo 列表。

// 获取订阅列表
autoSubscribe(myRoom) {
  let self = this;
  // 加入房间成功后,就可以通过访问myRTC.trackInfoList获取房间当前其他人的TrackInfo
  self.trackInfoList = myRoom.trackInfoList;
  console.log("房间当前音视频轨对象列表!", self.trackInfoList);

  self.userId = myRoom.userId;    // 自己的userId
  self.remoteUserList = [];
  for (var index in self.myRoom.users) {
    if (self.myRoom.users[index].userId !== self.userId) {
      self.remoteUserList.push({
        userId: self.myRoom.users[index].userId,
        isLive: true,
        screenStatus: false,
        audioStatus: false,
        drawStatus: false,
        user: self.myRoom.users[index]
      });
    }
  }
  for (let i = self.remoteUserList.length; i < 20; i++) {
    self.remoteUserList.push({
      userId: '',
      isLive: false,
      screenStatus: false,
      audioStatus: false,
      drawStatus: false,
      user: null
    });
  }
  console.log("房间当前用户", self.remoteUserList);

  myRoom.users:获取的user列表在页面遍历时,会发现存在重复,并包含自己的user信息。因此需要重新构造一个列表。

  但是用v-for在页面遍历时,会发现另一个问题:每次track发生变化,列表都会发生变化,随后均会导致v-for遍历生成的dom销毁重建。一般情况下不影响,但这里dom中包含视频和音频标签,会导致音频或视频丢失。因此用如上方式创建固定长度列表,仅替换元素值,不增减值。

2、订阅远端发布的音视频轨

  如上所示autoSubscribe()方法中,获取到房间中 TrackInfo 列表和用户列表后。订阅房间已经有的所有track。

if (self.trackInfoList.length > 0) {
  // 取出每个 TrackInfo 的 trackId 当作参数发起订阅
  self.subscribe(self.trackInfoList)
    .then(() => console.log("订阅成功!"))
    .catch(e => console.error("订阅失败", e));
}

  官方文档中发起订阅核心示例方法:

// 过滤 tag 为 screen_track 的 TrackInfo
const filterTrackInfoList = trackInfoList.filter(info => info.tag !== "screen_track");

// 取出每个 TrackInfo 的 trackId 当作参数发起订阅
const tracks = await myRoom.subscribe(filterTrackInfoList.map(info => info.trackId));

  文档中返回的 tracks 就是 Track 对象列表,对应相应的 TrackInfo,可以访问 Track 的 info 来查看它的 TrackInfo。

  订阅远端发布的track并用play方法在页面播放:

// 订阅远端发布的音视频轨
// trackInfoList 是一个 trackInfo 的列表,订阅支持多个 track 同时订阅
async subscribe(trackInfoList) {
  // 通过传入 trackId 调用订阅方法发起订阅,成功会返回相应的Track对象,也就是远端的 Track列表
  let remoteTracks = await this.myRoom.subscribe(trackInfoList.map(info => info.trackId));
  console.log('远端Track列表', remoteTracks);

  // 遍历返回远端的Track,调用play方法完成页面播放
  for (const remoteTrack of remoteTracks) {
    // 选择页面上的一个元素作为元素,播放远端的音视频轨
    let mainElement = document.getElementById("maintracks");
    let remoteElement = document.getElementById(remoteTrack.userId);
    let remoteVolElement = document.getElementById(remoteTrack.userId + '_vol');
    // 如果这是麦克风采集的音频Track,则不播放它
    if (remoteTrack.info.tag === "screen") {
      remoteTrack.play(mainElement, true);
    } else if (remoteTrack.info.tag === "video") {
      remoteTrack.play(remoteElement, true);
    } else if (remoteTrack.info.tag === "audio") {
      remoteTrack.play(remoteVolElement, false)
    }
  }
  console.log('调阅后的远端Track列表', remoteTracks);
},

3、取消订阅

  当成功订阅获取 Track 之后,就可以选择这些 Track 来取消订阅了。

  取消订阅操作完成后,SDK会自动释放相应的媒体对象。

// 取消订阅
async unsubscribe(trackInfoList) {
  var self = this;
  // 从刚刚订阅返回的 tracks 中找到视频轨
  let remoteTracks = await self.myRoom.unsubscribe(trackInfoList.map(info => info.trackId));
},

 

  以清除已存在的scream媒体流,自己投屏为例:

if (key === 'speaker') {
  // 清除已存在的screen媒体流
  self.trackInfoList = self.myRoom.trackInfoList;
  for (let item of self.trackInfoList) {
    if (item.tag === "screen") {
      // 自己停止订阅
      self.unsubscribe([item]);
      // 通知对方停止投屏
      self.myRoom.sendCustomMessage('stopScreen', [item.userId]);
    }
  }

 

四、事件监听处理

  TrackModeSession 是 Track 模式下的房间管理模块,所有和房间有关的操作都通过该模块实现。

  官方API文档地址:https://doc.qnsdk.com/rtn/web/docs/api_track_mode_session

  在加入房间时,已经初始化了一个房间TrackModeSession对象:

import * as QNRTC from "pili-rtc-web";

const myRoom = new TrackModeSession();

1、监听新track发布事件

  前面在进入房间时订阅了所有已经存在的track,但是后面再进入房间的客户或者新产生的track,需要实时监听并订阅。

  track-add:可以监听到房间内其他用户发布的Track。

        事件参数:tracks——Array<TrackInfo> 新发布Track的TrackInfo。

// 添加事件监听。当房间出现新的 Track 时触发,参数是 trackInfo 列表
myRoom.on("track-add", trackInfoList => {
  // 房间里有新的track发布
  console.log("Track新增!", trackInfoList);

  self.subscribe(trackInfoList)
    .then(() => console.log("订阅成功!"))
    .catch(e => console.log("订阅失败!", e))
});

 

  在监控到新Track,执行订阅前,可以触发更新房间user列表及执行其他操作,这里省略。

2、监听track取消发布事件

  track-remove:监听到其他用户取消发布了Track。可以和前面的unpublish产生配合。

         事件参数:tracks——Array<TrackInfo>取消发布Track的TrackInfo。

<script>
  export default {
    name: 'VideoConference',

    methods: {
      // 获取订阅列表
      autoSubscribe(myRoom) {

        myRoom.on("track-remove", trackInfoList => {
          // 房间里有 Track 取消发布
          console.log("Track移除", trackInfoList, self.myRoom.users);

          self.unsubscribe(trackInfoList)
            .then(() => console.log("取消订阅成功!"))
            .catch(e => console.log("取消订阅失败!", e))
        });

  在监控到Track取消发布时,执行取消订阅前,同样可以触发更新房间user列表。

3、消息发送和接收

  虽然RTN的SDK中没有介绍,但是它的核心功能是利用webSocket实现。查看源码可以找到消息发送和接收的方法。

(1)发送消息

  这里以发送是否允许投屏为例展示:

// 控制其他用户投屏权限
for (let remoteUser of self.remoteUserList) {
  if (remoteUser.userId === id) {
    if (!remoteUser.screenStatus) {
      // 开启禁止投屏
      self.myRoom.sendCustomMessage('enableScreen', [id]);
      remoteUser.screenStatus = true;
    } else {
      // 取消禁止投屏
      self.myRoom.sendCustomMessage('disableScreen', [id]);
      remoteUser.screenStatus = false;
    }
    return;
  }
}

 

(2)接收消息

  这里以接收投屏消息为例:

myRoom.on("messages-received", trackInfoList => {
  console.log("send-message", trackInfoList);
  if (trackInfoList[0].data === 'enableScreen') {
    // 允许投屏
    self.localScreenDisable = false;
    self.setSpeaker("localtracks");
  } else if (trackInfoList[0].data === 'disableScreen') {
    // 禁止投屏
    self.localScreenDisable = true;
    self.setSpeaker("localtracks");
  }

 

上一篇:【转】回溯思想团灭排列、组合、子集问题


下一篇:3GPP R16 Track Report(2)