微信小程序接入腾讯TRTC实时视频实践

4,737 阅读7分钟

说起来这不是我第一次接触直播流了, 从 videoJS 到 flvJs, 网上关于直播流的轮子很多, 研究好文档直接使用就可以了, 这次公司使用了腾讯的实时音视频解决方案(Tencent Real-Time Communication,TRTC), 完成之后做个记录.

TRTC简介

腾讯推出的多人音视频通话和低延时互动直播两大场景化方案, 主打全平台互通, 提供小程序、Web、Android、iOS、Electron、Windows、macOS 等平台的 SDK。

官方产品架构: 114231634710748_.pic_hd.jpg

实现的基本功能:视频通话、语音通话、视频互动直播、语音互动直播.

官方文档地址: cloud.tencent.com/document/pr…

接入流程

一个完成的视频demo包含三个文件: trtc-room.wxml、trtc-room.js、trtc-wx.js:

  1. trtc-room.wxml 是自己编写的 wxml 文件,其中包括 和 节点。小程序的实时音视频就是基于微信原生组件标签 和 实现的.

  2. trtc-wx.js 是一个专门为您管理 TRTC 状态的一个类,一个纯 js 模块, 管理所有与实时音视频相关的状态,以及调用挂载在 和 上的方法。

  3. trtc-room.js 是业务层代码,引用 trtc-wx.js

image.png

接入准备

  1.  注册腾讯云 账号,并完成 实名认证
  2. 创建新的应用, 官方文档指引: cloud.tencent.com/document/pr…, 下载demo. 修改其中的配置参数

image.png 3. 在本地浏览器运行demo, 可以获得必要参数:SDKAPPID、userID、userSig、roomID.

image.png

trtc-room.js实现

  1. 引入
// npm 安装
npm install trtc-wx-sdk

// 本地引入
import TRTC from './utils/trtc-wx.js'
  1. 初始化TRTC 绑定事件
onLoad(){
  this.TRTC = new TRTC(this)
  // pusher 初始化参数
  const pusherConfig = {
     beautyLevel: 9,
     cloudenv: "DEV",
  }
  this.setData({
      pusher: this.TRTC.createPusher(pusherConfig).pusherAttributes
  })
  // 绑定事件
  this.bindEvent();
}
  1. 进入房间 发布本地流
    this.setData({
      pusher: this.TRTC.enterRoom(this.data.trtcConfig),
    }, () => {
      // 开始进行推流
      this.TRTC.getPusherInstance().start({
        success: function (event) {
          console.log("推流成功--------", event)
        },
        fail: function (err) {
          console.log("推流失败--------", err)
        }
      }) 
    })
  1. 监听远端用户进房, 订阅远端流
// 监听远端用户进房
this.TRTC.on(EVENT.REMOTE_USER_JOIN, this.onRemoteJoin, this)
// 监听远端视频流增加
this.TRTC.on(EVENT.REMOTE_VIDEO_ADD, this.onRemoteChange,this)
// 监听远端音频流增加
this.TRTC.on(EVENT.REMOTE_VIDEO_REMOVE, this.onRemoteChange, this)
  1. 退房,重置所有状态
 const result = this.TRTC.exitRoom()
 this.setData({
      pusher: result.pusher,
      playerList: result.playerList,
 })
  1. 完整demo
