Browse Source

feat: 多人音视频功能

master
xsx 2 years ago
parent
commit
b135f97ba2
  1. 5
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java
  2. 31
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java
  3. 3
      im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java
  4. 27
      im-ui/src/App.vue
  5. 47
      im-ui/src/api/enums.js
  6. 34
      im-ui/src/assets/iconfont/iconfont.css
  7. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  8. 1622
      im-ui/src/components/chat/ChatBox.vue
  9. 28
      im-ui/src/components/common/HeadImage.vue
  10. 28
      im-ui/src/components/group/AddGroupMember.vue
  11. 2
      im-ui/src/components/rtc/RtcPrivateAcceptor.vue
  12. 1
      im-ui/src/main.js
  13. 30
      im-ui/src/view/Home.vue
  14. 22
      im-uniapp/pages/chat/chat-box.vue

5
im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java

@ -21,6 +21,9 @@ public class WebrtcGroupDeviceDTO {
private Long groupId;
@ApiModelProperty(value = "是否开启摄像头")
private Boolean isCamera = false;
private Boolean isCamera;
@ApiModelProperty(value = "是否开启麦克风")
private Boolean isMicroPhone;
}

31
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java

@ -59,6 +59,9 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
@Override
public void setup(WebrtcGroupSetupDTO dto) {
UserSession userSession = SessionContext.getSession();
if(!imClient.isOnline(userSession.getUserId())){
throw new GlobalException("您已断开连接,请重新登陆");
}
if (dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) {
throw new GlobalException("最多支持" + webrtcConfig.getMaxChannel() + "人进行通话");
}
@ -78,8 +81,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
List<Long> busyUserIds = new LinkedList<>();
for (WebrtcUserInfo userInfo : dto.getUserInfos()) {
if (!imClient.isOnline(userInfo.getId())) {
//userInfos.add(userInfo);
offlineUserIds.add(userInfo.getId());
userInfos.add(userInfo);
//offlineUserIds.add(userInfo.getId());
} else if (userStateUtils.isBusy(userInfo.getId())) {
busyUserIds.add(userInfo.getId());
} else {
@ -99,7 +102,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
if (!offlineUserIds.isEmpty()) {
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO();
vo.setUserIds(offlineUserIds);
vo.setReason("用户不在线");
vo.setReason("用户当前不在线");
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), userInfo, JSON.toJSONString(vo));
}
if (!busyUserIds.isEmpty()) {
@ -209,20 +212,24 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
if (Objects.isNull(member) || member.getQuit()) {
throw new GlobalException("您不在群里中");
}
// 防止重复进入
if (isInchat(webrtcSession, userSession.getUserId())) {
throw new GlobalException("已在通话");
IMUserInfo mine = findInChatUser(webrtcSession, userSession.getUserId());
if(!Objects.isNull(mine) && mine.getTerminal() != userSession.getTerminal()){
throw new GlobalException("已在其他设备加入通话");
}
WebrtcUserInfo userInfo = new WebrtcUserInfo();
userInfo.setId(userSession.getUserId());
userInfo.setNickName(member.getAliasName());
userInfo.setHeadImage(member.getHeadImage());
// 默认是开启麦克风,关闭摄像头
userInfo.setIsCamera(false);
userInfo.setIsMicroPhone(true);
// 将当前用户加入通话用户列表中
if (!isExist(webrtcSession, userSession.getUserId())) {
webrtcSession.getUserInfos().add(userInfo);
}
webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
if (!isInchat(webrtcSession, userSession.getUserId())) {
webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
}
saveWebrtcSession(groupId, webrtcSession);
// 进入忙线状态
userStateUtils.setBusy(userSession.getUserId());
@ -237,7 +244,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
public void invite(WebrtcGroupInviteDTO dto) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId());
if (dto.getUserInfos().size() + dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) {
if (webrtcSession.getUserInfos().size() + dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) {
throw new GlobalException("最多支持" + webrtcConfig.getMaxChannel() + "人进行通话");
}
if (!groupMemberService.isInGroup(dto.getGroupId(), getRecvIds(dto.getUserInfos()))) {
@ -259,7 +266,9 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
continue;
}
if (!imClient.isOnline(userInfo.getId())) {
offlineUserIds.add(userInfo.getId());
// offlineUserIds.add(userInfo.getId());
userStateUtils.setBusy(userInfo.getId());
newUserInfos.add(userInfo);
} else if (userStateUtils.isBusy(userInfo.getId())) {
busyUserIds.add(userInfo.getId());
} else {
@ -275,7 +284,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
if (!offlineUserIds.isEmpty()) {
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO();
vo.setUserIds(offlineUserIds);
vo.setReason("用户不在线");
vo.setReason("用户当前不在线");
IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal());
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo));
}
@ -417,6 +426,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
}
// 更新设备状态
userInfo.setIsCamera(dto.getIsCamera());
userInfo.setIsMicroPhone(dto.getIsMicroPhone());
saveWebrtcSession(dto.getGroupId(), webrtcSession);
// 广播信令
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos());
@ -446,7 +456,6 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
host.setId(hostId);
host.setNickName(member.getAliasName());
host.setHeadImage(member.getHeadImage());
host.setIsCamera(false);
}
vo.setHost(host);
}

