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