说起来这不是我第一次接触直播流了, 从 videoJS 到 flvJs, 网上关于直播流的轮子很多, 研究好文档直接使用就可以了, 这次公司使用了腾讯的实时音视频解决方案(Tencent Real-Time Communication,TRTC), 完成之后做个记录.
TRTC简介
腾讯推出的多人音视频通话和低延时互动直播两大场景化方案, 主打全平台互通, 提供小程序、Web、Android、iOS、Electron、Windows、macOS 等平台的 SDK。
官方产品架构:
实现的基本功能:视频通话、语音通话、视频互动直播、语音互动直播.
官方文档地址: cloud.tencent.com/document/pr…
接入流程
一个完成的视频demo包含三个文件: trtc-room.wxml、trtc-room.js、trtc-wx.js:
-
trtc-room.wxml是自己编写的 wxml 文件,其中包括 和 节点。小程序的实时音视频就是基于微信原生组件标签 和 实现的. -
trtc-wx.js是一个专门为您管理 TRTC 状态的一个类,一个纯 js 模块, 管理所有与实时音视频相关的状态,以及调用挂载在 和 上的方法。 -
trtc-room.js是业务层代码,引用trtc-wx.js
接入准备
- 注册腾讯云 账号,并完成 实名认证。
- 创建新的应用, 官方文档指引: cloud.tencent.com/document/pr…, 下载demo. 修改其中的配置参数
3. 在本地浏览器运行demo, 可以获得必要参数:SDKAPPID、userID、userSig、roomID.
trtc-room.js实现
- 引入
// npm 安装
npm install trtc-wx-sdk
// 本地引入
import TRTC from './utils/trtc-wx.js'
- 初始化TRTC 绑定事件
onLoad(){
this.TRTC = new TRTC(this)
// pusher 初始化参数
const pusherConfig = {
beautyLevel: 9,
cloudenv: "DEV",
}
this.setData({
pusher: this.TRTC.createPusher(pusherConfig).pusherAttributes
})
// 绑定事件
this.bindEvent();
}
- 进入房间 发布本地流
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.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)
- 退房,重置所有状态
const result = this.TRTC.exitRoom()
this.setData({
pusher: result.pusher,
playerList: result.playerList,
})
- 完整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;
}
效果:
- 需要注意的点
- 小程序后台需开通小程序类目与推拉流标签权限。 推拉流标签为、,腾讯推出的TRTC也是基于推拉流的,所以也需要权限. 这个需要企业号才可以申请
- 测试需要在真机上进行调试, 开发者工具不支持推流标签.
git地址:gitee.com/taishangaic…
如有问题,欢迎探讨,如果满意,请手动点赞,谢谢!🙏
及时获取更多姿势,请您关注!!