You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

500 lines
13 KiB

<template>
<div>
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false"
:visible.sync="showRoom" width="50%" height="70%" :before-close="onQuit">
<div class="rtc-private-video">
<div v-show="isVideo" class="rtc-video-box">
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.3)" >
<head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName"
:url="friend.headImage">
</head-image>
<video ref="remoteVideo" autoplay=""></video>
</div>
<div class="rtc-video-mine">
<video ref="localVideo" autoplay=""></video>
</div>
</div>
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.3)">
<head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName"
:url="friend.headImage">
<div class="rtc-voice-name">{{friend.nickName}}</div>
2 years ago
</head-image>
</div>
<div class="rtc-control-bar">
<div title="取消" class="icon iconfont icon-phone-reject reject"
style="color: red;" @click="onQuit()"></div>
</div>
</div>
</el-dialog>
<rtc-private-acceptor v-if="!isHost&&isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept"
@reject="onReject"></rtc-private-acceptor>
</div>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue';
import ImWebRtc from '@/api/webrtc';
import ImCamera from '@/api/camera';
import RtcPrivateApi from '@/api/rtcPrivateApi'
export default {
name: 'rtcPrivateVideo',
components: {
HeadImage,
RtcPrivateAcceptor
},
data() {
return {
camera: new ImCamera(), // 摄像头和麦克风
webrtc: new ImWebRtc(), // webrtc相关
API: new RtcPrivateApi(), // API
audio: new Audio(), // 呼叫音频
showRoom: false,
friend: {},
isHost: false, // 是否发起人
state: "CLOSE", // CLOSE:关闭 WAITING:等待呼叫或接听 CHATING:聊天中 ERROR:出现异常
mode: 'video', // 模式 video:视频聊 voice:语音聊天
localStream: null, // 本地视频流
remoteStream: null, // 对方视频流
videoTime: 0,
videoTimer: null,
heartbeatTimer: null,
candidates: [],
}
},
methods: {
open(rtcInfo) {
this.showRoom = true;
this.mode = rtcInfo.mode;
this.isHost = rtcInfo.isHost;
this.friend = rtcInfo.friend;
if (this.isHost) {
this.onCall();
}
},
initAudio() {
let url = require(`@/assets/audio/call.wav`);
this.audio.src = url;
this.audio.loop = true;
},
initRtc() {
this.webrtc.init(this.configuration)
this.webrtc.setupPeerConnection((stream) => {
this.$refs.remoteVideo.srcObject = stream;
this.remoteStream = stream;
})
// 监听候选信息
this.webrtc.onIcecandidate((candidate) => {
if (this.state == "CHATING") {
// 连接已就绪,直接发送
this.API.sendCandidate(this.friend.id, candidate);
} else {
// 连接未就绪,缓存起来,连接后再发送
this.candidates.push(candidate)
}
})
// 监听连接成功状态
this.webrtc.onStateChange((state) => {
if (state == "connected") {
console.log("webrtc连接成功")
} else if (state == "disconnected") {
console.log("webrtc连接断开")
}
})
},
onCall() {
if (!this.checkDevEnable()) {
this.close();
}
// 初始化webrtc
this.initRtc();
// 启动心跳
this.startHeartBeat();
// 打开摄像头
this.openStream().then(() => {
this.webrtc.setStream(this.localStream);
this.webrtc.createOffer().then((offer) => {
// 发起呼叫
this.API.call(this.friend.id, this.mode, offer).then(() => {
// 直接进入聊天状态
this.state = "WAITING";
// 播放呼叫铃声
this.audio.play();
}).catch(()=>{
this.close();
})
})
}).catch(()=>{
// 呼叫方必须能打开摄像头,否则无法正常建立连接
this.close();
})
},
onAccept() {
if (!this.checkDevEnable()) {
this.API.failed(this.friend.id, "对方设备不支持通话")
this.close();
return;
}
// 进入房间
this.showRoom = true;
this.state = "CHATING";
// 停止呼叫铃声
this.audio.pause();
// 初始化webrtc
this.initRtc();
// 打开摄像头
this.openStream().finally(() => {
this.webrtc.setStream(this.localStream);
this.webrtc.createAnswer(this.offer).then((answer) => {
this.API.accept(this.friend.id, answer);
// 记录时长
this.startChatTime();
// 清理定时器
this.waitTimer && clearTimeout(this.waitTimer);
})
})
},
onReject() {
console.log("onReject")
// 退出通话
this.API.reject(this.friend.id);
// 退出
this.close();
},
onHandup() {
this.API.handup(this.friend.id)
this.$message.success("您已挂断,通话结束")
this.close();
},
onCancel() {
this.API.cancel(this.friend.id)
this.$message.success("已取消呼叫,通话结束")
this.close();
},
onRTCMessage(msg) {
// 除了发起通话,如果在关闭状态就无需处理
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
this.isClose) {
return;
}
// RTC信令处理
switch (msg.type) {
case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE:
this.onRTCCall(msg, 'voice')
break;
case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO:
this.onRTCCall(msg, 'video')
break;
case this.$enums.MESSAGE_TYPE.RTC_ACCEPT:
this.onRTCAccept(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_REJECT:
this.onRTCReject(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_CANCEL:
this.onRTCCancel(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_FAILED:
this.onRTCFailed(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_HANDUP:
this.onRTCHandup(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE:
this.onRTCCandidate(msg)
break;
}
},
onRTCCall(msg, mode) {
this.offer = JSON.parse(msg.content);
this.isHost = false;
this.mode = mode;
this.$http({
url: `/friend/find/${msg.sendId}`,
method: 'get'
}).then((friend) => {
this.friend = friend;
this.state = "WAITING";
this.audio.play();
this.startHeartBeat();
// 30s未接听自动挂掉
this.waitTimer = setTimeout(() => {
this.API.failed(this.friend.id,"对方无应答");
this.$message.error("您未接听");
this.close();
}, 30000)
})
},
onRTCAccept(msg) {
if (msg.selfSend) {
// 在其他设备接听
this.$message.success("已在其他设备接听");
this.close();
} else {
// 对方接受了的通话
let offer = JSON.parse(msg.content);
this.webrtc.setRemoteDescription(offer);
// 状态为聊天中
this.state = 'CHATING'
// 停止播放语音
this.audio.pause();
// 发送candidate
this.candidates.forEach((candidate) => {
this.API.sendCandidate(this.friend.id, candidate);
})
// 开始计时
this.startChatTime();
}
},
onRTCReject(msg) {
if (msg.selfSend) {
this.$message.success("已在其他设备拒绝");
this.close();
} else {
this.$message.error("对方拒绝了您的通话请求");
this.close();
}
},
onRTCFailed(msg) {
// 呼叫失败
this.$message.error(msg.content)
this.close();
},
onRTCCancel() {
// 对方取消通话
this.$message.success("对方取消了呼叫");
this.close();
},
onRTCHandup() {
// 对方挂断
this.$message.success("对方已挂断");
this.close();
},
onRTCCandidate(msg) {
let candidate = JSON.parse(msg.content);
this.webrtc.addIceCandidate(candidate);
},
openStream() {
return new Promise((resolve, reject) => {
if (this.isVideo) {
// 打开摄像头+麦克风
this.camera.openVideo().then((stream) => {
this.localStream = stream;
this.$nextTick(() => {
this.$refs.localVideo.srcObject = stream;
this.$refs.localVideo.muted = true;
})
resolve(stream);
}).catch((e) => {
this.$message.error("打开摄像头失败")
console.log("本摄像头打开失败:" + e.message)
reject(e);
})
} else {
// 打开麦克风
this.camera.openAudio().then((stream) => {
this.localStream = stream;
this.$refs.localVideo.srcObject = stream;
resolve(stream);
}).catch((e) => {
this.$message.error("打开麦克风失败")
console.log("打开麦克风失败:" + e.message)
reject(e);
})
}
})
},
startChatTime() {
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.videoTimer = setInterval(() => {
this.videoTime++;
}, 1000)
},
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;
},
startHeartBeat() {
// 每15s推送一次心跳
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
this.API.heartbeat(this.friend.id);
}, 15000)
},
close() {
this.showRoom = false;
this.camera.close();
this.webrtc.close();
this.audio.pause();
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.waitTimer && clearTimeout(this.waitTimer);
this.videoTimer = null;
this.heartbeatTimer = null;
this.waitTimer = null;
this.state = 'CLOSE';
this.candidates = [];
},
onQuit() {
if (this.isChating) {
this.onHandup()
} else if (this.isWaiting) {
this.onCancel();
} else {
this.close();
}
}
},
computed: {
title() {
let strTitle = `${this.modeText}通话-${this.friend.nickName}`;
if (this.isChating) {
strTitle += `(${this.currentTime})`;
} else if (this.isWaiting) {
strTitle += `(呼叫中)`;
}
return strTitle;
},
currentTime() {
let min = Math.floor(this.videoTime / 60);
let sec = this.videoTime % 60;
let strTime = min < 10 ? "0" : "";
strTime += min;
strTime += ":"
strTime += sec < 10 ? "0" : "";
strTime += sec;
return strTime;
},
configuration() {
const iceServers = this.$store.state.configStore.webrtc.iceServers;
return {
iceServers: iceServers
}
},
isVideo() {
return this.mode == "video"
},
modeText() {
return this.isVideo ? "视频" : "语音";
},
isChating() {
return this.state == "CHATING";
},
isWaiting() {
return this.state == "WAITING";
},
isClose() {
return this.state == "CLOSE";
}
},
mounted() {
// 初始化音频文件
this.initAudio();
},
created() {
// 监听页面刷新事件
window.addEventListener('beforeunload', () => {
this.onQuit();
});
},
beforeUnmount() {
this.onQuit();
}
}
</script>
<style lang="scss">
.rtc-private-video {
position: relative;
.el-loading-text {
color: white !important;
font-size: 16px !important;
}
.path {
stroke: white !important;
}
.rtc-video-box {
position: relative;
border: #4880b9 solid 1px;
background-color: #eeeeee;
.rtc-video-friend {
height: 70vh;
.friend-head-image {
position: absolute;
}
video {
width: 100%;
height: 100%;
2 years ago
object-fit: cover;
transform: rotateY(180deg);
}
}
.rtc-video-mine {
position: absolute;
z-index: 99999;
width: 25vh;
right: 0;
bottom: 0;
box-shadow: 0px 0px 5px #ccc;
background-color: #cccccc;
video {
width: 100%;
2 years ago
object-fit: cover;
transform: rotateY(180deg);
}
}
}
.rtc-voice-box {
position: relative;
display: flex;
justify-content: center;
border: #4880b9 solid 1px;
width: 100%;
height: 50vh;
padding-top: 10vh;
background-color: aliceblue;
.rtc-voice-name {
text-align: center;
font-size: 22px;
font-weight: 600;
}
}
.rtc-control-bar {
display: flex;
justify-content: space-around;
padding: 10px;
.icon {
font-size: 50px;
cursor: pointer;
}
}
}
</style>