<view class="template-1v1">
    <view wx:for="{{streamList}}" wx:key="streamID" wx:if="{{item.src && (item.hasVideo || item.hasAudio)}}" class="view-container player-container {{item.isVisible?'':'none'}}">
      <live-player
        class="player"
        id="{{item.streamID}}"
        data-userid="{{item.userID}}"
        data-streamid="{{item.streamID}}"
        data-streamtype="{{item.streamType}}"
        src= "{{item.src}}"
        mode= "RTC"
        autoplay= "{{item.autoplay}}"
        mute-audio= "{{item.muteAudio}}"
        mute-video= "{{item.muteVideo}}"
        orientation= "{{item.orientation}}"
        object-fit= "{{item.objectFit}}"
        background-mute= "{{item.enableBackgroundMute}}"
        min-cache= "{{item.minCache}}"
        max-cache= "{{item.maxCache}}"
        sound-mode= "{{item.soundMode}}"
        enable-recv-message= "{{item.enableRecvMessage}}"
        auto-pause-if-navigate= "{{item.autoPauseIfNavigate}}"
        auto-pause-if-open-native= "{{item.autoPauseIfOpenNative}}"
        debug="{{debug}}"
        bindstatechange="_playerStateChange"
        bindfullscreenchange="_playerFullscreenChange"
        bindnetstatus="_playerNetStatus"
        bindaudiovolumenotify  ="_playerAudioVolumeNotify"
      />
    </view>
    <view class="view-container pusher-container {{pusher.isVisible?'':'none'}} {{streamList.length===0? 'fullscreen':''}}">
      <live-pusher
        class="pusher"
        url="{{pusher.url}}"
        mode="{{pusher.mode}}"
        autopush="{{pusher.autopush}}"
        enable-camera="{{pusher.enableCamera}}"
        enable-mic="{{pusher.enableMic}}"
        muted="{{!pusher.enableMic}}"
        enable-agc="{{pusher.enableAgc}}"
        enable-ans="{{pusher.enableAns}}"
        enable-ear-monitor="{{pusher.enableEarMonitor}}"
        auto-focus="{{pusher.enableAutoFocus}}"
        zoom="{{pusher.enableZoom}}"
        min-bitrate="{{pusher.minBitrate}}"
        max-bitrate="{{pusher.maxBitrate}}"
        video-width="{{pusher.videoWidth}}"
        video-height="{{pusher.videoHeight}}"
        beauty="{{pusher.beautyLevel}}"
        whiteness="{{pusher.whitenessLevel}}"
        orientation="{{pusher.videoOrientation}}"
        aspect="{{pusher.videoAspect}}"
        device-position="{{pusher.frontCamera}}"
        remote-mirror="{{pusher.enableRemoteMirror}}"
        local-mirror="{{pusher.localMirror}}"
        background-mute="{{pusher.enableBackgroundMute}}"
        audio-quality="{{pusher.audioQuality}}"
        audio-volume-type="{{pusher.audioVolumeType}}"
        audio-reverb-type="{{pusher.audioReverbType}}"
        waiting-image="{{pusher.waitingImage}}"
        debug="{{debug}}"
        bindstatechange="_pusherStateChangeHandler"
        bindnetstatus="_pusherNetStatusHandler"
        binderror="_pusherErrorHandler"
        bindbgmstart="_pusherBGMStartHandler"
        bindbgmprogress="_pusherBGMProgressHandler"
        bindbgmcomplete="_pusherBGMCompleteHandler"
        bindaudiovolumenotify="_pusherAudioVolumeNotify"
      />
      <view class="loading" wx:if="{{streamList.length === 0 && !isConnectioned}}">
        <view class="loading-img">
          <image src="./static/loading.png" class="rotate-img"></image>
        </view>
        <view class="loading-text">等待...</view>
      </view>
    </view>
    <view class="bottom-btns">
      <view class="btn-hangup" bindtap="_hangUp">
        <image class="btn-image" src="./static/hangup.png"></image>
      </view>
      <view class="btn-normal" bindtap="_switchCamera" >
        <image class="btn-image" src="./static/switch.png"></image>
      </view>
    </view> 
  </view>

