Browse Source

代码优化

master
xsx 2 years ago
parent
commit
f019c7541d
  1. 10
      README.md
  2. 17
      im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java
  3. 2
      im-platform/src/main/java/com/bx/implatform/config/props/JwtProperties.java
  4. 32
      im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java
  5. 124
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java
  6. 2
      im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java
  7. 78
      im-platform/src/main/java/com/bx/implatform/service/WebrtcGroupService.java
  8. 2
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  9. 583
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java
  10. 36
      im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java
  11. 2
      im-platform/src/main/resources/application-dev.yml
  12. 2
      im-platform/src/main/resources/application-prod.yml
  13. 2
      im-platform/src/main/resources/application-test.yml
  14. 2
      im-uniapp/hybrid/html/rtc-group/index.html
  15. 2
      im-uniapp/hybrid/html/rtc-private/index.html
  16. 6
      im-web/src/components/rtc/RtcGroupVideo.vue

10
README.md

@ -26,18 +26,22 @@
#### 在线体验
账号:张三/123456 李四/123456,也可以在网页端自行注册账号
账号:张三/123456 李四/123456,也可以在自行注册账号
网页端:https://www.boxim.online
移动安卓端:https://www.boxim.online/download/boxim.apk
移动ios端: 已上架至app store,搜索"盒子IM",下载安装即可
移动H5端: https://www.boxim.online/h5/ ,或扫码:
![输入图片说明](%E6%88%AA%E5%9B%BE/h5%E4%BA%8C%E7%BB%B4%E7%A0%81.png)
由于微信小程序每次发布审核过于严苛和繁琐,暂时不再提供体验环境,但uniapp端依然会继续兼容小程序
说明:
1.由于微信小程序每次发布审核过于严苛和繁琐,暂时不再提供体验环境,但uniapp端依然会继续兼容小程序
2.体验环境部署的是商业版本,与开源版本功能存在一定差异,具体请参考:
https://www.yuque.com/u1475064/imk5n2/qtezcg32q1d0dr29
#### 项目结构
| 模块 | 功能 |

17
im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java

@ -1,5 +1,6 @@
package com.bx.implatform.config;
import com.bx.implatform.config.props.MinioProperties;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
@ -8,20 +9,12 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class MinIoClientConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
public MinioClient minioClient(MinioProperties minioProps) {
// 注入minio 客户端
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
.endpoint(minioProps.getEndpoint())
.credentials(minioProps.getAccessKey(), minioProps.getSecretKey())
.build();
}
}

2
im-platform/src/main/java/com/bx/implatform/config/JwtProperties.java → im-platform/src/main/java/com/bx/implatform/config/props/JwtProperties.java

@ -1,4 +1,4 @@
package com.bx.implatform.config;
package com.bx.implatform.config.props;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;

32
im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java

@ -0,0 +1,32 @@
package com.bx.implatform.config.props;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author: Blue
* @date: 2024-09-28
* @version: 1.0
*/
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String domain;
private String bucketName;
private String imagePath;
private String filePath;
private String videoPath;
}

124
im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java

