Browse Source

feat:音视频结果加入会话消息

master
xsx 2 years ago
parent
commit
37d1075116
  1. 4
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java
  2. 2
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java
  3. 7
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java
  4. 17
      im-ui/src/api/enums.js
  5. 10
      im-ui/src/assets/iconfont/iconfont.css
  6. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  7. 48
      im-ui/src/components/chat/ChatBox.vue
  8. 76
      im-ui/src/components/chat/ChatMessageItem.vue
  9. 260
      im-ui/src/components/chat/ChatPrivateVideo.vue
  10. 155
      im-ui/src/components/chat/ChatVideoAcceptor.vue
  11. 8
      im-ui/src/store/chatStore.js
  12. 28
      im-ui/src/store/uiStore.js
  13. 22
      im-ui/src/store/userStore.js
  14. 2
      im-ui/src/view/Friend.vue
  15. 61
      im-ui/src/view/Home.vue
  16. 2
      im-uniapp/common/enums.js
  17. 48
      im-uniapp/components/chat-message-item/chat-message-item.vue
  18. 20
      im-uniapp/manifest.json
  19. 18
      im-uniapp/pages/chat/chat-box.vue
  20. 113
      im-uniapp/pages/chat/chat-video.vue
  21. 10
      im-uniapp/static/icon/iconfont.css
  22. BIN
      im-uniapp/static/icon/iconfont.ttf
  23. 10
      im-uniapp/store/chatStore.js

4
im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java

@ -57,8 +57,8 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "挂断") @ApiOperation(httpMethod = "POST", value = "挂断")
@PostMapping("/handup") @PostMapping("/handup")
public Result leave(@RequestParam Long uid) { public Result handup(@RequestParam Long uid) {
webrtcService.leave(uid); webrtcService.handup(uid);
return ResultUtils.success(); return ResultUtils.success();
} }

2
im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java

@ -22,7 +22,7 @@ public interface IWebrtcService {
void failed(Long uid, String reason); void failed(Long uid, String reason);
void leave(Long uid); void handup(Long uid);
void candidate(Long uid, String candidate); void candidate(Long uid, String candidate);

7
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java

@ -152,8 +152,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>(); IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(uid); sendMessage.setRecvId(uid);
// 告知其他终端已经会话失败,中止呼叫 sendMessage.setSendToSelf(false);
sendMessage.setSendToSelf(true);
sendMessage.setSendResult(false); sendMessage.setSendResult(false);
sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal())); sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal()));
sendMessage.setData(messageInfo); sendMessage.setData(messageInfo);
@ -163,7 +162,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
} }
@Override @Override
public void leave(Long uid) { public void handup(Long uid) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 查询webrtc会话 // 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid); WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
@ -219,7 +218,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
String key = getSessionKey(userId, uid); String key = getSessionKey(userId, uid);
WebrtcSession webrtcSession = (WebrtcSession)redisTemplate.opsForValue().get(key); WebrtcSession webrtcSession = (WebrtcSession)redisTemplate.opsForValue().get(key);
if (webrtcSession == null) { if (webrtcSession == null) {
throw new GlobalException("视频通话已结束"); throw new GlobalException("通话已结束");
} }
return webrtcSession; return webrtcSession;
} }

17
im-ui/src/api/enums.js