//trtc.js
import TRTC from "./utils/trtc-wx"
const TAG_NAME = 'TRTC-ROOM'
Page({

  /**
   * 页面的初始数据
   */
  data: {
    pusher: null,
    streamList: [], // 用于渲染player列表,存储stram
    debug: false, // 是否打开player pusher 的调试信息
    cameraPosition: '', // 摄像头位置,用于debug
    trtcConfig:{
      sdkAppID: '1400573664', // 开通实时音视频服务创建应用后分配的 SDKAppID
      userID: 'user_55273738', // 用户 ID,可以由您的帐号系统指定
      userSig: 'eJwtzM0KgkAUBeB3ma2ht-kVoUWCmBWzsFzoRoKZ7BKJqIkUvXuiLs93DudLrueLO9iWBIS6QDZzRmPrHu8487uzbSkEVUwxfx105nlrGjQk2HIAoZiUfGl6fNlJJeO*EsDponZssJ1cAvcB1g*spvdE6-HBjmky7NOQO8lBx3HUFae8CPXHM5HNM0-WmFUO7MjvD-psMbc_', // 身份签名,相当于登录密码的作用
      template: '1v1', // 画面排版模式
      roomID: '24193',  //房间号
      debugMode: false,
      enableMic: true,
      enableCamera: true,
    },
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    // 设置屏幕常亮
    wx.setKeepScreenOn({
      keepScreenOn: true,
    })
    this.TRTC = new TRTC(this);
  
    // 创建pusher
    this.createPusher();
    // 绑定事件
    this.bindEvent();
    // 初始化页面状态
    this.initStatus();
    // 进入房间
    this.enterRoom()
  },
  /**
   * @description: 数据初始化
   * @return: void
   */
  createPusher(){
    // pusher 初始化参数
    const pusherConfig = {
      beautyLevel: 9,
      cloudenv: "DEV",
    }
    const pusher = this.TRTC.createPusher(pusherConfig)
    this.setData({
      pusher: pusher.pusherAttributes,
    })
  },
  /**
   * @description: 发布本地流,订阅事件。进入房间
   * @return: void
   */
  enterRoom: function () {
    if (!this.checkParam(this.data.trtcConfig)) {
      console.log('checkParam false: 缺少必要参数, 进房未执行')
      return
    }
    this.setData({
      pusher: this.TRTC.enterRoom(this.data.trtcConfig),
    }, () => {
      // 开始进行推流
      this.TRTC.getPusherInstance().start({
        success: function (event) {
          console.log("推流成功--------", event)
        },
        fail: function (err) {
          console.log("推流失败--------", err)
        }
      }) 
      this.status.isPush = true;
    })
  },
  /**
    * @description: 退房,停止推流和拉流,并重置数据
    * @return: void
    */
  exitRoom(){
    const result = this.TRTC.exitRoom()
    this.setData({
        pusher: result.pusher,
        streamList: result.playerList,
    })
  },
  /**
   * @description: 初始化页面状态
   * @return: void
   */
  initStatus() {
    this.status = {
      isPush: false, // 推流状态
      isPending: false, // 挂起状态,触发5000事件标记为true,onShow后标记为false
    }
  },
  /**
    * @description: 绑定事件 监听 trtc 状态
    * @return: void
    */
  bindEvent(){
    const EVENT = this.TRTC.EVENT;
    // 本地用户进房
    this.TRTC.on(EVENT.LOCAL_JOIN, this.onLocalJoin, this);
    // 本地用户离开
    this.TRTC.on(EVENT.LOCAL_LEAVE, this.onLocalLeave, this);
    // 远端用户进房
    this.TRTC.on(EVENT.REMOTE_USER_JOIN, this.onRemoteJoin, this)
    // 远端用户离开
    this.TRTC.on(EVENT.REMOTE_USER_LEAVE, this.onRemoteLeave, this)
    // 视频状态 true
    this.TRTC.on(EVENT.REMOTE_VIDEO_ADD, this.onRemoteChange,this)
    // 视频状态 false
    this.TRTC.on(EVENT.REMOTE_VIDEO_REMOVE, this.onRemoteChange, this)
    // 音频可用
    this.TRTC.on(EVENT.REMOTE_AUDIO_ADD, this.onRemoteChange, this)
    // 音频不可用
    this.TRTC.on(EVENT.REMOTE_AUDIO_REMOVE, this.onRemoteChange, this)
    // 错误处理
    this.TRTC.on(EVENT.ERROR, this.onTrtrError)
    // 本地推流网络状态变更
    this.TRTC.on(EVENT.LOCAL_NET_STATE_UPDATE, this.onLocalNetStateChange)
    // 远端推流网络状态变更
    this.TRTC.on(EVENT.REMOTE_NET_STATE_UPDATE, this.onRemoteNetStateUpdate)
  },
  /**
   * @description: trtc 事件监听绑定函数
   * @param {*} event 
   * @return: void
   */
  onRemoteJoin(event){
    this.log("远端用户进房", event)
    const { data } = event;
    const { playerList } = data;
    this.setData({
      streamList: playerList,
    }, () => {
      // 接通后业务
    })
  },
  onRemoteLeave(event){
    this.log("远端用户离开", event)
    const { data, eventCode } = event;
    const { playerList, userID } = data;
    const _this = this;
    if (userID) {
      this.setList({
        streamList: playerList
      }).then(() => {
        // 执行用户离开逻辑
      })
    }
  },
  onRemoteChange(event){
    const { data, eventCode } = event;
    const { player } = data;
    let option = {}
    switch (eventCode) {
      case "REMOTE_AUDIO_REMOVE":
        Object.assign(option, { muteAudio: true })
        this.log("远端音频移除", event)
        break;
      case "REMOTE_AUDIO_ADD":
        Object.assign(option, { muteAudio: false })
        this.log("远端音频可用", event)
        break;
      case "REMOTE_VIDEO_REMOVE":
        Object.assign(option, { muteVideo: true })
        this.log("远端视频移除", event)
        break; 
      case "REMOTE_VIDEO_ADD":
        Object.assign(option, { muteVideo: false })
        this.log("远端视频可用", event)
        break; 
    }
    this.setPlayerAttributesHandler(player, option)
  },
  onLocalJoin(event){
    this.log("本地用户进房", event)
  },
  onLocalLeave(event){
    this.log("本地用户离开", event)
  },
  onTrtrError(event){
    this.log("Trtr Error", event)
  },
  onLocalNetStateChange(event){
    this.log("本地网络变化", event)
    const pusher = event.data.pusher
    this.setData({
      pusher: pusher
    })
  },
  onRemoteNetStateUpdate(event){
    this.log("远端网络变化", event)
    const { playerList } = event.data;
    this.setData({
      streamList: playerList
    })
  },
  /**
   * @description 设置某个 player 属性
   * @param {*} player 
   * @param {*} options { muteAudio: true/false , muteVideo: true/false }
   * @return: void
   */
  setPlayerAttributesHandler(player, options) {
    this.setData({
      streamList: this.TRTC.setPlayerAttributes(player.streamID, options),
    })
  },
  /**
   * @description 切换前后摄像头
   */
  _switchCamera() {
    if (!this.data.cameraPosition) {
      // this.data.pusher.cameraPosition 是初始值,不支持动态设置
      this.data.cameraPosition = this.data.pusher.frontCamera
    }
    console.log(TAG_NAME, 'switchCamera', this.data.cameraPosition)
    this.data.cameraPosition = this.data.cameraPosition === 'front' ? 'back' : 'front'
    this.setData({
      cameraPosition: this.data.cameraPosition,
    }, () => {
      console.log(TAG_NAME, 'switchCamera success', this.data.cameraPosition)
    })
    // wx 7.0.9 不支持动态设置 pusher.frontCamera ,只支持调用 API switchCamer() 设置,这里修改 cameraPosition 是为了记录状态
    this.TRTC.getPusherInstance().switchCamera() 
  },
  /**
   * @description 点击挂断通话按钮 退出通话
   */
  _hangUp() {
    let _this = this;
    setTimeout(() => {
      _this.exitRoom();
    }, 1000);
  },
  /**
   * @description 设置列表数据,并触发页面渲染
   * @param {Object} params include  stramList
   * @returns {Promise}
   */
  setList(params) {
    console.log(TAG_NAME, 'setList', params, this.data.template)
    const { streamList } = params
    return new Promise((resolve, reject) => {
      const data = {
        streamList: streamList || this.data.streamList,
      }
      this.setData(data, () => {
        resolve(params)
      })
    })
  },
  /**
   * @description trtc 初始化room 必选参数检测
   * @param {Object} rtcConfig rtc参数
   * @returns {Boolean}
   */
  checkParam(rtcConfig) {
    console.log(TAG_NAME, 'checkParam config:', rtcConfig)
    if (!rtcConfig.sdkAppID) {
      console.error('未设置 sdkAppID')
      return false
    }
    if (rtcConfig.roomID === undefined) {
      console.error('未设置 roomID')
      return false
    }
    if (rtcConfig.roomID < 1 || rtcConfig.roomID > 4294967296) {
      console.error('roomID 超出取值范围 1 ~ 4294967295')
      return false
    }
    if (!rtcConfig.userID) {
      console.error('未设置 userID')
      return false
    }
    if (!rtcConfig.userSig) {
      console.error('未设置 userSig')
      return false
    }
    if (!rtcConfig.template) {
      console.error('未设置 template')
      return false
    }
    return true
  },
  /**
   * @description pusher event handler
   * @param {*} event 事件实例
   */
  _pusherStateChangeHandler(event) {
    console.log(event, "pusherEventHandler")
    this.TRTC.pusherEventHandler(event)
    const code = event.detail.code
    const message = event.detail.message
    switch (code) {
        case 5000:
          console.log(TAG_NAME, '小程序被挂起: ', code)
          break
        case 5001:
          // 20200421 仅有 Android 微信会触发该事件
          console.log(TAG_NAME, '小程序悬浮窗被关闭: ', code)
          console.log(this.status.isPush, "this.status.isPush")
          this.status.isPending = true
          if (this.status.isPush) {
              this.exitRoom()
          }
          break
    }
  },
  _pusherNetStatusHandler(event) {
    this.warnLog('NetStatus', event)
    this.TRTC.pusherNetStatusHandler(event)
  },
  _pusherErrorHandler(event) {
    this.warnLog('pusherErro', event)
    this.TRTC.pusherErrorHandler(event)
  },
  _pusherBGMStartHandler(event) {
    this.warnLog('pusherBGMStart', event)
    this.TRTC.pusherBGMStartHandler(event)
  },
  _pusherBGMProgressHandler(event) {
    this.warnLog('BGMProgress', event)
    this.TRTC.pusherBGMProgressHandler(event)
  },
  _pusherBGMCompleteHandler(event) {
    this.warnLog('BGMComplete', event)
    this.TRTC.pusherBGMCompleteHandler(event)
  },
  _pusherAudioVolumeNotify(event) {
    this.warnLog('AudioVolume', event)
    this.TRTC.pusherAudioVolumeNotify(event)
  },
  _playerStateChange(event) {
    this.warnLog('playerStateChange', event)
    this.TRTC.playerEventHandler(event)
  },
  _playerFullscreenChange(event) {
    this.warnLog('Fullscreen', event)
    this.TRTC.playerFullscreenChange(event)
  },
  _playerNetStatus(event) {
    this.warnLog('playerNetStatus', event)
    this.TRTC.playerNetStatus(event)
  },
  _playerAudioVolumeNotify(event) {
    this.warnLog('playerAudioVolume', event)
    this.TRTC.playerAudioVolumeNotify(event)
  },
  /**
   * @description console.warn 方法
   * @param {*} msg: message detail string
   * @param {*} event : event object
   */
  warnLog (msg, event) {
    console.warn(TAG_NAME, msg, event)
  },
  /**
   * @description console.log 方法
   * @param {*} msg: message detail string
   * @param {*} event : event object
   * @return: void
   */
  log(msg, event){
    console.log(TAG_NAME, msg, event)
  },
})

