小程序提供的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))
}