@ -5,13 +5,16 @@ const MESSAGE_TYPE = {
FILE:2, FILE:2,
AUDIO:3, AUDIO:3,
VIDEO:4, VIDEO:4,
RT_VOICE:5,
RT_VIDEO:6,
RECALL:10, RECALL:10,
READED:11, READED:11,
RECEIPT:12, RECEIPT:12,
TIP_TIME:20, TIP_TIME:20,
TIP_TEXT:21, TIP_TEXT:21,
LOADDING:30, LOADDING:30,
RTC_CALL: 101, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,
RTC_REJECT: 103, RTC_REJECT: 103,
RTC_CANCEL: 104, RTC_CANCEL: 104,
@ -20,10 +23,12 @@ const MESSAGE_TYPE = {
RTC_CANDIDATE: 107 RTC_CANDIDATE: 107
} }
const USER_STATE = { const RTC_STATE = {
OFFLINE: 0, FREE: 0, //空闲,可以被呼叫
FREE: 1, WAIT_CALL: 1, // 呼叫后等待
BUSY: 2 WAIT_ACCEPT: 2, // 被呼叫后等待
ACCEPTED: 3, // 已接受聊天,等待建立连接
CHATING:4 // 聊天中
} }
const TERMINAL_TYPE = { const TERMINAL_TYPE = {
@ -41,7 +46,7 @@ const MESSAGE_STATUS = {
export { export {
MESSAGE_TYPE, MESSAGE_TYPE,
USER_STATE, RTC_STATE,
TERMINAL_TYPE, TERMINAL_TYPE,
MESSAGE_STATUS MESSAGE_STATUS
} }

10
im-ui/src/assets/iconfont/iconfont.css

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3791506 */ font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.ttf?t=1706022894868') format('truetype'); src: url('iconfont.ttf?t=1710567233281') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-chat-video:before {
content: "\e73b";
}
.icon-chat-voice:before {
content: "\e633";
}
.icon-ok:before { .icon-ok:before {
content: "\e6ac"; content: "\e6ac";
} }

BIN
im-ui/src/assets/iconfont/iconfont.ttf

Binary file not shown.

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

@ -13,7 +13,9 @@
<div class="im-chat-box"> <div class="im-chat-box">
<ul> <ul>
<li v-for="(msgInfo, idx) in chat.messages" :key="idx"> <li v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item v-show="idx >= showMinIdx" :mine="msgInfo.sendId == mine.id" <chat-message-item v-show="idx >= showMinIdx"
@call="onCall(msgInfo.type)"
:mine="msgInfo.sendId == mine.id"
:headImage="headImage(msgInfo)" :showName="showName(msgInfo)" :msgInfo="msgInfo" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" :msgInfo="msgInfo"
:groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage"> :groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage">
</chat-message-item> </chat-message-item>
@ -44,8 +46,11 @@
</div> </div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()"> <div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</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="showVideoBox()"> @click="showChatVideo('voice')">
</div>
<div title="视频通话" v-show="chat.type == 'PRIVATE'" class="el-icon-video-camera"
@click="showChatVideo('video')">
</div> </div>
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div> <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div> </div>
@ -133,13 +138,19 @@ export default {
methods: { methods: {
moveChatToTop(){ moveChatToTop(){
let chatIdx = this.$store.getters.findChatIdx(this.chat); let chatIdx = this.$store.getters.findChatIdx(this.chat);
console.log(chatIdx);
this.$store.commit("moveTop",chatIdx); this.$store.commit("moveTop",chatIdx);
}, },
closeRefBox() { closeRefBox() {
this.$refs.emoBox.close(); this.$refs.emoBox.close();
this.$refs.atBox.close(); this.$refs.atBox.close();
}, },
onCall(type){
if(type == this.$enums.MESSAGE_TYPE.RT_VOICE){
this.showChatVideo('voice');
}else if(type == this.$enums.MESSAGE_TYPE.RT_VIDEO){
this.showChatVideo('video');
}
},
onKeyDown() { onKeyDown() {
if (this.$refs.atBox.show) { if (this.$refs.atBox.show) {
this.$refs.atBox.moveDown() this.$refs.atBox.moveDown()
@ -433,11 +444,17 @@ export default {
closeVoiceBox() { closeVoiceBox() {
this.showVoice = false; this.showVoice = false;
}, },
showVideoBox() { showChatVideo(mode) {
this.$store.commit("showChatPrivateVideoBox", { let rtcInfo = {
mode: mode,
isHost: true,
friend: this.friend, friend: this.friend,
master: true sendId: this.$store.state.userStore.userInfo.id,
}); recvId: this.friend.id,
offer: "",
state: this.$enums.RTC_STATE.WAIT_CALL
}
this.$store.commit("setRtcInfo",rtcInfo);
}, },
showHistoryBox() { showHistoryBox() {
this.showHistory = true; this.showHistory = true;
@ -686,6 +703,12 @@ export default {
}, },
unreadCount() { unreadCount() {
return this.chat.unreadCount; return this.chat.unreadCount;
},
messageSize() {
if (!this.chat || !this.chat.messages) {
return 0;
}
return this.chat.messages.length;
} }
}, },
watch: { watch: {
@ -716,9 +739,9 @@ export default {
}, },
immediate: true immediate: true
}, },
unreadCount: { messageSize: {
handler(newCount, oldCount) { handler(newSize, oldSize) {
if (newCount > 0) { if (newSize > oldSize) {
// //
this.scrollToBottom(); this.scrollToBottom();
} }
@ -812,7 +835,6 @@ export default {
} }
} }
.send-content-area { .send-content-area {
position: relative; position: relative;
display: flex; display: flex;
@ -820,8 +842,6 @@ export default {
height: 100%; height: 100%;
background-color: white !important; background-color: white !important;
.send-text-area { .send-text-area {
box-sizing: border-box; box-sizing: border-box;
padding: 5px; padding: 5px;

76
im-ui/src/components/chat/ChatMessageItem.vue

@ -1,6 +1,9 @@
<template> <template>
<div class="chat-msg-item"> <div class="chat-msg-item">
<div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">{{ msgInfo.content }}</div> <div class="chat-msg-tip"
v-show="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{ msgInfo.content }}
</div>
<div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME"> <div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{ $date.toTimeText(msgInfo.sendTime) }} {{ $date.toTimeText(msgInfo.sendTime) }}
</div> </div>
@ -48,30 +51,39 @@
<div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()"> <div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> <audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div> </div>
<div class="chat-realtime chat-msg-text" v-if="isRealtime">
<span v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VOICE" title="重新呼叫"
@click="$emit('call')" class="iconfont icon-chat-voice"></span>
<span v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VIDEO" title="重新呼叫"
@click="$emit('call')" class="iconfont icon-chat-video"></span>
<span>{{msgInfo.content}}</span>
</div>
<div class="chat-msg-status" v-if="!isRealtime">
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId <span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span> && msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId <span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span> && msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span>
</div>
<div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox"> <div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span> <span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span>
<span v-else>{{msgInfo.readedCount}}人已读</span> <span v-else>{{msgInfo.readedCount}}人已读</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show = false" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@select="onSelectMenu"></right-menu> @close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
<chat-group-readed ref="chatGroupReadedBox" :msgInfo="msgInfo" :groupMembers="groupMembers"></chat-group-readed> <chat-group-readed ref="chatGroupReadedBox" :msgInfo="msgInfo" :groupMembers="groupMembers"></chat-group-readed>
</div> </div>
</template> </template>
<script> <script>
import HeadImage from "../common/HeadImage.vue"; import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue'; import ChatGroupReaded from './ChatGroupReaded.vue';
export default { export default {
name: "messageItem", name: "messageItem",
components: { components: {
HeadImage, HeadImage,
@ -188,13 +200,17 @@ export default {
}); });
} }
return items; return items;
},
isRealtime() {
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE ||
this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO
}
} }
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.chat-msg-item { .chat-msg-item {
.chat-msg-tip { .chat-msg-tip {
line-height: 50px; line-height: 50px;
@ -245,14 +261,14 @@ export default {
.chat-msg-text { .chat-msg-text {
display: block; display: block;
position: relative; position: relative;
line-height: 30px; line-height: 26px;
margin-top: 3px; margin-top: 3px;
padding: 7px; padding: 7px;
background-color: white; background-color: white;
border-radius: 10px; border-radius: 10px;
color: black; color: black;
display: block; display: block;
font-size: 16px; font-size: 14px;
text-align: left; text-align: left;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
@ -298,6 +314,7 @@ export default {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
padding-bottom: 5px; padding-bottom: 5px;
.chat-file-box { .chat-file-box {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -352,19 +369,34 @@ export default {
} }
} }
.chat-unread { .chat-realtime {
font-size: 12px; display: flex;
color: #f23c0f; align-items: center;
font-weight: 600;
.iconfont {
cursor: pointer;
font-size: 22px;
padding-right: 8px;
}
} }
.chat-msg-status {
display: block;
.chat-readed { .chat-readed {
font-size: 12px; font-size: 12px;
color: #888; color: #888;
font-weight: 600; font-weight: 600;
} }
.chat-receipt{ .chat-unread {
font-size: 12px;
color: #f23c0f;
font-weight: 600;
}
}
.chat-receipt {
font-size: 13px; font-size: 13px;
color: blue; color: blue;
cursor: pointer; cursor: pointer;
@ -425,10 +457,18 @@ export default {
.chat-msg-file { .chat-msg-file {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.chat-realtime {
flex-direction: row-reverse;
.iconfont {
transform: rotateY(180deg);
}
}
} }
} }
} }
} }
} }
</style> </style>

260
im-ui/src/components/chat/ChatPrivateVideo.vue

@ -1,13 +1,12 @@
<template> <template>
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false" <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"> :visible="isShow" width="50%" height="70%" :before-close="handleClose">
<div class="chat-video"> <div class="chat-video">
<div class="chat-video-box"> <div v-show="rtcInfo.mode=='video'" class="chat-video-box">
<div class="chat-video-friend" v-loading="loading" element-loading-text="等待对方接听..." <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)"> element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)">
<head-image class="friend-head-image" <head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="80" :name="rtcInfo.friend.nickName"
:id="friend.id" :size="80" :name="friend.nickName" :url="rtcInfo.friend.headImage">
:url="friend.headImage">
</head-image> </head-image>
<video ref="friendVideo" autoplay=""></video> <video ref="friendVideo" autoplay=""></video>
</div> </div>
@ -15,11 +14,18 @@
<video ref="mineVideo" autoplay=""></video> <video ref="mineVideo" autoplay=""></video>
</div> </div>
</div> </div>
<div v-show="rtcInfo.mode=='voice'" class="chat-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="chat-voice-name">{{rtcInfo.friend.nickName}}</div>
</head-image>
</div>
<div class="chat-video-controllbar"> <div class="chat-video-controllbar">
<div v-show="state=='CONNECTING'" title="取消呼叫" class="icon iconfont icon-phone-reject reject" <div v-show="isWaiting" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;"
style="color: red;" @click="cancel()"></div> @click="cancel()"></div>
<div v-show="state=='CONNECTED'" title="挂断" class="icon iconfont icon-phone-reject reject" <div v-show="isAccepted" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;"
style="color: red;" @click="handup()"></div> @click="handup()"></div>
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@ -34,30 +40,15 @@
components: { components: {
HeadImage HeadImage
}, },
props: {
visible: {
type: Boolean
},
friend: {
type: Object
},
master: {
type: Boolean
},
offer: {
type: Object
}
},
data() { data() {
return { return {
callerId: null, isShow: false,
stream: null, stream: null,
audio: new Audio(), audio: new Audio(),
loading: false, loading: false,
peerConnection: null, peerConnection: null,
videoTime: 0, videoTime: 0,
videoTimer: null, videoTimer: null,
state: 'NOT_CONNECTED',
candidates: [], candidates: [],
configuration: { configuration: {
iceServers: [] iceServers: []
@ -66,10 +57,12 @@
}, },
methods: { methods: {
init() { init() {
this.isShow = true;
if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) { if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) {
this.$message.error("初始化失败,原因可能是: 1.未部署ssl证书 2.您的浏览器不支持WebRTC"); this.$message.error("初始化失败,原因可能是: 1.未部署ssl证书 2.您的浏览器不支持WebRTC");
if (!this.master) { this.insertMessage("设备不支持通话");
this.sendFailed("对方浏览器不支持WebRTC") if (!this.rtcInfo.isHost) {
this.sendFailed("对方设备不支持通话")
} }
return; return;
} }
@ -77,30 +70,30 @@
this.openCamera((stream) => { this.openCamera((stream) => {
// webrtc // webrtc
this.setupPeerConnection(stream); this.setupPeerConnection(stream);
if (this.master) { if (this.rtcInfo.isHost) {
// //
this.call(); this.call();
} else { } else {
// //
this.accept(this.offer); this.accept(this.rtcInfo.offer);
} }
}); });
}, },
openCamera(callback) { openCamera(callback) {
navigator.getUserMedia({ navigator.getUserMedia({
video: true, video: this.isVideo,
audio: true audio: true
}, }, (stream) => {
(stream) => { console.log(this.loading)
this.stream = stream; this.stream = stream;
this.$refs.mineVideo.srcObject = stream; this.$refs.mineVideo.srcObject = stream;
this.$refs.mineVideo.muted = true; this.$refs.mineVideo.muted = true;
callback(stream) callback(stream)
}, }, (error) => {
(error) => { let devText = this.isVideo ? "摄像头" : "麦克风"
this.$message.error("打开摄像头失败:" + error); this.$message.error(`打开${devText}失败:${error}`);
callback() callback()
}); })
}, },
closeCamera() { closeCamera() {
if (this.stream) { if (this.stream) {
@ -110,7 +103,6 @@
this.$refs.mineVideo.srcObject = null; this.$refs.mineVideo.srcObject = null;
this.stream = null; this.stream = null;
} }
}, },
setupPeerConnection(stream) { setupPeerConnection(stream) {
this.peerConnection = new RTCPeerConnection(this.configuration); this.peerConnection = new RTCPeerConnection(this.configuration);
@ -119,7 +111,7 @@
}; };
this.peerConnection.onicecandidate = (event) => { this.peerConnection.onicecandidate = (event) => {
if (event.candidate) { if (event.candidate) {
if (this.state == 'CONNECTED') { if (this.isAccepted) {
// , // ,
this.sendCandidate(event.candidate); this.sendCandidate(event.candidate);
} else { } else {
@ -140,78 +132,104 @@
this.resetTime(); this.resetTime();
} }
}; };
}, },
handleMessage(msg) { insertMessage(messageTip) {
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) { //
if (msg.selfSend) { let chat = {
// type: 'PRIVATE',
this.$message.success("已在其他设备接听"); targetId: this.rtcInfo.friend.id,
this.close(); showName: this.rtcInfo.friend.nickName,
} else { 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: this.isChating ? "通话时长 " + this.currentTime : messageTip,
status: 1,
selfSend: this.rtcInfo.isHost,
sendTime: new Date().getTime()
}
this.$store.commit("insertMessage", msgInfo);
},
onRTCMessage(msg) {
if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
//
this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content))); this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content)));
// //
this.loading = false; this.loading = false;
// //
this.state = 'CONNECTED'; this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING)
// //
this.audio.pause(); this.audio.pause();
// candidate // candidate
this.candidates.forEach((candidate) => { this.candidates.forEach((candidate) => {
this.sendCandidate(candidate); this.sendCandidate(candidate);
}) })
} } else if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
//
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) { this.$message.error("对方拒绝了您的通话请求");
if (msg.selfSend) { //
// this.insertMessage("对方已拒绝")
this.$message.success("已在其他设备拒绝通话"); //
this.close(); this.close();
} else {
//
this.$message.error("对方拒绝了您的视频请求");
this.close();
}
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) { } else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) {
//
this.$message.error(msg.content) this.$message.error(msg.content)
//
this.insertMessage(msg.content)
this.close(); this.close();
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { } else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
// 线
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content))); this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content)));
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) { } else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) {
this.$message.success("对方挂断了视频通话"); //
this.$message.success("对方已挂断");
//
this.insertMessage("对方已挂断")
this.close(); this.close();
} }
}, },
call() { call() {
this.peerConnection.createOffer((offer) => { let offerParam = {
offerToRecieveAudio: 1,
offerToRecieveVideo: this.isVideo ? 1 : 0
}
this.peerConnection.createOffer(offerParam).then((offer) => {
this.peerConnection.setLocalDescription(offer); this.peerConnection.setLocalDescription(offer);
this.$http({ this.$http({
url: `/webrtc/private/call?uid=${this.friend.id}`, url: `/webrtc/private/call?uid=${this.rtcInfo.friend.id}&mode=${this.rtcInfo.mode}`,
method: 'post', method: 'post',
data: JSON.stringify(offer) data: JSON.stringify(offer)
}).then(() => { }).then(() => {
this.callId = this.$store.state.userStore.userInfo.id;
this.loading = true; this.loading = true;
this.state = 'CONNECTING'; //
this.audio.play(); this.audio.play();
}) })
},(error) => { }, (error) => {
this.insertMessage("未接通")
this.$message.error(error); this.$message.error(error);
}); });
}, },
accept(offer) { accept(offer) {
let offerParam = {
offerToRecieveAudio: 1,
offerToRecieveVideo: this.isVideo ? 1 : 0
}
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
this.peerConnection.createAnswer((answer) => { this.peerConnection.createAnswer(offerParam).then((answer) => {
this.peerConnection.setLocalDescription(answer); this.peerConnection.setLocalDescription(answer);
this.$http({ this.$http({
url: `/webrtc/private/accept?uid=${this.friend.id}`, url: `/webrtc/private/accept?uid=${this.rtcInfo.friend.id}`,
method: 'post', method: 'post',
data: JSON.stringify(answer) data: JSON.stringify(answer)
}).then(() => { }).then(() => {
this.state = 'CONNECTED'; //
this.callerId = this.friend.id; this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING)
}) })
}, },
@ -222,43 +240,43 @@
}, },
handup() { handup() {
this.$http({ this.$http({
url: `/webrtc/private/handup?uid=${this.friend.id}`, url: `/webrtc/private/handup?uid=${this.rtcInfo.friend.id}`,
method: 'post' method: 'post'
}) })
this.insertMessage("已挂断")
this.close(); this.close();
this.$message.success("已挂断视频通话") this.$message.success("您已挂断,通话结束")
}, },
cancel() { cancel() {
this.$http({ this.$http({
url: `/webrtc/private/cancel?uid=${this.friend.id}`, url: `/webrtc/private/cancel?uid=${this.rtcInfo.friend.id}`,
method: 'post' method: 'post'
}) })
this.insertMessage("已取消")
this.close(); this.close();
this.$message.success("已停止呼叫视频通话") this.$message.success("已取消呼叫,通话结束")
}, },
sendFailed(reason) { sendFailed(reason) {
this.$http({ this.$http({
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`, url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`,
method: 'post' method: 'post'
}) })
}, },
sendCandidate(candidate) { sendCandidate(candidate) {
this.$http({ this.$http({
url: `/webrtc/private/candidate?uid=${this.friend.id}`, url: `/webrtc/private/candidate?uid=${this.rtcInfo.friend.id}`,
method: 'post', method: 'post',
data: JSON.stringify(candidate) data: JSON.stringify(candidate)
}) })
}, },
close() { close() {
this.$emit("close"); this.isShow = false;
this.closeCamera(); this.closeCamera();
this.loading = false; this.loading = false;
this.state = 'NOT_CONNECTED';
this.videoTime = 0; this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer); this.videoTimer && clearInterval(this.videoTimer);
this.audio.pause(); this.audio.pause();
this.candidates = []; this.candidates = [];
this.$store.commit("setUserState", this.$enums.USER_STATE.FREE);
if (this.peerConnection) { if (this.peerConnection) {
this.peerConnection.close(); this.peerConnection.close();
this.peerConnection.onicecandidate = null; this.peerConnection.onicecandidate = null;
@ -267,7 +285,8 @@
if (this.$refs.friendVideo) { if (this.$refs.friendVideo) {
this.$refs.friendVideo.srcObject = null; this.$refs.friendVideo.srcObject = null;
} }
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
}, },
resetTime() { resetTime() {
this.videoTime = 0; this.videoTime = 0;
@ -277,9 +296,9 @@
}, 1000) }, 1000)
}, },
handleClose() { handleClose() {
if (this.state == 'CONNECTED') { if (this.isAccepted) {
this.handup() this.handup()
} else if (this.state == 'CONNECTING') { } else if (this.isWaiting) {
this.cancel(); this.cancel();
} else { } else {
this.close(); this.close();
@ -316,39 +335,57 @@
}, },
watch: { watch: {
visible: { rtcState: {
handler(newValue, oldValue) { handler(newState, oldState) {
if (newValue) { // WAIT_CALLACCEPTED
if (newState == this.$enums.RTC_STATE.WAIT_CALL ||
newState == this.$enums.RTC_STATE.ACCEPTED) {
this.init(); this.init();
//
this.$store.commit("setUserState", this.$enums.USER_STATE.BUSY);
} }
} }
} }
}, },
computed: { computed: {
title() { title() {
let strTitle = `视频聊天-${this.friend.nickName}`; let strTitle = `${this.modeText}通话-${this.rtcInfo.friend.nickName}`;
if (this.state == 'CONNECTED') { if (this.isChating) {
strTitle += `(${this.currentTime})`; strTitle += `(${this.currentTime})`;
} else if (this.state == 'CONNECTING') { } else if (this.isWaiting) {
strTitle += `(呼叫中)`; strTitle += `(呼叫中)`;
} }
return strTitle; return strTitle;
}, },
currentTime() { currentTime() {
let currentTime = 0; let min = Math.floor(this.videoTime / 60);
if (this.state == 'CONNECTED' && this.videoTime) { let sec = this.videoTime % 60;
currentTime = Math.floor(this.videoTime);
}
let min = Math.floor(currentTime / 60);
let sec = currentTime % 60;
let strTime = min < 10 ? "0" : ""; let strTime = min < 10 ? "0" : "";
strTime += min; strTime += min;
strTime += ":" strTime += ":"
strTime += sec < 10 ? "0" : ""; strTime += sec < 10 ? "0" : "";
strTime += sec; strTime += sec;
return strTime; return strTime;
},
rtcInfo() {
return this.$store.state.userStore.rtcInfo;
},
rtcState() {
return this.rtcInfo.state;
},
isVideo() {
return this.rtcInfo.mode == "video"
},
modeText() {
return this.isVideo ? "视频" : "语音";
},
isAccepted() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING ||
this.rtcInfo.state == this.$enums.RTC_STATE.ACCEPTED
},
isWaiting() {
return this.rtcInfo.state == this.$enums.RTC_STATE.WAIT_CALL;
},
isChating() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING;
} }
}, },
mounted() { mounted() {
@ -358,10 +395,20 @@
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.chat-video { .chat-video {
position: relative; position: relative;
.el-loading-text {
color: white !important;
font-size: 16px !important;
}
.el-icon-loading {
color: white !important;
font-size: 30px !important;
}
.chat-video-box { .chat-video-box {
position: relative; position: relative;
border: #4880b9 solid 1px; border: #4880b9 solid 1px;
@ -396,6 +443,23 @@
} }
} }
.chat-voice-box {
position: relative;
display: flex;
justify-content: center;
border: #4880b9 solid 1px;
width: 100%;
height: 50vh;
padding-top: 10vh;
background-color: aliceblue;
.chat-voice-name {
text-align: center;
font-size: 22px;
font-weight: 600;
}
}
.chat-video-controllbar { .chat-video-controllbar {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;

155
im-ui/src/components/chat/ChatVideoAcceptor.vue

@ -1,10 +1,8 @@
<template> <template>
<div class="chat-video-acceptor"> <div v-show="isShow" class="chat-video-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">
{{friend.nickName}} 请求和您进行视频通话... {{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="accpet()" title="接受"></div>
@ -21,70 +19,154 @@
components: { components: {
HeadImage HeadImage
}, },
props: {
friend: {
type: Object
}
},
data() { data() {
return { return {
offer: {}, isShow: false,
audio: new Audio() audio: new Audio()
} }
}, },
methods: { methods: {
accpet() { accpet() {
let info = { //
friend: this.friend, this.$store.commit("setRtcState", this.$enums.RTC_STATE.ACCEPTED);
master: false, //
offer: this.offer
}
this.$store.commit("showChatPrivateVideoBox", info);
this.close(); this.close();
}, },
reject() { reject() {
this.$http({ this.$http({
url: `/webrtc/private/reject?uid=${this.friend.id}`, url: `/webrtc/private/reject?uid=${this.rtcInfo.friend.id}`,
method: 'post' method: 'post'
}) })
//
this.insertMessage("已拒绝");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close(); this.close();
}, },
failed(reason) { failed(reason) {
this.$http({ this.$http({
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`, url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`,
method: 'post' method: 'post'
}) })
//
this.insertMessage("未接听");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close(); this.close();
}, },
onCall(msgInfo) { onRtcCall(msgInfo, friend, mode) {
this.offer = JSON.parse(msgInfo.content); console.log("onRtcCall")
if (this.$store.state.userStore.state == this.$enums.USER_STATE.BUSY) { //
this.failed("对方正忙,暂时无法接听"); 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; 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 && clearTimeout(this.timer);
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
this.failed("对方未接听"); this.failed("对方无应答");
}, 30000) }, 30000)
this.audio.play();
}, },
onCancel() { onRtcCancel(msgInfo) {
//
if (msgInfo.sendId != this.rtcInfo.friend.id) {
return;
}
// //
this.$message.success("对方取消了呼叫"); this.$message.success("对方取消了呼叫");
//
this.insertMessage("对方已取消");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close(); this.close();
}, },
handleMessage(msgInfo) { onRtcAccept(msgInfo) {
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL) { //
this.onCall(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) { } else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) {
this.onCancel(); 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() { close() {
this.timer && clearTimeout(this.timer); this.timer && clearTimeout(this.timer);
this.audio.pause(); this.audio.pause();
this.$emit("close"); this.isShow = false;
}, },
initAudio() { initAudio() {
let url = require(`@/assets/audio/call.wav`); let url = require(`@/assets/audio/call.wav`);
@ -92,10 +174,18 @@
this.audio.loop = true; this.audio.loop = true;
} }
}, },
computed: {
tip() {
let modeText = this.mode == "video" ? "视频" : "语音"
return `${this.rtcInfo.friend.nickName} 请求和您进行${modeText}通话...`
},
rtcInfo(){
return this.$store.state.userStore.rtcInfo;
}
},
mounted() { mounted() {
// //
this.initAudio(); this.initAudio();
} }
} }
</script> </script>
@ -131,6 +221,7 @@
font-size: 60px; font-size: 60px;
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
&.accept { &.accept {
color: green; color: green;
animation: anim 2s ease-in infinite, vibration 2s ease-in infinite; animation: anim 2s ease-in infinite, vibration 2s ease-in infinite;

8
im-ui/src/store/chatStore.js

@ -128,6 +128,10 @@ export default {
let message = this.getters.findMessage(chat, msgInfo); let message = this.getters.findMessage(chat, msgInfo);
if (message) { if (message) {
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
this.commit("saveToStorage"); this.commit("saveToStorage");
return; return;
} }
@ -140,6 +144,10 @@ export default {
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
} else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) { } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} else if (msgInfo.type == MESSAGE_TYPE.RT_VOICE) {
chat.lastContent = "[语音通话]";
} else if (msgInfo.type == MESSAGE_TYPE.RT_VIDEO) {
chat.lastContent = "[视频通话]";
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;

28
im-ui/src/store/uiStore.js

@ -11,18 +11,7 @@ export default {
fullImage: { // 全屏大图 fullImage: { // 全屏大图
show: false, show: false,
url: "" url: ""
},
chatPrivateVideo:{ // 私人视频聊天
show: false,
master: false, // 是否房主
friend:{},
offer:{} // 对方发起带过过来的sdp信息
},
videoAcceptor:{ // 视频呼叫选择
show: false,
friend:{}
} }
}, },
mutations: { mutations: {
showUserInfoBox(state,user){ showUserInfoBox(state,user){
@ -45,23 +34,6 @@ export default {
}, },
closeFullImageBox(state){ closeFullImageBox(state){
state.fullImage.show = false; state.fullImage.show = false;
},
showChatPrivateVideoBox(state,info){
state.chatPrivateVideo.show = true;
state.chatPrivateVideo.friend = info.friend;
state.chatPrivateVideo.master = info.master;
state.chatPrivateVideo.offer = info.offer;
},
closeChatPrivateVideoBox(state){
state.chatPrivateVideo.show = false;
},
showVideoAcceptorBox(state,friend){
state.videoAcceptor.show = true;
state.videoAcceptor.friend = friend;
},
closeVideoAcceptorBox(state){
state.videoAcceptor.show = false;
} }
} }
} }

22
im-ui/src/store/userStore.js

@ -1,25 +1,35 @@
import {USER_STATE} from "../api/enums.js"
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
import {RTC_STATE} from "../api/enums.js"
export default { export default {
state: { state: {
userInfo: { userInfo: {
}, },
state: USER_STATE.FREE rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
}
}, },
mutations: { mutations: {
setUserInfo(state, userInfo) { setUserInfo(state, userInfo) {
state.userInfo = userInfo state.userInfo = userInfo
}, },
setUserState(state, userState) { setRtcInfo(state, rtcInfo ){
state.state = userState; state.rtcInfo = rtcInfo;
},
setRtcState(state,rtcState){
state.rtcInfo.state = rtcState;
}, },
clear(state){ clear(state){
state.userInfo = {}; state.userInfo = {};
state.state = USER_STATE.FREE; state.rtcInfo = {
friend: {},
mode: "video",
state: RTC_STATE.FREE
};
} }
}, },
actions:{ actions:{

2
im-ui/src/view/Friend.vue

@ -129,7 +129,7 @@
type: 'PRIVATE', type: 'PRIVATE',
targetId: user.id, targetId: user.id,
showName: user.nickName, showName: user.nickName,
headImage: user.headImage, headImage: user.headImageThumb,
}; };
this.$store.commit("openChat", chat); this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0); this.$store.commit("activeChat", 0);

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

@ -40,25 +40,20 @@
@close="$store.commit('closeUserInfoBox')"></user-info> @close="$store.commit('closeUserInfoBox')"></user-info>
<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>
<chat-private-video ref="privateVideo" :visible="uiStore.chatPrivateVideo.show" <chat-private-video ref="privateVideo"></chat-private-video>
:friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master" <chat-video-acceptor ref="videoAcceptor"></chat-video-acceptor>
:offer="uiStore.chatPrivateVideo.offer" @close="$store.commit('closeChatPrivateVideoBox')">
</chat-private-video>
<chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show" :friend="uiStore.videoAcceptor.friend"
@close="$store.commit('closeVideoAcceptorBox')">
</chat-video-acceptor>
</el-container> </el-container>
</template> </template>
<script> <script>
import HeadImage from '../components/common/HeadImage.vue'; import HeadImage from '../components/common/HeadImage.vue';
import Setting from '../components/setting/Setting.vue'; import Setting from '../components/setting/Setting.vue';
import UserInfo from '../components/common/UserInfo.vue'; import UserInfo from '../components/common/UserInfo.vue';
import FullImage from '../components/common/FullImage.vue'; import FullImage from '../components/common/FullImage.vue';
import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue'; import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue';
import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue'; import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue';
export default { export default {
components: { components: {
HeadImage, HeadImage,
Setting, Setting,
@ -70,7 +65,7 @@ export default {
data() { data() {
return { return {
showSettingDialog: false, showSettingDialog: false,
lastPlayAudioTime: new Date() - 1000 lastPlayAudioTime: new Date().getTime() - 1000
} }
}, },
methods: { methods: {
@ -108,7 +103,8 @@ export default {
if (e.code != 3000) { if (e.code != 3000) {
// 线 // 线
this.$message.error("连接断开,正在尝试重新连接..."); this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
"accessToken"));
} }
}); });
}).catch((e) => { }).catch((e) => {
@ -143,7 +139,9 @@ export default {
} }
// , // ,
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
this.$store.commit("readedMessage", { friendId: msg.sendId }) this.$store.commit("readedMessage", {
friendId: msg.sendId
})
return; return;
} }
// //
@ -156,16 +154,17 @@ export default {
}, },
insertPrivateMessage(friend, msg) { insertPrivateMessage(friend, msg) {
// webrtc // webrtc
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL && if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
let rtcInfo = this.$store.state.userStore.rtcInfo;
// //
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL || if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) { msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO ||
this.$store.commit("showVideoAcceptorBox", friend); rtcInfo.state == this.$enums.RTC_STATE.FREE ||
this.$refs.videoAcceptor.handleMessage(msg) rtcInfo.state == this.$enums.RTC_STATE.WAIT_ACCEPT) {
this.$refs.videoAcceptor.onRTCMessage(msg,friend)
} else { } else {
this.$refs.videoAcceptor.close() this.$refs.privateVideo.onRTCMessage(msg)
this.$refs.privateVideo.handleMessage(msg)
} }
return; return;
} }
@ -242,8 +241,8 @@ export default {
location.href = "/"; location.href = "/";
}, },
playAudioTip() { playAudioTip() {
if (new Date() - this.lastPlayAudioTime > 1000) { if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date(); this.lastPlayAudioTime = new Date().getTime();
let audio = new Audio(); let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`); let url = require(`@/assets/audio/tip.wav`);
audio.src = url; audio.src = url;
@ -318,11 +317,11 @@ export default {
unmounted() { unmounted() {
this.$wsApi.close(); this.$wsApi.close();
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.navi-bar { .navi-bar {
background: #333333; background: #333333;
padding: 10px; padding: 10px;
padding-top: 50px; padding-top: 50px;
@ -381,13 +380,13 @@ export default {
color: white !important; color: white !important;
} }
} }
} }
.content-box { .content-box {
padding: 0; padding: 0;
background-color: #E9EEF3; background-color: #E9EEF3;
color: #333; color: #333;
text-align: center; text-align: center;
} }
</style> </style>

2
im-uniapp/common/enums.js

@ -5,6 +5,8 @@ const MESSAGE_TYPE = {
FILE:2, FILE:2,
AUDIO:3, AUDIO:3,
VIDEO:4, VIDEO:4,
RT_VOICE:5,
RT_VIDEO:6,
RECALL:10, RECALL:10,
READED:11, READED:11,
RECEIPT:12, RECEIPT:12,

48
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -40,14 +40,24 @@
<text title="发送失败" v-if="loadFail" @click="onSendFail" <text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text> class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
<view class="chat-realtime chat-msg-text" v-if="isRTMessage">
<text v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VOICE"
class="iconfont icon-chat-voice"></text>
<text v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VIDEO"
class="iconfont icon-chat-video"></text>
<text>{{msgInfo.content}}</text>
</view>
<view class="chat-msg-status" v-if="!isRTMessage">
<text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId <text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text> && msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId <text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text> && msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text>
</view>
<view class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox"> <view class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text> <text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
<text v-else>{{msgInfo.readedCount}}人已读</text> <text v-else>{{msgInfo.readedCount}}人已读</text>
</view> </view>
<!-- <!--
<view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()"> <view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
@ -188,6 +198,10 @@
}); });
} }
return items; return items;
},
isRTMessage() {
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE ||
this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO
} }
} }
@ -343,17 +357,33 @@
} }
.chat-unread {
font-size: 10px;
color: #f23c0f; .chat-realtime {
font-weight: 600; display: flex;
align-items: center;
.iconfont {
font-size: 20px;
padding-right: 8px;
}
} }
.chat-msg-status {
display: block;
.chat-readed { .chat-readed {
font-size: 10px; font-size: 10px;
color: #ccc; color: #ccc;
font-weight: 600; font-weight: 600;
} }
.chat-unread {
font-size: 10px;
color: #f23c0f;
font-weight: 600;
}
}
.chat-receipt { .chat-receipt {
font-size: 13px; font-size: 13px;
color: darkblue; color: darkblue;
@ -405,6 +435,14 @@
.chat-msg-file { .chat-msg-file {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.chat-realtime {
display: flex;
flex-direction: row-reverse;
.iconfont {
transform: rotateY(180deg);
}
}
} }
} }
} }

20
im-uniapp/manifest.json

@ -2,8 +2,8 @@
"name" : "盒子IM", "name" : "盒子IM",
"appid" : "__UNI__69DD57A", "appid" : "__UNI__69DD57A",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.6",
"versionCode" : "100", "versionCode" : 106,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {
@ -18,7 +18,9 @@
}, },
/* */ /* */
"modules" : { "modules" : {
"Camera" : {} "Camera" : {},
"Record" : {},
"Bluetooth" : {}
}, },
/* */ /* */
"distribute" : { "distribute" : {
@ -39,14 +41,20 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>", "<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />"
], ],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ], "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 21 "minSdkVersion" : 21
}, },
/* ios */ /* ios */
"ios" : { "ios" : {
"dSYMs" : false "dSYMs" : false,
"privacyDescription" : {
"NSMicrophoneUsageDescription" : "",
"NSCameraUsageDescription" : ""
}
}, },
/* SDK */ /* SDK */
"sdkConfigs" : { "sdkConfigs" : {
@ -117,3 +125,5 @@
} }
} }
} }
/* ios *//* SDK */

18
im-uniapp/pages/chat/chat-box.vue

@ -10,6 +10,7 @@
:scroll-into-view="'chat-item-'+scrollMsgIdx"> :scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx"> <view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" <chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
@click="onClickMessage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage" :showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx"
:msgInfo="msgInfo" :groupMembers="groupMembers"> :msgInfo="msgInfo" :groupMembers="groupMembers">
@ -76,11 +77,11 @@
<view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view> <view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view>
<view class="tool-name">回执消息</view> <view class="tool-name">回执消息</view>
</view> </view>
<view class="chat-tools-item" @click="onVideoCall()"> <view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVideoCall()">
<view class="tool-icon iconfont icon-video"></view> <view class="tool-icon iconfont icon-video"></view>
<view class="tool-name">视频通话</view> <view class="tool-name">视频通话</view>
</view> </view>
<view class="chat-tools-item" @click="onVoiceCall()"> <view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVoiceCall()">
<view class="tool-icon iconfont icon-call"></view> <view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">语音通话</view> <view class="tool-name">语音通话</view>
</view> </view>
@ -125,6 +126,13 @@
icon: "none" icon: "none"
}) })
}, },
onClickMessage(msgInfo){
if(msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE){
this.onVoiceCall();
}else if(msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO){
this.onVideoCall();
}
},
onVideoCall(){ onVideoCall(){
const friendInfo = encodeURIComponent(JSON.stringify(this.friend)); const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({ uni.navigateTo({
@ -599,8 +607,6 @@
this.showMinIdx = size > 30 ? size - 30 : 0; this.showMinIdx = size > 30 ? size - 30 : 0;
// //
this.$store.commit("activeChat", options.chatIdx); this.$store.commit("activeChat", options.chatIdx);
//
this.scrollToBottom();
// //
this.readedMessage() this.readedMessage()
// //
@ -613,6 +619,10 @@
// //
this.isReceipt = false; this.isReceipt = false;
}, },
onShow() {
//
this.scrollToBottom();
},
onUnload() { onUnload() {
this.$store.commit("activeChat", -1); this.$store.commit("activeChat", -1);
} }

113
im-uniapp/pages/chat/chat-video.vue

@ -0,0 +1,113 @@
<template>
<view class="page chat-video">
<web-view id="chat-video-wv" @message="onMessage" :src="url"></web-view>
</view>
</template>
<script>
import UNI_APP from '@/.env.js'
export default {
data() {
return {
url: "",
wv: '',
mode: "video",
isHost: true,
friend: {}
}
},
methods: {
onMessage(e) {
this.onWebviewMessage(e.detail.data[0]);
},
onInsertMessage(msgInfo){
let chat = {
type: 'PRIVATE',
targetId: this.friend.id,
showName: this.friend.nickName,
headImage: this.friend.headImageThumb,
};
this.$store.commit("openChat",chat);
this.$store.commit("insertMessage", msgInfo);
},
onWebviewMessage(event) {
console.log("来自webview的消息:" + JSON.stringify(event))
switch (event.key) {
case "WV_READY":
this.initWebView();
break;
case "WV_CLOSE":
uni.navigateBack();
break;
case "INSERT_MESSAGE":
this.onInsertMessage(event.data);
break;
}
},
sendMessageToWebView(key, message) {
// webview100ms
if (!this.wv) {
setTimeout(() => this.sendMessageToWebView(key, message), 100)
return;
}
let event = {
key: key,
data: message
}
// #ifdef APP-PLUS
this.wv.evalJS(`onEvent('${encodeURIComponent(JSON.stringify(event))}')`)
// #endif
// #ifdef H5
this.wv.postMessage(event, '*');
// #endif
},
initWebView() {
// #ifdef APP-PLUS
// APPwebview
this.wv = this.$scope.$getAppWebview().children()[0]
// #endif
// #ifdef H5
// H5webviewiframe
this.wv = document.getElementById('chat-video-wv').contentWindow
// #endif
},
initUrl(){
this.url = "/hybrid/html/index.html";
this.url += "?mode="+this.mode;
this.url += "&isHost="+this.isHost;
this.url += "&baseUrl="+UNI_APP.BASE_URL;
this.url += "&loginInfo="+JSON.stringify(uni.getStorageSync("loginInfo"));
this.url += "&userInfo="+JSON.stringify(this.$store.state.userStore.userInfo);
this.url += "&friend="+JSON.stringify(this.friend);
},
},
onBackPress() {
this.sendMessageToWebView("NAV_BACK",{})
},
onLoad(options) {
uni.$on('WS_RTC', msg => {
// web-view
this.sendMessageToWebView("RTC_MESSAGE", msg);
})
// #ifdef H5
window.onmessage = (e) => {
this.onWebviewMessage(e.data.data.arg);
}
// #endif
// /
this.mode = options.mode;
//
this.isHost = JSON.parse(options.isHost);
//
this.friend = JSON.parse(decodeURIComponent(options.friend));
// url
this.initUrl();
},
onUnload() {
uni.$off('WS_RTC')
}
}
</script>
<style lang="scss" scoped>
</style>

10
im-uniapp/static/icon/iconfont.css

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4272106 */ font-family: "iconfont"; /* Project id 4272106 */
src: url('iconfont.ttf?t=1710059877142') format('truetype'); src: url('iconfont.ttf?t=1710556421604') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-chat-video:before {
content: "\e73b";
}
.icon-chat-voice:before {
content: "\e633";
}
.icon-video:before { .icon-video:before {
content: "\e685"; content: "\e685";
} }

BIN
im-uniapp/static/icon/iconfont.ttf

Binary file not shown.

10
im-uniapp/store/chatStore.js

@ -127,6 +127,10 @@ export default {
let message = this.getters.findMessage(chat, msgInfo); let message = this.getters.findMessage(chat, msgInfo);
if(message){ if(message){
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if(msgInfo.type == MESSAGE_TYPE.RECALL){
chat.lastContent = msgInfo.content;
}
this.commit("saveToStorage"); this.commit("saveToStorage");
return; return;
} }
@ -140,6 +144,10 @@ export default {
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
} else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) { } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} else if (msgInfo.type == MESSAGE_TYPE.RT_VOICE) {
chat.lastContent = "[语音通话]";
} else if (msgInfo.type == MESSAGE_TYPE.RT_VIDEO) {
chat.lastContent = "[视频通话]";
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;
@ -250,7 +258,7 @@ export default {
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
}else{ }else{
chat.lastContent = ""; chat.lastContent = "";
chat.lastSendTime = new Date() chat.lastSendTime = new Date().getTime()
} }
}) })
state.chats.sort((chat1, chat2) => { state.chats.sort((chat1, chat2) => {

Loading…
Cancel
Save