@ -1,124 +0,0 @@
package com.bx.implatform.controller;
import com.bx.implatform.dto.*;
import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.WebrtcGroupService;
import com.bx.implatform.vo.WebrtcGroupInfoVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* @author: Blue
* @date: 2024-06-01
* @version: 1.0
*/
@Tag(name = "多人通话")
@RestController
@RequestMapping("/webrtc/group")
@RequiredArgsConstructor
public class WebrtcGroupController {
private final WebrtcGroupService webrtcGroupService;
@Operation(summary = "发起群视频通话")
@PostMapping("/setup")
public Result setup(@Valid @RequestBody WebrtcGroupSetupDTO dto) {
webrtcGroupService.setup(dto);
return ResultUtils.success();
}
@Operation(summary = "接受通话")
@PostMapping("/accept")
public Result accept(@RequestParam("groupId") Long groupId) {
webrtcGroupService.accept(groupId);
return ResultUtils.success();
}
@Operation(summary = "拒绝通话")
@PostMapping("/reject")
public Result reject(@RequestParam("groupId") Long groupId) {
webrtcGroupService.reject(groupId);
return ResultUtils.success();
}
@Operation(summary = "通话失败")
@PostMapping("/failed")
public Result failed(@Valid @RequestBody WebrtcGroupFailedDTO dto) {
webrtcGroupService.failed(dto);
return ResultUtils.success();
}
@Operation(summary = "进入视频通话")
@PostMapping("/join")
public Result join(@RequestParam("groupId") Long groupId) {
webrtcGroupService.join(groupId);
return ResultUtils.success();
}
@Operation(summary = "取消通话")
@PostMapping("/cancel")
public Result cancel(@RequestParam("groupId") Long groupId) {
webrtcGroupService.cancel(groupId);
return ResultUtils.success();
}
@Operation(summary = "离开视频通话")
@PostMapping("/quit")
public Result quit(@RequestParam("groupId") Long groupId) {
webrtcGroupService.quit(groupId);
return ResultUtils.success();
}
@Operation(summary = "推送offer信息")
@PostMapping("/offer")
public Result offer(@Valid @RequestBody WebrtcGroupOfferDTO dto) {
webrtcGroupService.offer(dto);
return ResultUtils.success();
}
@Operation(summary = "推送answer信息")
@PostMapping("/answer")
public Result answer(@Valid @RequestBody WebrtcGroupAnswerDTO dto) {
webrtcGroupService.answer(dto);
return ResultUtils.success();
}
@Operation(summary = "邀请用户进入视频通话")
@PostMapping("/invite")
public Result invite(@Valid @RequestBody WebrtcGroupInviteDTO dto) {
webrtcGroupService.invite(dto);
return ResultUtils.success();
}
@Operation(summary = "同步candidate")
@PostMapping("/candidate")
public Result candidate(@Valid @RequestBody WebrtcGroupCandidateDTO dto) {
webrtcGroupService.candidate(dto);
return ResultUtils.success();
}
@Operation(summary = "设备操作")
@PostMapping("/device")
public Result device(@Valid @RequestBody WebrtcGroupDeviceDTO dto) {
webrtcGroupService.device(dto);
return ResultUtils.success();
}
@Operation(summary = "获取通话信息")
@GetMapping("/info")
public Result<WebrtcGroupInfoVO> info(@RequestParam("groupId") Long groupId) {
return ResultUtils.success(webrtcGroupService.info(groupId));
}
@Operation(summary = "心跳")
@PostMapping("/heartbeat")
public Result heartbeat(@RequestParam("groupId") Long groupId) {
webrtcGroupService.heartbeat(groupId);
return ResultUtils.success();
}
}

2
im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java

@ -3,7 +3,7 @@ package com.bx.implatform.interceptor;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.bx.imcommon.util.JwtUtil;
import com.bx.implatform.config.JwtProperties;
import com.bx.implatform.config.props.JwtProperties;
import com.bx.implatform.enums.ResultCode;
import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.session.UserSession;

78
im-platform/src/main/java/com/bx/implatform/service/WebrtcGroupService.java

@ -1,78 +0,0 @@
package com.bx.implatform.service;
import com.bx.implatform.dto.*;
import com.bx.implatform.vo.WebrtcGroupInfoVO;
public interface WebrtcGroupService {
/**
* 发起通话
*/
void setup(WebrtcGroupSetupDTO dto);
/**
* 接受通话
*/
void accept(Long groupId);
/**
* 拒绝通话
*/
void reject(Long groupId);
/**
* 通话失败,如设备不支持用户忙等(此接口为系统自动调用,无需用户操作所以不抛异常)
*/
void failed(WebrtcGroupFailedDTO dto);
/**
* 主动加入通话
*/
void join(Long groupId);
/**
* 通话过程中继续邀请用户加入通话
*/
void invite(WebrtcGroupInviteDTO dto);
/**
* 取消通话,仅通话发起人可以取消通话
*/
void cancel(Long groupId);
/**
* 退出通话如果当前没有人在通话中将取消整个通话
*/
void quit(Long groupId);
/**
* 推送offer信息给对方
*/
void offer(WebrtcGroupOfferDTO dto);
/**
* 推送answer信息给对方
*/
void answer(WebrtcGroupAnswerDTO dto);
/**
* 推送candidate信息给对方
*/
void candidate(WebrtcGroupCandidateDTO dto);
/**
* 用户进行了设备操作如果关闭摄像头
*/
void device(WebrtcGroupDeviceDTO dto);
/**
* 查询通话信息
*/
WebrtcGroupInfoVO info(Long groupId);
/**
* 心跳保持, 用户每15s上传一次心跳
*/
void heartbeat(Long groupId);
}

