说明
小程序这边一进来就会建立websocket链接,将必要参数给到后端,然后等待后端生成房间号并等待客服进入房间,才下发房间号给到小程序.这里是小程序收到房间号后进入房间的实现.
效果图
等待接听…
接听后…
安装引入
npm i trtc-wx-sdk
import TRTC from 'trtc-wx-sdk'
代码如下
这里我封装成了组件,这里sdkappid和secretKey以及签名都是来自后端接口,没有使用debug的模拟.
页面使用:
<div class="calling-video">
<business-tencentCalling id="trtcComponent" :config="playVidoe.input.rtcConfig" ref="trtcComponent" :isEnterRoom.sync="isEnterRoom"></business-tencentCalling>
</div>
组件内部
<template>
<view class="template-1v1">
<view v-for="(item,index) in playerList" :key="index" v-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"
@statechange="_playerStateChange"
@fullscreenchange="_playerFullscreenChange"
@netstatus="_playerNetStatus"
@audiovolumenotify ="_playerAudioVolumeNotify"
/>
</view>
<view v-if="pusher.enableMic" :class="'view-container pusher-container ' + (pusher.isVisible ? '' : 'none') + ' ' +'fullscreen'">
<live-pusher
class="pusher"
v-show="pusher.enableCamera"
: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"
@statechange="pusherStateChangeHandlerFun"
@netstatus="_pusherNetStatusHandler"
@error="_pusherErrorHandler"
@bgmstart="_pusherBGMStartHandler"
@bgmprogress="_pusherBGMProgressHandler"
@bgmcomplete="_pusherBGMCompleteHandler"
@audiovolumenotify="_pusherAudioVolumeNotify"
/>
<view class="loading" v-if="!_isEnterRoom">
<view class="loading-text">等待接听中...</view>
</view>
</view>
<cover-view v-if="_isEnterRoom" class="calling-time">通话时间 {{ videoOrVoiceObj.msgIPtime.longTime}}</cover-view>
<view class="handle-btns" v-if="videoBtnsVisible&&hasEnterRoom">
<!--视频/语音 toggleVideoFun -->
<cover-view class="box-wrap">
<cover-view class="btn-normal" @click="_toggleVideoFun">
<cover-image class="closeVideoImg" v-if="pusher.enableCamera" src="/static/img/closeCamera.png"></cover-image>
<cover-image class="videoImg" v-else src="/static/img/camera.png"></cover-image>
</cover-view>
<cover-view class="btn-word">{{pusher.enableCamera?'关闭摄像头':'打开摄像头'}}</cover-view>
</cover-view>
<!-- 挂断 -->
<cover-view class="box-wrap">
<cover-view class="btn-hangup" @click="_hangUp"><cover-image src="/static/img/hangup.png"></cover-image></cover-view>
<cover-view class="btn-word">挂断</cover-view>
</cover-view>
<!-- 翻转镜头 -->
<cover-view class="box-wrap">
<cover-view class="btn-normal" @click="_pusherSwitchCamera"><cover-image src="/static/img/turnCamera.png"></cover-image></cover-view>
<cover-view class="btn-word">翻转镜头</cover-view>
</cover-view>
</view>
<cover-view class="bottom-btns" v-if="videoBtnsVisible&&hasEnterRoom">
<cover-view class="box-wrap" >
<cover-view class="btn-normal" @click="_setPlayerSoundMode">
<cover-image class="closeVideoImg" v-if="playerList[0].soundMode === 'ear'" src="/static/img/ear_sound.png"></cover-image>
<cover-image class="videoImg" v-else src="/static/img/outer_sound.png"></cover-image>
</cover-view>
<cover-view class="btn-word" v-if="playerList[0].soundMode === 'ear'">听筒</cover-view>
<cover-view class="btn-word" v-else>免提</cover-view>
</cover-view>
</cover-view>
</view>
</template>
<style lang="scss" scoped>
/* 1v1 视频电话模式 */
.template-1v1{
width: 100vw;
display: flex;
height: 100vh;
position: relative;
}
.template-1v1 .pusher-container{
width: 100vw;
height: 100vh;
position: absolute;
right: 0;
top: 0;
z-index: 2;
}
.template-1v1 .pusher-container.fullscreen{
width: 100vw;
height: 100vh !important;
display: flex;
height: 0rpx;
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: 18vh;
right: 0;
width: 100vw;
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.template-1v1 .bottom-btns {
position: absolute;
z-index: 3;
bottom: 2vh;
right: 0;
width: 100vw;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.calling-time{
position: absolute;
z-index: 3;
bottom: 35vh;
font-size: 36rpx;
left: 0;
width: 100vw;
color: #fff;
text-align: center;
}
.template-1v1 .handle-btns .box-wrap,.template-1v1 .bottom-btns .box-wrap{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 140rpx;
height: auto;
}
.template-1v1 .handle-btns .box-wrap .btn-word,.template-1v1 .bottom-btns .box-wrap .btn-word{
color: #fff;
margin-top: 30rpx;
}
.template-1v1 .btn-normal {
width: 110rpx;
height: 110rpx;
box-sizing: border-box;
display: flex;
border:1rpx solid white;
border-radius: 50%;
justify-content: center;
align-items: center;
border-radius: 50%;
}
.template-1v1 .btn-normal cover-image{
width: 50rpx;
height: auto;
margin-bottom: 2rpx;
}
.template-1v1 .btn-normal .closeVideoImg{
width: 58rpx;
}
.template-1v1 .btn-normal .videoImg{
width: 64rpx;
}
.template-1v1 .btn-hangup {
width: 110rpx;
height: 110rpx;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
}
.template-1v1 .btn-hangup cover-image{
width: 110rpx;
height: 110rpx;
}
</style>
import TRTC from 'trtc-wx-sdk'
export default {
data () {
return {
TRTC:undefined,
pusher:undefined,
playerList:[],
//控制视频按钮显隐
videoBtnsVisible:true,
//进房状态
// isEnterRoom:false,
//控制视频按钮显隐
videoBtnsVisible:true,
//自己写一个计时功能
courentTime:0,
videoOrVoiceObj:{
//计时器
msgIPtime:{
timer: "",
content: "",
hour: 0,
minutes: 0,
seconds: 0,
longTime:'00:00:00'
}
},
//是否执行进房操作
hasEnterRoom:false
}
},
beforeMount(options) {
this.TRTC = new TRTC()
//初始化数据 房间号 签名等
this.init(options)
this.bindTRTCRoomEvent()
},
props:{
config:{
type: Object,
default: () => ({
sdkAppID: '',
userID: '',
userSig: '',
template: '',
debugMode: ''
})
},
//进房结果 成功:true/失败:false
isEnterRoom:{
type: Boolean,
default: false
},
},
methods:{
init() {
// pusher 初始化参数
const pusherConfig = {
beautyLevel: 9,
}
const pusher = this.TRTC.createPusher(pusherConfig)
console.log(pusher.pusherAttributes, '000')
this.pusher= pusher.pusherAttributes
},
toJSON(){
return this
},
async enterRoom(options,callback) {
//执行了进房操作
this.hasEnterRoom = true
//进房状态
let enterRoomStatus={
isOk:false,
msg:undefined,
info:{}
}
const roomID = options.roomID
const config = Object.assign(this.config, { roomID })
//校验参数
if (!this.methods('checkParamFun',this.config)) {
enterRoomStatus.msg = '缺少必要参数'
enterRoomStatus.info = {}
callback(enterRoomStatus)
return;
}
//
config.enableMic= true, // 进房默认开启音频上行
config.enableCamera= false, // 进房默认开启视频上行
config.frontCamera= 'back', // 进房默认开启视频上行
new Promise((r)=>{
try{
this.pusher=this.TRTC.enterRoom(config)
r()
}catch(res){
console.error(TAG_NAME, '进房失败', res);
enterRoomStatus.msg = '进房失败'
enterRoomStatus.info = res
wx.showToast({
title: '组件内进房失败'+JSON.stringify(res),
})
callback(enterRoomStatus)
r()
}
}).then(()=>{
this.TRTC.getPusherInstance().start() // 开始推流(autoPush的模式下不需要)
console.log('开始推流',this.pusher)
enterRoomStatus.msg = '进房成功'
enterRoomStatus.isOk = true
enterRoomStatus.info = {}
callback(enterRoomStatus)
})
},
exitRoom() {
const result = this.TRTC.exitRoom()
this.pusher= result.pusher
this.playerList=result.playerList
},
//隐藏
hiddenBtns(){
console.log('隐藏')
this.videoBtnsVisible = false
},
//显示
refreshBtns(){
console.log('显示')
this.videoBtnsVisible = true
},
//我写的计时功能
initTime() {
console.log('不执行吗?')
/*时间处理*/
this.timer = setInterval(() => {
this.courentTime = new Date().getTime();
}, 1000);
},
//通话时长
ipVoiceOrVideoLongTimeClick(){
this.videoOrVoiceObj.msgIPtime.timer = setInterval(this.startMsgIPtimeTimer, 1000);
},
startMsgIPtimeTimer () {
this.videoOrVoiceObj.msgIPtime.seconds += 1;
if ( this.videoOrVoiceObj.msgIPtime.seconds >= 60) {
this.videoOrVoiceObj.msgIPtime.seconds = 0;
this.videoOrVoiceObj.msgIPtime.minutes += 1;
}
if ( this.videoOrVoiceObj.msgIPtime.minutes >= 60) {
this.videoOrVoiceObj.msgIPtime.minutes = 0;
this.videoOrVoiceObj.msgIPtime.hour = this.videoOrVoiceObj.msgIPtime.hour + 1;
}
this.videoOrVoiceObj.msgIPtime.longTime =( this.videoOrVoiceObj.msgIPtime.hour < 10 ? '0' + this.videoOrVoiceObj.msgIPtime.hour : this.videoOrVoiceObj.msgIPtime.hour) + ':'+ ( this.videoOrVoiceObj.msgIPtime.minutes < 10 ? '0' + this.videoOrVoiceObj.msgIPtime.minutes : this.videoOrVoiceObj.msgIPtime.minutes) + ':' + ( this.videoOrVoiceObj.msgIPtime.seconds < 10 ? '0' + this.videoOrVoiceObj.msgIPtime.seconds : this.videoOrVoiceObj.msgIPtime.seconds);
},
resetMsgIPtime(){ //重置
clearInterval(this.videoOrVoiceObj.msgIPtime.timer);
this.videoOrVoiceObj.msgIPtime.hour=0;
this.videoOrVoiceObj.msgIPtime.minute=0;
this.videoOrVoiceObj.msgIPtime.ms=0;
this.videoOrVoiceObj.msgIPtime.seconds=0;
this.videoOrVoiceObj.msgIPtime.longTime="00:00:00";
},
stopMsgIPtime(){ //暂停
clearInterval(this.videoOrVoiceObj.msgIPtime.timer);
},
// 设置某个 player 属性
setPlayerAttributesHandler(player, options) {
this.playerList=this.TRTC.setPlayerAttributes(player.streamID, options)
},
// 事件监听
bindTRTCRoomEvent() {
console.log('有被调用')
const TRTC_EVENT = this.TRTC.EVENT
// 初始化事件订阅
this.TRTC.on(TRTC_EVENT.LOCAL_JOIN, (event) => {
this._isEnterRoom = true
//进房成功 开启计时功能
this.initTime()
this.ipVoiceOrVideoLongTimeClick()
console.log('* room LOCAL_JOIN 进房成功了', event)
console.log('进房成功人员',this.playerList)
})
this.TRTC.on(TRTC_EVENT.LOCAL_LEAVE, (event) => {
console.log('* room LOCAL_LEAVE', event)
})
this.TRTC.on(TRTC_EVENT.ERROR, (event) => {
console.log('* room ERROR', event)
})
// 远端用户退出
this.TRTC.on(TRTC_EVENT.REMOTE_USER_LEAVE, (event) => {
const { playerList } = event.data
this.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 })
})
},
// 是否订阅某一个player Audio
_mutePlayerAudio(event) {
const player = event.currentTarget.dataset.value
if (player.hasAudio && player.muteAudio) {
this.setPlayerAttributesHandler(player, { muteAudio: false })
return
}
if (player.hasAudio && !player.muteAudio) {
this.setPlayerAttributesHandler(player, { muteAudio: true })
return
}
},
// 订阅 / 取消订阅某一个player Video
_mutePlayerVideo(event) {
const player = event.currentTarget.dataset.value
if (player.hasVideo && player.muteVideo) {
this.setPlayerAttributesHandler(player, { muteVideo: false })
return
}
if (player.hasVideo && !player.muteVideo) {
this.setPlayerAttributesHandler(player, { muteVideo: true })
return
}
},
//打开摄像头
_toggleVideoFun() {
if (this.pusher.enableCamera) {
this.publishLocalVideo(false);
} else {
this.publishLocalVideo(true);
}
},
/**
* 摄像头开关
* @returns {Promise}
*/
publishLocalVideo(val) {
this.setPusherAttributesHandler({enableCamera: val})
},
/**
* 设置推流参数并触发页面渲染更新
* @param {Object} options live-pusher 的配置
* @returns {Promise}
*/
setPusherAttributesHandler(options) {
this.pusher= this.TRTC.setPusherAttributes(options)
this.TRTC.getPusherInstance().start() // 开始推流(autoPush的模式下不需要)
console.log('看下人员',this.playerList)
},
// 挂断退出房间
_hangUp() {
this.exitRoom()
uni.reLaunch({
url: '/pages/callOver/index',
fail: e=>{
uni.switchTab({
url:'/pages/callOver/index'
})
}
})
setTimeout(()=>{
this.exitRoom();
//停止定时器,重置计时时间
this.resetMsgIPtime()
this.stopMsgIPtime()
},300)
},
// 设置美颜
_setPusherBeautyHandle() {
const beautyLevel = this.pusher.beautyLevel === 0 ? 3 : 0
this.setPusherAttributesHandler({ beautyLevel })
},
// 发布 / 取消发布 Audio
_pusherAudioHandler() {
if (this.pusher.enableMic) {
this.setPusherAttributesHandler({ enableMic: false })
} else {
this.setPusherAttributesHandler({ enableMic: true })
}
},
_pusherSwitchCamera() {
const frontCamera = this.pusher.frontCamera === 'front' ? 'back' : 'front'
this.TRTC.getPusherInstance().switchCamera(frontCamera)
},
_setPlayerSoundMode() {
if (this.playerList.length === 0) {
return
}
const player = this.TRTC.getPlayerList()
const soundMode = player[0].soundMode === 'speaker' ? 'ear' : 'speaker'
this.setPlayerAttributesHandler(player[0], { soundMode })
},
// 请保持跟 wxml 中绑定的事件名称一致
pusherStateChangeHandlerFun(event) {
const code = event.detail.code;
const message = event.detail.message;
console.log('监控状态码', 'pusherStateChange:', code, 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)
},
//校验
checkParamFun(rtcConfig){
if (!rtcConfig.sdkAppID) {
console.error('未设置 sdkAppID');
this.methods("tips",'未设置 sdkAppID')
return false;
}
if (rtcConfig.roomID === undefined) {
console.error('未设置 roomID');
this.methods("tips",'未设置 roomID')
return false;
}
if (rtcConfig.roomID < 1 || rtcConfig.roomID > 4294967296) {
console.error('roomID 超出取值范围 1 ~ 4294967295');
this.methods("tips",'roomID 超出取值范围 1 ~ 4294967295')
return false;
}
if (!rtcConfig.userID) {
console.error('未设置 userID');
this.methods("tips",'未设置 userID')
return false;
}
if (!rtcConfig.userSig) {
console.error('未设置 userSig');
this.methods("tips",'未设置 userSig')
return false;
}
if (!rtcConfig.template) {
console.error('未设置 template');
this.methods("tips",'未设置 template')
return false;
}
return true
}
//提示
tips(str,len){
uni.showToast({
icon: "none",
duration: len?len:1000,
position: 'top',
title: str,
})
}
},
watch:{
config: {
handler: function(newVal, oldVal) {
console.log('watch config',newVal);
},
deep: true
},
},
computed:{
_isEnterRoom:{
get () {
return this.isEnterRoom || false
},
set (val) {
this.$emit('update:isEnterRoom', val)
}
},
}
}
注意一些坑sdkId和secretKey一定要对,签名可以先用debug的模拟.
出现这个是因为小程序appid不对
''渲染失败,错误原因:
insertLivePusher:fail jsapi has no permission,
event=insertLivePusher, runningState=foreground, permissionMsg-permission got, detail=jsapi has nopermission
添加v-if使得数据更新
,组件在一进页面就加载了,然后初始化了一个pusher,但是没有url,所以没有画面也接不通,等到进房之后重新赋值就友东西触发页面更新了,就有画面了.
无画面问题,但是这个v-if也使得元素消失,写法不对导致元素节点消失,所以关闭摄像头导致没有声音了.
有问题可以私聊详细交流