Bootstrap

微信小程序实时音视频的使用

小程序提供的live-pusher组件和live-player组件主要用于两种场景:一个是用于视频直播,另一个是用于为您的小程序提供实时音视频相关的功能。live-pusher和live-player两个组件,都有一个叫做 RTC 的模式,通过这种模式,可以在小程序中实现实时视频通话功能。本文我们介绍如何使用live-pusher组件和live-player组件以及腾讯云实时音视频产品来开发一个视频通话的小程序。

实时音视频产品架构

腾讯实时音视频(Tencent Real-Time Communication,TRTC),将腾讯多年来在网络与音视频技术上的深度积累,主打低延时互动直播和多人音视频两大解决方案,通过腾讯云服务向开发者开放,致力于帮助开发者快速搭建低成本、低延时、高品质的音视频互动解决方案。
在这里插入图片描述

产品功能

  • 视频通话
    即两人或多人视频通话,支持720P、1080P高清画质。单个房间最多支持300人同时在线,最多支持50人同时开启摄像头。
  • 语音通话
    即两人或多人语音通话,支持 48kHz,支持双声道。单个房间最多支持300人同时在线,最多支持50人同时开启麦克风。
  • 视频互动直播
    支持主播与观众视频连麦互动。支持主播跨房间(跨直播间)PK。支持平滑上下麦,切换过程无需等待,主播延时小于300ms。单个房间可上麦人数无限制,最多支持50人同时连麦。低延时直播模式下,支持10万观众同时播放,播放延时低至1000ms。CDN 旁路直播模式下,观众数量无限制。
  • 语音互动直播
    支持主播与观众语音连麦互动。支持主播跨房间(跨直播间)PK。支持平滑上下麦,切换过程无需等待,主播延时小于300ms。单个房间可上麦人数无限制,最多支持50人同时连麦。低延时直播模式下,支持10万观众同时播放,播放延时低至1000ms。CDN 旁路直播模式下,观众数量无限制。

实时音视频SDK使用说明

使用腾讯实时音视频SDK前需要首先注册腾讯云账号并完成 实名认证。注册成功后需要在实时音视频产品中创建一个实时音视频应用,腾讯云会为您的应用分配一个SDKAppID以及相应的UserSig的密钥, 后续的编码中会用到这两个参数。创建实时音视频应用的操作方法为:登录实时音视频控制台,选择【应用管理】->【创建应用】:
在这里插入图片描述
创建实时音视频应用成功后可以在应用的列表界面中看到SDKAppID:
在这里插入图片描述
点击上图中的【功能配置】,可以在打开页面的【快速上手】中看到UserSig的密钥:
在这里插入图片描述
另外在编码前还需要下载trtc-wx.js文件,该文件将用于接下来的小程序视频通话页面,请通过以下的url地址来下载trtc-wx.js文件:
https://web.sdk.qcloud.com/trtc/miniapp/download/trtc-wx.zip

API 使用指引

下图是腾讯实时音视频SDK文档中给出的实时音视频小程序API的调用时序图:
在这里插入图片描述

步骤1:初始化TRTC

在 trtc-wx 包中导出的是一个名为 TRTC 的类,您需要在页面onLoad函数中实例化这个类,同时创建 Pusher,并监听 TRTC 抛出的事件。

onLoad(options) {
    //初始化 TRTC 实例
    this.TRTC = new TRTC(this)
//创建 Pusher
const pusher = this.TRTC.createPusher({beautyLevel: 9})
    //事件监听
    this.bindTRTCRoomEvent()
  },

使用TRTC类的on(EventCode, handler, context)方法监听TRTC抛出的事件,例如:

//事件监听
  bindTRTCRoomEvent() {
    const TRTC_EVENT = this.TRTC.EVENT
    this.TRTC.on(TRTC_EVENT.ERROR, (event) => {
      console.log('* room ERROR', event)
    })
}
步骤2:开始推流

首先需要调用enterRoom方法进入TRTC房间,之后您可以调用start() 方法开始推流。

onLoad(options) {
    this.TRTC = new TRTC(this)
    const pusher = this.TRTC.createPusher({beautyLevel: 9})
    this.bindTRTCRoomEvent()
//进入房间
this.TRTC.enterRoom(this.data._rtcConfig)
//开始推流
this.TRTC.getPusherInstance().start() 
  }

