Преглед на файлове

feat:多人视频-开发中

xsx преди 1 година
родител
ревизия
c26c478afb

+ 4 - 3
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)
      */

+ 13 - 0
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<WebrtcGroupInfoVO> 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();
+    }
 }

+ 9 - 10
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);
 
 }

+ 94 - 12
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<String, Object> 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<WebrtcUserInfo> userInfos = new LinkedList<>();
+        // 离线用户
         List<Long> offlineUserIds = new LinkedList<>();
+        // 忙线用户
+        List<Long> 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<Long> 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<Long> 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<Long> recvIds = getRecvIds(webrtcSession.getUserInfos());
         sendMessage1(MessageType.RTC_GROUP_JOIN, groupId, recvIds, JSON.toJSONString(userInfo));
@@ -207,6 +232,8 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
         List<Long> userIds = getRecvIds(userInfos);
         // 离线用户id
         List<Long> offlineUserIds = new LinkedList<>();
+        // 忙线用户
+        List<Long> busyUserIds = new LinkedList<>();
         // 新加入的用户
         List<WebrtcUserInfo> 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<Long> 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<Long> recvIds = getRecvIds(webrtcSession.getUserInfos());
         sendMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, "");
@@ -269,11 +309,14 @@ public class WebrtcGroupServiceImpl implements IWebrtcGroupService {
         List<WebrtcUserInfo> 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<Long> 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<Long> 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<Long> recvIds, String content) {

+ 44 - 0
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<String, Object> 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);
+    }
+
+}

+ 28 - 0
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<WebrtcUserInfo> userInfos;
+}

+ 15 - 3
im-uniapp/pages/chat/chat-box.vue

@@ -110,6 +110,7 @@
 		<!-- 群语音通话时选择成员 -->
 		<group-member-selector ref="selBox" :members="groupMembers"
 			@complete="onSelectMember"></group-member-selector>
+		<group-rtc-join ref="rtcJoin" :groupId="group.id"></group-rtc-join>
 	</view>
 </template>
 
@@ -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 = [];

+ 1 - 1
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) {