前置准备工作
- 请在微信小程序后台 -> 开发 -> 开发设置 -> 服务器域名配置中,将
wss://rtmpgate.cloudvdn.com
加到 socket 合法域名中,将https://pili-rtc-qos.qiniuapi.com
添加到 request 合法域名中 - 在小程序的开发后台打开实时播放/录制音视频流的开关(小程序后台 -> 开发 -> 接口设置)。
首先,先引入QNRTC,
npm install --save qnwxapp-rtc
然后在需要接入视频的地方
- 初始化client
- 绑定监听事件
- 获取roomtoken
- 调用join加入房间
import QNRTC, { QNLogLevel } from 'qnwxapp-rtc'
// 创建连接
this.client = QNRTC.createClient()
// 设置日志级别 避免太多log影响数据观测
QNRTC.setLogLevel(QNLogLevel.ERROR);
// 绑定监听事件
this.client.on("user-joined", (user, userData) => {
console.log("event: user-joined - 用户加入房间", user, userData);
});
this.client.on("user-left", (user) => {
console.log("event: user-left - 用户离开房间", user);
uni.$u.toast('对方已挂断通话')
});
this.client.on("user-published", async (user, tracks) => {
console.log("event: user-published - 用户发布", user, tracks);
if (user === userId) {
return false;
}
let config = {};
let videoTrackIndex = remoteTracks.findIndex((item) => item.isVideo());
let audioTrackIndex = remoteTracks.findIndex((item) => item.isAudio());
if (videoTrackIndex >= 0) {
config.videoTrack = remoteTracks[videoTrackIndex];
}
if (audioTrackIndex >= 0) {
config.audioTrack = remoteTracks[audioTrackIndex];
}
const url = await client.subscribe(config);
me.subscribeList = [...me.subscribeList,{
url,
key: user + Math.random().toString(36).slice(-8),
userID: user,
audioTrack: remoteTracks[audioTrackIndex],
videoTrack: remoteTracks[videoTrackIndex]
}]
console.log(me.subscribeList,'订阅列表')
});
this.client.on("user-unpublished", (user, tracks) => {
console.log("event: user-unpublished - 用户取消发布", user, tracks);
for (const track of tracks) {
this.subscribeList.map((ele, index) => {
if (ele.url.indexOf(track.trackID) !== -1) {
this.subscribeList.splice(index, 1);
}
});
}
});
this.client.on("connection-state-changed", (state, info) => {
console.log("event: connection-state-changed - 用户连接状态发生改变", state, info);
if (state === "DISCONNECTED" && info.reason === "ERROR") {
this.reconnect();
}
});
// 获取rooktoken getRoomToken(后端提供)
const {status,message, result} =await me.$api.getRoomToken({data:{room:'XXXX'}})
if(status === 200){
// 加入房间
await this.client.join(result,userInfo.get().userId);
this.publishHandler(this.client)
}else{
uni.$u.toast(message);
}
// 发布主题
publishHandler(client){
const me = this
me.client.publish((status, data) => {
/**
* status 共有 READY 、COMPLETED、ERROR 三种状态
* 其中 READY 表示推流就绪,返回推流的 rtmp 地址
* COMPLETED 表示推流成功,返回推流生成的音频轨和视频轨
* ERROR 则表示推流失败
*/
if(status === "READY") {
me.publishPath = data.url
console.log(data,'READY')
}else if(status === 'COMPLETED') {
console.log(data.tracks,'COMPLETED')
}else if(status === 'ERROR') {
console.log("发布失败")
}else{
console.log(status,data,'err')
}
})
console.log('已调用发布')
}
视频通话页面,基本代码源码 ,简单的参考下
<template>
<view class="content" :style="{width: windowWidth + 'px',height: windowHeight + 'px'}">
<!-- 远端用户 -->
<view class="video_local_1" >
<view style="width:200rpx;height: 300rpx;">
<live-player
ref="location"
style="width:200rpx;height: 300rpx;"
v-for="(item,index) in subscribeList" :key="index"
min-cache="0.1"
max-cache="0.1"
:playerid="item.key"
mode="RTC"
object-fit="fillCrop"
:src="item.url"
:muted="volume"
autoplay='true'
>
</live-player>
</view>
</view>
<!-- 本地用户 -->
<view class="video_local" :style="{width: windowWidth + 'px',height: windowHeight + 'px'}">
<view :style="{width: windowWidth + 'px',height: windowHeight + 'px'}">
<live-pusher
ref="location"
style="width:100%;height: 100%;"
autopush
mode="RTC"
:url="video? publishPath : ''"
min-bitrate="200"
max-bitrate="400"
:device-position="devicePosition"
:enable-camera="video"
:muted="!mic"
>
</live-pusher>
</view>
</view>
<!-- 相关操作 -->
<view class="options" >
<view style="display:flex;flex-direction: row;justify-content:space-around;margin-bottom: 20px;">
<view class="icon" @click="videoFn">
<image class="icon_img" src="@/static/vi_on.png" v-if="video"></image>
<image class="icon_img" src="@/static/vi_in.png" v-else></image>
<text class="icon_text">视频</text>
</view>
<view class="icon" @click="switchCamera">
<image class="icon_img" src="@/static/camera.png" mode=""></image>
<text class="icon_text">摄像{{camera?'前':'后'}}</text>
</view>
</view>
<view style="display:flex;" :style="{flexDirection: 'row',justifyContent: subscribeList.length ? 'space-between':'center'}">
<view class="icon" @click="audioFn" v-if="subscribeList.length">
<image class="icon_img" src="@/static/au_in.png" v-if="mic"></image>
<image class="icon_img" src="@/static/au_on.png" v-else></image>
<text class="icon_text">静音</text>
</view>
<view class="icon" @click="closeFn">
<image class="icon_img" src="@/static/over.png"></image>
<text class="icon_text">挂断</text>
</view>
<view class="icon" @click="speakerphoneFn" v-if="subscribeList.length">
<image class="icon_img" src="@/static/icon_speakers.png" v-if="!volume"></image>
<image class="icon_img" src="@/static/icon_speaker.png" v-else></image>
<text class="icon_text">扬声器</text>
</view>
</view>
</view>
</view>
</template>
<script>
import QNRTC, { QNLogLevel } from 'qnwxapp-rtc'
export default {
data() {
return {
publishPath:'',
enableCamera:true,
playerId:'',
// 远端视频容器样式
windowWidth: 200,
windowHeight: 400,
// 相关操作
mic: true,
audio: true, // 音频开关
Speakerphone: true, // 免提
video: true, // 视频开关
camera: true, // 摄像头前后
switchover: false, // 大小切换
client:null,
volume:false,
ctx:null
};
},
onLoad(){
this.ctx = wx.createLivePusherContext('pusher')
},
methods:{
speakerphoneFn(){
this.volume = !this.volume
},
// 切换摄像头
switchCamera() {
this.ctx.switchCamera()
this.camera = !this.camera
// this.devicePosition = this.devicePosition=='front' ? 'back' : 'front'
// if (this.pushContext) {
// this.pushContext.switchCamera();
// }
},
videoFn(){
this.video = !this.video
},
audioFn(){
this.mic = !this.mic
},
}
}
<script>
<style lang="scss" scoped>
.content {
background-color: #2F3041;
position: relative;
width: 100%;
}
.text {
color: #FFFFFF;
margin-top: 20px;
}
/* 提示 */
.hint {
position: fixed;
align-items: center;
}
.location {
width: 153px;
height: 204px;
border-radius: 6px;
position: fixed;
top: 60px;
right: 20px;
background-color: #2C405A;
}
.option {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.location-none {
flex: 1;
position: absolute;
top: 0;
bottom: 0;
right: 0;
justify-content: center;
align-items: center;
}
/* 用户标识 */
.user-hint {
position: absolute;
bottom: 0;
left: 0;
background-color: #2F3041;
opacity: 0.5;
padding: 4px 10px;
}
.hint-text {
color: #FFFFFF;
opacity: 1;
}
.CanvasView {
flex-wrap: wrap;
flex-direction: row;
padding: 60px 0 0;
}
.video_local {
flex: 1;
}
/* 相关操作 */
.options {
position: fixed;
bottom: 20px;
margin: 0 20px;
width: 90vw;
display:flex;
flex-direction: column;
justify-content: space-between;
}
.icon {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.icon_img {
width: 60px;
height: 60px;
}
.icon_text {
font-size: 28rpx;
color: #FFFFFF;
margin: 10px;
}
</style>