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

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

@ -33,9 +33,11 @@ ImWebRtc.prototype.setStream = function(stream) {
if(this.stream){ if(this.stream){
this.peerConnection.removeStream(this.stream) this.peerConnection.removeStream(this.stream)
} }
stream.getTracks().forEach((track) => { if(stream){
this.peerConnection.addTrack(track, stream); stream.getTracks().forEach((track) => {
}); this.peerConnection.addTrack(track, stream);
});
}
this.stream = stream; this.stream = stream;
} }
@ -111,6 +113,7 @@ ImWebRtc.prototype.close = function(uid) {
this.peerConnection.close(); this.peerConnection.close();
this.peerConnection.onicecandidate = null; this.peerConnection.onicecandidate = null;
this.peerConnection.onaddstream = 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" <div title="回执消息" v-show="chat.type == 'GROUP'" class="icon iconfont icon-receipt"
:class="isReceipt ? 'chat-tool-active' : ''" @click="onSwitchReceipt"> :class="isReceipt ? 'chat-tool-active' : ''" @click="onSwitchReceipt">
</div> </div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()"> <div title="发送语音" class="el-icon-microphone" @click="showRecordBox()">
</div> </div>
<div title="语音通话" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline" <div title="语音通话" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@click="showPrivateVideo('voice')"> @click="showPrivateVideo('voice')">
@ -88,7 +88,7 @@
<emotion ref="emoBox" @emotion="onEmotion"></Emotion> <emotion ref="emoBox" @emotion="onEmotion"></Emotion>
<chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers" :search-text="atSearchText" <chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers" :search-text="atSearchText"
@select="onAtSelect"></chat-at-box> @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> <group-member-selector ref="rtcSel" :groupId="group.id" @complete="onInviteOk"></group-member-selector>
<rtc-group-join ref="rtcJoin" :groupId="group.id"></rtc-group-join> <rtc-group-join ref="rtcJoin" :groupId="group.id"></rtc-group-join>
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" <chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group"
@ -102,7 +102,7 @@
import ChatMessageItem from "./ChatMessageItem.vue"; import ChatMessageItem from "./ChatMessageItem.vue";
import FileUpload from "../common/FileUpload.vue"; import FileUpload from "../common/FileUpload.vue";
import Emotion from "../common/Emotion.vue"; import Emotion from "../common/Emotion.vue";
import ChatVoice from "./ChatVoice.vue"; import ChatRecord from "./ChatRecord.vue";
import ChatHistory from "./ChatHistory.vue"; import ChatHistory from "./ChatHistory.vue";
import ChatAtBox from "./ChatAtBox.vue" import ChatAtBox from "./ChatAtBox.vue"
import GroupMemberSelector from "../group/GroupMemberSelector.vue" import GroupMemberSelector from "../group/GroupMemberSelector.vue"
@ -116,7 +116,7 @@
FileUpload, FileUpload,
ChatGroupSide, ChatGroupSide,
Emotion, Emotion,
ChatVoice, ChatRecord,
ChatHistory, ChatHistory,
ChatAtBox, ChatAtBox,
GroupMemberSelector, GroupMemberSelector,
@ -136,7 +136,7 @@
sendImageFile: "", sendImageFile: "",
placeholder: "", placeholder: "",
isReceipt: true, isReceipt: true,
showVoice: false, // showRecord: false, //
showSide: false, // showSide: false, //
showHistory: false, // showHistory: false, //
lockMessage: false, // lockMessage: false, //
@ -452,23 +452,20 @@
range.collapse() range.collapse()
}, },
showVoiceBox() { showRecordBox() {
this.showVoice = true; this.showRecord = true;
}, },
closeVoiceBox() { closeRecordBox() {
this.showVoice = false; this.showRecord = false;
}, },
showPrivateVideo(mode) { showPrivateVideo(mode) {
let rtcInfo = { let rtcInfo = {
mode: mode, mode: mode,
isHost: true, isHost: true,
friend: this.friend, 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() { onGroupVideo() {
this.$http({ this.$http({
@ -517,7 +514,7 @@
closeHistoryBox() { closeHistoryBox() {
this.showHistory = false; this.showHistory = false;
}, },
onSendVoice(data) { onSendRecord(data) {
let msgInfo = { let msgInfo = {
content: JSON.stringify(data), content: JSON.stringify(data),
type: 3, type: 3,
@ -544,7 +541,7 @@
// //
this.scrollToBottom(); this.scrollToBottom();
// //
this.showVoice = false; this.showRecord = false;
this.isReceipt = false; this.isReceipt = false;
}) })

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

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

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

@ -1,12 +1,12 @@
<template> <template>
<div v-show="isShow" class="rtc-private-acceptor"> <div class="rtc-private-acceptor">
<head-image :id="rtcInfo.friend.id" :name="rtcInfo.friend.nickName" :url="rtcInfo.friend.headImage" :size="100"></head-image> <head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"></head-image>
<div class="acceptor-text"> <div class="acceptor-text">
{{tip}} {{tip}}
</div> </div>
<div class="acceptor-btn-group"> <div class="acceptor-btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="accpet()" title="接受"></div> <div class="icon iconfont icon-phone-accept accept" @click="$emit('accept')" title="接受"></div>
<div class="icon iconfont icon-phone-reject reject" @click="reject()" title="拒绝"></div> <div class="icon iconfont icon-phone-reject reject" @click="$emit('reject')" title="拒绝"></div>
</div> </div>
</div> </div>
</template> </template>
@ -20,172 +20,21 @@
HeadImage HeadImage
}, },
data() { data() {
return { return {}
isShow: false,
audio: new Audio()
}
}, },
methods: { props: {
accpet() { mode:{
// type: String
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)
}, },
onRtcCancel(msgInfo) { friend:{
// type: Object
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;
} }
}, },
computed: { computed: {
tip() { tip() {
let modeText = this.mode == "video" ? "视频" : "语音" let modeText = this.mode == "video" ? "视频" : "语音"
return `${this.rtcInfo.friend.nickName} 请求和您进行${modeText}通话...` return `${this.friend.nickName} 请求和您进行${modeText}通话...`
},
rtcInfo(){
return this.$store.state.userStore.rtcInfo;
} }
},
mounted() {
//
this.initAudio();
} }
} }
</script> </script>

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

@ -1,362 +1,367 @@
<template> <template>
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false" <div>
:visible="isShow" width="50%" height="70%" :before-close="handleClose"> <el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false"
<div class="rtc-private-video"> :visible.sync="showRoom" width="50%" height="70%" :before-close="onQuit">
<div v-show="rtcInfo.mode=='video'" class="rtc-video-box"> <div class="rtc-private-video">
<div class="rtc-video-friend" v-loading="loading" element-loading-text="等待对方接听..." <div v-show="isVideo" class="rtc-video-box">
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)"> <div class="rtc-video-friend" v-loading="!isChating"
<head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="80" :name="rtcInfo.friend.nickName" element-loading-spinner="el-icon-loading" >
:url="rtcInfo.friend.headImage"> <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> </head-image>
<video ref="friendVideo" autoplay=""></video>
</div> </div>
<div class="rtc-video-mine"> <div class="rtc-control-bar">
<video ref="mineVideo" autoplay=""></video> <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> </div>
<div v-show="rtcInfo.mode=='voice'" class="rtc-voice-box" v-loading="loading" element-loading-text="等待对方接听..." </el-dialog>
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)"> <rtc-private-acceptor v-if="!isHost&&isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept"
<head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="200" :name="rtcInfo.friend.nickName" @reject="onReject"></rtc-private-acceptor>
:url="rtcInfo.friend.headImage"> </div>
<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>
</template> </template>
<script> <script>
import HeadImage from '../common/HeadImage.vue'; 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 { export default {
name: 'rtcPrivateVideo', name: 'rtcPrivateVideo',
components: { components: {
HeadImage HeadImage,
RtcPrivateAcceptor
}, },
data() { data() {
return { return {
isShow: false, camera: new ImCamera(), //
stream: null, webrtc: new ImWebRtc(), // webrtc
audio: new Audio(), API: new RtcPrivateApi(), // API
loading: false, audio: new Audio(), //
peerConnection: null, showRoom: false,
friend: {},
isHost: false, //
state: "CLOSE", // CLOSE: WAITING: CHATING: ERROR:
mode: 'video', // video: voice:
localStream: null, //
remoteStream: null, //
videoTime: 0, videoTime: 0,
videoTimer: null, videoTimer: null,
heartbeatTimer: null, heartbeatTimer: null,
candidates: [], candidates: [],
configuration: {
iceServers: []
}
} }
}, },
methods: { methods: {
init() { open(rtcInfo) {
this.isShow = true; this.showRoom = true;
if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) { this.mode = rtcInfo.mode;
this.$message.error("初始化失败,原因可能是: 1.未部署ssl证书 2.您的浏览器不支持WebRTC"); this.isHost = rtcInfo.isHost;
this.insertMessage("设备不支持通话"); this.friend = rtcInfo.friend;
if (!this.rtcInfo.isHost) { if (this.isHost) {
this.sendFailed("对方设备不支持通话") this.onCall();
}
return;
} }
// },
this.openCamera((stream) => { initAudio() {
// webrtc let url = require(`@/assets/audio/call.wav`);
this.setupPeerConnection(stream); this.audio.src = url;
if (this.rtcInfo.isHost) { this.audio.loop = true;
// },
this.call(); 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 { } 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) { onCall() {
navigator.getUserMedia({ if (!this.checkDevEnable()) {
video: this.isVideo, this.close();
audio: true }
}, (stream) => { // webrtc
console.log(this.loading) this.initRtc();
this.stream = stream; //
this.$refs.mineVideo.srcObject = stream; this.startHeartBeat();
this.$refs.mineVideo.muted = true; //
callback(stream) this.openStream().finally(() => {
}, (error) => { this.webrtc.setStream(this.localStream);
let devText = this.isVideo ? "摄像头" : "麦克风" this.webrtc.createOffer().then((offer) => {
this.$message.error(`打开${devText}失败:${error}`); //
callback() this.API.call(this.friend.id, this.mode, offer).then(() => {
//
this.state = "WAITING";
//
this.audio.play();
}).catch(()=>{
this.close();
})
})
}) })
}, },
closeCamera() { onAccept() {
if (this.stream) { if (!this.checkDevEnable()) {
this.stream.getTracks().forEach((track) => { this.API.failed(this.friend.id, "对方设备不支持通话")
track.stop(); this.close();
}); return;
this.$refs.mineVideo.srcObject = null;
this.stream = null;
} }
//
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) { onReject() {
this.peerConnection = new RTCPeerConnection(this.configuration); console.log("onReject")
this.peerConnection.ontrack = (e) => { // 退
this.$refs.friendVideo.srcObject = e.streams[0]; this.API.reject(this.friend.id);
}; // 退
this.peerConnection.onicecandidate = (event) => { this.close();
if (event.candidate) { },
if (this.isAccepted) { onHandup() {
// , this.API.handup(this.friend.id)
this.sendCandidate(event.candidate); this.$message.success("您已挂断,通话结束")
} else { this.close();
// , },
this.candidates.push(event.candidate) 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) { // RTC
stream.getTracks().forEach((track) => { switch (msg.type) {
this.peerConnection.addTrack(track, stream); 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) { onRTCCall(msg, mode) {
// this.offer = JSON.parse(msg.content);
let chat = { this.isHost = false;
type: 'PRIVATE', this.mode = mode;
targetId: this.rtcInfo.friend.id, this.$http({
showName: this.rtcInfo.friend.nickName, url: `/friend/find/${msg.sendId}`,
headImage: this.rtcInfo.friend.headImage, method: 'get'
}; }).then((friend) => {
this.$store.commit("openChat", chat); this.friend = friend;
// this.state = "WAITING";
let MESSAGE_TYPE = this.$enums.MESSAGE_TYPE; this.audio.play();
let msgInfo = { this.startHeartBeat();
type: this.rtcInfo.mode == "video" ? MESSAGE_TYPE.RT_VIDEO : MESSAGE_TYPE.RT_VOICE, // 30s
sendId: this.rtcInfo.sendId, this.waitTimer = setTimeout(() => {
recvId: this.rtcInfo.recvId, this.API.failed(this.friend.id,"对方无应答");
content: this.isChating ? "通话时长 " + this.currentTime : messageTip, this.$message.error("您未接听");
status: 1, this.close();
selfSend: this.rtcInfo.isHost, }, 30000)
sendTime: new Date().getTime() })
}
this.$store.commit("insertMessage", msgInfo);
}, },
onRTCMessage(msg) { onRTCAccept(msg) {
if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) { if (msg.selfSend) {
//
this.$message.success("已在其他设备接听");
this.close();
} else {
// //
this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content))); let offer = JSON.parse(msg.content);
// this.webrtc.setRemoteDescription(offer);
this.loading = false;
// //
this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING) this.state = 'CHATING'
// //
this.audio.pause(); this.audio.pause();
// candidate // candidate
this.candidates.forEach((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() { onRTCReject(msg) {
let offerParam = { if (msg.selfSend) {
offerToRecieveAudio: 1, this.$message.success("已在其他设备拒绝");
offerToRecieveVideo: this.isVideo ? 1 : 0 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) { onRTCFailed(msg) {
let offerParam = { //
offerToRecieveAudio: 1, this.$message.error(msg.content)
offerToRecieveVideo: this.isVideo ? 1 : 0 this.close();
}
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);
});
}, },
handup() { onRTCCancel() {
this.$http({ //
url: `/webrtc/private/handup?uid=${this.rtcInfo.friend.id}`, this.$message.success("对方取消了呼叫");
method: 'post'
})
this.insertMessage("已挂断")
this.close(); this.close();
this.$message.success("您已挂断,通话结束")
}, },
cancel() { onRTCHandup() {
this.$http({ //
url: `/webrtc/private/cancel?uid=${this.rtcInfo.friend.id}`, this.$message.success("对方已挂断");
method: 'post'
})
this.insertMessage("已取消")
this.close(); this.close();
this.$message.success("已取消呼叫,通话结束")
}, },
sendFailed(reason) { onRTCCandidate(msg) {
this.$http({ let candidate = JSON.parse(msg.content);
url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`, this.webrtc.addIceCandidate(candidate);
method: 'post'
})
}, },
sendCandidate(candidate) {
this.$http({ openStream() {
url: `/webrtc/private/candidate?uid=${this.rtcInfo.friend.id}`, return new Promise((resolve, reject) => {
method: 'post', if (this.isVideo) {
data: JSON.stringify(candidate) // +
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() { startChatTime() {
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() {
this.videoTime = 0; this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer); this.videoTimer && clearInterval(this.videoTimer);
this.videoTimer = setInterval(() => { this.videoTimer = setInterval(() => {
this.videoTime++; this.videoTime++;
}, 1000) }, 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() { startHeartBeat() {
// 15s // 15s
this.heartbeatTimer && clearInterval(this.heartbeatTimer); this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => { this.heartbeatTimer = setInterval(() => {
this.$http({ this.API.heartbeat(this.friend.id);
url: `/webrtc/private/heartbeat?uid=${this.rtcInfo.friend.id}`,
method: 'post'
})
}, 15000) }, 15000)
}, },
handleClose() { close() {
if (this.isAccepted) { this.showRoom = false;
this.handup() 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) { } else if (this.isWaiting) {
this.cancel(); this.onCancel();
} else { } else {
this.close(); 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: { computed: {
title() { title() {
let strTitle = `${this.modeText}通话-${this.rtcInfo.friend.nickName}`; let strTitle = `${this.modeText}通话-${this.friend.nickName}`;
if (this.isChating) { if (this.isChating) {
strTitle += `(${this.currentTime})`; strTitle += `(${this.currentTime})`;
} else if (this.isWaiting) { } else if (this.isWaiting) {
@ -374,32 +379,40 @@
strTime += sec; strTime += sec;
return strTime; return strTime;
}, },
rtcInfo() { configuration() {
return this.$store.state.userStore.rtcInfo; const iceServers = this.$store.state.configStore.webrtc.iceServers;
}, return {
rtcState() { iceServers: iceServers
return this.rtcInfo.state; }
}, },
isVideo() { isVideo() {
return this.rtcInfo.mode == "video" return this.mode == "video"
}, },
modeText() { modeText() {
return this.isVideo ? "视频" : "语音"; return this.isVideo ? "视频" : "语音";
}, },
isAccepted() { isChating() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING || return this.state == "CHATING";
this.rtcInfo.state == this.$enums.RTC_STATE.ACCEPTED
}, },
isWaiting() { isWaiting() {
return this.rtcInfo.state == this.$enums.RTC_STATE.WAIT_CALL; return this.state == "WAITING";
}, },
isChating() { isClose() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING; return this.state == "CLOSE";
} }
}, },
mounted() { mounted() {
//
this.initAudio(); this.initAudio();
this.initICEServers(); },
created() {
//
window.addEventListener('beforeunload', () => {
this.onQuit();
});
},
beforeUnmount() {
this.onQuit();
} }
} }
</script> </script>
@ -412,12 +425,11 @@
color: white !important; color: white !important;
font-size: 16px !important; font-size: 16px !important;
} }
.el-icon-loading { .path {
color: white !important; stroke: white !important;
font-size: 30px !important;
} }
.rtc-video-box { .rtc-video-box {
position: relative; position: relative;
border: #4880b9 solid 1px; 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" <full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image> @close="$store.commit('closeFullImageBox')"></full-image>
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video> <rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
<rtc-private-acceptor ref="rtcPrivateAcceptor"></rtc-private-acceptor>
<rtc-group-video ref="rtcGroupVideo" ></rtc-group-video> <rtc-group-video ref="rtcGroupVideo" ></rtc-group-video>
</el-container> </el-container>
</template> </template>
@ -73,9 +72,12 @@
}, },
methods: { methods: {
init() { init() {
this.$eventBus.$on('openPrivateVideo', (rctInfo)=>{
//
this.$refs.rtcPrivateVideo.open(rctInfo);
});
this.$eventBus.$on('openGroupVideo', (rctInfo)=>{ this.$eventBus.$on('openGroupVideo', (rctInfo)=>{
// //
console.log(this.$refs.rtcGroupVideo)
this.$refs.rtcGroupVideo.open(rctInfo); this.$refs.rtcGroupVideo.open(rctInfo);
}); });
@ -155,6 +157,11 @@
} }
// //
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; 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 // id
let friendId = msg.selfSend ? msg.recvId : msg.sendId; let friendId = msg.selfSend ? msg.recvId : msg.sendId;
this.loadFriendInfo(friendId).then((friend) => { this.loadFriendInfo(friendId).then((friend) => {
@ -162,21 +169,7 @@
}) })
}, },
insertPrivateMessage(friend, msg) { 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 = { let chatInfo = {
type: 'PRIVATE', type: 'PRIVATE',
targetId: friend.id, targetId: friend.id,

Loading…
Cancel
Save