基于七牛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,从而允许自动播放声音
各个浏览器实现:
- Chrome:Autoplay Policy Changes
- Safari:Auto-Play Policy Changes for macOS
- FireFox:Allow or block media autoplay in Firefox
二、发布/取消本地的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"); }