3
im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java

@ -23,4 +23,7 @@ public class WebrtcUserInfo {
@ApiModelProperty(value = "是否开启摄像头")
private Boolean isCamera;
@ApiModelProperty(value = "是否开启麦克风")
private Boolean isMicroPhone;
}

27
im-ui/src/App.vue

@ -85,4 +85,31 @@
.el-button {
padding: 8px 15px !important;
}
.el-checkbox {
display: flex;
align-items: center;
//
.el-checkbox__inner {
width: 20px;
height: 20px;
//
&::after {
height: 12px;
left: 7px;
}
}
//
.el-checkbox__input.is-checked+.el-checkbox__label {
color: #333333;
}
.el-checkbox__label {
line-height: 20px;
padding-left: 8px;
}
}
</style>

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

@ -1,18 +1,17 @@
const MESSAGE_TYPE = {
TEXT: 0,
IMAGE:1,
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,
IMAGE: 1,
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_VOICE: 100,
RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102,
@ -20,7 +19,19 @@ const MESSAGE_TYPE = {
RTC_CANCEL: 104,
RTC_FAILED: 105,
RTC_HANDUP: 106,
RTC_CANDIDATE: 107
RTC_CANDIDATE: 107,
RTC_GROUP_SETUP: 200,
RTC_GROUP_ACCEPT: 201,
RTC_GROUP_REJECT: 202,
RTC_GROUP_FAILED: 203,
RTC_GROUP_CANCEL: 204,
RTC_GROUP_QUIT: 205,
RTC_GROUP_INVITE: 206,
RTC_GROUP_JOIN: 207,
RTC_GROUP_OFFER: 208,
RTC_GROUP_ANSWER: 209,
RTC_GROUP_CANDIDATE: 210,
RTC_GROUP_DEVICE: 211
}
const RTC_STATE = {
@ -28,7 +39,7 @@ const RTC_STATE = {
WAIT_CALL: 1, // 呼叫后等待
WAIT_ACCEPT: 2, // 被呼叫后等待
ACCEPTED: 3, // 已接受聊天,等待建立连接
CHATING:4 // 聊天中
CHATING: 4 // 聊天中
}
const TERMINAL_TYPE = {
@ -39,8 +50,8 @@ const TERMINAL_TYPE = {
const MESSAGE_STATUS = {
UNSEND: 0,
SENDED: 1,
RECALL:2,
READED:3
RECALL: 2,
READED: 3
}
@ -49,4 +60,4 @@ export {
RTC_STATE,
TERMINAL_TYPE,
MESSAGE_STATUS
}
}

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

@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.ttf?t=1714220334746') format('truetype');
src: url('iconfont.ttf?t=1718373714629') format('truetype');
}
.iconfont {
@ -11,6 +11,38 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-invite-rtc:before {
content: "\e65f";
}
.icon-quit:before {
content: "\e606";
}
.icon-camera-off:before {
content: "\e6b5";
}
.icon-speaker-off:before {
content: "\ea3c";
}
.icon-microphone-on:before {
content: "\e63b";
}
.icon-speaker-on:before {
content: "\e6a4";
}
.icon-camera-on:before {
content: "\e627";
}
.icon-microphone-off:before {
content: "\efe5";
}
.icon-chat:before {
content: "\e600";
}

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

Binary file not shown.

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

File diff suppressed because it is too large

28
im-ui/src/components/common/HeadImage.vue

@ -28,6 +28,16 @@
type: Number,
default: 50
},
width: {
type: Number
},
height: {
type: Number
},
radius:{
type: String,
default: "10%"
},
url: {
type: String
},
@ -54,12 +64,18 @@
}
},
computed:{
avatarImageStyle(){
return `width:${this.size}px; height:${this.size}px;`
avatarImageStyle() {
let w = this.width ? this.width : this.size;
let h = this.height ? this.height : this.size;
return `width:${w}px; height:${h}px;
border-radius: ${this.radius};`
},
avatarTextStyle(){
return `width: ${this.size}px;height:${this.size}px;
color:${this.textColor};font-size:${this.size*0.6}px;`
avatarTextStyle() {
let w = this.width ? this.width : this.size;
let h = this.height ? this.height : this.size;
return `width: ${w}px;height:${h}px;
color:${this.textColor};font-size:${w*0.6}px;
border-radius: ${this.radius};`
},
textColor(){
let hash = 0;
@ -79,7 +95,7 @@
.avatar-image {
position: relative;
overflow: hidden;
border-radius: 10%;
display: block;
}
.avatar-text{

28
im-ui/src/components/group/AddGroupMember.vue

@ -136,33 +136,7 @@
border-radius: 5px;
overflow: hidden;
.el-checkbox {
display: flex;
align-items: center;
//
.el-checkbox__inner {
width: 20px;
height: 20px;
//
&::after {
height: 12px;
left: 7px;
}
}
//
.el-checkbox__input.is-checked+.el-checkbox__label {
color: #333333;
}
.el-checkbox__label {
line-height: 20px;
padding-left: 8px;
}
}
.agm-friend-checkbox {
margin-right: 20px;
}

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

@ -15,7 +15,7 @@
import HeadImage from '../common/HeadImage.vue';
export default {
name: "videoAcceptor",
name: "rtcPrivateAcceptor",
components: {
HeadImage
},

1
im-ui/src/main.js

@ -21,6 +21,7 @@ Vue.prototype.$http = httpRequest // http请求方法
Vue.prototype.$emo = emotion; // emo表情
Vue.prototype.$elm = element; // 元素操作
Vue.prototype.$enums = enums; // 枚举
Vue.prototype.$eventBus = new Vue(); // 全局事件
Vue.config.productionTip = false;
new Vue({

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

@ -42,6 +42,7 @@
@close="$store.commit('closeFullImageBox')"></full-image>
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
<rtc-private-acceptor ref="rtcPrivateAcceptor"></rtc-private-acceptor>
<rtc-group-video ref="rtcGroupVideo" ></rtc-group-video>
</el-container>
</template>
@ -52,6 +53,7 @@
import FullImage from '../components/common/FullImage.vue';
import RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue';
import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue';
import RtcGroupVideo from '../components/rtc/RtcGroupVideo.vue';
export default {
components: {
@ -60,7 +62,8 @@
UserInfo,
FullImage,
RtcPrivateVideo,
RtcPrivateAcceptor
RtcPrivateAcceptor,
RtcGroupVideo
},
data() {
return {
@ -70,6 +73,12 @@
},
methods: {
init() {
this.$eventBus.$on('openGroupVideo', (rctInfo)=>{
//
console.log(this.$refs.rtcGroupVideo)
this.$refs.rtcGroupVideo.open(rctInfo);
});
this.$store.dispatch("load").then(() => {
// ws
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
@ -153,9 +162,8 @@
})
},
insertPrivateMessage(friend, msg) {
// webrtc
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
// webrtc
if (msg.type >= 100 && msg.type <= 199) {
let rtcInfo = this.$store.state.userStore.rtcInfo;
//
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE ||
@ -180,7 +188,8 @@
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
if (!msg.selfSend && msg.type < 10
&& msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
@ -214,12 +223,20 @@
}
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
//
if (msg.type >= 200 && msg.type <= 299) {
this.$nextTick(()=>{
this.$refs.rtcGroupVideo.onRTCMessage(msg);
})
return;
}
this.loadGroupInfo(msg.groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
},
insertGroupMessage(group, msg) {
let chatInfo = {
type: 'GROUP',
targetId: group.id,
@ -231,7 +248,8 @@
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
if (!msg.selfSend && msg.type < 10
&& msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},

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

@ -81,11 +81,11 @@
</view>
<!-- #ifndef MP-WEIXIN -->
<!-- 音视频不支持小程序 -->
<view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVideoCall()">
<view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onPriviteVideo()">
<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 v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onPriviteVoice()">
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">语音通话</view>
</view>
@ -110,7 +110,7 @@
<!-- 群语音通话时选择成员 -->
<group-member-selector ref="selBox" :members="groupMembers"
:maxSize="$store.state.configStore.webrtc.maxChannel"
@complete="onSelectMember"></group-member-selector>
@complete="onInviteOk"></group-member-selector>
<group-rtc-join ref="rtcJoin" :groupId="group.id"></group-rtc-join>
</view>
</template>
@ -175,18 +175,18 @@
},
onRtCall(msgInfo) {
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE) {
this.onVoiceCall();
this.onPriviteVoice();
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO) {
this.onVideoCall();
this.onPriviteVideo();
}
},
onVideoCall() {
onPriviteVideo() {
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-private-video?mode=video&friend=${friendInfo}&isHost=true`
})
},
onVoiceCall() {
onPriviteVoice() {
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-private-video?mode=voice&friend=${friendInfo}&isHost=true`
@ -208,7 +208,10 @@
}
})
},
onSelectMember(ids) {
onInviteOk(ids) {
if(ids.length < 2){
return;
}
let users = [];
ids.forEach(id => {
let m = this.groupMembers.find(m => m.userId == id);
@ -217,7 +220,8 @@
id: m.userId,
nickName: m.aliasName,
headImage: m.headImage,
isCamera: false
isCamera: false,
isMicroPhone: true
})
})
const groupId = this.group.id;

Loading…
Cancel
Save