专栏首页跟铭哥学音视频技术TRTC学习之旅(一)--多人聊天室web篇(官方demo)
原创

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

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

获取demo

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

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

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

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

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中获取用户签名

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

rtc-client.js

rtc-client代码结构

从这个结构里边我们可以看出大概的设计思路和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;
      }
    }
  }
}

share-client.js

这个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的具体参数和使用规则。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • TRTC学习之旅(三)-- 使用vue+ts集成互动直播

    上次我们已经用vue+ts实现了多人会议室的搭建,这次我们继续在上次项目的基础上,实现互动直播功能。

    黑眼圈云豆
  • TRTC学习之旅(二)-- 使用vue+ts集成TRTC实现多人会议室

    根据上回学习了官方TRTC demo之后,已经了解了一个基础的多人会议室创建的流程,接下来我需要将自己学到的转换为自己能够运用的。

    黑眼圈云豆
  • IM即时通信探索(三)-- 实现一个简单的直播聊天室

    今天我们用IM来简单的实现一个直播聊天室场景。不过在这之前呢,我们还需要先来熟悉一下IM直播功能的一些特性。本文以web端进行代码讲解,与其它端可能会有些差异,...

    黑眼圈云豆
  • 使用Box2D实现物体的碰撞检测和实现自动化背景布置

    我们本节要实现的是,当用户把小球投入篮框,如果小球能从篮框中间漏下去,那么就可以算得分。这就需要我们进行碰撞检测,Box2D给我们提供良好机制能实现这点功能。我...

    望月从良
  • 【React】249-当我开始使用React 时,我希望我知道这些知识

      可以给每个方法加上.bind(this)来解决 this 指向的问题,因为大多数教程都告诉你这样做。如果你有几个受控组件,那么constructor(){}...

    pingan8787
  • Mybatis深入源码分析之Mapper与接口绑定原理源码分析

    紧接上篇文章:Mybatis深入源码分析之SqlSessionFactoryBuilder源码分析,这里再来分析下,Mapper与接口绑定原理。

    须臾之余
  • 【Flutter 专题】61 图解基本 Button 按钮小结 (一)

    Button 在日常中是必不可少的,和尚尝试过不同类型的 Button,也根据需求自定义过,今天和尚系统的学习一下最基本的 Button;

    阿策
  • 一文秒杀Java中this关键字

    在这里插入图片描述 运行的结果的顺序其实很简单,关键就是要理解this到底指着谁,this表示对当前对象的引用,也就是subclass对象

    润森
  • bug 回忆录(四)

    @author Ken @time 2020-09-27 21:30:59 @description 转载请备注出处,谢谢

    公众号---志学Python
  • three.js 郭先生制作太阳系

    今天郭先生收到评论,想要之前制作太阳系的案例,因为找不到了,于是在vue版本又制作一版太阳系,在线案例请点击three.js制作太阳系(加载时间比较长,请稍等一...

    郭先生的博客

扫码关注云+社区

领取腾讯云代金券