Browse Source

!52 加入移动端音视频功能

Merge pull request !52 from blue/v_2.0.0
master
blue 2 years ago
committed by Gitee
parent
commit
4fa83467c0
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 10
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java
  2. 9
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  3. 6
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java
  4. 15
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java
  5. 17
      im-ui/src/api/enums.js
  6. BIN
      im-ui/src/assets/audio/call.wav
  7. 10
      im-ui/src/assets/iconfont/iconfont.css
  8. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  9. 48
      im-ui/src/components/chat/ChatBox.vue
  10. 60
      im-ui/src/components/chat/ChatMessageItem.vue
  11. 258
      im-ui/src/components/chat/ChatPrivateVideo.vue
  12. 155
      im-ui/src/components/chat/ChatVideoAcceptor.vue
  13. 8
      im-ui/src/store/chatStore.js
  14. 28
      im-ui/src/store/uiStore.js
  15. 22
      im-ui/src/store/userStore.js
  16. 2
      im-ui/src/view/Friend.vue
  17. 37
      im-ui/src/view/Home.vue
  18. 22
      im-uniapp/App.vue
  19. 5
      im-uniapp/common/enums.js
  20. 48
      im-uniapp/components/chat-message-item/chat-message-item.vue
  21. 13
      im-uniapp/hybrid/html/index.html
  22. 20
      im-uniapp/manifest.json
  23. 7
      im-uniapp/pages.json
  24. 45
      im-uniapp/pages/chat/chat-box.vue
  25. 113
      im-uniapp/pages/chat/chat-video.vue
  26. 14
      im-uniapp/static/icon/iconfont.css
  27. BIN
      im-uniapp/static/icon/iconfont.ttf
  28. 10
      im-uniapp/store/chatStore.js

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

