大家好,我是刚入坑TRTC的小菜鸡,黑圆圈云豆。因为我的主要技术方向是web,所以我就从基于web开发的TRTC demo进行学习和知识分享。
获取demo的步骤我还是简要说一下吧,以防有小萌新不知道怎么弄。
1.先在腾讯云官网注册账号;
2.找到实时音视频产品,最简单的办法就是用顶部的搜索框查找;
3.跟着文档步骤一步步来就好,不过要先去认证,然后拿到密钥和appId你才能进行后边的操作。新用户体验有10000分钟的体验时间,如果是自己想测试玩玩,一定要注意控制好时间哦。
获取到的代码目录如下
接下来我主要会讲一下rtc-client.js和share-client.js,主要的TRTC api的应用都在这两个js里边。其它js,我也会大概说一下功能,
1.common.js,这个js主要封装了一些demo会用到的封装方法和全局变量在里边。比如说登录、加入房间、离开房间之类的,有兴趣的朋友可以看看封装思路;
2.index.js可以说是入口js,主要工作是初始化和检测设备等;
3.popper.js主要是用来操作dom的工具方法,里边功能也挺全面的,感兴趣的朋友可以了解了解;
4.presetting.js是一个预设置类,初始化一些数据和btn的事件监听等;
5.lib-generate-test-usersig.min.js在demo里边是用来配合秘钥对用户id进行加密操作,生成用户签名,注意仅适合用在demo里边,在实际开发中加密操作是要放在后台里边的;
6.bootstrap-material-design.js是bootstrap的UI脚本,trtc.js就是TRTC api的核心代码了。
从这个结构里边我们可以看出大概的设计思路和demo会用到的功能。这里主要把一些客户端流和远端流的处理集成在了一个类里边,例如发布本地流、订阅远端流之类的。接下来就贴上代码进行学习。
class RtcClient {
constructor(options) {
this.sdkAppId_ = options.sdkAppId; //获取appId
this.userId_ = options.userId; //获取用户id
/**
* 用户签名
* 目的是保护数据的安全性,因为appId和userId在浏览器里边是可以获取到的
* 而签名是根据appId、秘钥、时间戳等元素进行算法加密生成的,确保了数据的安全性
*/
this.userSig_ = options.userSig;
this.roomId_ = options.roomId; //获取聊天室房间id
this.isJoined_ = false; //当前客户端是否已经加入房间
this.isPublished_ = false; //当前客户端是否已经发布流
this.isAudioMuted = false; //客户端音频是否被禁用
this.isVideoMuted = false; //客户端视频是否被禁用
this.localStream_ = null; //本地流
this.remoteStreams_ = []; //远端流数组(因为是多人聊天室)
this.members_ = new Map(); //成员map,用来映射聊天室成员和对应的流
// 创建一个客户端对象
this.client_ = TRTC.createClient({
mode: 'rtc', //模式,'rtc' 实时通话模式,'live' 互动直播模式
sdkAppId: this.sdkAppId_,
userId: this.userId_,
userSig: this.userSig_
});
this.handleEvents(); //进行事件绑定
}
/**
* 加入聊天室
*/
async join() {
if (this.isJoined_) { //判断是否已经加入过
console.warn('duplicate RtcClient.join() observed');
return;
}
try {
// 加入聊天室
await this.client_.join({
roomId: this.roomId_
});
console.log('join room success');
this.isJoined_ = true; //修改状态
// 获取摄像头和麦克风设备id
// 这里主要是判断/选择额外的音视频设备的,比如双摄像头或者带了耳机之类的
if (getCameraId() && getMicrophoneId()) {
this.localStream_ = TRTC.createStream({ //创建本地流
audio: true, //开启音频
video: true, //开启视频
userId: this.userId_,
cameraId: getCameraId(),
microphoneId: getMicrophoneId(),
mirror: true
});
} else {
// not to specify cameraId/microphoneId to avoid OverConstrainedError
this.localStream_ = TRTC.createStream({
audio: true,
video: true,
userId: this.userId_,
mirror: true
});
}
try {
// 初始化本地流
await this.localStream_.initialize();
console.log('initialize local stream success');
//监听本地流状态
//state: PLAYING(开始播放), PAUSED(暂停播放), STOPPED(停止播放)
//type: video(视频), audio(音频)
//reason(状态变化原因): playing(开始播放), mute(音视频轨道暂时未能提供数据), unmute(音视频轨道恢复提供数据), ended(音视频轨道已被关闭)
this.localStream_.on('player-state-changed', event => {
console.log(`local stream ${event.type} player is ${event.state}`);
});
// 发布本地流
await this.publish();
//在对应div容器进行播放
//该方法会自动在对应容器中生成video或者audio标签,接收的可以是div的容器id也可以是HTMLDivElement对象
this.localStream_.play('main-video');
$('#main-video-btns').show();
$('#mask_main').appendTo($('#player_' + this.localStream_.getId()));
} catch (e) {
console.error('failed to initialize local stream - ' + e);
}
} catch (e) {
console.error('join room failed! ' + e);
}
//获取聊天室内远端用户音视频的mute状态
let states = this.client_.getRemoteMutedState();
for (let state of states) {
if (state.audioMuted) { //音频被禁用
$('#' + state.userId)
.find('.member-audio-btn')
.attr('src', './img/mic-off.png');
}
if (state.videoMuted) { //视频被禁用
$('#' + state.userId)
.find('.member-video-btn')
.attr('src', './img/camera-off.png');
$('#mask_' + this.members_.get(state.userId).getId()).show();
}
}
}
/**
* 离开房间
*/
async leave() {
if (!this.isJoined_) { //是否已经加入了房间
console.warn('leave() - please join() firstly');
return;
}
// 取消本地流的发布
await this.unpublish();
// 离开房间
await this.client_.leave();
this.localStream_.stop(); //停止本地流的播放,同时删除由play方法创建的音视频标签
this.localStream_.close(); //关闭音视频流,同时释放摄像头和麦克风
this.localStream_ = null; //清空本地流对象
this.isJoined_ = false;
resetView(); //重置状态(common.js)
}
/**
* 发布本地流
*/
async publish() {
if (!this.isJoined_) {
console.warn('publish() - please join() firstly');
return;
}
if (this.isPublished_) {
console.warn('duplicate RtcClient.publish() observed');
return;
}
try {
//发布流,该方法需要在join方法调用之后
//一次只允许发布一个流,如果需要改动,需先取消当前的流
await this.client_.publish(this.localStream_);
} catch (e) {
console.error('failed to publish local stream ' + e);
this.isPublished_ = false;
}
this.isPublished_ = true;
}
/**
* 取消当前流的发布
*/
async unpublish() {
if (!this.isJoined_) { //是否加入了房间
console.warn('unpublish() - please join() firstly');
return;
}
if (!this.isPublished_) { //是否发布了本地流
console.warn('RtcClient.unpublish() called but not published yet');
return;
}
//取消本地流的发布
await this.client_.unpublish(this.localStream_);
this.isPublished_ = false;
}
/**
* 禁止本地音频
*/
muteLocalAudio() {
this.localStream_.muteAudio();
}
/**
* 打开本地音频
*/
unmuteLocalAudio() {
this.localStream_.unmuteAudio();
}
/**
* 禁止本地视频
*/
muteLocalVideo() {
this.localStream_.muteVideo();
}
/**
* 打开本地视频
*/
unmuteLocalVideo() {
this.localStream_.unmuteVideo();
}
/**
* 恢复播放所有流
* 该方法主要是解决视频无法自动播放的问题
*/
resumeStreams() {
this.localStream_.resume();
for (let stream of this.remoteStreams_) {
stream.resume();
}
}
/**
* 绑定监听事件
*/
handleEvents() {
//客户端错误监听
this.client_.on('error', err => {
console.error(err);
alert(err);
location.reload();
});
//客户端被踢出房间
this.client_.on('client-banned', err => {
console.error('client has been banned for ' + err);
if (!isHidden()) {
alert('您已被踢出房间');
location.reload();
} else {
document.addEventListener(
'visibilitychange',
() => {
if (!isHidden()) {
alert('您已被踢出房间');
location.reload();
}
},
false
);
}
});
// 远端用户进房通知,只有主动推流的用户能够接收到
this.client_.on('peer-join', evt => {
const userId = evt.userId;
console.log('peer-join ' + userId);
if (userId !== shareUserId) {
addMemberView(userId); //在左侧用户列表添加用户
}
});
// 远端用户离开房间通知
this.client_.on('peer-leave', evt => {
const userId = evt.userId;
removeView(userId);
console.log('peer-leave ' + userId);
});
// 远端流用户发布流通知
this.client_.on('stream-added', evt => {
const remoteStream = evt.stream; //获取到远端流
const id = remoteStream.getId(); //获取流的id
const userId = remoteStream.getUserId(); //获取流的用户id
this.members_.set(userId, remoteStream); //将远端流和用户id加入映射
console.log(`remote stream added: [${userId}] ID: ${id} type: ${remoteStream.getType()}`);
//判断远端流是否是自己的共享屏的流,如果是就不进行订阅操作
//因为demo有共享功能,所以在common.js中有实例化共享客户端对象,shareUserId存的就是自己的共享id
if (remoteStream.getUserId() === shareUserId) {
// don't need screen shared by us
this.client_.unsubscribe(remoteStream);
} else {
console.log('subscribe to this remote stream');
this.client_.subscribe(remoteStream); //订阅远端流
}
});
// 远端流订阅成功通知
this.client_.on('stream-subscribed', evt => {
const uid = evt.userId;
const remoteStream = evt.stream;
const id = remoteStream.getId();
this.remoteStreams_.push(remoteStream); //将订阅到的远端流保存起来
//监听远端流的播放状态
remoteStream.on('player-state-changed', event => {
console.log(`${event.type} player is ${event.state}`);
if (event.type == 'video' && event.state == 'STOPPED') {
$('#mask_' + remoteStream.getId()).show();
$('#' + remoteStream.getUserId())
.find('.member-video-btn')
.attr('src', 'img/camera-off.png');
}
if (event.type == 'video' && event.state == 'PLAYING') {
$('#mask_' + remoteStream.getId()).hide();
$('#' + remoteStream.getUserId())
.find('.member-video-btn')
.attr('src', 'img/camera-on.png');
}
});
addVideoView(id); //在右侧小视频区添加div容器
// objectFit 为播放的填充模式,详细参考:https://trtc-1252463788.file.myqcloud.com/web/docs/Stream.html#play
//将远端流在右侧设置好的容器中进行播放
remoteStream.play(id, { objectFit: 'contain' });
//添加“摄像头未打开”遮罩
let mask = $('#mask_main').clone();
mask.attr('id', 'mask_' + id);
mask.appendTo($('#player_' + id));
mask.hide();
if (!remoteStream.hasVideo()) { //检测远端流是否打开了摄像头
mask.show();
$('#' + remoteStream.getUserId())
.find('.member-video-btn')
.attr('src', 'img/camera-off.png');
}
console.log('stream-subscribed ID: ', id);
});
// 远端流移除事件,当远端流执行unpublish方法的时候触发
this.client_.on('stream-removed', evt => {
const remoteStream = evt.stream;
const id = remoteStream.getId();
remoteStream.stop(); //停止播放远端流,同时移除div中的标签
//更新本地保存的远端流数据
this.remoteStreams_ = this.remoteStreams_.filter(stream => {
return stream.getId() !== id;
});
removeView(id); //移除右侧小视频容器
console.log(`stream-removed ID: ${id} type: ${remoteStream.getType()}`);
});
//远端流的更新事件监听,当远端用户添加、移除或更换音视频轨道后会收到该通知
this.client_.on('stream-updated', evt => {
const remoteStream = evt.stream;
let uid = this.getUidByStreamId(remoteStream.getId()); //通过远端的流id获取本地保存的远端流的uid
if (!remoteStream.hasVideo()) {
$('#' + uid)
.find('.member-video-btn')
.attr('src', 'img/camera-off.png');
}
console.log(
'type: ' +
remoteStream.getType() +
' stream-updated hasAudio: ' +
remoteStream.hasAudio() +
' hasVideo: ' +
remoteStream.hasVideo() +
' uid: ' +
uid
);
});
//监听远端流禁止音频
this.client_.on('mute-audio', evt => {
console.log(evt.userId + ' mute audio');
$('#' + evt.userId)
.find('.member-audio-btn')
.attr('src', 'img/mic-off.png');
});
//监听远端流打开音频
this.client_.on('unmute-audio', evt => {
console.log(evt.userId + ' unmute audio');
$('#' + evt.userId)
.find('.member-audio-btn')
.attr('src', 'img/mic-on.png');
});
//监听远端流禁止视频
this.client_.on('mute-video', evt => {
console.log(evt.userId + ' mute video');
$('#' + evt.userId)
.find('.member-video-btn')
.attr('src', 'img/camera-off.png');
let streamId = this.members_.get(evt.userId).getId();
if (streamId) {
$('#mask_' + streamId).show();
}
});
//监听远端流打开视频
this.client_.on('unmute-video', evt => {
console.log(evt.userId + ' unmute video');
$('#' + evt.userId)
.find('.member-video-btn')
.attr('src', 'img/camera-on.png');
const stream = this.members_.get(evt.userId);
if (stream) {
let streamId = stream.getId();
if (streamId) {
$('#mask_' + streamId).hide();
}
}
});
}
//展示流的设备状态
showStreamState(stream) {
console.log('has audio: ' + stream.hasAudio() + ' has video: ' + stream.hasVideo());
}
//根据流id获取成员映射中的uid
getUidByStreamId(streamId) {
for (let [uid, stream] of this.members_) {
if (stream.getId() == streamId) {
return uid;
}
}
}
}
这个js主要的功能是实现屏幕的共享,大部分内容跟rtc-client.js相同,所以下面我只会列举一些不同点。
class ShareClient {
constructor(options) {
this.sdkAppId_ = options.sdkAppId;
this.userId_ = options.userId;
this.userSig_ = options.userSig;
this.roomId_ = options.roomId;
this.isJoined_ = false;
this.isPublished_ = false;
this.localStream_ = null;
this.client_ = TRTC.createClient({
mode: 'rtc',
sdkAppId: this.sdkAppId_,
userId: this.userId_,
userSig: this.userSig_
});
//设置默认不接收远端流
//因为这个客户端是用来做屏幕共享的,在本地的rtc客户端我们已经接收过远端流了,这里就没必要再进行接收
this.client_.setDefaultMuteRemoteStreams(true);
this.handleEvents(); //绑定事件监听
}
async join() {
if (this.isJoined_) {
console.warn('duplicate RtcClient.join() observed');
return;
}
try {
await this.client_.join({
roomId: this.roomId_
});
console.log('ShareClient join room success');
this.isJoined_ = true;
// create a local stream for screen share
this.localStream_ = TRTC.createStream({
// 不打开音频,因为在rtc客户端已经打开过,如果这里打开,就会在发布两次音频了
audio: false,
// 采集屏幕分享流
screen: true,
userId: this.userId_
});
try {
//初始化并发布本地分享流
//......
} catch (e) {
console.error('ShareClient failed to initialize local stream - ' + e);
//用户取消分享屏幕导致推流失败
await this.client_.leave();
this.isJoined_ = false;
$('#screen-btn').attr('src', 'img/screen-off.png');
}
} catch (e) {
console.error('ShareClient join room failed! ' + e);
}
}
async leave() {...}
handleEvents() {...}
}
我们可以大致总结一下多人会议的实现流程:
1.创建客户端对象TRTC.createClient(),并绑定客户端对远端流的监听事件;
2.加入聊天室,Client.join();
3.创建本地流,TRTC.createStream(),并进行初始化,Stream.initialize();
4.使用客户端发布本地流,Client.publish(Stream),并进行播放,Stream.play();
5.取消本地流的发布Client.unpublish(Stream),客户端离开Client.leave();
6.本地流停止播放Stream.stop(),关闭本地流,释放摄像头和麦克风Stream.close()。
在官方的这个demo里边,已经基本实现了一个多人会议室的功能,结合官方的api我们就可以自己上手做一个多人会议室了。这篇文章知识大概的介绍了一下部分api的功能,推荐大家多去官网看看api的具体参数和使用规则。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。