4 changed files with 468 additions and 1 deletions
@ -0,0 +1,21 @@ |
|||||
|
|
||||
|
const MESSAGE_TYPE = { |
||||
|
RTC_CALL: 101, |
||||
|
RTC_ACCEPT: 102, |
||||
|
RTC_REJECT: 103, |
||||
|
RTC_CANCEL: 104, |
||||
|
RTC_FAILED: 105, |
||||
|
RTC_HANDUP: 106, |
||||
|
RTC_CANDIDATE: 107 |
||||
|
} |
||||
|
|
||||
|
const USER_STATE = { |
||||
|
OFFLINE: 0, |
||||
|
FREE: 1, |
||||
|
BUSY: 2 |
||||
|
} |
||||
|
|
||||
|
export { |
||||
|
MESSAGE_TYPE, |
||||
|
USER_STATE |
||||
|
} |
||||
@ -0,0 +1,322 @@ |
|||||
|
<template> |
||||
|
<el-dialog :title="title" :visible.sync="visible" width="800px" :before-close="handleClose"> |
||||
|
<div class="chat-video"> |
||||
|
<div class="chat-video-box"> |
||||
|
<div class="chat-video-friend" |
||||
|
v-loading="loading" |
||||
|
element-loading-text="等待对方接听..." |
||||
|
element-loading-spinner="el-icon-loading" |
||||
|
element-loading-background="rgba(0, 0, 0, 0.9)"> |
||||
|
<video ref="friendVideo" autoplay=""></video> |
||||
|
</div> |
||||
|
<div class="chat-video-mine"> |
||||
|
<video ref="mineVideo" autoplay=""></video> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chat-video-controllbar"> |
||||
|
|
||||
|
<div v-show="state=='CONNECTING'" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="cancel()"></div> |
||||
|
<div v-show="state=='CONNECTED'" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="handup()"></div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
|
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'chatVideo', |
||||
|
props: { |
||||
|
visible: { |
||||
|
type: Boolean |
||||
|
}, |
||||
|
friend: { |
||||
|
type: Object |
||||
|
}, |
||||
|
master: { |
||||
|
type: Boolean |
||||
|
}, |
||||
|
offer: { |
||||
|
type: Object |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
stream: null, |
||||
|
loading: false, |
||||
|
peerConnection: null, |
||||
|
state: 'NOT_CONNECTED', |
||||
|
candidates: [], |
||||
|
configuration: { |
||||
|
iceServers: [{ |
||||
|
"urls": navigator.mozGetUserMedia ? "stun:stun.services.mozilla.com" : navigator.webkitGetUserMedia ? |
||||
|
"stun:stun.l.google.com:19302" : "stun:23.21.150.121" |
||||
|
}, |
||||
|
{ |
||||
|
urls: "stun:stun.l.google.com:19302" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
init() { |
||||
|
if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) { |
||||
|
this.$message.error("您的浏览器不支持WebRTC"); |
||||
|
if (!this.master) { |
||||
|
this.sendFailed("对方浏览器不支持WebRTC") |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 打开摄像头 |
||||
|
this.openCamera((stream) => { |
||||
|
// 初始化webrtc连接 |
||||
|
this.setupPeerConnection(stream); |
||||
|
if (this.master) { |
||||
|
// 发起呼叫 |
||||
|
this.call(); |
||||
|
} else { |
||||
|
// 接受呼叫 |
||||
|
this.accept(this.offer); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
}, |
||||
|
openCamera(callback) { |
||||
|
navigator.getUserMedia({ |
||||
|
video: true, |
||||
|
audio: true |
||||
|
}, |
||||
|
(stream) => { |
||||
|
this.stream = stream; |
||||
|
this.$refs.mineVideo.srcObject = stream; |
||||
|
this.$refs.mineVideo.muted = true; |
||||
|
callback(stream) |
||||
|
}, |
||||
|
(error) => { |
||||
|
this.$message.error("打开摄像头失败:" + error); |
||||
|
callback() |
||||
|
}); |
||||
|
}, |
||||
|
closeCamera(){ |
||||
|
if(this.stream){ |
||||
|
this.stream.getVideoTracks().forEach((track) =>{ |
||||
|
track.stop(); |
||||
|
this.$refs.mineVideo.srcObject = null; |
||||
|
}); |
||||
|
this.stream = null; |
||||
|
} |
||||
|
|
||||
|
}, |
||||
|
setupPeerConnection(stream) { |
||||
|
this.peerConnection = new RTCPeerConnection(this.configuration); |
||||
|
this.peerConnection.onaddstream = (e) => { |
||||
|
console.log("onaddstream") |
||||
|
this.$refs.friendVideo.srcObject = e.stream; |
||||
|
}; |
||||
|
this.peerConnection.onicecandidate = (event) => { |
||||
|
if (event.candidate) { |
||||
|
if(this.state == 'CONNECTED'){ |
||||
|
// 已连接,直接发送 |
||||
|
this.sendCandidate(event.candidate); |
||||
|
}else{ |
||||
|
// 未连接,缓存起来,连接后再发送 |
||||
|
this.candidates.push(event.candidate) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (stream) { |
||||
|
this.peerConnection.addStream(stream); |
||||
|
} |
||||
|
}, |
||||
|
handleMessage(msg) { |
||||
|
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) { |
||||
|
this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content))); |
||||
|
// 关闭等待提示 |
||||
|
this.loading = false; |
||||
|
// 状态为连接中 |
||||
|
this.state = 'CONNECTED'; |
||||
|
// 发送candidate |
||||
|
this.candidates.forEach((candidate) => { |
||||
|
this.sendCandidate(candidate); |
||||
|
}) |
||||
|
} |
||||
|
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) { |
||||
|
this.$message.error("对方拒绝了您的视频请求"); |
||||
|
this.peerConnection.close(); |
||||
|
// 关闭等待提示 |
||||
|
this.loading = false; |
||||
|
// 状态为未连接 |
||||
|
this.state = 'NOT_CONNECTED'; |
||||
|
} |
||||
|
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) { |
||||
|
this.$message.error(msg.content) |
||||
|
// 关闭等待提示 |
||||
|
this.loading = false; |
||||
|
// 状态为未连接 |
||||
|
this.state = 'NOT_CONNECTED'; |
||||
|
} |
||||
|
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { |
||||
|
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content))); |
||||
|
} |
||||
|
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) { |
||||
|
this.$message.success("对方已挂断"); |
||||
|
this.close(); |
||||
|
} |
||||
|
}, |
||||
|
call() { |
||||
|
this.peerConnection.createOffer((offer) => { |
||||
|
this.peerConnection.setLocalDescription(offer); |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/call?uid=${this.friend.id}`, |
||||
|
method: 'post', |
||||
|
data: offer |
||||
|
}).then(()=>{ |
||||
|
this.loading = true; |
||||
|
this.state = 'CONNECTING'; |
||||
|
}); |
||||
|
}, |
||||
|
(error) => { |
||||
|
this.$message.error(error); |
||||
|
}); |
||||
|
|
||||
|
}, |
||||
|
accept(offer) { |
||||
|
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); |
||||
|
this.peerConnection.createAnswer((answer) => { |
||||
|
this.peerConnection.setLocalDescription(answer); |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/accept?uid=${this.friend.id}`, |
||||
|
method: 'post', |
||||
|
data: answer |
||||
|
}) |
||||
|
this.state='CONNECTED'; |
||||
|
}, |
||||
|
(error) => { |
||||
|
this.$message.error(error); |
||||
|
}); |
||||
|
|
||||
|
}, |
||||
|
handup() { |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/handup?uid=${this.friend.id}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
this.close(); |
||||
|
}, |
||||
|
cancel(){ |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/cancel?uid=${this.friend.id}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
this.close(); |
||||
|
}, |
||||
|
sendFailed(reason) { |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
}, |
||||
|
sendCandidate(candidate) { |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/candidate?uid=${this.friend.id}`, |
||||
|
method: 'post', |
||||
|
data: candidate |
||||
|
}) |
||||
|
}, |
||||
|
close() { |
||||
|
this.$emit("close"); |
||||
|
this.closeCamera(); |
||||
|
this.loading = false; |
||||
|
this.state = 'NOT_CONNECTED'; |
||||
|
this.candidates = []; |
||||
|
this.$store.commit("setUserState",this.$enums.USER_STATE.FREE); |
||||
|
this.$refs.friendVideo.srcObject = null; |
||||
|
this.peerConnection.close(); |
||||
|
this.peerConnection.onicecandidate = null; |
||||
|
this.peerConnection.onaddstream = null; |
||||
|
|
||||
|
}, |
||||
|
handleClose(){ |
||||
|
if(this.state=='CONNECTED'){ |
||||
|
this.handup() |
||||
|
}else if(this.state == 'CONNECTING'){ |
||||
|
this.cancel(); |
||||
|
}else{ |
||||
|
this.close(); |
||||
|
} |
||||
|
}, |
||||
|
hasUserMedia() { |
||||
|
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || |
||||
|
navigator.msGetUserMedia; |
||||
|
return !!navigator.getUserMedia; |
||||
|
}, |
||||
|
hasRTCPeerConnection() { |
||||
|
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; |
||||
|
window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; |
||||
|
window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; |
||||
|
return !!window.RTCPeerConnection; |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
visible: { |
||||
|
handler(newValue, oldValue) { |
||||
|
if (newValue) { |
||||
|
this.init(); |
||||
|
// 用户忙状态 |
||||
|
this.$store.commit("setUserState",this.$enums.USER_STATE.BUSY); |
||||
|
console.log(this.$store.state.userStore.state) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
title() { |
||||
|
return `视频聊天-${this.friend.nickName}`; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.chat-video { |
||||
|
|
||||
|
.chat-video-box { |
||||
|
position: relative; |
||||
|
border: #2C3E50 solid 1px; |
||||
|
background-color: #eeeeee; |
||||
|
|
||||
|
.chat-video-friend { |
||||
|
height: 600px; |
||||
|
video { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chat-video-mine { |
||||
|
position: absolute; |
||||
|
z-index: 99999; |
||||
|
width: 200px; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
|
||||
|
video { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chat-video-controllbar { |
||||
|
display: flex; |
||||
|
justify-content: space-around; |
||||
|
padding: 10px; |
||||
|
.icon { |
||||
|
font-size: 50px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,124 @@ |
|||||
|
<template> |
||||
|
<div class="video-acceptor"> |
||||
|
<div> |
||||
|
<head-image :size="120" :url="this.friend.headImage" :id="this.friend.id"></head-image> |
||||
|
</div> |
||||
|
<div> |
||||
|
{{friend.nickName}} 请求和您进行视频通话... |
||||
|
</div> |
||||
|
<div class="video-acceptor-btn-group"> |
||||
|
<div class="icon iconfont icon-phone-accept accept" @click="accpet()"></div> |
||||
|
<div class="icon iconfont icon-phone-reject reject" @click="reject()"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import HeadImage from '../common/HeadImage.vue'; |
||||
|
|
||||
|
export default { |
||||
|
name: "videoAcceptor", |
||||
|
components:{HeadImage}, |
||||
|
props: { |
||||
|
friend:{ |
||||
|
type: Object |
||||
|
} |
||||
|
}, |
||||
|
data(){ |
||||
|
return { |
||||
|
offer:{} |
||||
|
} |
||||
|
}, |
||||
|
methods:{ |
||||
|
accpet(){ |
||||
|
let info ={ |
||||
|
friend: this.friend, |
||||
|
master: false, |
||||
|
offer: this.offer |
||||
|
} |
||||
|
this.$store.commit("showChatPrivateVideoBox",info); |
||||
|
this.close(); |
||||
|
}, |
||||
|
reject(){ |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/reject?uid=${this.friend.id}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
this.close(); |
||||
|
}, |
||||
|
failed(reason){ |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
this.close(); |
||||
|
}, |
||||
|
onCall(msgInfo){ |
||||
|
console.log("onCall") |
||||
|
this.offer = JSON.parse(msgInfo.content); |
||||
|
if(this.$store.state.userStore.state == this.$enums.USER_STATE.BUSY){ |
||||
|
this.failed("对方正忙,暂时无法接听"); |
||||
|
return; |
||||
|
} |
||||
|
// 超时未接听 |
||||
|
this.timer && clearTimeout(this.timer); |
||||
|
this.timer = setTimeout(()=>{ |
||||
|
this.failed("对方未接听"); |
||||
|
},30000) |
||||
|
}, |
||||
|
onCancel(){ |
||||
|
this.$message.success("对方取消了呼叫"); |
||||
|
this.close(); |
||||
|
}, |
||||
|
handleMessage(msgInfo){ |
||||
|
if(msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL){ |
||||
|
this.onCall(msgInfo); |
||||
|
}else if(msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL){ |
||||
|
this.onCancel(); |
||||
|
} |
||||
|
}, |
||||
|
close(){ |
||||
|
this.timer && clearTimeout(this.timer); |
||||
|
this.$emit("close"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
.video-acceptor { |
||||
|
position: absolute; |
||||
|
right: 1px; |
||||
|
bottom: 1px; |
||||
|
width: 250px; |
||||
|
height: 250px; |
||||
|
padding: 10px; |
||||
|
text-align: center; |
||||
|
background-color: #eeeeee; |
||||
|
border: #dddddd solid 1px; |
||||
|
|
||||
|
.video-acceptor-btn-group { |
||||
|
display: flex; |
||||
|
justify-content: space-around; |
||||
|
margin-top: 20px; |
||||
|
|
||||
|
.icon { |
||||
|
font-size: 50px; |
||||
|
cursor: pointer; |
||||
|
&.accept { |
||||
|
color: green; |
||||
|
} |
||||
|
&.reject { |
||||
|
color: red; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue