Browse Source

feat: 单人音视频功能改造

master
xsx 2 years ago
parent
commit
8f15c0fa24
  1. 3
      .gitignore
  2. 790
      im-ui/src/components/rtc/RtcGroupVideo.vue

3
.gitignore

@ -13,3 +13,6 @@
/im-uniapp/unpackage/
/im-uniapp/hybrid/
/im-uniapp/package-lock.json
/im-ui/src/components/rtc/LocalVideo.vue
/im-ui/src/components/rtc/RemoteVideo.vue
/im-ui/src/components/rtc/RtcGroupAcceptor.vue

790
im-ui/src/components/rtc/RtcGroupVideo.vue

@ -1,802 +1,42 @@
<template>
<div>
<el-dialog v-dialogDrag top="5vh" :close-on-click-modal="false" :close-on-press-escape="false"
:visible.sync="showRoom" :fullscreen="isFullScreen" :width="width+'px'" :before-close="onQuit">
<el-dialog v-dialogDrag top="5vh" title="语音通话" :close-on-click-modal="false" :close-on-press-escape="false"
:visible.sync="isShow" width="50%">
<div class='rtc-group-video'>
<div class="video-box" ref="videos" :style="`width:${width}px;`">
<div class="video-list">
<div :style="videoStyle" v-for="user in userInfos" :key="user.id">
<local-video v-if="user.id==mine.id" ref="localVideo" :userInfo="mine" :width="vw"
:height="vh"></local-video>
<remote-video v-if="user.id!=mine.id" :ref="'remoteVideo'+user.id" :userInfo="user"
:groupId="groupId" :width="vw" :height="vh"></remote-video>
<div style="padding-top:30px;font-weight: 600; text-align: center;font-size: 16px;">
多人音视频通话为付费功能有需要请联系作者...
</div>
<div style="padding-top:50px; text-align: center;font-size: 16px;">
点击下方文档了解详细信息:
</div>
<div style="padding-top:10px; text-align: center;font-size: 16px;">
<a href="https://www.yuque.com/u1475064/mufu2a/vi7engzluty594s2" target="_blank">
付费-音视频通话源码
</a>
</div>
<div class="control-bar">
<div class="icon-box" v-show="isReady && isMicroPhone">
<div class="icon iconfont icon-microphone-on icon-front" @click="onSwitchMicroPhone()"></div>
<div class="icon-text">麦克风已开</div>
</div>
<div class="icon-box" v-show="isReady && !isMicroPhone">
<div class="icon iconfont icon-microphone-off icon-back" @click="onSwitchMicroPhone()"></div>
<div class="icon-text">麦克风已关</div>
</div>
<div class="icon-box" v-show="isReady && isSpeaker">
<div class="icon iconfont icon-speaker-on icon-front" @click="onSwitchSpeaker()"></div>
<div class="icon-text">扬声器已开</div>
</div>
<div class="icon-box" v-show="isReady && !isSpeaker">
<div class="icon iconfont icon-speaker-off icon-back" @click="onSwitchSpeaker()"></div>
<div class="icon-text">扬声器已关</div>
</div>
<div class="icon-box" v-show="isReady && isCamera">
<div class="icon iconfont icon-camera-on icon-front" @click="onSwitchCamera()"></div>
<div class="icon-text">摄像头已开</div>
</div>
<div class="icon-box" v-show="isReady && !isCamera">
<div class="icon iconfont icon-camera-off icon-back" @click="onSwitchCamera()"></div>
<div class="icon-text">摄像头已关</div>
</div>
<div class="icon-box" v-show="isReady">
<i class="icon iconfont icon-invite-rtc icon-back" @click="onInviteMmeber()"></i>
<div class="icon-text">邀请</div>
</div>
<div class="icon-box">
<div class="icon iconfont icon-quit" @click="onQuit()"></div>
<div class="icon-text">退出</div>
</div>
</div>
</div>
<template slot="title">
<div class="header">
<span class="title">
{{title}}
</span>
<i class="icon el-icon-full-screen" @click="onSwitchFullScreen()"></i>
</div>
</template>
</el-dialog>
<rtc-group-acceptor v-if="isWaiting" ref="acceptor" :groupId="groupId" :userInfos="userInfos"
:inviterId="inviterId" @accept="onAccept" @reject="onReject"></rtc-group-acceptor>
<group-member-selector ref="rtcSel" :groupId="groupId" @complete="onInviteOk"></group-member-selector>
</div>
</template>
<script>
import RemoteVideo from './RemoteVideo.vue';
import LocalVideo from './LocalVideo.vue';
import RtcGroupAcceptor from './RtcGroupAcceptor'
import ImWebRtc from '@/api/webrtc';
import ImCamera from '@/api/camera';
import RtcGroupApi from '@/api/rtcGroupApi'
import GroupMemberSelector from '@/components/group/GroupMemberSelector'
export default {
name: "rtcGroupVideo",
components: {
LocalVideo,
RemoteVideo,
RtcGroupAcceptor,
GroupMemberSelector
},
data() {
return {
camera: new ImCamera(), //
webrtc: new ImWebRtc(), // webrtc
API: new RtcGroupApi(),
audio: new Audio(), //
showRoom: false, //
groupId: null, // id
isHost: false, //
inviterId: null, // i
userInfos: [], //
stream: null, //
isMicroPhone: true, //
isSpeaker: true, //
isCamera: false, //
chatTime: 0, //
waitTimer: null, //
chatTimer: null, //
heartbeatTimer: null, //
tipTimer: null, //
lastTipTime: null, //
state: "INIT", // INIT: WAITING: CHATING: CLOSE: ERROR:
isFullScreen: false, //
width: 800, //
vw: 200, //
vh: 200 //
isShow: false
}
},
methods: {
open(rtcInfo) {
this.showRoom = true;
this.groupId = rtcInfo.groupId;
this.isHost = rtcInfo.isHost;
this.inviterId = rtcInfo.inviterId;
this.userInfos = rtcInfo.userInfos;
this.init();
if (this.isHost) {
//
this.onSetup();
} else if (this.mine.id == this.inviterId) {
//
this.onJoin();
}
},
init() {
//
this.initAudio();
//
this.startHeartBeat();
},
initAudio() {
let url = require(`@/assets/audio/call.wav`);
this.audio.src = url;
this.audio.loop = true;
},
onSwitchFullScreen() {
this.isFullScreen = !this.isFullScreen;
this.refreshVideoWH();
},
onInviteMmeber() {
let ids = [];
this.userInfos.forEach((user) => {
ids.push(user.id);
})
this.$refs.rtcSel.open(this.maxChannel, ids, ids);
},
onInviteOk(members) {
//
let userInfos = [];
members.forEach(m => {
if (!this.isExist(m.userId)) {
userInfos.push({
id: m.userId,
nickName: m.aliasName,
headImage: m.headImage,
isCamera: false,
isMicroPhone: true
})
}
})
if (userInfos.length > 0) {
this.API.invite(this.groupId, userInfos).then(() => {
this.appendUser(userInfos);
});
}
},
onSwitchMicroPhone() {
this.isMicroPhone = !this.isMicroPhone;
this.$refs.localVideo[0].setMicroPhone(this.isMicroPhone);
// /
this.API.device(this.groupId, this.isCamera, this.isMicroPhone);
console.log("麦克风:" + this.isMicroPhone)
},
onSwitchCamera() {
this.isCamera = !this.isCamera;
this.mine.isCamera = this.isCamera;
//
this.openStream().then(() => {
//
this.userInfos.forEach((user) => {
if (user.id != this.mine.id) {
const refName = 'remoteVideo' + user.id;
//
this.$refs[refName][0].reconnect(this.stream);
}
})
// /
this.API.device(this.groupId, this.isCamera, this.isMicroPhone);
console.log("摄像头:" + this.isCamera);
})
},
onSwitchSpeaker() {
this.isSpeaker = !this.isSpeaker;
//
this.userInfos.forEach((user) => {
if (user.id != this.mine.id) {
const refName = 'remoteVideo' + user.id;
this.$refs[refName][0].setMute(!this.isSpeaker);
}
})
console.log("扬声器切换:" + this.isSpeaker);
},
onSetup() {
if (!this.checkDevEnable()) {
this.close();
}
//
this.openStream().finally(() => {
//
this.initUserVideo()
//
this.API.setup(this.groupId, this.userInfos).then(() => {
//
this.state = "READY";
//
this.audio.play();
}).catch(() => {
this.close();
})
})
},
onJoin() {
if (!this.checkDevEnable()) {
this.close();
}
//
this.openStream().finally(() => {
//
this.initUserVideo();
//
this.API.join(this.groupId).then(() => {
//
this.state = "CHATING";
}).catch((e) => {
this.close();
});
})
},
onAccept() {
if (!this.checkDevEnable()) {
this.API.failed(this.groupId, "设备不支持通话")
this.close();
return;
}
//
this.showRoom = true;
this.state = "CHATING";
//
this.audio.pause();
//
this.openStream().finally(() => {
//
this.initUserVideo();
//
this.API.accept(this.groupId).then(() => {
//
this.startChatTime();
//
this.waitTimer && clearTimeout(this.waitTimer);
}).catch(() => {
this.close();
})
})
},
onReject() {
// 退
this.API.reject(this.groupId);
// 退
this.close();
},
onQuit() {
if (this.isClose) {
return;
}
if (this.isHost && !this.isChating) {
//
console.log("onQuit")
this.API.cancel(this.groupId);
} else if (!this.isHost && this.state == "WAITING") {
//
this.API.reject(this.groupId);
} else if (this.isChating) {
// 退
console.log("强制退出聊天");
this.API.quit(this.groupId);
}
// 退
this.close();
},
onCancel() {
console.log("onCancel")
//
this.API.cancel(this.groupId);
// 退
this.close();
},
onRTCMessage(msg) {
//
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_GROUP_SETUP &&
this.state == 'CLOSE') {
return;
}
// RTC
switch (msg.type) {
case this.$enums.MESSAGE_TYPE.RTC_GROUP_SETUP:
this.onRTCSetup(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_ACCEPT:
this.onRTCAccept(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_REJECT:
this.onRTCReject(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_JOIN:
this.onRTCJoin(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_FAILED:
this.onRTCFailed(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_CANCEL:
this.onRTCCancel(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_QUIT:
this.onRTCQuit(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_INVITE:
this.onRTCInvite(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_OFFER:
this.onRTCOffer(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_ANSWER:
this.onRTCAnswer(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_CANDIDATE:
this.onRTCCandidate(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_GROUP_DEVICE:
this.onRTCDevice(msg)
break;
}
},
onRTCSetup(msg) {
//
this.isHost = false;
this.userInfos = JSON.parse(msg.content);
this.inviterId = msg.sendId;
this.groupId = msg.groupId;
//
this.init();
//
this.state = "WAITING";
//
this.audio.play();
// 30s
this.waitTimer = setTimeout(() => {
this.API.reject(this.groupId);
this.close("您未接听");
}, 30000)
},
onRTCAccept(msg) {
if (msg.selfSend) {
//
this.close("已在其他设备接听");
return;
}
//
const remoteUserId = msg.sendId;
if (this.isChating) {
//
const refName = 'remoteVideo' + remoteUserId;
this.$refs[refName][0].connect();
} else if (this.isHost) {
//
const refName = 'remoteVideo' + remoteUserId;
this.$refs[refName][0].connect();
//
this.state = 'CHATING';
//
this.audio.pause();
//
this.startChatTime();
}
},
onRTCReject(msg) {
if (msg.selfSend) {
//
this.close("已在其他设备拒绝");
return;
}
//
const remoteUserId = msg.sendId;
//
this.removeUser(remoteUserId, "未进入通话")
},
onRTCFailed(msg) {
//
const remoteUserId = msg.sendId;
const failedInfo = JSON.parse(msg.content);
let nickNames = []
failedInfo.userIds.forEach((userId) => {
nickNames.push(`'${this.userInfos.find(u => u.id == userId).nickName}'`);
})
let tip = nickNames.join(",") + failedInfo.reason;
this.$message.warning(tip)
//
failedInfo.userIds.forEach((userId) => this.removeUser(userId));
},
onRTCJoin(msg) {
//
const remoteUserId = msg.sendId;
let userInfo = JSON.parse(msg.content);
if (!this.isExist(remoteUserId)) {
this.userInfos.push(userInfo);
}
this.$nextTick(() => {
//
this.initUserVideo();
//
const refName = 'remoteVideo' + remoteUserId;
this.$refs[refName][0].connect();
})
//
this.state = 'CHATING';
//
this.audio.pause();
//
this.startChatTime();
},
onRTCOffer(msg) {
//
const remoteUserId = msg.sendId;
const offer = JSON.parse(msg.content)
//
const refName = 'remoteVideo' + remoteUserId;
this.$refs[refName][0].onOffer(offer);
},
onRTCAnswer(msg) {
//
const remoteUserId = msg.sendId;
const answer = JSON.parse(msg.content);
// answer
const refName = 'remoteVideo' + remoteUserId;
this.$refs[refName][0].onAnswer(answer);
},
onRTCCancel() {
// 退
this.close("通话已取消");
},
onRTCQuit(msg) {
// 退
const remoteUserId = msg.sendId;
//
this.removeUser(remoteUserId, "退出通话");
},
onRTCInvite(msg) {
//
let userInfos = JSON.parse(msg.content);
//
this.appendUser(userInfos);
},
onRTCCandidate(msg) {
//
const remoteUserId = msg.sendId;
const candidate = JSON.parse(msg.content);
// answer
const refName = 'remoteVideo' + remoteUserId;
this.$refs[refName][0].setCandidate(candidate);
},
onRTCDevice(msg) {
//
const remoteUserId = msg.sendId;
const devInfo = JSON.parse(msg.content);
// /
let userInfo = this.userInfos.find(u => u.id == remoteUserId);
userInfo.isCamera = devInfo.isCamera;
userInfo.isMicroPhone = devInfo.isMicroPhone;
},
checkDevEnable() {
//
if (!this.camera.isEnable()) {
this.message.error("访问摄像头失败");
return false;
}
// webrtc
if (!this.webrtc.isEnable()) {
this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC");
return false;
}
return true;
},
openStream() {
return new Promise((resolve, reject) => {
if (this.isCamera) {
// +
this.camera.openVideo().then((stream) => {
console.log("摄像头打开成功")
this.stream = stream;
//
this.$refs.localVideo[0].open(stream);
resolve(stream);
}).catch((e) => {
this.$message.error("打开摄像头失败")
console.log("本地摄像头打开失败:" + e.message)
reject(e);
})
} else {
//
this.camera.openAudio().then((stream) => {
console.log("麦克风打开成功")
this.stream = stream;
//
this.$refs.localVideo[0].open(stream);
resolve(stream);
}).catch((e) => {
this.$message.error("打开麦克风失败")
console.log("本地摄像头打开失败:" + e.message)
reject(e);
})
open() {
this.isShow = true;
}
})
},
initUserVideo() {
//
this.userInfos.forEach(user => {
if (user.id != this.mine.id) {
const refName = 'remoteVideo' + user.id
//
if (!this.$refs[refName][0].isInit) {
this.$refs[refName][0].init(this.stream)
}
}
})
},
removeUser(userId, tip) {
if (!this.isExist(userId)) {
return;
}
//
const refName = 'remoteVideo' + userId;
if (this.$refs[refName]) {
this.$refs[refName][0].close();
}
//
const idx = this.userInfos.findIndex(user => user.id == userId);
//
const userInfo = this.userInfos.splice(idx, 1)[0];
if (this.isHost && tip) {
this.$message.warning(`'${userInfo.nickName}'${tip}`);
}
// 退
if (this.userInfos.length <= 1) {
this.onQuit();
}
},
appendUser(userInfos) {
userInfos.forEach(user => {
if (!this.isExist(user.id)) {
this.userInfos.push(user);
console.log(`'${user.nickName}'加入通话`);
}
})
//
this.$nextTick(() => {
this.initUserVideo();
})
},
isExist(userId) {
return !!this.userInfos.find(u => u.id == userId);
},
refreshVideoWH() {
let extendH = this.isFullScreen ? 150 : 250;
let w = window.innerWidth;
let h = window.innerHeight - extendH;
let row = Math.ceil(Math.sqrt(this.userInfos.length));
let col = Math.ceil(this.userInfos.length / row);
let vw = w / row;
let vh = h / col;
let size = Math.min(vw, vh);
if (vw > vh * 1.2) {
this.vw = 1.2 * size;
this.vh = size;
} else if (vh > vw * 1.2) {
this.vh = 1.2 * size;
this.vw = size;
} else {
this.vh = size;
this.vw = size;
}
this.width = this.vw * row;
},
startChatTime() {
if (!this.chatTimer) {
this.chatTime = 0;
this.chatTimer = setInterval(() => {
this.chatTime++;
}, 1000)
}
},
startHeartBeat() {
// 15s
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
this.API.heartbeat(this.groupId);
}, 15000)
},
close(tip) {
//
tip && this.$message.success(tip);
//
this.audio.pause();
//
this.waitTimer && clearTimeout(this.waitTimer);
this.chatTimer && clearInterval(this.chatTimer);
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
//
this.isMicroPhone = true,
this.isSpeaker = true,
this.isCamera = false,
this.chatTime = 0,
//
this.state = "CLOSE";
//
this.camera.close();
//
this.userInfos.forEach((user) => {
if (user.id != this.mine.id) {
const refName = 'remoteVideo' + user.id
if (this.$refs[refName]) {
this.$refs[refName][0].close();
}
}
})
//
this.showRoom = false;
//
this.userInfos = [];
this.groupId = null;
this.isHost = false;
this.inviterId = null;
this.isMicroPhone = true;
this.isSpeaker = true;
this.isCamera = false;
this.chatTime = 0;
this.waitTimer = null;
this.chatTimer = null;
this.heartbeatTimer = null;
}
},
computed: {
mine() {
let userId = this.$store.state.userStore.userInfo.id;
return this.userInfos.find(user => user.id == userId);
},
isChating() {
return this.state == "CHATING";
},
isReady() {
return this.state == "CHATING" || this.state == "READY";
},
isWaiting() {
return this.state == "WAITING";
},
isClose() {
return this.state == "CLOSE";
},
videoStyle() {
return `width:${this.vw}px;height:${this.vh}px;`
},
userCount() {
return this.userInfos.length;
},
chatTimeString() {
let min = Math.floor(this.chatTime / 60);
let sec = this.chatTime % 60;
let strTime = min < 10 ? "0" : "";
strTime += min;
strTime += ":";
strTime += sec < 10 ? "0" : "";
strTime += sec;
return strTime;
},
title() {
let title = "语音通话";
if (this.isChating) {
title += `(${this.chatTimeString})`
}
return title;
},
maxChannel() {
return this.$store.state.configStore.webrtc.maxChannel;
}
},
watch: {
userCount: {
handler(newCount, oldCount) {
this.refreshVideoWH();
}
}
},
created() {
//
window.addEventListener('beforeunload', () => {
this.onQuit();
});
},
beforeUnmount() {
//
console.log("beforeUnmount")
this.onQuit();
}
}
</script>
<style lang="scss">
.header {
position: relative;
color: white;
.icon {
position: absolute;
right: 30px;
line-height: 20px;
cursor: pointer;
}
}
.el-dialog__body {
padding: 0 !important;
}
.rtc-group-video {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
.video-box {
position: relative;
background-color: white;
.video-list {
display: flex;
flex-wrap: wrap;
position: relative;
}
}
.control-bar {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100px;
height: 300px;
background-color: #E8F2FF;
.icon-box {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 25px;
.icon {
border-radius: 50%;
padding: 10px;
font-size: 25px;
cursor: pointer;
}
.icon-front {
color: black;
background-color: white;
}
.icon-back {
color: white;
background-color: black;
}
.icon-quit {
color: white;
background-color: red;
}
.icon-text {
color: black;
font-size: 14px;
margin-top: 10px;
}
}
}
}
</style>
Loading…
Cancel
Save