css样式

   /* 1v1 视频电话模式 */
.template-1v1{
 width: 100vw;
 height: 100vh;
 position: relative;
}
.pusher {
 width: 100%;
 height: 100%;
}

.player {
 width: 100%;
 height: 100%;
}
.template-1v1 .pusher-container{
 width: 240rpx;
 height: 320rpx;
 position: absolute;
 right: 20rpx;
 top: 160rpx;
 z-index: 2;
}

.template-1v1 .pusher-container.fullscreen{
 width: 100vw;
 height: 100vh;
 top: 0;
 right: 0;
}

.template-1v1 .loading {
 position: absolute;
 top: 40vh;
 left: 50vw;
 transform: translate(-50%, 0);
 width: 300rpx;
 height: 250rpx;
 border-radius: 12rpx;
 background: rgba(0,0,0,0.6);
 color: white;
 padding: 40rpx;
 display: flex;
 flex-direction: column;
}
.template-1v1 .loading-img {
 height: 200rpx;
 display:flex;
 justify-content: center;
 align-items: center;
 animation: rotate 2s linear infinite;
}
.template-1v1 .rotate-img {
 width:160rpx;
 height: 160rpx;
}
.template-1v1 .loading-text {
 width: 100%;
 padding-top: 40rpx;
 text-align: center;
}
@keyframes rotate {
 0%{ transform: rotate(0deg);}
 50%{ transform: rotate(180deg);}
 100%{ transform: rotate(360deg);}
}
.template-1v1 .player-container:nth-child(1){
 width: 100vw;
 height: 100vh;
}

