diff --git a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java index c63f0e3..026e3c9 100644 --- a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java +++ b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java @@ -2,9 +2,10 @@ package com.bx.implatform.contant; public final class RedisKey { - private RedisKey() { - } - + /** + * 用户状态 无值:空闲 1:正在忙 + */ + public static final String IM_USER_STATE = "im:user:state"; /** * 已读群聊消息位置(已读最大id) */ diff --git a/im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java index 192fea6..71574ed 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java @@ -4,6 +4,7 @@ import com.bx.implatform.dto.*; import com.bx.implatform.result.Result; import com.bx.implatform.result.ResultUtils; import com.bx.implatform.service.IWebrtcGroupService; +import com.bx.implatform.vo.WebrtcGroupInfoVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -108,4 +109,16 @@ public class WebrtcGroupController { return ResultUtils.success(); } + @ApiOperation(httpMethod = "GET", value = "获取通话信息") + @GetMapping("/info") + public Result info(@RequestParam Long groupId) { + return ResultUtils.success(webrtcGroupService.info(groupId)); + } + + @ApiOperation(httpMethod = "POST", value = "获取通话信息") + @PostMapping("/heartbeat") + public Result heartbeat(@RequestParam Long groupId) { + webrtcGroupService.heartbeat(groupId); + return ResultUtils.success(); + } } diff --git a/im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java b/im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java index 66264bf..144a118 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java @@ -1,36 +1,32 @@ package com.bx.implatform.service; import com.bx.implatform.dto.*; +import com.bx.implatform.vo.WebrtcGroupInfoVO; public interface IWebrtcGroupService { /** * 发起通话 - * @param dto */ void setup(WebrtcGroupSetupDTO dto); /** * 接受通话 - * @groupId 群id */ void accept(Long groupId); /** * 拒绝通话 - * @groupId 群id */ void reject(Long groupId); /** * 通话失败,如设备不支持、用户忙等(此接口为系统自动调用,无需用户操作,所以不抛异常) - * @dto dto */ void failed(WebrtcGroupFailedDTO dto); /** * 主动加入通话 - * @groupId 群id */ void join(Long groupId); @@ -51,29 +47,32 @@ public interface IWebrtcGroupService { /** * 推送offer信息给对方 - * @dto dto */ void offer(WebrtcGroupOfferDTO dto); /** * 推送answer信息给对方 - * @dto dto */ void answer(WebrtcGroupAnswerDTO dto); /** * 推送candidate信息给对方 - * @dto dto */ void candidate(WebrtcGroupCandidateDTO dto); /** * 用户进行了设备操作,如果关闭摄像头 - * @dto dto */ void device(WebrtcGroupDeviceDTO dto); + /** + * 查询通话信息 + */ + WebrtcGroupInfoVO info(Long groupId); - + /** + * 心跳保持, 用户每15s上传一次心跳 + */ + void heartbeat(Long groupId); } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java index 283beb6..2a4df50 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java @@ -17,14 +17,17 @@ import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.UserSession; import com.bx.implatform.session.WebrtcGroupSession; import com.bx.implatform.session.WebrtcUserInfo; +import com.bx.implatform.util.UserStateUtils; import com.bx.implatform.vo.GroupMessageVO; import com.bx.implatform.vo.WebrtcGroupFailedVO; +import com.bx.implatform.vo.WebrtcGroupInfoVO; import com.google.common.collect.Lists; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; +import java.lang.reflect.Member; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -44,6 +47,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { private final IGroupMemberService groupMemberService; private final RedisTemplate redisTemplate; private final IMClient imClient; + private final UserStateUtils userStateUtils; /** * 最多支持8路视频 */ @@ -61,14 +65,22 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { if (!groupMemberService.isInGroup(dto.getGroupId(), userIds)) { throw new GlobalException("存在不在群聊中的用户"); } - // 离线用户处理 + // 有效用户 List userInfos = new LinkedList<>(); + // 离线用户 List offlineUserIds = new LinkedList<>(); + // 忙线用户 + List busyUserIds = new LinkedList<>(); for (WebrtcUserInfo userInfo : dto.getUserInfos()) { - if (imClient.isOnline(userInfo.getId())) { - userInfos.add(userInfo); - } else { + if (!imClient.isOnline(userInfo.getId())) { + //userInfos.add(userInfo); offlineUserIds.add(userInfo.getId()); + } else if (userStateUtils.isBusy(userInfo.getId())) { + busyUserIds.add(userInfo.getId()); + } else { + userInfos.add(userInfo); + // 设置用户忙线状态 + userStateUtils.setBusy(userInfo.getId()); } } // 创建通话session @@ -79,12 +91,19 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { webrtcSession.getInChatUsers().add(userInfo); saveWebrtcSession(dto.getGroupId(), webrtcSession); // 向发起邀请者推送邀请失败消息 - if(!offlineUserIds.isEmpty()){ + if (!offlineUserIds.isEmpty()) { WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); vo.setUserIds(offlineUserIds); vo.setReason("用户不在线"); sendMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), userInfo, JSON.toJSONString(vo)); } + if (!busyUserIds.isEmpty()) { + WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); + vo.setUserIds(busyUserIds); + vo.setReason("用户正忙"); + IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal()); + sendMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo)); + } // 向被邀请的用户广播消息,发起呼叫 List recvIds = getRecvIds(dto.getUserInfos()); sendMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), recvIds, JSON.toJSONString(userInfos)); @@ -132,6 +151,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { .collect(Collectors.toList()); webrtcSession.setUserInfos(userInfos); saveWebrtcSession(groupId, webrtcSession); + // 进入空闲状态 + userStateUtils.setFree(userSession.getUserId()); // 广播消息给的所有用户 List recvIds = getRecvIds(userInfos); sendMessage1(MessageType.RTC_GROUP_REJECT, groupId, recvIds, ""); @@ -156,6 +177,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { .collect(Collectors.toList()); webrtcSession.setUserInfos(userInfos); saveWebrtcSession(dto.getGroupId(), webrtcSession); + // 进入空闲状态 + userStateUtils.setFree(userSession.getUserId()); // 广播信令 WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); vo.setUserIds(Arrays.asList(userSession.getUserId())); @@ -172,7 +195,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { WebrtcGroupSession webrtcSession = getWebrtcSession(groupId); // 校验 GroupMember member = groupMemberService.findByGroupAndUserId(groupId, userSession.getUserId()); - if (Objects.isNull(member)) { + if (Objects.isNull(member) || member.getQuit()) { throw new GlobalException("您不在群里中"); } // 防止重复进入 @@ -190,6 +213,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { } webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); saveWebrtcSession(groupId, webrtcSession); + // 进入忙线状态 + userStateUtils.setBusy(userSession.getUserId()); // 广播信令 List recvIds = getRecvIds(webrtcSession.getUserInfos()); sendMessage1(MessageType.RTC_GROUP_JOIN, groupId, recvIds, JSON.toJSONString(userInfo)); @@ -207,6 +232,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { List userIds = getRecvIds(userInfos); // 离线用户id List offlineUserIds = new LinkedList<>(); + // 忙线用户 + List busyUserIds = new LinkedList<>(); // 新加入的用户 List newUserInfos = new LinkedList<>(); for (WebrtcUserInfo userInfo : dto.getUserInfos()) { @@ -214,23 +241,34 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { // 防止重复进入 continue; } - if (imClient.isOnline(userInfo.getId())) { - newUserInfos.add(userInfo); - } else { + if (!imClient.isOnline(userInfo.getId())) { offlineUserIds.add(userInfo.getId()); + } else if (userStateUtils.isBusy(userInfo.getId())) { + busyUserIds.add(userInfo.getId()); + } else { + // 进入忙线状态 + userStateUtils.setBusy(userInfo.getId()); + newUserInfos.add(userInfo); } } // 更新会话信息 userInfos.addAll(newUserInfos); saveWebrtcSession(dto.getGroupId(), webrtcSession); // 向发起邀请者推送邀请失败消息 - if(!offlineUserIds.isEmpty()){ + if (!offlineUserIds.isEmpty()) { WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); vo.setUserIds(offlineUserIds); vo.setReason("用户不在线"); IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal()); sendMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo)); } + if (!busyUserIds.isEmpty()) { + WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); + vo.setUserIds(busyUserIds); + vo.setReason("用户正在忙"); + IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal()); + sendMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo)); + } // 向被邀请的发起呼叫 List newUserIds = getRecvIds(newUserInfos); sendMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), newUserIds, JSON.toJSONString(userInfos)); @@ -251,6 +289,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { // 移除rtc session String key = buildWebrtcSessionKey(groupId); redisTemplate.delete(key); + // 进入空闲状态 + webrtcSession.getUserInfos().forEach(user -> userStateUtils.setFree(user.getId())); // 广播消息给的所有用户 List recvIds = getRecvIds(webrtcSession.getUserInfos()); sendMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, ""); @@ -269,11 +309,14 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { List userInfos = webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId())) .collect(Collectors.toList()); + // 如果群聊中没有人已经接受了通话,则直接取消整个通话 if (inChatUsers.isEmpty() || userInfos.isEmpty()) { // 移除rtc session String key = buildWebrtcSessionKey(groupId); redisTemplate.delete(key); + // 进入空闲状态 + webrtcSession.getUserInfos().forEach(user -> userStateUtils.setFree(user.getId())); // 广播给还在呼叫中的用户,取消通话 List recvIds = getRecvIds(webrtcSession.getUserInfos()); sendMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, ""); @@ -283,6 +326,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { webrtcSession.setInChatUsers(inChatUsers); webrtcSession.setUserInfos(userInfos); saveWebrtcSession(groupId, webrtcSession); + // 进入空闲状态 + userStateUtils.setFree(userSession.getUserId()); // 广播信令 List recvIds = getRecvIds(userInfos); sendMessage1(MessageType.RTC_GROUP_QUIT, groupId, recvIds, ""); @@ -359,6 +404,44 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { dto.getIsCamera()); } + @Override + public WebrtcGroupInfoVO info(Long groupId) { + WebrtcGroupInfoVO vo = new WebrtcGroupInfoVO(); + String key = buildWebrtcSessionKey(groupId); + WebrtcGroupSession webrtcSession = (WebrtcGroupSession)redisTemplate.opsForValue().get(key); + if (Objects.isNull(webrtcSession)) { + // 群聊当前没有通话 + vo.setIsChating(false); + } else { + // 群聊正在通话中 + vo.setIsChating(true); + vo.setUserInfos(webrtcSession.getUserInfos()); + Long hostId = webrtcSession.getHost().getId(); + WebrtcUserInfo host = findUserInfo(webrtcSession,hostId); + if (Objects.isNull(host)) { + // 如果发起人已经退出了通话,则从数据库查询发起人数据 + GroupMember member = groupMemberService.findByGroupAndUserId(groupId,hostId); + host = new WebrtcUserInfo(); + host.setId(hostId); + host.setNickName(member.getAliasName()); + host.setHeadImage(member.getHeadImage()); + host.setIsCamera(false); + } + vo.setHost(host); + } + return vo; + } + + @Override + public void heartbeat(Long groupId) { + UserSession userSession = SessionContext.getSession(); + // 给通话session续命 + String key = buildWebrtcSessionKey(groupId); + redisTemplate.expire(key,30,TimeUnit.SECONDS); + // 用户忙线状态续命 + userStateUtils.expire(userSession.getUserId()); + } + private WebrtcGroupSession getWebrtcSession(Long groupId) { String key = buildWebrtcSessionKey(groupId); WebrtcGroupSession webrtcSession = (WebrtcGroupSession)redisTemplate.opsForValue().get(key); @@ -370,7 +453,7 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { private void saveWebrtcSession(Long groupId, WebrtcGroupSession webrtcSession) { String key = buildWebrtcSessionKey(groupId); - redisTemplate.opsForValue().set(key, webrtcSession, 2, TimeUnit.HOURS); + redisTemplate.opsForValue().set(key, webrtcSession, 30, TimeUnit.SECONDS); } private String buildWebrtcSessionKey(Long groupId) { @@ -407,7 +490,6 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService { private Boolean isExist(WebrtcGroupSession webrtcSession, Long userId) { return webrtcSession.getUserInfos().stream().anyMatch(user -> user.getId().equals(userId)); - } private void sendMessage1(MessageType messageType, Long groupId, List recvIds, String content) { diff --git a/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java b/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java new file mode 100644 index 0000000..40678b2 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java @@ -0,0 +1,44 @@ +package com.bx.implatform.util; + +import cn.hutool.core.util.StrUtil; +import com.bx.implatform.contant.RedisKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * @author: 谢绍许 + * @date: 2024-06-10 + * @version: 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class UserStateUtils { + + private final RedisTemplate redisTemplate; + + public void setBusy(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.opsForValue().set(key,1,30, TimeUnit.SECONDS); + } + + public void expire(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.expire(key,30, TimeUnit.SECONDS); + } + + public void setFree(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.delete(key); + } + + public Boolean isBusy(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + return redisTemplate.hasKey(key); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java new file mode 100644 index 0000000..8706b41 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java @@ -0,0 +1,28 @@ +package com.bx.implatform.vo; + +import com.bx.implatform.session.WebrtcUserInfo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +/** + * @author: 谢绍许 + * @date: 2024-06-09 + * @version: 1.0 + */ +@Data +@ApiModel("群通话信息VO") +public class WebrtcGroupInfoVO { + + + @ApiModelProperty(value = "是否在通话中") + private Boolean isChating; + + @ApiModelProperty(value = "通话发起人") + WebrtcUserInfo host; + + @ApiModelProperty(value = "通话用户列表") + private List userInfos; +} diff --git a/im-uniapp/pages/chat/chat-box.vue b/im-uniapp/pages/chat/chat-box.vue index 84df209..0c50723 100644 --- a/im-uniapp/pages/chat/chat-box.vue +++ b/im-uniapp/pages/chat/chat-box.vue @@ -110,6 +110,7 @@ + @@ -191,9 +192,20 @@ }) }, onGroupVideo() { - let ids = [this.mine.id]; - this.$refs.selBox.init(ids, ids); - this.$refs.selBox.open(); + this.$http({ + url: "/webrtc/group/info?groupId="+this.group.id, + method: 'GET' + }).then((rtcInfo)=>{ + if(rtcInfo.isChating){ + // 已在通话中,可以直接加入通话 + this.$refs.rtcJoin.open(rtcInfo); + }else { + // 邀请成员发起通话 + let ids = [this.mine.id]; + this.$refs.selBox.init(ids, ids); + this.$refs.selBox.open(); + } + }) }, onSelectMember(ids) { let users = []; diff --git a/im-uniapp/pages/chat/chat-group-video.vue b/im-uniapp/pages/chat/chat-group-video.vue index 6056d42..9b11494 100644 --- a/im-uniapp/pages/chat/chat-group-video.vue +++ b/im-uniapp/pages/chat/chat-group-video.vue @@ -76,10 +76,10 @@ this.url += "&isHost=" + this.isHost; this.url += "&loginInfo=" + JSON.stringify(uni.getStorageSync("loginInfo")); this.url += "&userInfos=" + JSON.stringify(this.userInfos); - console.log(this.url) }, }, onBackPress() { + console.log("onBackPress") this.sendMessageToWebView("NAV_BACK", {}) }, onLoad(options) {