根据上回学习了官方TRTC demo之后,已经了解了一个基础的多人会议室创建的流程,接下来我需要将自己学到的转换为自己能够运用的。
这个项目是使用的框架是vue2 + typescript,项目搭建工具使用的是vue-cli。vue-cli搭建项目比webpack方便快捷,还是比较推荐大家使用的。
TRTC是的web sdk是集成了npm的,也有ts版本,在项目中可以使用下面的命令进行安装部署
npm install trtc-js-sdk --save-dev
实现的功能有开闭音视频、监听音量、会议室登录登出等基础功能。
先给大家看看我实现的效果图。
功能是demo基本上差不多,项目ui使用的是element-ui,也是现在用的比较多的UI框架了,想必大家并不陌生。
view目录是我新建的目录,主要是用来存放一些视图展示的vue文件。
1.Client类是一个基础类,主要用于处理一些客户端初始化的方法(不过由于本人设计思想还不到位,这个类其实包含的逻辑就是RTCClient所需要的处理的逻辑);
2.RTCClient和ShareClient这两个类分别是处理音视频通话客户端和屏幕共享客户端的逻辑;
3.Generate类是用来加密生成用户签名的类,处理加密逻辑(在开发过程中,加密逻辑是放在服务器端,一定要注意!我这里为了方便,就放在本地处理了);
4.video-stream-item主要是用来处理单个音视频流的显示和样式逻辑,流的事件监听和播放也是放在这个组件里边;
5.video-stream-list主要功能是管理批量流的组件,控制多个视频的排列逻辑等;
6.trtc-room会议室客户端页面代码。
import TRTC from 'trtc-js-sdk';
import { TRTCMode, AppConfig } from './../enum/mode';
import Generate from './Generate'
/**
* trtc音视频客户端base类
*/
export default class Client {
userId: string; //用户id
userSig: string; //用户签名
roomId: string; //会议室id
isPublished: boolean; //是否已经发布过流
isJoined: boolean; //是否已经加入过房间
localStream: any; //本地流
client: any; //客户端
cameraId: string; //摄像头id
microphoneId: string; //麦克风id
/**
* 用本我思考的是在实例化客户端的时候就把流的视图dom绑定好,但是后面发现这样并不灵活
* 我把流的视图dom绑定放在了video-stream-item里面
*/
// DOMContainer: HTMLDivElement | string;
remoteStreams: Map<String, any>; //远端流的map
constructor(options: {
userId: string,
roomId: string,
cameraId: string,
microphoneId: string,
// DOMContainer: string
}) {
this.userId = options.userId;
this.roomId = options.roomId;
this.cameraId = options.cameraId || '';
this.microphoneId = options.microphoneId || '';
// this.DOMContainer = options.DOMContainer;
const generate = new Generate(this.userId);
this.userSig = generate.getUserSig();
// console.warn(this.userSig)
this.isPublished = false;
this.isJoined = false;
this.localStream = null;
this.remoteStreams = new Map();
this.createClient();
}
createClient() {
/**
* 这里先暂时不实现切换视频设备和麦克风的逻辑,
* 因为切换逻辑一般是在初始化之后再做
*/
let params = {
mode: <any>TRTCMode.VIDEOCALL,
sdkAppId: AppConfig.SDKAPPID,
userId: this.userId,
userSig: this.userSig,
// cameraId: this.cameraId,
// microphoneId: this.microphoneId,
}
// console.warn(params)
this.client = TRTC.createClient(params)
}
/**
* 绑定监听事件
* @param error 客户端报错
* @param clientBanned 客户端被踢出房间
* @param peerJoin 客户端加入房间
* @param peerLeave 客户端离开房间
* @param streamAdded 远程流加入房间,返回boolean,代表要不要订阅
* @param streamSubscribed 订阅远程流
* @param streamRemoved 移除远程流
* @param streamUpdated 远程流更新
*/
handleEvents({
error = _ => {},
clientBanned = _ => {},
peerJoin = _ => {},
peerLeave = _ => {},
streamAdded = _ => { return true },
streamSubscribed = _ => {},
streamRemoved = _ => {},
streamUpdated = _ => {},
unmuteVideo = _ => {},
muteVideo = _ => {},
unmuteAudio = _ => {},
muteAudio = _ => {},
}) {
this.client.on('error', (err: any) => {
console.error(err);
error(err);
location.reload();
})
this.client.on('client-banned', (err: any) => {
alert('您被提出了房间');
clientBanned(err);
location.reload();
})
this.client.on('peer-join', (e: any) => {
peerJoin(e);
console.log(`用户${e.userId}加入了房间`);
})
this.client.on('peer-leave', (e: any) => {
peerLeave(e);
console.log(`用户${e.userId}离开了房间`);
})
this.client.on('stream-added', (e: any) => {
if (!streamAdded(e)) {
return;
}
const remoteStream = e.stream;
const id = remoteStream.getId();
const userId = remoteStream.getUserId();
console.log(`用户${userId}的${remoteStream.getType()}远端流(ID:${id})接入`);
//订阅过的远端流不再订阅
if (this.remoteStreams.has(userId)) return;
this.client.subscribe(remoteStream);
})
this.client.on('stream-subscribed', (e: any) => {
const remoteStream = e.stream;
const id = remoteStream.getId();
const userId = remoteStream.getUserId();
console.log(`用户${userId}的${remoteStream.getType()}远端流(ID:${id})订阅成功`);
this.remoteStreams.set(userId, remoteStream);
streamSubscribed(e);
/**
* 订阅成功之后,不再直接进行play和监听状态操作
*/
// remoteStream.on('player-state-change', e => {
// console.log(`用户${userId}的${e.type}状态改变为${e.state},原因是${e.reason}`)
// });
// remoteStream.play(streamSubscribed(e) || id, { objectFit: 'contain' });
})
this.client.on('stream-removed', (e: any) => {
const remoteStream = e.stream;
const userId = remoteStream.getUserId();
const id = remoteStream.getId();
if (this.remoteStreams.has(userId)) this.remoteStreams.delete(userId);
/**
* 移除操作我们还是可以直接进行的
*/
remoteStream.stop();
streamRemoved(e);
console.log(`stream-removed ID: ${id} type: ${remoteStream.getType()}`);
})
this.client.on('stream-updated', (e: any) => {
const remoteStream = e.stream;
const id = remoteStream.getId();
const userId = remoteStream.getUserId();
console.log('远端流状态更新')
// this.remoteStreams.set(userId, remoteStream);
streamUpdated(e);
})
//有远端流禁用了音频
this.client.on('mute-audio', (e: any) => {
console.log('mute-audio', e)
muteAudio(e)
})
//有远端流开启了音频
this.client.on('unmute-audio', (e: any) => {
console.log('unmute-audio', e)
unmuteAudio(e)
})
//有远端流禁用了视频
this.client.on('mute-video', (e: any) => {
console.log('mute-video', e)
muteVideo(e)
})
//有远端流开启了视频
this.client.on('unmute-video', (e: any) => {
console.log('unmute-video', e)
unmuteVideo(e)
})
}
/**
* 初始化房间
*/
async initRoom() {
if (this.isJoined) {
console.log('客户端已经加入过房间');
return;
}
try {
await this.client.join({
roomId: this.roomId
});
console.log('成功加入聊天室');
this.isJoined = true;
await this.initStream();
} catch(err) {
console.log('加入房间失败', err)
}
}
getLocalStream() {
return this.localStream;
}
/**
* 初始化流
*/
async initStream() {
this.localStream = TRTC.createStream({
audio: true,
video: true,
// cameraId: this.cameraId,
// microphoneId: this.microphoneId,
mirror: true
})
try {
await this.localStream.initialize();
console.log('本地流初始化成功');
this.localStream.on('player-state-change', e => {
console.log(`本地流${e.type},状态改变=>${e.state},原因=>${e.reason}`);
})
await this.publishStream();
// this.localStream.play(this.DOMContainer);
} catch (err) {
console.error('初始化本地流错误', err);
}
}
async publishStream() {
if (!this.isJoined) {
console.warn('本地客户端还没有加入任何聊天室');
return
}
if (this.isPublished) {
console.warn('本地客户端已经发布过音视频流了');
return
}
try {
await this.client.publish(this.localStream);
console.log('本地流发布成功');
this.isPublished = true;
} catch (err) {
this.isPublished = false;
console.error('发布本地流出现错误', err);
}
}
async unpublishStream() {
if (!this.isJoined) {
console.warn('尚未加入房间,无法实行取消流发布');
return;
}
if (!this.isPublished) {
console.warn('尚未发布任何本地流,无法取消发布');
return;
}
await this.client.unpublish(this.localStream);
this.isPublished = false;
}
async leave() {
if (!this.isJoined) {
console.warn('未加入房间,无法执行离开操作')
return;
}
await this.unpublishStream();
await this.client.leave();
this.localStream.stop();
this.localStream.close();
this.localStream = null;
this.isJoined = false;
}
/**
* 恢复播放所有流
* 该方法主要是解决视频无法自动播放的问题
*/
// resumeStreams() {
// this.localStream.resume();
// for (let [userId, stream] of this.remoteStreams) {
// stream.resume();
// }
// }
/**
* 禁止本地音频
*/
muteLocalAudio() {
return this.localStream.muteAudio();
}
/**
* 打开本地音频
*/
unmuteLocalAudio() {
return this.localStream.unmuteAudio();
}
/**
* 禁止本地视频
*/
muteLocalVideo() {
return this.localStream.muteVideo();
}
/**
* 打开本地视频
*/
unmuteLocalVideo() {
return this.localStream.unmuteVideo();
}
}
import Client from './Client';
export default class RTCClient extends Client {
constructor(options: any) {
super(options);
}
}
好尴尬呀,因为设计缺陷,我把逻辑都放在Client类了,还在思考能怎么拆分,有大佬有想法的,可以在评论告知我一些思路。因为我是感觉那些逻辑无论是哪个客户端都需要进行。
import Client from './Client';
import TRTC from 'trtc-js-sdk';
import { TRTCMode, AppConfig } from './../enum/mode';
export class ShareClient extends Client {
constructor(options: any) {
super(options);
}
createClient() {
this.client = TRTC.createClient({
mode: TRTCMode.VIDEOCALL,
sdkAppId: AppConfig.SDKAPPID,
userId: this.userId,
userSig: this.userSig
})
//于普通的客户端不同,共享屏幕的客户端不接收远端流,所以要重写创建客户端方法
this.client.setDefaultMuteRemoteStreams(true);
}
/**
* 共享屏幕的流也需要重新初始化流的方法
*/
async initStream() {
this.localStream = TRTC.createStream({
audio: false,
screen: true,
video: true,
})
try {
this.localStream.initialize();
console.log('本地流初始化成功');
this.localStream.on('player-state-change', e => {
console.log(`本地流${e.type},状态改变=>${e.state},原因=>${e.reason}`);
})
await this.publishStream();
//同理这里也不进行play操作
// this.localStream.play(this.DOMContainer);
} catch (err) {
console.error('初始化本地流错误', err);
}
}
}
<template>
<el-container class="trtc-room">
<el-header class="trtc-room_header" height="44px">
会议室{{roomId}}
<el-button
class="float-right close-btn"
type="primary"
size="small"
@click="closeMeeting">退出会议室
</el-button>
</el-header>
<el-container>
<el-aside class="trtc-room_aside" width="240px">
<video-stream-item
ref="videoStreamItem"
v-if="hasInit"
:user-id="userId"
:show-user-id="false"
:stream='rtcClient.localStream'>
</video-stream-item>
<div class="header-info">
<p>{{userId}}</p>
</div>
<div class="device-control">
<div class="icon-container" @click="changeAudio">
<icon :icon="isMuteAudio ? 'audio' : 'audio-off'" size="large"></icon>
</div>
<div class="icon-container" @click="changeVideo">
<icon :icon="isMuteVideo ? 'video' : 'video-off'" size="large"></icon>
</div>
</div>
</el-aside>
<el-main>
<video-stream-list :streams="remoteStreams" ref="videoStreamList"></video-stream-list>
</el-main>
</el-container>
</el-container>
</template>
<script>
import RTCClient from './js/client/class/RTCClient'
import VideoStreamItem from '@/components/trtc-room/components/video-stream-item'
import VideoStreamList from '@/components/trtc-room/components/video-stream-list'
import Icon from '@/components/icon'
import { Component, Vue, Prop } from 'vue-property-decorator';
@Component({
components: {
RTCClient,
VideoStreamItem,
Icon,
VideoStreamList
}
})
export default class TrtcRoom extends Vue {
rtcClient = null; //客户端
hasInit = false; //是否已经完成初始化
remoteStreams = []; //远端流
isMuteVideo = false; //是否禁用视频
isMuteAudio = false; //是否禁用音频
@Prop() roomId;
@Prop() userId;
created() {
console.log('trtc-room created')
}
async mounted() {
const options = {
userId: this.userId,
roomId: this.roomId,
};
this.rtcClient = new RTCClient(options);
await this.init();
this.hasInit = true;
}
/**
* 切换音频状态
*/
changeAudio() {
const actionFunc = () => {
return this.isMuteAudio
? this.rtcClient.unmuteLocalAudio()
: this.rtcClient.muteLocalAudio();
}
actionFunc()
? this.isMuteAudio = !this.isMuteAudio
: this.$message({
type: 'warning',
message: '没有视频设备'
})
//调用组件修改状态
this.$refs.videoStreamItem.changeAudio(this.isMuteAudio);
}
/**
* 切换视频状态
*/
changeVideo() {
const actionFunc = () => {
return this.isMuteVideo
? this.rtcClient.unmuteLocalVideo()
: this.rtcClient.muteLocalVideo();
}
actionFunc()
? this.isMuteVideo = !this.isMuteVideo
: this.$message({
type: 'warning',
message: '没有视频设备'
})
//调用组件修改状态
this.$refs.videoStreamItem.changeVideo(this.isMuteVideo);
}
/**
* 初始化
*/
async init() {
//监听事件绑定
const events = {
error: this.error,
clientBanned: this.clientBanned,
peerJoin: this.peerJoin,
peerLeave: this.peerLeave,
streamAdded: this.streamAdded,
streamSubscribed: this.streamSubscribed,
streamRemoved: this.streamRemoved,
streamUpdated: this.streamUpdated,
unmuteAudio: this.unmuteAudio,
muteAudio: this.muteAudio,
unmuteVideo: this.unmuteVideo,
muteVideo: this.muteVideo,
}
this.rtcClient.handleEvents(events);
try {
await this.rtcClient.initRoom();
} catch (error) {
console.error(error)
}
}
/**
* 退出会议室
*/
async closeMeeting() {
try {
await this.rtcClient.leave();
this.$router.push({
name: 'Login'
})
} catch (error) {
console.error(error)
}
}
/**
* 更新远端流
*/
updateRemoteStreams() {
let streams = Array.from(this.rtcClient.remoteStreams.values());
this.remoteStreams = streams;
}
error(e) {
console.log(e)
}
clientBanned(e) {
console.log(e)
}
peerJoin(e) {
console.log(e)
}
peerLeave(e) {
console.log(e)
}
streamAdded(e) {
console.log(e);
return true;
}
streamSubscribed(e) {
console.log('订阅到新远端流了', e.userId)
this.updateRemoteStreams();
}
streamRemoved(e) {
console.log('有一位远端流离开了房间', e)
this.updateRemoteStreams();
}
streamUpdated(e) {
console.log(e)
}
muteAudio(e) {
console.log('远端禁用音频', e)
//调用组件修改状态
this.$refs.videoStreamList.changeAudio(e.userId, true);
}
unmuteAudio(e) {
console.log(e)
//调用组件修改状态
this.$refs.videoStreamList.changeAudio(e.userId, false);
}
muteVideo(e) {
//调用组件修改状态
this.$refs.videoStreamList.changeVideo(e.userId, true);
console.log('远端禁用视频', e)
}
unmuteVideo(e) {
//调用组件修改状态
this.$refs.videoStreamList.changeVideo(e.userId, false);
console.log(e)
}
}
</script>
<style lang="less" scoped>
.trtc-room_header {
line-height: 44px;
box-shadow: 0 5px 5px #ddd;
margin-bottom: 10px;
}
.trtc-room_aside {
box-shadow: 5px 0px 5px #ddd;
}
.header-info {
text-align: center;
height: 48px;
line-height: 48px;
border-bottom: 1px solid #ddd;
}
.device-control {
display: flex;
.icon-container {
flex: 1 1 50%;
text-align: center;
padding: 15px;
cursor: pointer;
transition: all .3s ease 0s;
&:hover {
background-color: rgba(221, 221, 221, .3);
}
&:first-child {
border-right: 1px solid #ddd;
}
}
}
.close-btn {
margin-top: 6px;
}
</style>
<style lang="less">
</style>
<template>
<div class="video-play-plane">
<div class="video-play-view" :id="videoId"></div>
<div class="video-play-desc">
<span class="user-name" v-if="showUserId">{{userId}}</span>
<icon
:icon="isMuteAudio ? 'audio-off' : 'audio'"
size="normal"
class="audio-icon">
</icon>
<div class="audio-level" :style="levelStyle"></div>
</div>
</div>
</template>
<script>
import { Vue, Prop, Component } from 'vue-property-decorator';
import Icon from '@/components/icon'
@Component({
components: {
Icon
}
})
export default class VideoStreamItem extends Vue {
@Prop({
required: true,
}) stream
@Prop({
required: true
}) userId
@Prop({
type: Boolean,
default: true
}) showUserId
videoId = 'trtcVideo'; //单个流的id
isMuteVideo = false; //是否禁用了视频
isMuteAudio = false; //是否禁用了音频
audioLevel = 0; //音量等级(0 - 1)
interval = null; //定时器(偶用来监听声音等级变化)
/**
* 计算当前音量样式
*/
get levelStyle() {
const baseHeight = 11;
const baseWidth = 6;
const baseTop = 21;
let realHeight =
!this.isMuteAudio
? baseHeight * this.audioLevel
: 0;
let realTop = baseTop - realHeight;
return {
height: realHeight + 'px',
width: baseWidth + 'px',
top: realTop + 'px'
}
}
created() {
this.videoId = this.videoId + this.stream.getId();
}
mounted() {
//监听当前流的状态
this.stream.on('player-state-changed', e => {
console.log(`用户${this.userId}的${e.type}状态改变为${e.state},原因是${e.reason}`)
//由于浏览器自动播放策略可能会导致视频不会自动播放,需要手动吊起
if (event.state === 'PAUSED') {
this.stream.resume();
}
});
//执行play,流的音视频在页面上展示
this.stream.play(this.videoId);
//设置定时器监听音量变化
this.interval = setInterval(() => {
this.audioLevel = this.stream.getAudioLevel();
//对于等于0.1就相当于有在说话了
if (this.audioLevel >= 0.1) {
console.log(`user ${this.stream.getUserId()} is speaking`);
}
}, 200);
}
destroyed() {
//清楚定时器
clearInterval(this.interval);
}
changeVideo(sign) {
this.isMuteVideo = sign;
}
changeAudio(sign) {
this.isMuteAudio = sign;
}
}
</script>
<style lang="less" scoped>
.video-play-plane {
position: relative;
width: 240px;
height: 180px;
}
.video-play-view {
width: 240px;
height: 180px;
background-color: red;
}
.video-play-desc {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 37px;
padding: 5px;
box-sizing: border-box;
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8));
.user-name {
color: #fff;
}
}
.audio-icon {
float: right;
color: #e8ff26;
position: absolute;
right: 0;
bottom: 5px;
z-index: 1;
}
.audio-level {
background-color: #52c352;
position: absolute;
right: 9.5px;
// top: 210px;
z-index: 0;
}
</style>
<template>
<el-card class="stream-list-container">
<header class="header-title" slot="header">会议成员</header>
<video-stream-item
v-for="stream in streams"
:key="stream.getId()"
:ref="stream.getUserId()"
:user-id="stream.getUserId()"
:stream="stream">
</video-stream-item>
<div class="no-data" v-if="!streams.length">暂无其他参会人员</div>
</el-card>
</template>
<script>
import { Vue, Component, Prop } from 'vue-property-decorator'
import VideoStreamItem from './video-stream-item'
@Component({
components: {
VideoStreamItem
}
})
export default class VideoStreamList extends Vue {
@Prop({
required: true,
type: Array,
default() {
return []
}
}) streams;
changeVideo(userId, sign) {
const ref = this.$refs[userId][0];
if (ref) ref.changeVideo(sign);
}
changeAudio(userId, sign) {
const ref = this.$refs[userId][0];
if (ref) ref.changeAudio(sign);
}
}
</script>
从上手程度上来看,用trtc搭建一个简单的多人聊天室速度基本上比较快,难度不高,基本上能看懂官网的文档就可以直接上手使用了。但是里面有几点要注意一下:
1.在包裹了音视频流播放标签的父标签中设置text-align: center,会导致视频偏移;
2.在client监听的音视频切换(mute-audio、mute-video等四个),都是远端的流的切换事件,目前我还没找到怎么监听本地流;
3.stream上绑定player-state-changed,似乎无法监听到关闭和打开音视频流,不知道是不是我写的有问题,大家可以一起试试。
下一篇文章我会基于这次的项目更新直播模式,实现多人小课堂的模式,大概功能如下:
1.屏幕共享;
3.模拟房管功能;
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。