调用enterRoom(params)方法后,若房间不存在,系统将自动创建一个新房间,否则直接进入房间,enterRoom(params)的参数说明如下:

  • sdkAppID:您的腾讯云账号的 sdkAppID
  • userID:您进房的 userID
  • userSig:您服务器签发的 userSig
  • roomID:您要进入的房间号,如该房间不存在,系统会为您自动创建
  • strRoomID:您要进入的字符串房间号,如填写该参数,将优先进入字符串房间
  • scene:必填参数,使用场景:
    rtc:实时通话,采用优质线路,同一房间中的人数不应超过300人。
    live:直播模式,采用混合线路,支持单一房间十万人在线(同时上麦的人数应控制在50人以内)
步骤3:对于远端流进行处理

如果收到远端新增视频流,您可以开始播放这路视频,将这个 player 的 muteVideo 状态设置为 false,这里需要您传入这个 player 的 id,更新完成后会返回给您更新后的playerList列表,您只需要将页面的 playerList 同步更新即可。

this.TRTC.on(TRTC_EVENT.REMOTE_VIDEO_ADD, (event) => {
      console.log('* room REMOTE_VIDEO_ADD',  event)
      const { player } = event.data
      this.setPlayerAttributesHandler(player, { muteVideo: false })
    })

如果收到这个远端流减少的通知时,您可以取消对这一路的订阅,将 muteAudio 设置为 true。

    this.TRTC.on(TRTC_EVENT.REMOTE_VIDEO_REMOVE, (event) => {
      console.log('* room REMOTE_VIDEO_REMOVE', event)
      const { player } = event.data
      this.setPlayerAttributesHandler(player, { muteVideo: true })
    })
步骤4:结束音视频通话

调用exitRoom()退出房间,退出房间时需要重置状态机的状态,并同步到页面中,防止下次进房发生状态的混乱。

_hangUp() {
    const result = this.TRTC.exitRoom()
    this.setData({
      pusher: result.pusher,
      playerList: result.playerList,
    })
    wx.navigateBack({delta: 1,})
  }
步骤5:控制是否上行本地音视频流

如若需要变更 live-pusher 标签上 enable-mic 和 enable-camera 的属性,您可以通过调用 setPusherAttributes 函数对状态机中管理的推流状态进行改变,并将返回给您更新后的状态值更新到页面中。

//上行音频流
this.setData({
  pusher: this.TRTC.setPusherAttributes({enableMic: true})
})
//上行视频流
this.setData({
  pusher: this.TRTC.setPusherAttributes({enableCamera: true})
})

实时音视频代码示例

接下的代码中我们来实现一个视频对话功能的小程序,在小程序的入口页面(home页面)中我们为通话的双方用户指定一个房间号,并为当前用户产生一个随机用户ID,然后跳转到通话页面(room页面)进行视频通话。下面的代码是room页面的代码:

示例代码:room.wxml
<view class="template-1v1">
  <view wx:for="{{playerList}}" 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'}} {{playerList.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="{{playerList.length === 0}}">
      <view class="loading-img">
        <image src="../../../static/images/loading.png" class="rotate-img"></image>
      </view>
      <view class="loading-text">等待接听中...</view>
    </view>
  </view>
  <view class="handle-btns">
    <view class="btn-normal" bindtap="_pusherAudioHandler">
      <image class="btn-image" src="{{pusher.enableMic? '../../../static/images/audio-true.png': '../../../static/images/audio-false.png'}} "></image>
    </view>
    <view class="btn-normal" bindtap="_pusherSwitchCamera" >
      <image class="btn-image" src="../../../static/images/switch.png"></image>
    </view>
    <view class="btn-normal" bindtap="_setPlayerSoundMode">
      <image class="btn-image" src="{{playerList[0].soundMode === 'ear' ? '../../../static/images/speaker-false.png': '../../../static/images/speaker-true.png'}} "></image>
    </view>
  </view>
  <view class="bottom-btns">
    <view class="btn-normal" data-key="beautyLevel" data-value="9|0" data-value-type="number" bindtap="_setPusherBeautyHandle">
      <image class="btn-image" src="{{pusher.beautyLevel == 9 ? '../../../static/images/beauty-true.png': '../../../static/images/beauty-false.png'}} "></image>
    </view>
    <view class="btn-hangup" bindtap="_hangUp">
      <image class="btn-image" src="../../../static/images/hangup.png"></image>
    </view>
  </view>
