前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TRTC学习之旅(一)--多人聊天室web篇(官方demo)

TRTC学习之旅(一)--多人聊天室web篇(官方demo)

原创
作者头像
黑眼圈云豆
修改2020-06-24 14:47:32
4.4K0
修改2020-06-24 14:47:32
举报

大家好,我是刚入坑TRTC的小菜鸡,黑圆圈云豆。因为我的主要技术方向是web,所以我就从基于web开发的TRTC demo进行学习和知识分享。

获取demo

获取demo的步骤我还是简要说一下吧,以防有小萌新不知道怎么弄。

1.先在腾讯云官网注册账号;

2.找到实时音视频产品,最简单的办法就是用顶部的搜索框查找;

3.跟着文档步骤一步步来就好,不过要先去认证,然后拿到密钥和appId你才能进行后边的操作。新用户体验有10000分钟的体验时间,如果是自己想测试玩玩,一定要注意控制好时间哦。

demo代码分析

获取到的代码目录如下

demo结构
demo结构
demo首页
demo首页
音视频页面
音视频页面

接下来我主要会讲一下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里边,在实际开发中加密操作是要放在后台里边的;

GenerateTestUserSig.js中获取用户签名
GenerateTestUserSig.js中获取用户签名

6.bootstrap-material-design.js是bootstrap的UI脚本,trtc.js就是TRTC api的核心代码了。

rtc-client.js

rtc-client代码结构
rtc-client代码结构

从这个结构里边我们可以看出大概的设计思路和demo会用到的功能。这里主要把一些客户端流和远端流的处理集成在了一个类里边,例如发布本地流、订阅远端流之类的。接下来就贴上代码进行学习。

代码语言:javascript
复制
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;
      }
    }
  }
}

share-client.js

这个js主要的功能是实现屏幕的共享,大部分内容跟rtc-client.js相同,所以下面我只会列举一些不同点。

代码语言:javascript
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 获取demo
  • demo代码分析
  • rtc-client.js
  • share-client.js
  • 总结
相关产品与服务
实时音视频
实时音视频(Tencent RTC)基于腾讯21年来在网络与音视频技术上的深度积累,以多人音视频通话和低延时互动直播两大场景化方案,通过腾讯云服务向开发者开放,致力于帮助开发者快速搭建低成本、低延时、高品质的音视频互动解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档