.template-1v1 .handle-btns {
 position: absolute;
 z-index: 3;
 bottom: 15vh;
 width: 100vw;
 display: flex;
 flex-direction: row;
 justify-content: space-around;
}

.template-1v1 .bottom-btns {
 position: absolute;
 z-index: 3;
 bottom: 7vh;
 width: 100vw;
 display: flex;
 flex-direction: row;
 justify-content: space-around;
}

/* .template-1v1 image {
 width: 4vh;
 height: 4vh;
} */

.template-1v1 .btn-normal {
 width: 8vh;
 height: 8vh;
 box-sizing: border-box;
 display: flex;
 background: white;
 justify-content: center;
 align-items: center;
 border-radius: 50%;
}
.template-1v1 .btn-hangup .btn-image,
.template-1v1 .btn-normal .btn-image{
 width: 4vh;
 height: 4vh;
}
.template-1v1 .btn-hangup  {
 width: 8vh;
 height: 8vh;
 background: #f75c45;
 box-sizing: border-box;
 display: flex;
 justify-content: center;
 align-items: center;
 border-radius: 50%;
}

.template-1v1 .btn-timer {
 position: relative;
 top: -30px;
 background: none;
 border: none;
 color: #fff;
}

效果:

WeChatc1605b6d41dfa72f88e59dc80ac19caf.png

  1. 需要注意的点
  • 小程序后台需开通小程序类目与推拉流标签权限。 推拉流标签为、,腾讯推出的TRTC也是基于推拉流的,所以也需要权限. 这个需要企业号才可以申请

image.png

  • 测试需要在真机上进行调试, 开发者工具不支持推流标签.

image.png


git地址:gitee.com/taishangaic…

如有问题,欢迎探讨,如果满意,请手动点赞,谢谢!🙏

及时获取更多姿势,请您关注!!