</view>
示例代码:room.js
import TRTC from '../../../static/trtc-wx'
Page({
  data: {
    _rtcConfig: {
      sdkAppID: '', //开通实时音视频服务创建应用后分配的sdkAppID
      roomID: '', //房间号可以由您的系统指定
      userID: '', //用户ID可以由您的系统指定
      userSig: '', //身份签名,相当于登录密码的作用
    },
    roomID: 0,
    pusher: null,
    playerList: [],
  },
  onLoad(options) {
    this.TRTC = new TRTC(this)
    const pusher = this.TRTC.createPusher({beautyLevel: 9})
    this.setData({
      _rtcConfig: {
        userID: options.userID,
        sdkAppID: options.sdkAppID,
        userSig: options.userSig,
        roomID: options.roomID,
      },
      pusher: pusher.pusherAttributes
    })
    //事件监听
    this.bindTRTCRoomEvent()
    //进入房间
    this.setData({
      pusher: this.TRTC.enterRoom(this.data._rtcConfig),
    }, () => {
      this.TRTC.getPusherInstance().start() 
    })
  },
  //设置pusher属性
  setPusherAttributesHandler(options) {
    this.setData({
      pusher: this.TRTC.setPusherAttributes(options),
    })
  },
  //设置某个player属性
  setPlayerAttributesHandler(player, options) {
    this.setData({
      playerList: this.TRTC.setPlayerAttributes(player.streamID, options),
    })
  },
  //事件监听
  bindTRTCRoomEvent() {
    const TRTC_EVENT = this.TRTC.EVENT
    this.TRTC.on(TRTC_EVENT.ERROR, (event) => {
      console.log('* room ERROR', event)
    })
    //成功进入房间
    this.TRTC.on(TRTC_EVENT.LOCAL_JOIN, (event) => {
      console.log('* room LOCAL_JOIN', event)
      this.setPusherAttributesHandler({ enableCamera: true })
      this.setPusherAttributesHandler({ enableMic: true })
    })
    //成功离开房间
    this.TRTC.on(TRTC_EVENT.LOCAL_LEAVE, (event) => {
      console.log('* room LOCAL_LEAVE', event)
    })
    //远端用户退出
    this.TRTC.on(TRTC_EVENT.REMOTE_USER_LEAVE, (event) => {
      const { playerList } = event.data
      this.setData({
        playerList: playerList
      })
      console.log('* room REMOTE_USER_LEAVE', event)
    })
    //远端用户推送视频
    this.TRTC.on(TRTC_EVENT.REMOTE_VIDEO_ADD, (event) => {
      console.log('* room REMOTE_VIDEO_ADD',  event)
      const { player } = event.data
      // 开始播放远端的视频流,默认是不播放的
      this.setPlayerAttributesHandler(player, { muteVideo: false })
    })
    // 远端用户取消推送视频
    this.TRTC.on(TRTC_EVENT.REMOTE_VIDEO_REMOVE, (event) => {
      console.log('* room REMOTE_VIDEO_REMOVE', event)
      const { player } = event.data
      this.setPlayerAttributesHandler(player, { muteVideo: true })
    })
    // 远端用户推送音频
    this.TRTC.on(TRTC_EVENT.REMOTE_AUDIO_ADD, (event) => {
      console.log('* room REMOTE_AUDIO_ADD', event)
      const { player } = event.data
      this.setPlayerAttributesHandler(player, { muteAudio: false })
    })
    // 远端用户取消推送音频
    this.TRTC.on(TRTC_EVENT.REMOTE_AUDIO_REMOVE, (event) => {
      console.log('* room REMOTE_AUDIO_REMOVE', event)
      const { player } = event.data
      this.setPlayerAttributesHandler(player, { muteAudio: true })
    })
  },
  //挂断退出房间
  _hangUp() {
    const result = this.TRTC.exitRoom()
    this.setData({
      pusher: result.pusher,
      playerList: result.playerList,
    })
    wx.navigateBack({delta: 1,})
  },
  //设置美颜
  _setPusherBeautyHandle() {
    const  beautyLevel = this.data.pusher.beautyLevel === 0 ? 9 : 0
    this.setPusherAttributesHandler({ beautyLevel })
  },
  //发布/取消发布Audio
  _pusherAudioHandler() {
    if (this.data.pusher.enableMic) {
      this.setPusherAttributesHandler({ enableMic: false })
    } else {
      this.setPusherAttributesHandler({ enableMic: true })
    }
  },
  _pusherSwitchCamera() {
    const  frontCamera = this.data.pusher.frontCamera === 'front' ? 'back' : 'front'
    this.TRTC.getPusherInstance().switchCamera(frontCamera)
  },
  _setPlayerSoundMode() {
    if (this.data.playerList.length === 0) {
      return
    }
    const player = this.TRTC.getPlayerList()
    const soundMode = player[0].soundMode === 'speaker' ? 'ear' : 'speaker'
    this.setPlayerAttributesHandler(player[0], { soundMode })
  },
  _pusherStateChangeHandler(event) {
    this.TRTC.pusherEventHandler(event)
  },
  _pusherNetStatusHandler(event) {
    this.TRTC.pusherNetStatusHandler(event)
  },
  _pusherErrorHandler(event) {
    this.TRTC.pusherErrorHandler(event)
  },
  _pusherBGMStartHandler(event) {
    this.TRTC.pusherBGMStartHandler(event)
  },
  _pusherBGMProgressHandler(event) {
    this.TRTC.pusherBGMProgressHandler(event)
  },
  _pusherBGMCompleteHandler(event) {
    this.TRTC.pusherBGMCompleteHandler(event)
  },
  _pusherAudioVolumeNotify(event) {
    this.TRTC.pusherAudioVolumeNotify(event)
  },
  _playerStateChange(event) {
    this.TRTC.playerEventHandler(event)
  },
  _playerFullscreenChange(event) {
    this.TRTC.playerFullscreenChange(event)
  },
  _playerNetStatus(event) {
    this.TRTC.playerNetStatus(event)
  },
  _playerAudioVolumeNotify(event) {
    this.TRTC.playerAudioVolumeNotify(event)
  },
})
生成用户签名