@ -21,8 +21,8 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "呼叫视频通话")
@PostMapping("/call")
public Result call(@RequestParam Long uid, @RequestBody String offer) {
webrtcService.call(uid, offer);
public Result call(@RequestParam Long uid, @RequestParam(defaultValue = "video") String mode, @RequestBody String offer) {
webrtcService.call(uid, mode, offer);
return ResultUtils.success();
}
@ -57,15 +57,15 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "挂断")
@PostMapping("/handup")
public Result leave(@RequestParam Long uid) {
webrtcService.leave(uid);
public Result handup(@RequestParam Long uid) {
webrtcService.handup(uid);
return ResultUtils.success();
}
@PostMapping("/candidate")
@ApiOperation(httpMethod = "POST", value = "同步candidate")
public Result forwardCandidate(@RequestParam Long uid, @RequestBody String candidate) {
public Result candidate(@RequestParam Long uid, @RequestBody String candidate) {
webrtcService.candidate(uid, candidate);
return ResultUtils.success();
}

9
im-platform/src/main/java/com/bx/implatform/enums/MessageType.java

@ -53,10 +53,15 @@ public enum MessageType {
* 消息加载标记
*/
LOADDING(30,"加载中"),
/**
* 语音呼叫
*/
RTC_CALL_VOICE(100, "语音呼叫"),
/**
* 呼叫
* 视频呼叫
*/
RTC_CALL(101, "呼叫"),
RTC_CALL_VIDEO(101, "视频呼叫"),
/**
* 接受
*/

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

@ -12,9 +12,9 @@ import java.util.List;
*/
public interface IWebrtcService {
void call(Long uid, String offer);
void call(Long uid, String mode,String offer);
void accept(Long uid, @RequestBody String answer);
void accept(Long uid, String answer);
void reject(Long uid);
@ -22,7 +22,7 @@ public interface IWebrtcService {
void failed(Long uid, String reason);
void leave(Long uid);
void handup(Long uid);
void candidate(Long uid, String candidate);

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

@ -33,7 +33,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
private final ICEServerConfig iceServerConfig;
@Override
public void call(Long uid, String offer) {
public void call(Long uid, String mode, String offer) {
UserSession session = SessionContext.getSession();
if (!imClient.isOnline(uid)) {
throw new GlobalException("对方目前不在线");
@ -46,7 +46,8 @@ public class WebrtcServiceImpl implements IWebrtcService {
redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS);
// 向对方所有终端发起呼叫
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_CALL.code());
MessageType messageType = mode.equals("video") ? MessageType.RTC_CALL_VIDEO : MessageType.RTC_CALL_VOICE;
messageInfo.setType(messageType.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
messageInfo.setContent(offer);
@ -120,7 +121,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
removeWebrtcSession(session.getUserId(), uid);
// 向对方所有终端推送取消通话信令
PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_ACCEPT.code());
messageInfo.setType(MessageType.RTC_CANCEL.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
@ -146,12 +147,12 @@ public class WebrtcServiceImpl implements IWebrtcService {
messageInfo.setType(MessageType.RTC_FAILED.code());
messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId());
messageInfo.setContent(reason);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(uid);
// 告知其他终端已经会话失败,中止呼叫
sendMessage.setSendToSelf(true);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal()));
sendMessage.setData(messageInfo);
@ -161,7 +162,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
}
@Override
public void leave(Long uid) {
public void handup(Long uid) {
UserSession session = SessionContext.getSession();
// 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
@ -217,7 +218,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
String key = getSessionKey(userId, uid);
WebrtcSession webrtcSession = (WebrtcSession)redisTemplate.opsForValue().get(key);
if (webrtcSession == null) {
throw new GlobalException("视频通话已结束");
throw new GlobalException("通话已结束");
}
return webrtcSession;
}

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

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

BIN
im-ui/src/assets/audio/call.wav

Binary file not shown.

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

@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.ttf?t=1706022894868') format('truetype');
src: url('iconfont.ttf?t=1710567233281') format('truetype');
}
.iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-chat-video:before {
content: "\e73b";
}
.icon-chat-voice:before {
content: "\e633";
}
.icon-ok:before {
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">
<ul>
<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"
:groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage">
</chat-message-item>
@ -44,8 +46,11 @@
</div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</div>
<div title="视频聊天" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@click="showVideoBox()">
<div title="语音通话" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@click="showChatVideo('voice')">
</div>
<div title="视频通话" v-show="chat.type == 'PRIVATE'" class="el-icon-video-camera"
@click="showChatVideo('video')">
</div>
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div>
@ -133,13 +138,19 @@ export default {
methods: {
moveChatToTop(){
let chatIdx = this.$store.getters.findChatIdx(this.chat);
console.log(chatIdx);
this.$store.commit("moveTop",chatIdx);
},
closeRefBox() {
this.$refs.emoBox.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() {
if (this.$refs.atBox.show) {
this.$refs.atBox.moveDown()
@ -433,11 +444,17 @@ export default {
closeVoiceBox() {
this.showVoice = false;
},
showVideoBox() {
this.$store.commit("showChatPrivateVideoBox", {
showChatVideo(mode) {
let rtcInfo = {
mode: mode,
isHost: true,
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() {
this.showHistory = true;
@ -686,6 +703,12 @@ export default {
},
unreadCount() {
return this.chat.unreadCount;
},
messageSize() {
if (!this.chat || !this.chat.messages) {
return 0;
}
return this.chat.messages.length;
}
},
watch: {
@ -716,9 +739,9 @@ export default {
},
immediate: true
},
unreadCount: {
handler(newCount, oldCount) {
if (newCount > 0) {
messageSize: {
handler(newSize, oldSize) {
if (newSize > oldSize) {
//
this.scrollToBottom();
}
@ -812,7 +835,6 @@ export default {
}
}
.send-content-area {
position: relative;
display: flex;
@ -820,8 +842,6 @@ export default {
height: 100%;
background-color: white !important;
.send-text-area {
box-sizing: border-box;
padding: 5px;

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

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

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

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

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

@ -1,10 +1,8 @@
<template>
<div class="chat-video-acceptor">
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"></head-image>
<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>
<div class="acceptor-text">
{{friend.nickName}} 请求和您进行视频通话...
{{tip}}
</div>
<div class="acceptor-btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="accpet()" title="接受"></div>
@ -21,70 +19,154 @@
components: {
HeadImage
},
props: {
friend: {
type: Object
}
},
data() {
return {
offer: {},
isShow: false,
audio: new Audio()
}
},
methods: {
accpet() {
let info = {
friend: this.friend,
master: false,
offer: this.offer
}
this.$store.commit("showChatPrivateVideoBox", info);
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.ACCEPTED);
//
this.close();
},
reject() {
this.$http({
url: `/webrtc/private/reject?uid=${this.friend.id}`,
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.friend.id}&reason=${reason}`,
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();
},
onCall(msgInfo) {
this.offer = JSON.parse(msgInfo.content);
if (this.$store.state.userStore.state == this.$enums.USER_STATE.BUSY) {
this.failed("对方正忙,暂时无法接听");
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("对方未接听");
this.failed("对方无应答");
}, 30000)
this.audio.play();
},
onCancel() {
onRtcCancel(msgInfo) {
//
if (msgInfo.sendId != this.rtcInfo.friend.id) {
return;
}
//
this.$message.success("对方取消了呼叫");
//
this.insertMessage("对方已取消");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
},
handleMessage(msgInfo) {
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL) {
this.onCall(msgInfo);
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.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() {
this.timer && clearTimeout(this.timer);
this.audio.pause();
this.$emit("close");
this.isShow = false;
},
initAudio() {
let url = require(`@/assets/audio/call.wav`);
@ -92,10 +174,18 @@
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() {
//
this.initAudio();
}
}
</script>
@ -131,6 +221,7 @@
font-size: 60px;
cursor: pointer;
border-radius: 50%;
&.accept {
color: green;
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);
if (message) {
Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
this.commit("saveToStorage");
return;
}
@ -140,6 +144,10 @@ export default {
chat.lastContent = "[语音]";
} else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
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.sendNickName = msgInfo.sendNickName;

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

@ -11,18 +11,7 @@ export default {
fullImage: { // 全屏大图
show: false,
url: ""
},
chatPrivateVideo:{ // 私人视频聊天
show: false,
master: false, // 是否房主
friend:{},
offer:{} // 对方发起带过过来的sdp信息
},
videoAcceptor:{ // 视频呼叫选择
show: false,
friend:{}
}
},
mutations: {
showUserInfoBox(state,user){
@ -45,23 +34,6 @@ export default {
},
closeFullImageBox(state){
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 {RTC_STATE} from "../api/enums.js"
export default {
state: {
userInfo: {
},
state: USER_STATE.FREE
rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
}
},
mutations: {
setUserInfo(state, userInfo) {
state.userInfo = userInfo
},
setUserState(state, userState) {
state.state = userState;
setRtcInfo(state, rtcInfo ){
state.rtcInfo = rtcInfo;
},
setRtcState(state,rtcState){
state.rtcInfo.state = rtcState;
},
clear(state){
state.userInfo = {};
state.state = USER_STATE.FREE;
state.rtcInfo = {
friend: {},
mode: "video",
state: RTC_STATE.FREE
};
}
},
actions:{

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

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

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

@ -40,13 +40,8 @@
@close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
<chat-private-video ref="privateVideo" :visible="uiStore.chatPrivateVideo.show"
:friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master"
: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>
<chat-private-video ref="privateVideo"></chat-private-video>
<chat-video-acceptor ref="videoAcceptor"></chat-video-acceptor>
</el-container>
</template>
@ -70,7 +65,7 @@ export default {
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date() - 1000
lastPlayAudioTime: new Date().getTime() - 1000
}
},
methods: {
@ -108,7 +103,8 @@ export default {
if (e.code != 3000) {
// 线
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) => {
@ -143,7 +139,9 @@ export default {
}
// ,
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
this.$store.commit("readedMessage", { friendId: msg.sendId })
this.$store.commit("readedMessage", {
friendId: msg.sendId
})
return;
}
//
@ -156,16 +154,17 @@ export default {
},
insertPrivateMessage(friend, msg) {
// 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) {
let rtcInfo = this.$store.state.userStore.rtcInfo;
//
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) {
this.$store.commit("showVideoAcceptorBox", friend);
this.$refs.videoAcceptor.handleMessage(msg)
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.videoAcceptor.onRTCMessage(msg,friend)
} else {
this.$refs.videoAcceptor.close()
this.$refs.privateVideo.handleMessage(msg)
this.$refs.privateVideo.onRTCMessage(msg)
}
return;
}
@ -242,8 +241,8 @@ export default {
location.href = "/";
},
playAudioTip() {
if (new Date() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date();
if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date().getTime();
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;

22
im-uniapp/App.vue

@ -104,8 +104,26 @@
},
insertPrivateMessage(friend, msg) {
// webrtc
if (msg.type >= enums.MESSAGE_TYPE.RTC_CALL &&
msg.type <= enums.MESSAGE_TYPE.RTC_CANDIDATE) {}
if (msg.type >= enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type <= enums.MESSAGE_TYPE.RTC_CANDIDATE) {
//
if(msg.type == enums.MESSAGE_TYPE.RTC_CALL_VOICE
|| msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO){
let mode = msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO? "video":"voice";
let pages = getCurrentPages();
let curPage = pages[pages.length-1].route;
if(curPage != "pages/chat/chat-video"){
const friendInfo = encodeURIComponent(JSON.stringify(friend));
uni.navigateTo({
url: `/pages/chat/chat-video?mode=${mode}&friend=${friendInfo}&isHost=false`
})
}
}
setTimeout(() => {
uni.$emit('WS_RTC',msg);
},500)
return;
}
let chatInfo = {
type: 'PRIVATE',

5
im-uniapp/common/enums.js

@ -5,13 +5,16 @@ const MESSAGE_TYPE = {
FILE:2,
AUDIO:3,
VIDEO:4,
RT_VOICE:5,
RT_VIDEO:6,
RECALL:10,
READED:11,
RECEIPT:12,
TIP_TIME:20,
TIP_TEXT:21,
LOADDING:30,
RTC_CALL: 101,
RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102,
RTC_REJECT: 103,
RTC_CANCEL: 104,

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

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

13
im-uniapp/hybrid/html/index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="favicon.ico">
<title>视频通话</title>
</head>
<body>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话为付费功能,有需要请联系作者...</div>
</body>
</html>

20
im-uniapp/manifest.json

@ -2,8 +2,8 @@
"name" : "盒子IM",
"appid" : "__UNI__69DD57A",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"versionName" : "1.0.6",
"versionCode" : 106,
"transformPx" : false,
/* 5+App */
"app-plus" : {
@ -18,7 +18,9 @@
},
/* */
"modules" : {
"Camera" : {}
"Camera" : {},
"Record" : {},
"Bluetooth" : {}
},
/* */
"distribute" : {
@ -39,14 +41,20 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<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" ],
"minSdkVersion" : 21
},
/* ios */
"ios" : {
"dSYMs" : false
"dSYMs" : false,
"privacyDescription" : {
"NSMicrophoneUsageDescription" : "",
"NSCameraUsageDescription" : ""
}
},
/* SDK */
"sdkConfigs" : {
@ -117,3 +125,5 @@
}
}
}
/* ios *//* SDK */

7
im-uniapp/pages.json

@ -1,5 +1,5 @@
{
"lazyCodeLoading":"requiredComponents",
"pages": [{
"path": "pages/login/login"
}, {
@ -16,6 +16,8 @@
"path": "pages/common/user-info"
}, {
"path": "pages/chat/chat-box"
},{
"path": "pages/chat/chat-video"
}, {
"path": "pages/friend/friend-add"
}, {
@ -30,7 +32,8 @@
"path": "pages/mine/mine-edit"
},{
"path": "pages/mine/mine-password"
}],
}
],
"globalStyle": {
"navigationBarTitleText": "盒子IM",
"navigationBarTextStyle": "black",

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

@ -10,6 +10,7 @@
:scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
@click="onClickMessage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx"
:msgInfo="msgInfo" :groupMembers="groupMembers">
@ -30,9 +31,9 @@
<view class="send-bar">
<view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt?'[回执消息]':''"
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange"
@input="onTextInput" confirm-type="send" confirm-hold :hold-keyboard="true"></textarea>
:placeholder="isReceipt?'[回执消息]':''" :adjust-position="false" @confirm="sendTextMessage()"
@keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold
:hold-keyboard="true"></textarea>
</view>
<view v-if="chat.type=='GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view>
@ -76,9 +77,13 @@
<view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view>
<view class="tool-name">回执消息</view>
</view>
<view class="chat-tools-item" @click="showTip()">
<view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVideoCall()">
<view class="tool-icon iconfont icon-video"></view>
<view class="tool-name">视频通话</view>
</view>
<view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVoiceCall()">
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">呼叫</view>
<view class="tool-name">语音通话</view>
</view>
</view>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
@ -116,12 +121,30 @@
},
methods: {
showTip() {
uni.showToast({
title: "暂未支持...",
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(){
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-video?mode=video&friend=${friendInfo}&isHost=true`
})
},
onVoiceCall(){
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-video?mode=voice&friend=${friendInfo}&isHost=true`
})
},
moveChatToTop() {
let chatIdx = this.$store.getters.findChatIdx(this.chat);
this.$store.commit("moveTop", chatIdx);
@ -584,8 +607,6 @@
this.showMinIdx = size > 30 ? size - 30 : 0;
//
this.$store.commit("activeChat", options.chatIdx);
//
this.scrollToBottom();
//
this.readedMessage()
//
@ -598,6 +619,10 @@
//
this.isReceipt = false;
},
onShow() {
//
this.scrollToBottom();
},
onUnload() {
this.$store.commit("activeChat", -1);
}
@ -736,8 +761,8 @@
align-items: center;
.tool-icon {
padding: 18rpx;
font-size: 80rpx;
padding: 28rpx;
font-size: 60rpx;
background-color: white;
border-radius: 20%;

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>

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

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

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

Loading…
Cancel
Save