2
im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java

@ -8,7 +8,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient;
import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.util.JwtUtil;
import com.bx.implatform.config.JwtProperties;
import com.bx.implatform.config.props.JwtProperties;
import com.bx.implatform.dto.LoginDTO;
import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.dto.RegisterDTO;

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

@ -1,583 +0,0 @@
package com.bx.implatform.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.bx.imclient.IMClient;
import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.annotation.OnlineCheck;
import com.bx.implatform.annotation.RedisLock;
import com.bx.implatform.config.WebrtcConfig;
import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.dto.*;
import com.bx.implatform.entity.GroupMember;
import com.bx.implatform.entity.GroupMessage;
import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType;
import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.service.GroupMemberService;
import com.bx.implatform.service.GroupMessageService;
import com.bx.implatform.service.GroupService;
import com.bx.implatform.service.WebrtcGroupService;
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.BeanUtils;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 群语音通话服务类,所有涉及修改webtcSession的方法都要挂分布式锁
*
* @author: blue
* @date: 2024-06-01
* @version: 1.0
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WebrtcGroupServiceImpl implements WebrtcGroupService {
private final GroupService groupService;
private final GroupMemberService groupMemberService;
private final GroupMessageService groupMessageService;
private final RedisTemplate<String, Object> redisTemplate;
private final IMClient imClient;
private final UserStateUtils userStateUtils;
private final WebrtcConfig webrtcConfig;
@OnlineCheck
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId")
@Override
public void setup(WebrtcGroupSetupDTO dto) {
UserSession userSession = SessionContext.getSession();
groupService.getAndCheckById(dto.getGroupId());
if (dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) {
throw new GlobalException("最多支持" + webrtcConfig.getMaxChannel() + "人进行通话");
}
List<Long> userIds = getRecvIds(dto.getUserInfos());
if (!groupMemberService.isInGroup(dto.getGroupId(), userIds)) {
throw new GlobalException("部分用户不在群聊中");
}
String key = buildWebrtcSessionKey(dto.getGroupId());
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
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);
offlineUserIds.add(userInfo.getId());
} else if (userStateUtils.isBusy(userInfo.getId())) {
busyUserIds.add(userInfo.getId());
} else {
userInfos.add(userInfo);
// 设置用户忙线状态
userStateUtils.setBusy(userInfo.getId());
}
}
// 创建通话session
WebrtcGroupSession webrtcSession = new WebrtcGroupSession();
IMUserInfo userInfo = new IMUserInfo(userSession.getUserId(), userSession.getTerminal());
webrtcSession.setHost(userInfo);
webrtcSession.setUserInfos(userInfos);
webrtcSession.getInChatUsers().add(userInfo);
saveWebrtcSession(dto.getGroupId(), webrtcSession);
// 向发起邀请者推送邀请失败消息
if (!offlineUserIds.isEmpty()) {
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO();
vo.setUserIds(offlineUserIds);
vo.setReason("用户当前不在线");
sendRtcMessage2(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());
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo));
}
// 向被邀请的用户广播消息,发起呼叫
List<Long> recvIds = getRecvIds(userInfos);
sendRtcMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), recvIds, JSON.toJSONString(userInfos), false);
// 发送文字提示信息
WebrtcUserInfo mineInfo = findUserInfo(webrtcSession, userSession.getUserId());
String content = mineInfo.getNickName() + " 发起了语音通话";
sendTipMessage(dto.getGroupId(), content);
log.info("发起群通话,userId:{},groupId:{}", userSession.getUserId(), dto.getGroupId());
}
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId")
@Override
public void accept(Long groupId) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId);
// 校验
if (!isExist(webrtcSession, userSession.getUserId())) {
throw new GlobalException("您未被邀请通话");
}
// 防止重复进入
if (isInchat(webrtcSession, userSession.getUserId())) {
throw new GlobalException("您已在通话中");
}
// 将当前用户加入通话用户列表中
webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
saveWebrtcSession(groupId, webrtcSession);
// 广播信令
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos());
sendRtcMessage1(MessageType.RTC_GROUP_ACCEPT, groupId, recvIds, "", true);
log.info("加入群通话,userId:{},groupId:{}", userSession.getUserId(), groupId);
}
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId")
@Override
public void reject(Long groupId) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId);
// 校验
if (!isExist(webrtcSession, userSession.getUserId())) {
throw new GlobalException("您未被邀请通话");
}
// 防止重复进入
if (isInchat(webrtcSession, userSession.getUserId())) {
throw new GlobalException("您已在通话中");
}
// 将用户从列表中移除
List<WebrtcUserInfo> userInfos =
webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId()))
.collect(Collectors.toList());
webrtcSession.setUserInfos(userInfos);
saveWebrtcSession(groupId, webrtcSession);
// 进入空闲状态
userStateUtils.setFree(userSession.getUserId());
// 广播消息给的所有用户
List<Long> recvIds = getRecvIds(userInfos);
sendRtcMessage1(MessageType.RTC_GROUP_REJECT, groupId, recvIds, "", true);
log.info("拒绝群通话,userId:{},groupId:{}", userSession.getUserId(), groupId);
}
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId")
@Override
public void failed(WebrtcGroupFailedDTO dto) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId());
// 校验
if (!isExist(webrtcSession, userSession.getUserId())) {
return;
}
if (isInchat(webrtcSession, userSession.getUserId())) {
return;
}
// 将用户从列表中移除
List<WebrtcUserInfo> userInfos =
webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId()))
.collect(Collectors.toList());
webrtcSession.setUserInfos(userInfos);
saveWebrtcSession(dto.getGroupId(), webrtcSession);
// 进入空闲状态
userStateUtils.setFree(userSession.getUserId());
// 广播信令
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO();
vo.setUserIds(Arrays.asList(userSession.getUserId()));
vo.setReason(dto.getReason());
List<Long> recvIds = getRecvIds(userInfos);
sendRtcMessage1(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), recvIds, JSON.toJSONString(vo), false);
log.info("群通话失败,userId:{},groupId:{},原因:{}", userSession.getUserId(), dto.getGroupId(), dto.getReason());
}
@OnlineCheck
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId")
@Override
public void join(Long groupId) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId);
if (webrtcSession.getUserInfos().size() >= webrtcConfig.getMaxChannel()) {
throw new GlobalException("人员已满,无法进入通话");
}
GroupMember member = groupMemberService.findByGroupAndUserId(groupId, userSession.getUserId());
if (Objects.isNull(member) || member.getQuit()) {
throw new GlobalException("您不在群里中");
}
IMUserInfo mine = findInChatUser(webrtcSession, userSession.getUserId());
if (!Objects.isNull(mine) && mine.getTerminal().equals(userSession.getTerminal())) {
throw new GlobalException("已在其他设备加入通话");
}
WebrtcUserInfo userInfo = new WebrtcUserInfo();
userInfo.setId(userSession.getUserId());
userInfo.setNickName(member.getShowNickName());
userInfo.setHeadImage(member.getHeadImage());
// 默认是开启麦克风,关闭摄像头
userInfo.setIsCamera(false);
userInfo.setIsMicroPhone(true);
// 将当前用户加入通话用户列表中
if (!isExist(webrtcSession, userSession.getUserId())) {
webrtcSession.getUserInfos().add(userInfo);
}
if (!isInchat(webrtcSession, userSession.getUserId())) {
webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
}
saveWebrtcSession(groupId, webrtcSession);
// 进入忙线状态
userStateUtils.setBusy(userSession.getUserId());
// 广播信令
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos());
sendRtcMessage1(MessageType.RTC_GROUP_JOIN, groupId, recvIds, JSON.toJSONString(userInfo), false);
log.info("加入群通话,userId:{},groupId:{}", userSession.getUserId(), groupId);
}
@OnlineCheck
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId")
@Override
public void invite(WebrtcGroupInviteDTO dto) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId());
if (webrtcSession.getUserInfos().size() + dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) {
throw new GlobalException("最多支持" + webrtcConfig.getMaxChannel() + "人进行通话");
}
if (!groupMemberService.isInGroup(dto.getGroupId(), getRecvIds(dto.getUserInfos()))) {
throw new GlobalException("部分用户不在群聊中");
}
// 过滤掉已经在通话中的用户
List<WebrtcUserInfo> userInfos = webrtcSession.getUserInfos();
// 原用户id
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()) {
if (isExist(webrtcSession, userInfo.getId())) {
// 防止重复进入
continue;
}
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()) {
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO();
vo.setUserIds(offlineUserIds);
vo.setReason("用户当前不在线");
IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal());
sendRtcMessage2(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());
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo));
}
// 向被邀请的发起呼叫
List<Long> newUserIds = getRecvIds(newUserInfos);
sendRtcMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), newUserIds, JSON.toJSONString(userInfos), false);
// 向已在通话中的用户同步新邀请的用户信息
sendRtcMessage1(MessageType.RTC_GROUP_INVITE, dto.getGroupId(), userIds, JSON.toJSONString(newUserInfos),
false);
log.info("邀请加入群通话,userId:{},groupId:{},邀请用户:{}", userSession.getUserId(), dto.getGroupId(),
newUserIds);
}
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId")
@Override
public void cancel(Long groupId) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId);
if (!userSession.getUserId().equals(webrtcSession.getHost().getId())) {
throw new GlobalException("只有发起人可以取消通话");
}
// 移除rtc session
String key = buildWebrtcSessionKey(groupId);
redisTemplate.delete(key);
// 进入空闲状态
webrtcSession.getUserInfos().forEach(user -> userStateUtils.setFree(user.getId()));
// 广播消息给的所有用户
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos());
sendRtcMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, "", false);
// 发送文字提示信息
sendTipMessage(groupId, "通话结束");
log.info("取消群通话,userId:{},groupId:{}", userSession.getUserId(), groupId);
}
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId")
@Override
public void quit(Long groupId) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId);
// 将用户从列表中移除
List<IMUserInfo> inChatUsers =
webrtcSession.getInChatUsers().stream().filter(user -> !user.getId().equals(userSession.getUserId()))
.collect(Collectors.toList());
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());
sendRtcMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, "", false);
// 发送文字提示信息
sendTipMessage(groupId, "通话结束");
log.info("群通话结束,groupId:{}", groupId);
} else {
// 更新会话信息
webrtcSession.setInChatUsers(inChatUsers);
webrtcSession.setUserInfos(userInfos);
saveWebrtcSession(groupId, webrtcSession);
// 进入空闲状态
userStateUtils.setFree(userSession.getUserId());
// 广播信令
List<Long> recvIds = getRecvIds(userInfos);
sendRtcMessage1(MessageType.RTC_GROUP_QUIT, groupId, recvIds, "", false);
log.info("用户退出群通话,userId:{},groupId:{}", userSession.getUserId(), groupId);
}
}
@Override
public void offer(WebrtcGroupOfferDTO dto) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId());
IMUserInfo userInfo = findInChatUser(webrtcSession, dto.getUserId());
if (Objects.isNull(userInfo)) {
log.warn("对方未加入群通话,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(),
dto.getGroupId());
return;
}
// 推送offer给对方
sendRtcMessage2(MessageType.RTC_GROUP_OFFER, dto.getGroupId(), userInfo, dto.getOffer());
log.info("推送offer信息,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(),
dto.getGroupId());
}
@Override
public void answer(WebrtcGroupAnswerDTO dto) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId());
IMUserInfo userInfo = findInChatUser(webrtcSession, dto.getUserId());
if (Objects.isNull(userInfo)) {
// 对方未加入群通话
log.warn("对方未加入群通话,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(),
dto.getGroupId());
return;
}
// 推送answer信息给对方
sendRtcMessage2(MessageType.RTC_GROUP_ANSWER, dto.getGroupId(), userInfo, dto.getAnswer());
log.info("回复answer信息,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(),
dto.getGroupId());
}
@Override
public void candidate(WebrtcGroupCandidateDTO dto) {
UserSession userSession = SessionContext.getSession();
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId());
IMUserInfo userInfo = findInChatUser(webrtcSession, dto.getUserId());
if (Objects.isNull(userInfo)) {
// 对方未加入群通话
log.warn("对方未加入群通话,无法同步candidate,userId:{},remoteUserId:{},groupId:{}", userSession.getUserId(),
dto.getUserId(), dto.getGroupId());
return;
}
// 推送candidate信息给对方
sendRtcMessage2(MessageType.RTC_GROUP_CANDIDATE, dto.getGroupId(), userInfo, dto.getCandidate());
log.info("同步candidate信息,userId:{},groupId:{}", userSession.getUserId(), dto.getGroupId());
}
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId")
@Override
public void device(WebrtcGroupDeviceDTO dto) {
UserSession userSession = SessionContext.getSession();
// 查询会话信息
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId());
WebrtcUserInfo userInfo = findUserInfo(webrtcSession, userSession.getUserId());
if (Objects.isNull(userInfo)) {
throw new GlobalException("您已不在通话中");
}
// 更新设备状态
userInfo.setIsCamera(dto.getIsCamera());
userInfo.setIsMicroPhone(dto.getIsMicroPhone());
saveWebrtcSession(dto.getGroupId(), webrtcSession);
// 广播信令
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos());
sendRtcMessage1(MessageType.RTC_GROUP_DEVICE, dto.getGroupId(), recvIds, JSON.toJSONString(dto), false);
log.info("设备操作,userId:{},groupId:{},摄像头:{}", userSession.getUserId(), dto.getGroupId(),
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.getShowNickName());
host.setHeadImage(member.getHeadImage());
}
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);
if (Objects.isNull(webrtcSession)) {
throw new GlobalException("通话已结束");
}
return webrtcSession;
}
private void saveWebrtcSession(Long groupId, WebrtcGroupSession webrtcSession) {
String key = buildWebrtcSessionKey(groupId);
redisTemplate.opsForValue().set(key, webrtcSession, 30, TimeUnit.SECONDS);
}
private String buildWebrtcSessionKey(Long groupId) {
return StrUtil.join(":", RedisKey.IM_WEBRTC_GROUP_SESSION, groupId);
}
private IMUserInfo findInChatUser(WebrtcGroupSession webrtcSession, Long userId) {
for (IMUserInfo userInfo : webrtcSession.getInChatUsers()) {
if (userInfo.getId().equals(userId)) {
return userInfo;
}
}
return null;
}
private WebrtcUserInfo findUserInfo(WebrtcGroupSession webrtcSession, Long userId) {
for (WebrtcUserInfo userInfo : webrtcSession.getUserInfos()) {
if (userInfo.getId().equals(userId)) {
return userInfo;
}
}
return null;
}
private List<Long> getRecvIds(List<WebrtcUserInfo> userInfos) {
UserSession userSession = SessionContext.getSession();
return userInfos.stream().map(WebrtcUserInfo::getId).filter(id -> !id.equals(userSession.getUserId()))
.collect(Collectors.toList());
}
private Boolean isInchat(WebrtcGroupSession webrtcSession, Long userId) {
return webrtcSession.getInChatUsers().stream().anyMatch(user -> user.getId().equals(userId));
}
private Boolean isExist(WebrtcGroupSession webrtcSession, Long userId) {
return webrtcSession.getUserInfos().stream().anyMatch(user -> user.getId().equals(userId));
}
private void sendRtcMessage1(MessageType messageType, Long groupId, List<Long> recvIds, String content,
Boolean sendSelf) {
UserSession userSession = SessionContext.getSession();
GroupMessageVO messageInfo = new GroupMessageVO();
messageInfo.setType(messageType.code());
messageInfo.setGroupId(groupId);
messageInfo.setSendId(userSession.getUserId());
messageInfo.setContent(content);
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
sendMessage.setRecvIds(recvIds);
sendMessage.setSendToSelf(sendSelf);
sendMessage.setSendResult(false);
sendMessage.setData(messageInfo);
imClient.sendGroupMessage(sendMessage);
}
private void sendRtcMessage2(MessageType messageType, Long groupId, IMUserInfo receiver, String content) {
UserSession userSession = SessionContext.getSession();
GroupMessageVO messageInfo = new GroupMessageVO();
messageInfo.setType(messageType.code());
messageInfo.setGroupId(groupId);
messageInfo.setSendId(userSession.getUserId());
messageInfo.setContent(content);
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
sendMessage.setRecvIds(Arrays.asList(receiver.getId()));
sendMessage.setRecvTerminals(Arrays.asList(receiver.getTerminal()));
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
sendMessage.setData(messageInfo);
imClient.sendGroupMessage(sendMessage);
}
private void sendTipMessage(Long groupId, String content) {
UserSession userSession = SessionContext.getSession();
// 群聊成员列表
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId);
// 保存消息
GroupMessage msg = new GroupMessage();
msg.setGroupId(groupId);
msg.setContent(content);
msg.setSendId(userSession.getUserId());
msg.setSendTime(new Date());
msg.setStatus(MessageStatus.UNSEND.code());
msg.setSendNickName(userSession.getNickName());
msg.setType(MessageType.TIP_TEXT.code());
groupMessageService.save(msg);
// 群发罅隙
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
sendMessage.setRecvIds(userIds);
sendMessage.setSendResult(false);
sendMessage.setData(msgInfo);
imClient.sendGroupMessage(sendMessage);
}
}

