Browse Source

feat:移动端支持音视频通话

master
xsx 2 years ago
parent
commit
862c6f2b53
  1. 6
      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. 4
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java
  4. 8
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java
  5. BIN
      im-ui/src/assets/audio/call.wav
  6. 22
      im-uniapp/App.vue
  7. 3
      im-uniapp/common/enums.js
  8. 13
      im-uniapp/hybrid/html/index.html
  9. 7
      im-uniapp/pages.json
  10. 45
      im-uniapp/pages/chat/chat-box.vue
  11. 6
      im-uniapp/static/icon/iconfont.css
  12. BIN
      im-uniapp/static/icon/iconfont.ttf

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

@ -21,8 +21,8 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "呼叫视频通话") @ApiOperation(httpMethod = "POST", value = "呼叫视频通话")
@PostMapping("/call") @PostMapping("/call")
public Result call(@RequestParam Long uid, @RequestBody String offer) { public Result call(@RequestParam Long uid, @RequestParam(defaultValue = "video") String mode, @RequestBody String offer) {
webrtcService.call(uid, offer); webrtcService.call(uid, mode, offer);
return ResultUtils.success(); return ResultUtils.success();
} }
@ -65,7 +65,7 @@ public class WebrtcController {
@PostMapping("/candidate") @PostMapping("/candidate")
@ApiOperation(httpMethod = "POST", value = "同步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); webrtcService.candidate(uid, candidate);
return ResultUtils.success(); return ResultUtils.success();
} }

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

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

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

@ -12,9 +12,9 @@ import java.util.List;
*/ */
public interface IWebrtcService { 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); void reject(Long uid);

8
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; private final ICEServerConfig iceServerConfig;
@Override @Override
public void call(Long uid, String offer) { public void call(Long uid, String mode, String offer) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
if (!imClient.isOnline(uid)) { if (!imClient.isOnline(uid)) {
throw new GlobalException("对方目前不在线"); throw new GlobalException("对方目前不在线");
@ -46,7 +46,8 @@ public class WebrtcServiceImpl implements IWebrtcService {
redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS); redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS);
// 向对方所有终端发起呼叫 // 向对方所有终端发起呼叫
PrivateMessageVO messageInfo = new PrivateMessageVO(); 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.setRecvId(uid);
messageInfo.setSendId(session.getUserId()); messageInfo.setSendId(session.getUserId());
messageInfo.setContent(offer); messageInfo.setContent(offer);
@ -146,6 +147,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
messageInfo.setType(MessageType.RTC_FAILED.code()); messageInfo.setType(MessageType.RTC_FAILED.code());
messageInfo.setRecvId(uid); messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId()); messageInfo.setSendId(session.getUserId());
messageInfo.setContent(reason);
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()));
@ -215,7 +217,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
private WebrtcSession getWebrtcSession(Long userId, Long uid) { private WebrtcSession getWebrtcSession(Long userId, Long uid) {
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("视频通话已结束");
} }

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

Binary file not shown.

22
im-uniapp/App.vue

@ -104,8 +104,26 @@
}, },
insertPrivateMessage(friend, msg) { insertPrivateMessage(friend, msg) {
// webrtc // webrtc
if (msg.type >= enums.MESSAGE_TYPE.RTC_CALL && if (msg.type >= enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type <= enums.MESSAGE_TYPE.RTC_CANDIDATE) {} 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 = { let chatInfo = {
type: 'PRIVATE', type: 'PRIVATE',

3
im-uniapp/common/enums.js

@ -11,7 +11,8 @@ const MESSAGE_TYPE = {
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,

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>

7
im-uniapp/pages.json

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

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

@ -30,9 +30,9 @@
<view class="send-bar"> <view class="send-bar">
<view class="send-text"> <view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false" <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt?'[回执消息]':''" :placeholder="isReceipt?'[回执消息]':''" :adjust-position="false" @confirm="sendTextMessage()"
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange" @keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold
@input="onTextInput" confirm-type="send" confirm-hold :hold-keyboard="true"></textarea> :hold-keyboard="true"></textarea>
</view> </view>
<view v-if="chat.type=='GROUP'" class="iconfont icon-at" @click="openAtBox()"></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> <view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view>
@ -76,9 +76,13 @@
<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="showTip()"> <view class="chat-tools-item" @click="onVideoCall()">
<view class="tool-icon iconfont icon-video"></view>
<view class="tool-name">视频通话</view>
</view>
<view 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>
</view> </view>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true"> <scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
@ -116,17 +120,28 @@
}, },
methods: { methods: {
showTip() { showTip() {
uni.showToast({ uni.showToast({
title: "暂未支持...", title: "暂未支持...",
icon: "none" icon: "none"
}) })
}, },
moveChatToTop(){ 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); let chatIdx = this.$store.getters.findChatIdx(this.chat);
this.$store.commit("moveTop",chatIdx); this.$store.commit("moveTop", chatIdx);
}, },
switchReceipt(){ switchReceipt() {
this.isReceipt = !this.isReceipt; this.isReceipt = !this.isReceipt;
}, },
openAtBox() { openAtBox() {
@ -164,12 +179,12 @@
icon: "none" icon: "none"
}); });
} }
let receiptText = this.isReceipt? "【回执消息】":""; let receiptText = this.isReceipt ? "【回执消息】" : "";
let atText = this.createAtText(); let atText = this.createAtText();
let msgInfo = { let msgInfo = {
content: receiptText + this.sendText + atText, content: receiptText + this.sendText + atText,
atUserIds: this.atUserIds, atUserIds: this.atUserIds,
receipt : this.isReceipt, receipt: this.isReceipt,
type: 0 type: 0
} }
// id // id
@ -185,7 +200,7 @@
msgInfo.sendId = this.$store.state.userStore.userInfo.id; msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true; msgInfo.selfSend = true;
msgInfo.readedCount = 0, msgInfo.readedCount = 0,
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND; msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
// //
this.moveChatToTop(); this.moveChatToTop();
@ -736,12 +751,12 @@
align-items: center; align-items: center;
.tool-icon { .tool-icon {
padding: 18rpx; padding: 28rpx;
font-size: 80rpx; font-size: 60rpx;
background-color: white; background-color: white;
border-radius: 20%; border-radius: 20%;
&.active{ &.active {
background-color: #ddd; background-color: #ddd;
} }
} }

6
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=1706027587101') format('truetype'); src: url('iconfont.ttf?t=1710059877142') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-video:before {
content: "\e685";
}
.icon-receipt:before { .icon-receipt:before {
content: "\e61a"; content: "\e61a";
} }

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

Binary file not shown.
Loading…
Cancel
Save