Browse Source

feat: 单人音视频功能改造

master
xsx 2 years ago
parent
commit
c83e65ae99
  1. 35
      im-ui/src/api/rtcGroupApi.js
  2. 9
      im-ui/src/api/webrtc.js
  3. 29
      im-ui/src/components/chat/ChatBox.vue
  4. 14
      im-ui/src/components/chat/ChatRecord.vue
  5. 173
      im-ui/src/components/rtc/RtcPrivateAcceptor.vue
  6. 618
      im-ui/src/components/rtc/RtcPrivateVideo.vue
  7. 27
      im-ui/src/view/Home.vue

35
im-ui/src/api/rtcGroupApi.js

@ -1,18 +1,13 @@
import http from './httpRequest.js'
class RtcGroupApi {
constructor() {
this.http = http;
}
}
class RtcGroupApi {}
RtcGroupApi.prototype.setup = function(groupId, userInfos) {
let formData = {
groupId,
userInfos
}
return this.http({
return http({
url: '/webrtc/group/setup',
method: 'post',
data: formData
@ -20,14 +15,14 @@ RtcGroupApi.prototype.setup = function(groupId, userInfos) {
}
RtcGroupApi.prototype.accept = function(groupId) {
return this.http({
return http({
url: '/webrtc/group/accept?groupId='+groupId,
method: 'post'
})
}
RtcGroupApi.prototype.reject = function(groupId) {
return this.http({
return http({
url: '/webrtc/group/reject?groupId='+groupId,
method: 'post'
})
@ -38,7 +33,7 @@ RtcGroupApi.prototype.failed = function(groupId,reason) {
groupId,
reason
}
return this.http({
return http({
url: '/webrtc/group/failed',
method: 'post',
data: formData
@ -47,7 +42,7 @@ RtcGroupApi.prototype.failed = function(groupId,reason) {
RtcGroupApi.prototype.join = function(groupId) {
return this.http({
return http({
url: '/webrtc/group/join?groupId='+groupId,
method: 'post'
})
@ -58,7 +53,7 @@ RtcGroupApi.prototype.invite = function(groupId, userInfos) {
groupId,
userInfos
}
return this.http({
return http({
url: '/webrtc/group/invite',
method: 'post',
data: formData
@ -72,7 +67,7 @@ RtcGroupApi.prototype.offer = function(groupId, userId, offer) {
userId,
offer
}
return this.http({
return http({
url: '/webrtc/group/offer',
method: 'post',
data: formData
@ -85,7 +80,7 @@ RtcGroupApi.prototype.answer = function(groupId, userId, answer) {
userId,
answer
}
return this.http({
return http({
url: '/webrtc/group/answer',
method: 'post',
data: formData
@ -93,14 +88,14 @@ RtcGroupApi.prototype.answer = function(groupId, userId, answer) {
}
RtcGroupApi.prototype.quit = function(groupId) {
return this.http({
return http({
url: '/webrtc/group/quit?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.cancel = function(groupId) {
return this.http({
return http({
url: '/webrtc/group/cancel?groupId=' + groupId,
method: 'post'
})
@ -112,7 +107,7 @@ RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
userId,
candidate
}
return this.http({
return http({
url: '/webrtc/group/candidate',
method: 'post',
data: formData
@ -125,7 +120,7 @@ RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) {
isCamera,
isMicroPhone
}
return this.http({
return http({
url: '/webrtc/group/device',
method: 'post',
data: formData
@ -139,7 +134,7 @@ RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
userId,
candidate
}
return this.http({
return http({
url: '/webrtc/group/candidate',
method: 'post',
data: formData
@ -147,7 +142,7 @@ RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
}
RtcGroupApi.prototype.heartbeat = function(groupId) {
return this.http({
return http({
url: '/webrtc/group/heartbeat?groupId=' + groupId,
method: 'post'
})

9
im-ui/src/api/webrtc.js

@ -33,9 +33,11 @@ ImWebRtc.prototype.setStream = function(stream) {
if(this.stream){
this.peerConnection.removeStream(this.stream)
}
stream.getTracks().forEach((track) => {
this.peerConnection.addTrack(track, stream);
});
if(stream){
stream.getTracks().forEach((track) => {
this.peerConnection.addTrack(track, stream);
});
}
this.stream = stream;
}
@ -111,6 +113,7 @@ ImWebRtc.prototype.close = function(uid) {
this.peerConnection.close();
this.peerConnection.onicecandidate = null;
this.peerConnection.onaddstream = null;
this.peerConnection = null;
}
}

29
im-ui/src/components/chat/ChatBox.vue

@ -44,7 +44,7 @@
<div title="回执消息" v-show="chat.type == 'GROUP'" class="icon iconfont icon-receipt"
:class="isReceipt ? 'chat-tool-active' : ''" @click="onSwitchReceipt">
</div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
<div title="发送语音" class="el-icon-microphone" @click="showRecordBox()">
</div>
<div title="语音通话" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@click="showPrivateVideo('voice')">
@ -88,7 +88,7 @@
<emotion ref="emoBox" @emotion="onEmotion"></Emotion>
<chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers" :search-text="atSearchText"
@select="onAtSelect"></chat-at-box>
<chat-voice :visible="showVoice" @close="closeVoiceBox" @send="onSendVoice"></chat-voice>
<chat-record :visible="showRecord" @close="closeRecordBox" @send="onSendRecord"></chat-record>
<group-member-selector ref="rtcSel" :groupId="group.id" @complete="onInviteOk"></group-member-selector>
<rtc-group-join ref="rtcJoin" :groupId="group.id"></rtc-group-join>
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group"
@ -102,7 +102,7 @@
import ChatMessageItem from "./ChatMessageItem.vue";
import FileUpload from "../common/FileUpload.vue";
import Emotion from "../common/Emotion.vue";
import ChatVoice from "./ChatVoice.vue";
import ChatRecord from "./ChatRecord.vue";
import ChatHistory from "./ChatHistory.vue";
import ChatAtBox from "./ChatAtBox.vue"
import GroupMemberSelector from "../group/GroupMemberSelector.vue"
@ -116,7 +116,7 @@
FileUpload,
ChatGroupSide,
Emotion,
ChatVoice,
ChatRecord,
ChatHistory,
ChatAtBox,
GroupMemberSelector,
@ -136,7 +136,7 @@
sendImageFile: "",
placeholder: "",
isReceipt: true,
showVoice: false, //
showRecord: false, //
showSide: false, //
showHistory: false, //
lockMessage: false, //
@ -452,23 +452,20 @@
range.collapse()
},
showVoiceBox() {
this.showVoice = true;
showRecordBox() {
this.showRecord = true;
},
closeVoiceBox() {
this.showVoice = false;
closeRecordBox() {
this.showRecord = false;
},
showPrivateVideo(mode) {
let rtcInfo = {
mode: mode,
isHost: true,
friend: this.friend,
sendId: this.$store.state.userStore.userInfo.id,
recvId: this.friend.id,
offer: "",
state: this.$enums.RTC_STATE.WAIT_CALL
}
this.$store.commit("setRtcInfo", rtcInfo);
// home.vue
this.$eventBus.$emit("openPrivateVideo", rtcInfo);
},
onGroupVideo() {
this.$http({
@ -517,7 +514,7 @@
closeHistoryBox() {
this.showHistory = false;
},
onSendVoice(data) {
onSendRecord(data) {
let msgInfo = {
content: JSON.stringify(data),
type: 3,
@ -544,7 +541,7 @@
//
this.scrollToBottom();
//
this.showVoice = false;
this.showRecord = false;
this.isReceipt = false;
})

14
im-ui/src/components/chat/ChatVoice.vue → im-ui/src/components/chat/ChatRecord.vue

@ -1,12 +1,12 @@
<template>
<el-dialog class="chat-voice" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose">
<el-dialog class="chat-record" title="语音录制" :visible.sync="visible" width="600px" :before-close="onClose">
<div v-show="mode=='RECORD'">
<div class="chat-voice-tip">{{stateTip}}</div>
<div class="tip">{{stateTip}}</div>
<div>时长: {{state=='STOP'?0:parseInt(rc.duration)}}s</div>
</div>
<audio v-show="mode=='PLAY'" :src="url" controls ref="audio" @ended="onStopAudio()"></audio>
<el-divider content-position="center"></el-divider>
<el-row class="chat-voice-btn-group">
<el-row class="btn-group">
<el-button round type="primary" v-show="state=='STOP'" @click="onStartRecord()">开始录音</el-button>
<el-button round type="warning" v-show="state=='RUNNING'" @click="onPauseRecord()">暂停录音</el-button>
<el-button round type="primary" v-show="state=='PAUSE'" @click="onResumeRecord()">继续录音</el-button>
@ -27,7 +27,7 @@
import Recorder from 'js-audio-recorder';
export default {
name: 'chatVoice',
name: 'chatRecord',
props: {
visible: {
type: Boolean
@ -126,13 +126,13 @@
</script>
<style lang="scss">
.chat-voice {
.chat-record {
.chat-voice-tip {
.tip {
font-size: 18px;
}
.chat-voice-btn-group {
.btn-group {
margin-bottom: 20px;
}
}

173
im-ui/src/components/rtc/RtcPrivateAcceptor.vue

@ -1,12 +1,12 @@
<template>
<div v-show="isShow" class="rtc-private-acceptor">
<head-image :id="rtcInfo.friend.id" :name="rtcInfo.friend.nickName" :url="rtcInfo.friend.headImage" :size="100"></head-image>
<div class="rtc-private-acceptor">
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"></head-image>
<div class="acceptor-text">
{{tip}}
</div>
<div class="acceptor-btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="accpet()" title="接受"></div>
<div class="icon iconfont icon-phone-reject reject" @click="reject()" title="拒绝"></div>
<div class="icon iconfont icon-phone-accept accept" @click="$emit('accept')" title="接受"></div>
<div class="icon iconfont icon-phone-reject reject" @click="$emit('reject')" title="拒绝"></div>
</div>
</div>
</template>
@ -20,172 +20,21 @@
HeadImage
},
data() {
return {
isShow: false,
audio: new Audio()
}
return {}
},
methods: {
accpet() {
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.ACCEPTED);
//
this.close();
},
reject() {
this.$http({
url: `/webrtc/private/reject?uid=${this.rtcInfo.friend.id}`,
method: 'post'
})
//
this.insertMessage("已拒绝");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
},
failed(reason) {
this.$http({
url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`,
method: 'post'
})
//
this.insertMessage("未接听");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
},
onRtcCall(msgInfo, friend, mode) {
console.log("onRtcCall")
//
if (this.rtcInfo.state != this.$enums.RTC_STATE.FREE) {
//
let reason = "对方忙,无法与您通话";
this.$http({
url: `/webrtc/private/failed?uid=${msgInfo.sendId}&reason=${reason}`,
method: 'post'
})
return;
}
//
this.isShow = true;
// RTC
let rtcInfo = {
mode: mode,
isHost: false,
friend: friend,
sendId: msgInfo.sendId,
recvId: msgInfo.recvId,
offer: JSON.parse(msgInfo.content),
state: this.$enums.RTC_STATE.WAIT_ACCEPT
}
this.$store.commit("setRtcInfo", rtcInfo);
//
this.audio.play();
//
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.failed("对方无应答");
}, 30000)
props: {
mode:{
type: String
},
onRtcCancel(msgInfo) {
//
if (msgInfo.sendId != this.rtcInfo.friend.id) {
return;
}
//
this.$message.success("对方取消了呼叫");
//
this.insertMessage("对方已取消");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
},
onRtcAccept(msgInfo) {
//
if (msgInfo.selfSend) {
this.$message.success("已在其他设备接听");
//
this.insertMessage("已在其他设备接听")
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
}
},
onRtcReject(msgInfo){
//
if (msgInfo.selfSend) {
this.$message.success("已在其他设备拒绝通话");
//
this.insertMessage("已在其他设备拒绝")
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
}
},
onRTCMessage(msgInfo, friend) {
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE) {
this.onRtcCall(msgInfo, friend, "voice");
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO) {
this.onRtcCall(msgInfo, friend, "video");
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) {
this.onRtcCancel(msgInfo);
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
this.onRtcAccept(msgInfo);
}else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
this.onRtcReject(msgInfo);
}
},
insertMessage(messageTip) {
//
let chat = {
type: 'PRIVATE',
targetId: this.rtcInfo.friend.id,
showName: this.rtcInfo.friend.nickName,
headImage: this.rtcInfo.friend.headImageThumb,
};
this.$store.commit("openChat", chat);
//
let MESSAGE_TYPE = this.$enums.MESSAGE_TYPE;
let msgInfo = {
type: this.rtcInfo.mode == "video" ? MESSAGE_TYPE.RT_VIDEO : MESSAGE_TYPE.RT_VOICE,
sendId: this.rtcInfo.sendId,
recvId: this.rtcInfo.recvId,
content: messageTip,
status: 1,
selfSend: this.rtcInfo.isHost,
sendTime: new Date().getTime()
}
this.$store.commit("insertMessage", msgInfo);
},
close() {
this.timer && clearTimeout(this.timer);
this.audio.pause();
this.isShow = false;
},
initAudio() {
let url = require(`@/assets/audio/call.wav`);
this.audio.src = url;
this.audio.loop = true;
friend:{
type: Object
}
},
computed: {
tip() {
let modeText = this.mode == "video" ? "视频" : "语音"
return `${this.rtcInfo.friend.nickName} 请求和您进行${modeText}通话...`
},
rtcInfo(){
return this.$store.state.userStore.rtcInfo;
return `${this.friend.nickName} 请求和您进行${modeText}通话...`
}
},
mounted() {
//
this.initAudio();
}
}
</script>

618
im-ui/src/components/rtc/RtcPrivateVideo.vue

@ -1,362 +1,367 @@
<template>
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false"
:visible="isShow" width="50%" height="70%" :before-close="handleClose">
<div class="rtc-private-video">
<div v-show="rtcInfo.mode=='video'" class="rtc-video-box">
<div class="rtc-video-friend" v-loading="loading" element-loading-text="等待对方接听..."
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)">
<head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="80" :name="rtcInfo.friend.nickName"
:url="rtcInfo.friend.headImage">
<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-spinner="el-icon-loading" >
<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>
</head-image>
<video ref="friendVideo" autoplay=""></video>
</div>
<div class="rtc-video-mine">
<video ref="mineVideo" autoplay=""></video>
<div class="rtc-control-bar">
<div v-show="isWaiting" title="取消呼叫" class="icon iconfont icon-phone-reject reject"
style="color: red;" @click="onCancel()"></div>
<div v-show="isChating" title="挂断" class="icon iconfont icon-phone-reject reject"
style="color: red;" @click="onHandup()"></div>
</div>
</div>
<div v-show="rtcInfo.mode=='voice'" class="rtc-voice-box" v-loading="loading" element-loading-text="等待对方接听..."
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)">
<head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="200" :name="rtcInfo.friend.nickName"
:url="rtcInfo.friend.headImage">
<div class="rtc-voice-name">{{rtcInfo.friend.nickName}}</div>
</head-image>
</div>
<div class="rtc-control-bar">
<div v-show="isWaiting" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;"
@click="cancel()"></div>
<div v-show="isAccepted" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;"
@click="handup()"></div>
</div>
</div>
</el-dialog>
</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
HeadImage,
RtcPrivateAcceptor
},
data() {
return {
isShow: false,
stream: null,
audio: new Audio(),
loading: false,
peerConnection: null,
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: [],
configuration: {
iceServers: []
}
}
},
methods: {
init() {
this.isShow = true;
if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) {
this.$message.error("初始化失败,原因可能是: 1.未部署ssl证书 2.您的浏览器不支持WebRTC");
this.insertMessage("设备不支持通话");
if (!this.rtcInfo.isHost) {
this.sendFailed("对方设备不支持通话")
}
return;
open(rtcInfo) {
this.showRoom = true;
this.mode = rtcInfo.mode;
this.isHost = rtcInfo.isHost;
this.friend = rtcInfo.friend;
if (this.isHost) {
this.onCall();
}
//
this.openCamera((stream) => {
// webrtc
this.setupPeerConnection(stream);
if (this.rtcInfo.isHost) {
//
this.call();
},
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.accept(this.rtcInfo.offer);
// ,
this.candidates.push(candidate)
}
});
//
this.startHeartBeat();
})
//
this.webrtc.onStateChange((state) => {
if (state == "connected") {
console.log("webrtc连接成功")
} else if (state == "disconnected") {
console.log("webrtc连接断开")
}
})
},
openCamera(callback) {
navigator.getUserMedia({
video: this.isVideo,
audio: true
}, (stream) => {
console.log(this.loading)
this.stream = stream;
this.$refs.mineVideo.srcObject = stream;
this.$refs.mineVideo.muted = true;
callback(stream)
}, (error) => {
let devText = this.isVideo ? "摄像头" : "麦克风"
this.$message.error(`打开${devText}失败:${error}`);
callback()
onCall() {
if (!this.checkDevEnable()) {
this.close();
}
// webrtc
this.initRtc();
//
this.startHeartBeat();
//
this.openStream().finally(() => {
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();
})
})
})
},
closeCamera() {
if (this.stream) {
this.stream.getTracks().forEach((track) => {
track.stop();
});
this.$refs.mineVideo.srcObject = null;
this.stream = null;
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);
})
})
},
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.isAccepted) {
// ,
this.sendCandidate(event.candidate);
} else {
// ,
this.candidates.push(event.candidate)
}
}
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;
}
if (stream) {
stream.getTracks().forEach((track) => {
this.peerConnection.addTrack(track, stream);
});
// 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;
}
this.peerConnection.oniceconnectionstatechange = (event) => {
let state = event.target.iceConnectionState;
console.log("ICE connection status changed : " + state)
if (state == 'connected') {
this.resetTime();
}
};
},
insertMessage(messageTip) {
//
let chat = {
type: 'PRIVATE',
targetId: this.rtcInfo.friend.id,
showName: this.rtcInfo.friend.nickName,
headImage: this.rtcInfo.friend.headImage,
};
this.$store.commit("openChat", chat);
//
let MESSAGE_TYPE = this.$enums.MESSAGE_TYPE;
let msgInfo = {
type: this.rtcInfo.mode == "video" ? MESSAGE_TYPE.RT_VIDEO : MESSAGE_TYPE.RT_VOICE,
sendId: this.rtcInfo.sendId,
recvId: this.rtcInfo.recvId,
content: this.isChating ? "通话时长 " + this.currentTime : messageTip,
status: 1,
selfSend: this.rtcInfo.isHost,
sendTime: new Date().getTime()
}
this.$store.commit("insertMessage", msgInfo);
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)
})
},
onRTCMessage(msg) {
if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
onRTCAccept(msg) {
if (msg.selfSend) {
//
this.$message.success("已在其他设备接听");
this.close();
} else {
//
this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content)));
//
this.loading = false;
let offer = JSON.parse(msg.content);
this.webrtc.setRemoteDescription(offer);
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING)
this.state = 'CHATING'
//
this.audio.pause();
// candidate
this.candidates.forEach((candidate) => {
this.sendCandidate(candidate);
this.API.sendCandidate(this.friend.id, candidate);
})
} else if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
//
this.$message.error("对方拒绝了您的通话请求");
//
this.insertMessage("对方已拒绝")
//
this.close();
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) {
//
this.$message.error(msg.content)
//
this.insertMessage(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.insertMessage("对方已挂断")
this.close();
}
},
call() {
let offerParam = {
offerToRecieveAudio: 1,
offerToRecieveVideo: this.isVideo ? 1 : 0
onRTCReject(msg) {
if (msg.selfSend) {
this.$message.success("已在其他设备拒绝");
this.close();
} else {
this.$message.error("对方拒绝了您的通话请求");
this.close();
}
this.peerConnection.createOffer(offerParam).then((offer) => {
this.peerConnection.setLocalDescription(offer);
this.$http({
url: `/webrtc/private/call?uid=${this.rtcInfo.friend.id}&mode=${this.rtcInfo.mode}`,
method: 'post',
data: JSON.stringify(offer)
}).then(() => {
this.loading = true;
//
this.audio.play();
})
}, (error) => {
this.insertMessage("未接通")
this.$message.error(error);
});
},
accept(offer) {
let offerParam = {
offerToRecieveAudio: 1,
offerToRecieveVideo: this.isVideo ? 1 : 0
}
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
this.peerConnection.createAnswer(offerParam).then((answer) => {
this.peerConnection.setLocalDescription(answer);
this.$http({
url: `/webrtc/private/accept?uid=${this.rtcInfo.friend.id}`,
method: 'post',
data: JSON.stringify(answer)
}).then(() => {
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING)
})
},
(error) => {
this.$message.error(error);
});
onRTCFailed(msg) {
//
this.$message.error(msg.content)
this.close();
},
handup() {
this.$http({
url: `/webrtc/private/handup?uid=${this.rtcInfo.friend.id}`,
method: 'post'
})
this.insertMessage("已挂断")
onRTCCancel() {
//
this.$message.success("对方取消了呼叫");
this.close();
this.$message.success("您已挂断,通话结束")
},
cancel() {
this.$http({
url: `/webrtc/private/cancel?uid=${this.rtcInfo.friend.id}`,
method: 'post'
})
this.insertMessage("已取消")
onRTCHandup() {
//
this.$message.success("对方已挂断");
this.close();
this.$message.success("已取消呼叫,通话结束")
},
sendFailed(reason) {
this.$http({
url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`,
method: 'post'
})
onRTCCandidate(msg) {
let candidate = JSON.parse(msg.content);
this.webrtc.addIceCandidate(candidate);
},
sendCandidate(candidate) {
this.$http({
url: `/webrtc/private/candidate?uid=${this.rtcInfo.friend.id}`,
method: 'post',
data: JSON.stringify(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);
})
}
})
},
close() {
this.isShow = false;
this.closeCamera();
this.loading = false;
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.audio.pause();
this.candidates = [];
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection.onicecandidate = null;
this.peerConnection.onaddstream = null;
}
if (this.$refs.friendVideo) {
this.$refs.friendVideo.srcObject = null;
}
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
},
resetTime() {
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.$http({
url: `/webrtc/private/heartbeat?uid=${this.rtcInfo.friend.id}`,
method: 'post'
})
this.API.heartbeat(this.friend.id);
}, 15000)
},
handleClose() {
if (this.isAccepted) {
this.handup()
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.cancel();
this.onCancel();
} 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() {
let iceServers = this.$store.state.configStore.webrtc.iceServers;
this.configuration.iceServers = iceServers;
}
},
watch: {
rtcState: {
handler(newState, oldState) {
// WAIT_CALLACCEPTED
if (newState == this.$enums.RTC_STATE.WAIT_CALL ||
newState == this.$enums.RTC_STATE.ACCEPTED) {
this.init();
}
}
}
},
computed: {
title() {
let strTitle = `${this.modeText}通话-${this.rtcInfo.friend.nickName}`;
let strTitle = `${this.modeText}通话-${this.friend.nickName}`;
if (this.isChating) {
strTitle += `(${this.currentTime})`;
} else if (this.isWaiting) {
@ -374,32 +379,40 @@
strTime += sec;
return strTime;
},
rtcInfo() {
return this.$store.state.userStore.rtcInfo;
},
rtcState() {
return this.rtcInfo.state;
configuration() {
const iceServers = this.$store.state.configStore.webrtc.iceServers;
return {
iceServers: iceServers
}
},
isVideo() {
return this.rtcInfo.mode == "video"
return this.mode == "video"
},
modeText() {
return this.isVideo ? "视频" : "语音";
},
isAccepted() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING ||
this.rtcInfo.state == this.$enums.RTC_STATE.ACCEPTED
isChating() {
return this.state == "CHATING";
},
isWaiting() {
return this.rtcInfo.state == this.$enums.RTC_STATE.WAIT_CALL;
return this.state == "WAITING";
},
isChating() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING;
isClose() {
return this.state == "CLOSE";
}
},
mounted() {
//
this.initAudio();
this.initICEServers();
},
created() {
//
window.addEventListener('beforeunload', () => {
this.onQuit();
});
},
beforeUnmount() {
this.onQuit();
}
}
</script>
@ -412,12 +425,11 @@
color: white !important;
font-size: 16px !important;
}
.el-icon-loading {
color: white !important;
font-size: 30px !important;
.path {
stroke: white !important;
}
.rtc-video-box {
position: relative;
border: #4880b9 solid 1px;

27
im-ui/src/view/Home.vue

@ -41,7 +41,6 @@
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
<rtc-private-acceptor ref="rtcPrivateAcceptor"></rtc-private-acceptor>
<rtc-group-video ref="rtcGroupVideo" ></rtc-group-video>
</el-container>
</template>
@ -73,9 +72,12 @@
},
methods: {
init() {
this.$eventBus.$on('openPrivateVideo', (rctInfo)=>{
//
this.$refs.rtcPrivateVideo.open(rctInfo);
});
this.$eventBus.$on('openGroupVideo', (rctInfo)=>{
//
console.log(this.$refs.rtcGroupVideo)
this.$refs.rtcGroupVideo.open(rctInfo);
});
@ -155,6 +157,11 @@
}
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
// webrtc
if (msg.type >= 100 && msg.type <= 199) {
this.$refs.rtcPrivateVideo.onRTCMessage(msg)
return;
}
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
this.loadFriendInfo(friendId).then((friend) => {
@ -162,21 +169,7 @@
})
},
insertPrivateMessage(friend, msg) {
// webrtc
if (msg.type >= 100 && msg.type <= 199) {
let rtcInfo = this.$store.state.userStore.rtcInfo;
//
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO ||
rtcInfo.state == this.$enums.RTC_STATE.FREE ||
rtcInfo.state == this.$enums.RTC_STATE.WAIT_ACCEPT) {
this.$refs.rtcPrivateAcceptor.onRTCMessage(msg,friend)
} else {
this.$refs.rtcPrivateVideo.onRTCMessage(msg)
}
return;
}
let chatInfo = {
type: 'PRIVATE',
targetId: friend.id,

Loading…
Cancel
Save