从小程序的入口页面进入room页面需要传递以下参数:

  • sdkAppID:开通实时音视频服务创建应用后分配的sdkAppID
  • roomID:房间号
  • userID:用户ID
  • userSig:身份签名
    UserSig 是腾讯云设计的一种安全保护签名,目的是为了阻止恶意攻击者盗用您的云服务使用权。要使用云服务,您需要在相应 SDK 的初始化或登录函数中提供 SDKAppID,UserID和UserSig 三个关键信息。其中SDKAppID用于标识您的应用,UserID用于标识您的用户,而UserSig 则是基于前两者计算出的安全签名,它由 HMAC SHA256 加密算法计算得出。只要攻击者不能伪造UserSig,就无法盗用您的云服务流量。计算UserSig的服务端代码如下:
func GenUserSig(sdkappid int, key string, userid string, expire int) (string, error) {
    currTime := time.Now().Unix()
    var sigDoc map[string]interface{}
    sigDoc = make(map[string]interface{})
    sigDoc["TLS.ver"] = "2.0"
    sigDoc["TLS.identifier"] = identifier
    sigDoc["TLS.sdkappid"] = sdkappid
    sigDoc["TLS.expire"] = expire
    sigDoc["TLS.time"] = currTime
    sigDoc["TLS.sig"] = _hmacsha256(sdkappid, key, userid, currTime, expire)
    data, _:= json.Marshal(sigDoc)

    var b bytes.Buffer
    w := zlib.NewWriter(&b)
    w.Write(data)
    w.Close()
    return base64urlEncode(b.Bytes()), nil
}
func base64urlEncode(data []byte) string {
    str := base64.StdEncoding.EncodeToString(data)
    str = strings.Replace(str, "+", "*", -1)
    str = strings.Replace(str, "/", "-", -1)
    str = strings.Replace(str, "=", "_", -1)
    return str
}
func _hmacsha256(sdkappid int, key string, identifier string, currTime int64, expire int) string {
    content := "TLS.identifier:" + identifier + "\n"
    content += "TLS.sdkappid:" + strconv.Itoa(sdkappid) + "\n"
    content += "TLS.time:" + strconv.FormatInt(currTime, 10) + "\n"
    content += "TLS.expire:" + strconv.Itoa(expire) + "\n"
    h := hmac.New(sha256.New, []byte(key))
    h.Write([]byte(content ))
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
;