36
im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java

@ -1,5 +1,6 @@
package com.bx.implatform.service.thirdparty;
import com.bx.implatform.config.props.MinioProperties;
import com.bx.implatform.contant.Constant;
import com.bx.implatform.enums.FileType;
import com.bx.implatform.enums.ResultCode;
@ -13,7 +14,6 @@ import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@ -21,7 +21,7 @@ import java.io.IOException;
import java.util.Objects;
/**
* 通过校验文件MD5实现重复文件秒传
* todo 通过校验文件MD5实现重复文件秒传
* 文件上传服务
*
* @author Blue
@ -32,25 +32,17 @@ import java.util.Objects;
@RequiredArgsConstructor
public class FileService {
private final MinioUtil minioUtil;
@Value("${minio.public}")
private String minIoServer;
@Value("${minio.bucketName}")
private String bucketName;
@Value("${minio.imagePath}")
private String imagePath;
@Value("${minio.filePath}")
private String filePath;
@Value("${minio.videoPath}")
private String videoPath;
private final MinioProperties minioProps;
@PostConstruct
public void init() {
if (!minioUtil.bucketExists(bucketName)) {
if (!minioUtil.bucketExists(minioProps.getBucketName())) {
// 创建bucket
minioUtil.makeBucket(bucketName);
minioUtil.makeBucket(minioProps.getBucketName());
// 公开bucket
minioUtil.setBucketPublic(bucketName);
minioUtil.setBucketPublic(minioProps.getBucketName());
}
}
@ -62,7 +54,7 @@ public class FileService {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件大小不能超过20M");
}
// 上传
String fileName = minioUtil.upload(bucketName, filePath, file);
String fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getFilePath(), file);
if (StringUtils.isEmpty(fileName)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件上传失败");
}
@ -84,7 +76,7 @@ public class FileService {
}
// 上传原图
UploadImageVO vo = new UploadImageVO();
String fileName = minioUtil.upload(bucketName, imagePath, file);
String fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getImagePath(), file);
if (StringUtils.isEmpty(fileName)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败");
}
@ -92,7 +84,7 @@ public class FileService {
// 大于30K的文件需上传缩略图
if (file.getSize() > 30 * 1024) {
byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), 30);
fileName = minioUtil.upload(bucketName, imagePath, Objects.requireNonNull(file.getOriginalFilename()), imageByte, file.getContentType());
fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getImagePath(), Objects.requireNonNull(file.getOriginalFilename()), imageByte, file.getContentType());
if (StringUtils.isEmpty(fileName)) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败");
}
@ -108,16 +100,16 @@ public class FileService {
public String generUrl(FileType fileTypeEnum, String fileName) {
String url = minIoServer + "/" + bucketName;
String url = minioProps.getDomain() + "/" + minioProps.getBucketName();
switch (fileTypeEnum) {
case FILE:
url += "/" + filePath + "/";
url += "/" + minioProps.getFilePath() + "/";
break;
case IMAGE:
url += "/" + imagePath + "/";
url += "/" + minioProps.getImagePath() + "/";
break;
case VIDEO:
url += "/" + videoPath + "/";
url += "/" + minioProps.getVideoPath() + "/";
break;
default:
break;

2
im-platform/src/main/resources/application-dev.yml

@ -12,7 +12,7 @@ spring:
minio:
endpoint: http://127.0.0.1:9000 #内网地址
public: http://127.0.0.1:9000 #外网访问地址
domain: http://127.0.0.1:9000 #外网访问地址
accessKey: minioadmin
secretKey: minioadmin
bucketName: box-im

2
im-platform/src/main/resources/application-prod.yml

@ -12,7 +12,7 @@ spring:
minio:
endpoint: http://127.0.0.1:9001 #内网地址
public: https://www.boxim.online/file #外网访问地址
domain: https://www.boxim.online/file #外网访问地址
accessKey: admin
secretKey: 3fBSt6AkgFuD77D6
bucketName: box-im

2
im-platform/src/main/resources/application-test.yml

@ -12,7 +12,7 @@ spring:
minio:
endpoint: http://127.0.0.1:9001 #内网地址
public: https://www.boxim.online/file #外网访问地址
domain: https://www.boxim.online/file #外网访问地址
accessKey: admin
secretKey: 3fBSt6AkgFuD77D6
bucketName: box-im

2
im-uniapp/hybrid/html/rtc-group/index.html

@ -8,6 +8,6 @@
<title>语音通话</title>
</head>
<body>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话为付费功能,有需要请联系作者...</div>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话功能需升级至商业版有需要请联系作者...</div>
</body>
</html>

2
im-uniapp/hybrid/html/rtc-private/index.html

@ -8,6 +8,6 @@
<title>视频通话</title>
</head>
<body>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话为付费功能,有需要请联系作者...</div>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话功能需升级至商业版有需要请联系作者...</div>
</body>
</html>

6
im-web/src/components/rtc/RtcGroupVideo.vue

@ -3,14 +3,14 @@
:visible.sync="isShow" width="50%">
<div class='rtc-group-video'>
<div style="padding-top:30px;font-weight: 600; text-align: center;font-size: 16px;">
多人音视频通话为付费功能有需要请联系作者...
多人音视频通话需升级至商业版有需要请联系作者购买...
</div>
<div style="padding-top:50px; text-align: center;font-size: 16px;">
点击下方文档了解详细信息:
</div>
<div style="padding-top:10px; text-align: center;font-size: 16px;">
<a href="https://www.yuque.com/u1475064/mufu2a/vi7engzluty594s2" target="_blank">
付费-音视频通话源码
<a href="https://www.yuque.com/u1475064/imk5n2" target="_blank">
盒子IM商业版说明
</a>
</div>

Loading…
Cancel
Save