committed by
Gitee
40 changed files with 1083 additions and 172 deletions
@ -1,16 +1,18 @@ |
|||||
package com.bx.imcommon.enums; |
package com.bx.imcommon.enums; |
||||
|
|
||||
|
|
||||
public enum IMSendStatus { |
public enum IMSendCode { |
||||
|
|
||||
SUCCESS(0,"发送成功"), |
SUCCESS(0,"发送成功"), |
||||
FAIL(1,"发送失败"); |
NOT_ONLINE(1,"对方当前不在线"), |
||||
|
NOT_FIND_CHANNEL(2,"未找到对方的channel"), |
||||
|
UNKONW_ERROR(9999,"未知异常"); |
||||
|
|
||||
private int code; |
private int code; |
||||
private String desc; |
private String desc; |
||||
|
|
||||
// 构造方法
|
// 构造方法
|
||||
IMSendStatus(int code, String desc) { |
IMSendCode(int code, String desc) { |
||||
this.code = code; |
this.code = code; |
||||
this.desc = desc; |
this.desc = desc; |
||||
} |
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.bx.implatform.config; |
||||
|
|
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
|
||||
|
@Data |
||||
|
public class ICEServer { |
||||
|
|
||||
|
|
||||
|
private String urls; |
||||
|
|
||||
|
|
||||
|
private String username; |
||||
|
|
||||
|
|
||||
|
private String credential; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
package com.bx.implatform.config; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@Component |
||||
|
@ConfigurationProperties(prefix="webrtc") |
||||
|
public class ICEServerConfig { |
||||
|
|
||||
|
private List<ICEServer> iceServers = new ArrayList<>(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
package com.bx.implatform.controller; |
||||
|
|
||||
|
|
||||
|
import com.bx.imclient.IMClient; |
||||
|
import com.bx.imcommon.model.PrivateMessageInfo; |
||||
|
import com.bx.implatform.config.ICEServerConfig; |
||||
|
import com.bx.implatform.enums.MessageType; |
||||
|
import com.bx.implatform.result.Result; |
||||
|
import com.bx.implatform.result.ResultUtils; |
||||
|
import com.bx.implatform.session.SessionContext; |
||||
|
import io.swagger.annotations.Api; |
||||
|
import io.swagger.annotations.ApiOperation; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
@Api(tags = "webrtc视频单人通话") |
||||
|
@RestController |
||||
|
@RequestMapping("/webrtc/private") |
||||
|
public class WebrtcController { |
||||
|
|
||||
|
@Autowired |
||||
|
private IMClient imClient; |
||||
|
|
||||
|
@Autowired |
||||
|
private ICEServerConfig iceServerConfig; |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "呼叫视频通话") |
||||
|
@PostMapping("/call") |
||||
|
public Result call(@RequestParam Long uid, @RequestBody String offer) { |
||||
|
Long userId = SessionContext.getSession().getId(); |
||||
|
|
||||
|
PrivateMessageInfo message = new PrivateMessageInfo(); |
||||
|
message.setType(MessageType.RTC_CALL.code()); |
||||
|
message.setRecvId(uid); |
||||
|
message.setSendId(userId); |
||||
|
message.setContent(offer); |
||||
|
imClient.sendPrivateMessage(uid,message); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "接受视频通话") |
||||
|
@PostMapping("/accept") |
||||
|
public Result accept(@RequestParam Long uid,@RequestBody String answer) { |
||||
|
Long userId = SessionContext.getSession().getId(); |
||||
|
|
||||
|
PrivateMessageInfo message = new PrivateMessageInfo(); |
||||
|
message.setType(MessageType.RTC_ACCEPT.code()); |
||||
|
message.setRecvId(uid); |
||||
|
message.setSendId(userId); |
||||
|
message.setContent(answer); |
||||
|
imClient.sendPrivateMessage(uid,message); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "拒绝视频通话") |
||||
|
@PostMapping("/reject") |
||||
|
public Result reject(@RequestParam Long uid) { |
||||
|
Long userId = SessionContext.getSession().getId(); |
||||
|
PrivateMessageInfo message = new PrivateMessageInfo(); |
||||
|
message.setType(MessageType.RTC_REJECT.code()); |
||||
|
message.setRecvId(uid); |
||||
|
message.setSendId(userId); |
||||
|
imClient.sendPrivateMessage(uid,message); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "取消呼叫") |
||||
|
@PostMapping("/cancel") |
||||
|
public Result cancel(@RequestParam Long uid) { |
||||
|
Long userId = SessionContext.getSession().getId(); |
||||
|
PrivateMessageInfo message = new PrivateMessageInfo(); |
||||
|
message.setType(MessageType.RTC_CANCEL.code()); |
||||
|
message.setRecvId(uid); |
||||
|
message.setSendId(userId); |
||||
|
imClient.sendPrivateMessage(uid,message); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "呼叫失败") |
||||
|
@PostMapping("/failed") |
||||
|
public Result failed(@RequestParam Long uid,@RequestParam String reason) { |
||||
|
Long userId = SessionContext.getSession().getId(); |
||||
|
|
||||
|
PrivateMessageInfo message = new PrivateMessageInfo(); |
||||
|
message.setType(MessageType.RTC_FAILED.code()); |
||||
|
message.setRecvId(uid); |
||||
|
message.setSendId(userId); |
||||
|
message.setContent(reason); |
||||
|
imClient.sendPrivateMessage(uid,message); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "挂断") |
||||
|
@PostMapping("/handup") |
||||
|
public Result leave(@RequestParam Long uid) { |
||||
|
Long userId = SessionContext.getSession().getId(); |
||||
|
|
||||
|
PrivateMessageInfo message = new PrivateMessageInfo(); |
||||
|
message.setType(MessageType.RTC_HANDUP.code()); |
||||
|
message.setRecvId(uid); |
||||
|
message.setSendId(userId); |
||||
|
imClient.sendPrivateMessage(uid,message); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@PostMapping("/candidate") |
||||
|
@ApiOperation(httpMethod = "POST", value = "同步candidate") |
||||
|
public Result candidate(@RequestParam Long uid,@RequestBody String candidate ) { |
||||
|
Long userId = SessionContext.getSession().getId(); |
||||
|
PrivateMessageInfo message = new PrivateMessageInfo(); |
||||
|
message.setType(MessageType.RTC_CANDIDATE.code()); |
||||
|
message.setRecvId(uid); |
||||
|
message.setSendId(userId); |
||||
|
message.setContent(candidate); |
||||
|
imClient.sendPrivateMessage(uid,message); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/iceservers") |
||||
|
@ApiOperation(httpMethod = "GET", value = "获取iceservers") |
||||
|
public Result iceservers() { |
||||
|
return ResultUtils.success(iceServerConfig.getIceServers()); |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,383 @@ |
|||||
|
<template> |
||||
|
<el-dialog v-dialogDrag :title="title" top="5vh" |
||||
|
:close-on-click-modal="false" |
||||
|
:close-on-press-escape="false" |
||||
|
:visible.sync="visible" width="50%" height="70%" :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.5)"> |
||||
|
<head-image class="friend-head-image" :id="this.friend.id" :size="80" :url="this.friend.headImage"></head-image> |
||||
|
<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> |
||||
|
import HeadImage from '../common/HeadImage.vue'; |
||||
|
|
||||
|
export default { |
||||
|
name: 'chatVideo', |
||||
|
components: {HeadImage}, |
||||
|
props: { |
||||
|
visible: { |
||||
|
type: Boolean |
||||
|
}, |
||||
|
friend: { |
||||
|
type: Object |
||||
|
}, |
||||
|
master: { |
||||
|
type: Boolean |
||||
|
}, |
||||
|
offer: { |
||||
|
type: Object |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
stream: null, |
||||
|
audio: new Audio(), |
||||
|
loading: false, |
||||
|
peerConnection: null, |
||||
|
videoTime: 0, |
||||
|
videoTimer: null, |
||||
|
state: 'NOT_CONNECTED', |
||||
|
candidates: [], |
||||
|
configuration: { |
||||
|
iceServers: [] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
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; |
||||
|
console.log(this.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.getTracks().forEach((track) => { |
||||
|
track.stop(); |
||||
|
}); |
||||
|
this.$refs.mineVideo.srcObject = null; |
||||
|
this.stream = null; |
||||
|
} |
||||
|
|
||||
|
}, |
||||
|
setupPeerConnection(stream) { |
||||
|
this.peerConnection = new RTCPeerConnection(this.configuration); |
||||
|
this.peerConnection.ontrack = (e) => { |
||||
|
this.$refs.friendVideo.srcObject = e.streams[0]; |
||||
|
}; |
||||
|
this.peerConnection.onicecandidate = (event) => { |
||||
|
if (event.candidate) { |
||||
|
if (this.state == 'CONNECTED') { |
||||
|
// 已连接,直接发送 |
||||
|
this.sendCandidate(event.candidate); |
||||
|
} else { |
||||
|
// 未连接,缓存起来,连接后再发送 |
||||
|
this.candidates.push(event.candidate) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (stream) { |
||||
|
stream.getTracks().forEach((track) => { |
||||
|
this.peerConnection.addTrack(track, stream); |
||||
|
}); |
||||
|
} |
||||
|
this.peerConnection.oniceconnectionstatechange = (event) => { |
||||
|
let state = event.target.iceConnectionState; |
||||
|
console.log("ICE connection status changed : " + state) |
||||
|
if(state == 'connected'){ |
||||
|
this.resetTime(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
}, |
||||
|
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'; |
||||
|
// 停止播放语音 |
||||
|
this.audio.pause(); |
||||
|
// 发送candidate |
||||
|
this.candidates.forEach((candidate) => { |
||||
|
this.sendCandidate(candidate); |
||||
|
}) |
||||
|
} |
||||
|
else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) { |
||||
|
this.$message.error("对方拒绝了您的视频请求"); |
||||
|
this.close(); |
||||
|
} |
||||
|
else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) { |
||||
|
this.$message.error(msg.content) |
||||
|
this.close(); |
||||
|
} |
||||
|
else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { |
||||
|
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content))); |
||||
|
} |
||||
|
else 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: JSON.stringify(offer) |
||||
|
}).then(() => { |
||||
|
this.loading = true; |
||||
|
this.state = 'CONNECTING'; |
||||
|
this.audio.play(); |
||||
|
}); |
||||
|
}, |
||||
|
(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: JSON.stringify(answer) |
||||
|
}) |
||||
|
this.state = 'CONNECTED'; |
||||
|
}, |
||||
|
(error) => { |
||||
|
this.$message.error(error); |
||||
|
}); |
||||
|
|
||||
|
}, |
||||
|
handup() { |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/handup?uid=${this.friend.id}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
this.close(); |
||||
|
this.$message.success("已挂断视频通话") |
||||
|
}, |
||||
|
cancel() { |
||||
|
this.$http({ |
||||
|
url: `/webrtc/private/cancel?uid=${this.friend.id}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
this.close(); |
||||
|
this.$message.success("已停止呼叫视频通话") |
||||
|
}, |
||||
|
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: JSON.stringify(candidate) |
||||
|
}) |
||||
|
}, |
||||
|
close() { |
||||
|
this.$emit("close"); |
||||
|
this.closeCamera(); |
||||
|
this.loading = false; |
||||
|
this.state = 'NOT_CONNECTED'; |
||||
|
this.videoTime = 0; |
||||
|
this.videoTimer && clearInterval(this.videoTimer); |
||||
|
this.audio.pause(); |
||||
|
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; |
||||
|
}, |
||||
|
resetTime(){ |
||||
|
this.videoTime = 0; |
||||
|
this.videoTimer && clearInterval(this.videoTimer); |
||||
|
this.videoTimer = setInterval(()=>{ |
||||
|
this.videoTime++; |
||||
|
},1000) |
||||
|
}, |
||||
|
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; |
||||
|
}, |
||||
|
initAudio(){ |
||||
|
let url = require(`@/assets/audio/call.wav`); |
||||
|
this.audio.src = url; |
||||
|
this.audio.loop = true; |
||||
|
}, |
||||
|
initICEServers(){ |
||||
|
this.$http({ |
||||
|
url: '/webrtc/private/iceservers', |
||||
|
method: 'get' |
||||
|
}).then((servers) => { |
||||
|
this.configuration.iceServers = servers; |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
}, |
||||
|
watch: { |
||||
|
visible: { |
||||
|
handler(newValue, oldValue) { |
||||
|
if (newValue) { |
||||
|
this.init(); |
||||
|
// 用户忙状态 |
||||
|
this.$store.commit("setUserState", this.$enums.USER_STATE.BUSY); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
title() { |
||||
|
let strTitle = `视频聊天-${this.friend.nickName}`; |
||||
|
if(this.state == 'CONNECTED'){ |
||||
|
strTitle += `(${this.currentTime})`; |
||||
|
}else if(this.state == 'CONNECTING'){ |
||||
|
strTitle += `(呼叫中)`; |
||||
|
} |
||||
|
return strTitle; |
||||
|
}, |
||||
|
currentTime(){ |
||||
|
let currentTime = 0; |
||||
|
if(this.state == 'CONNECTED' && this.videoTime){ |
||||
|
currentTime = Math.floor(this.videoTime); |
||||
|
} |
||||
|
let min = Math.floor(currentTime/60); |
||||
|
let sec = currentTime%60; |
||||
|
let strTime = min<10?"0":""; |
||||
|
strTime += min; |
||||
|
strTime += ":" |
||||
|
strTime += sec<10?"0":""; |
||||
|
strTime += sec; |
||||
|
return strTime; |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.initAudio(); |
||||
|
this.initICEServers(); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.chat-video { |
||||
|
position: relative; |
||||
|
|
||||
|
.chat-video-box { |
||||
|
position: relative; |
||||
|
border: #4880b9 solid 1px; |
||||
|
background-color: #eeeeee; |
||||
|
|
||||
|
.chat-video-friend { |
||||
|
height: 70vh; |
||||
|
.friend-head-image { |
||||
|
position: absolute; |
||||
|
} |
||||
|
|
||||
|
video { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: fill; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chat-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%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chat-video-controllbar { |
||||
|
display: flex; |
||||
|
justify-content: space-around; |
||||
|
padding: 10px; |
||||
|
|
||||
|
.icon { |
||||
|
font-size: 50px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,131 @@ |
|||||
|
<template> |
||||
|
<div class="chat-video-acceptor"> |
||||
|
<div> |
||||
|
<head-image :size="120" :url="this.friend.headImage" :id="this.friend.id"></head-image> |
||||
|
</div> |
||||
|
<div> |
||||
|
{{friend.nickName}} 请求和您进行视频通话... |
||||
|
</div> |
||||
|
<div class="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:{}, |
||||
|
audio: new Audio() |
||||
|
} |
||||
|
}, |
||||
|
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) |
||||
|
this.audio.play(); |
||||
|
}, |
||||
|
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.audio.pause(); |
||||
|
this.$emit("close"); |
||||
|
}, |
||||
|
initAudio(){ |
||||
|
let url = require(`@/assets/audio/call.wav`); |
||||
|
this.audio.src = url; |
||||
|
this.audio.loop = true; |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
// 初始化语音 |
||||
|
this.initAudio(); |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
.chat-video-acceptor { |
||||
|
position: absolute; |
||||
|
right: 1px; |
||||
|
bottom: 1px; |
||||
|
width: 250px; |
||||
|
height: 250px; |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
background-color: #eeeeee; |
||||
|
border: #dddddd solid 1px; |
||||
|
|
||||
|
.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> |
||||
@ -0,0 +1,72 @@ |
|||||
|
import Vue from 'vue' |
||||
|
|
||||
|
// v-dialogDrag: 弹窗拖拽
|
||||
|
Vue.directive('dialogDrag', { |
||||
|
bind (el, binding, vnode, oldVnode) { |
||||
|
const dialogHeaderEl = el.querySelector('.el-dialog__header') |
||||
|
const dragDom = el.querySelector('.el-dialog') |
||||
|
dialogHeaderEl.style.cursor = 'move' |
||||
|
|
||||
|
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
|
||||
|
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null) |
||||
|
|
||||
|
dialogHeaderEl.onmousedown = (e) => { |
||||
|
// 鼠标按下,计算当前元素距离可视区的距离
|
||||
|
const disX = e.clientX - dialogHeaderEl.offsetLeft |
||||
|
const disY = e.clientY - dialogHeaderEl.offsetTop |
||||
|
const screenWidth = document.body.clientWidth; // body当前宽度
|
||||
|
const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
|
||||
|
const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
|
||||
|
const dragDomheight = dragDom.offsetHeight; // 对话框高度
|
||||
|
const minDragDomLeft = dragDom.offsetLeft; |
||||
|
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth; |
||||
|
const minDragDomTop = dragDom.offsetTop; |
||||
|
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight; |
||||
|
|
||||
|
// 获取到的值带px 正则匹配替换
|
||||
|
let styL, styT |
||||
|
|
||||
|
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
|
||||
|
if (sty.left.includes('%')) { |
||||
|
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100) |
||||
|
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100) |
||||
|
} else { |
||||
|
styL = +sty.left.replace(/\px/g, '') |
||||
|
styT = +sty.top.replace(/\px/g, '') |
||||
|
} |
||||
|
|
||||
|
document.onmousemove = function (e) { |
||||
|
// 获取body的页面可视宽高
|
||||
|
// var clientHeight = document.documentElement.clientHeight || document.body.clientHeight
|
||||
|
// var clientWidth = document.documentElement.clientWidth || document.body.clientWidth
|
||||
|
|
||||
|
// 通过事件委托,计算移动的距离
|
||||
|
var l = e.clientX - disX |
||||
|
var t = e.clientY - disY |
||||
|
|
||||
|
// 边界处理
|
||||
|
if (-l > minDragDomLeft) { |
||||
|
l = -minDragDomLeft; |
||||
|
} else if (l > maxDragDomLeft) { |
||||
|
l = maxDragDomLeft; |
||||
|
} |
||||
|
if (-t > minDragDomTop) { |
||||
|
t = -minDragDomTop; |
||||
|
} else if (t > maxDragDomTop) { |
||||
|
t = maxDragDomTop; |
||||
|
} |
||||
|
// 移动当前元素
|
||||
|
dragDom.style.left = `${l + styL}px` |
||||
|
dragDom.style.top = `${t + styT}px` |
||||
|
|
||||
|
// 将此时的位置传出去
|
||||
|
// binding.value({x:e.pageX,y:e.pageY})
|
||||
|
} |
||||
|
|
||||
|
document.onmouseup = function (e) { |
||||
|
document.onmousemove = null |
||||
|
document.onmouseup = null |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
Loading…
Reference in new issue