From 3453788448e6f0a2765c04eaac5f92e17a29340b Mon Sep 17 00:00:00 2001 From: xsx <825657193@qq.com> Date: Sun, 9 Jun 2024 22:40:24 +0800 Subject: [PATCH 01/18] =?UTF-8?q?feat:=E5=A4=9A=E4=BA=BA=E8=A7=86=E9=A2=91?= =?UTF-8?q?-=E5=BC=80=E5=8F=91=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- im-platform/pom.xml | 6 +- .../bx/implatform/annotation/RedisLock.java | 37 ++ .../bx/implatform/aspect/RedisLockAspect.java | 82 ++++ .../bx/implatform/config/RedissonConfig.java | 42 ++ .../com/bx/implatform/contant/RedisKey.java | 18 +- .../controller/WebrtcGroupController.java | 111 +++++ ...ller.java => WebrtcPrivateController.java} | 22 +- .../implatform/dto/WebrtcGroupAnswerDTO.java | 31 ++ .../dto/WebrtcGroupCandidateDTO.java | 32 ++ .../implatform/dto/WebrtcGroupDeviceDTO.java | 26 + .../implatform/dto/WebrtcGroupFailedDTO.java | 25 + .../implatform/dto/WebrtcGroupInviteDTO.java | 29 ++ .../bx/implatform/dto/WebrtcGroupJoinDTO.java | 23 + .../implatform/dto/WebrtcGroupOfferDTO.java | 31 ++ .../implatform/dto/WebrtcGroupSetupDTO.java | 29 ++ .../com/bx/implatform/enums/MessageType.java | 41 +- .../com/bx/implatform/enums/WebrtcMode.java | 27 ++ .../service/IGroupMemberService.java | 8 + .../service/IWebrtcGroupService.java | 79 ++++ ...ervice.java => IWebrtcPrivateService.java} | 3 +- .../service/impl/GroupMemberServiceImpl.java | 44 +- .../service/impl/WebrtcGroupServiceImpl.java | 445 ++++++++++++++++++ ...mpl.java => WebrtcPrivateServiceImpl.java} | 36 +- .../session/WebrtcGroupSession.java | 33 ++ ...Session.java => WebrtcPrivateSession.java} | 2 +- .../bx/implatform/session/WebrtcUserInfo.java | 26 + .../bx/implatform/vo/WebrtcGroupFailedVO.java | 23 + im-uniapp/App.vue | 41 +- im-uniapp/common/enums.js | 14 +- .../group-member-selector.vue | 162 +++++++ im-uniapp/hybrid/html/index.html | 14 +- im-uniapp/pages.json | 4 +- im-uniapp/pages/chat/chat-box.vue | 74 ++- im-uniapp/pages/chat/chat-group-video.vue | 143 ++++++ ...{chat-video.vue => chat-private-video.vue} | 8 +- im-uniapp/ssl/cert.crt | 19 + im-uniapp/ssl/cert.key | 27 ++ im-uniapp/vite.config.js | 10 +- 38 files changed, 1696 insertions(+), 131 deletions(-) create mode 100644 im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java create mode 100644 im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java create mode 100644 im-platform/src/main/java/com/bx/implatform/config/RedissonConfig.java create mode 100644 im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java rename im-platform/src/main/java/com/bx/implatform/controller/{WebrtcController.java => WebrtcPrivateController.java} (78%) create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java create mode 100644 im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java create mode 100644 im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java rename im-platform/src/main/java/com/bx/implatform/service/{IWebrtcService.java => IWebrtcPrivateService.java} (83%) create mode 100644 im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java rename im-platform/src/main/java/com/bx/implatform/service/impl/{WebrtcServiceImpl.java => WebrtcPrivateServiceImpl.java} (87%) create mode 100644 im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java rename im-platform/src/main/java/com/bx/implatform/session/{WebrtcSession.java => WebrtcPrivateSession.java} (92%) create mode 100644 im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java create mode 100644 im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java create mode 100644 im-uniapp/components/group-member-selector/group-member-selector.vue create mode 100644 im-uniapp/pages/chat/chat-group-video.vue rename im-uniapp/pages/chat/{chat-video.vue => chat-private-video.vue} (93%) create mode 100644 im-uniapp/ssl/cert.crt create mode 100644 im-uniapp/ssl/cert.key diff --git a/im-platform/pom.xml b/im-platform/pom.xml index 34b5834..c56461d 100644 --- a/im-platform/pom.xml +++ b/im-platform/pom.xml @@ -110,7 +110,11 @@ mybatis-plus-generator 3.3.2 - + + org.redisson + redisson + 3.17.3 + diff --git a/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java new file mode 100644 index 0000000..0421931 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java @@ -0,0 +1,37 @@ +package com.bx.implatform.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 分布式锁注解 + */ +@Retention(RetentionPolicy.RUNTIME)//运行时生效 +@Target(ElementType.METHOD)//作用在方法上 +public @interface RedisLock { + + /** + * key的前缀,prefixKey+key就是redis的key + */ + String prefixKey() ; + + /** + * spel 表达式 + */ + String key(); + + /** + * 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1 + */ + int waitTime() default -1; + + /** + * 等待锁的时间单位,默认毫秒 + * + */ + TimeUnit unit() default TimeUnit.MILLISECONDS; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java new file mode 100644 index 0000000..0cd5586 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java @@ -0,0 +1,82 @@ +package com.bx.implatform.aspect; + +import cn.hutool.core.util.StrUtil; +import com.bx.implatform.annotation.RedisLock; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.redisson.Redisson; +import org.redisson.RedissonLock; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.annotation.Order; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * @author: blue + * @date: 2024-06-09 + * @version: 1.0 + */ + +@Slf4j +@Aspect +@Order(0) +@Component +@RequiredArgsConstructor +public class RedisLockAspect { + + private ExpressionParser parser = new SpelExpressionParser(); + private DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + private final RedissonClient redissonClient; + + @Around("@annotation(com.bx.implatform.annotation.RedisLock)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + RedisLock annotation = method.getAnnotation(RedisLock.class); + // 解析表达式中的key + String key = parseKey(joinPoint); + String lockKey = StrUtil.join(":",annotation.prefixKey(),key); + // 上锁 + RLock lock = redissonClient.getLock(lockKey); + lock.lock(annotation.waitTime(),annotation.unit()); + try { + // 执行方法 + return joinPoint.proceed(); + }finally { + lock.unlock(); + } + } + + private String parseKey(ProceedingJoinPoint joinPoint){ + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + RedisLock annotation = method.getAnnotation(RedisLock.class); + // el解析需要的上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 参数名 + String[] params = parameterNameDiscoverer.getParameterNames(method); + if(Objects.isNull(params)){ + return annotation.key(); + } + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < params.length; i++) { + context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去 + } + Expression expression = parser.parseExpression(annotation.key()); + return expression.getValue(context, String.class); + } + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/RedissonConfig.java b/im-platform/src/main/java/com/bx/implatform/config/RedissonConfig.java new file mode 100644 index 0000000..6e7dc87 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/RedissonConfig.java @@ -0,0 +1,42 @@ +package com.bx.implatform.config; + +import cn.hutool.core.util.StrUtil; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.StringCodec; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author: 谢绍许 + * @date: 2024-06-09 + * @version: 1.0 + */ + +@Configuration +@ConditionalOnClass(Config.class) +@EnableConfigurationProperties(RedisProperties.class) +public class RedissonConfig { + + @Bean + RedissonClient redissonClient(RedisProperties redisProperties) { + Config config = new Config(); + config.setCodec(new StringCodec()); + String address = "redis://" + redisProperties.getHost()+":"+redisProperties.getPort(); + SingleServerConfig serverConfig = config.useSingleServer() + .setAddress(address) + .setDatabase(redisProperties.getDatabase()); + if(StrUtil.isNotEmpty(redisProperties.getPassword())) { + serverConfig.setPassword(redisProperties.getPassword()); + } + + return Redisson.create(config); + } + + +} 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 2585bb4..c63f0e3 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 @@ -10,9 +10,13 @@ public final class RedisKey { */ public static final String IM_GROUP_READED_POSITION = "im:readed:group:position"; /** - * webrtc 会话信息 + * webrtc 单人通话 */ - public static final String IM_WEBRTC_SESSION = "im:webrtc:session"; + public static final String IM_WEBRTC_PRIVATE_SESSION = "im:webrtc:private:session"; + /** + * webrtc 群通话 + */ + public static final String IM_WEBRTC_GROUP_SESSION = "im:webrtc:group:session"; /** * 缓存前缀 */ @@ -30,4 +34,14 @@ public final class RedisKey { */ public static final String IM_CACHE_GROUP_MEMBER_ID = IM_CACHE + "group_member_ids"; + /** + * 分布式锁前缀 + */ + public static final String IM_LOCK = "im:lock:"; + + /** + * 分布式锁前缀 + */ + public static final String IM_LOCK_RTC_GROUP = IM_LOCK + "rtc:group"; + } 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 new file mode 100644 index 0000000..192fea6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java @@ -0,0 +1,111 @@ +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.IWebrtcGroupService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Api(tags = "webrtc视频多人通话") +@RestController +@RequestMapping("/webrtc/group") +@RequiredArgsConstructor +public class WebrtcGroupController { + + private final IWebrtcGroupService webrtcGroupService; + + @ApiOperation(httpMethod = "POST", value = "发起群视频通话") + @PostMapping("/setup") + public Result setup(@Valid @RequestBody WebrtcGroupSetupDTO dto) { + webrtcGroupService.setup(dto); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "接受通话") + @PostMapping("/accept") + public Result accept(@RequestParam Long groupId) { + webrtcGroupService.accept(groupId); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "拒绝通话") + @PostMapping("/reject") + public Result reject(@RequestParam Long groupId) { + webrtcGroupService.reject(groupId); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "通话失败") + @PostMapping("/failed") + public Result failed(@Valid @RequestBody WebrtcGroupFailedDTO dto) { + webrtcGroupService.failed(dto); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "进入视频通话") + @PostMapping("/join") + public Result join(@RequestParam Long groupId) { + webrtcGroupService.join(groupId); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "取消通话") + @PostMapping("/cancel") + public Result cancel(@RequestParam Long groupId) { + webrtcGroupService.cancel(groupId); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "离开视频通话") + @PostMapping("/quit") + public Result quit(@RequestParam Long groupId) { + webrtcGroupService.quit(groupId); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "推送offer信息") + @PostMapping("/offer") + public Result offer(@Valid @RequestBody WebrtcGroupOfferDTO dto) { + webrtcGroupService.offer(dto); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "推送answer信息") + @PostMapping("/answer") + public Result answer(@Valid @RequestBody WebrtcGroupAnswerDTO dto) { + webrtcGroupService.answer(dto); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "邀请用户进入视频通话") + @PostMapping("/invite") + public Result invite(@Valid @RequestBody WebrtcGroupInviteDTO dto) { + webrtcGroupService.invite(dto); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "同步candidate") + @PostMapping("/candidate") + public Result candidate(@Valid @RequestBody WebrtcGroupCandidateDTO dto) { + webrtcGroupService.candidate(dto); + return ResultUtils.success(); + } + + @ApiOperation(httpMethod = "POST", value = "设备操作") + @PostMapping("/device") + public Result device(@Valid @RequestBody WebrtcGroupDeviceDTO dto) { + webrtcGroupService.device(dto); + return ResultUtils.success(); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java similarity index 78% rename from im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java rename to im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java index d75ec6d..8f37619 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java @@ -3,7 +3,7 @@ package com.bx.implatform.controller; import com.bx.implatform.config.ICEServer; import com.bx.implatform.result.Result; import com.bx.implatform.result.ResultUtils; -import com.bx.implatform.service.IWebrtcService; +import com.bx.implatform.service.IWebrtcPrivateService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -15,21 +15,21 @@ import java.util.List; @RestController @RequestMapping("/webrtc/private") @RequiredArgsConstructor -public class WebrtcController { +public class WebrtcPrivateController { - private final IWebrtcService webrtcService; + private final IWebrtcPrivateService webrtcPrivateService; @ApiOperation(httpMethod = "POST", value = "呼叫视频通话") @PostMapping("/call") public Result call(@RequestParam Long uid, @RequestParam(defaultValue = "video") String mode, @RequestBody String offer) { - webrtcService.call(uid, mode, offer); + webrtcPrivateService.call(uid, mode, offer); return ResultUtils.success(); } @ApiOperation(httpMethod = "POST", value = "接受视频通话") @PostMapping("/accept") public Result accept(@RequestParam Long uid, @RequestBody String answer) { - webrtcService.accept(uid, answer); + webrtcPrivateService.accept(uid, answer); return ResultUtils.success(); } @@ -37,28 +37,28 @@ public class WebrtcController { @ApiOperation(httpMethod = "POST", value = "拒绝视频通话") @PostMapping("/reject") public Result reject(@RequestParam Long uid) { - webrtcService.reject(uid); + webrtcPrivateService.reject(uid); return ResultUtils.success(); } @ApiOperation(httpMethod = "POST", value = "取消呼叫") @PostMapping("/cancel") public Result cancel(@RequestParam Long uid) { - webrtcService.cancel(uid); + webrtcPrivateService.cancel(uid); return ResultUtils.success(); } @ApiOperation(httpMethod = "POST", value = "呼叫失败") @PostMapping("/failed") public Result failed(@RequestParam Long uid, @RequestParam String reason) { - webrtcService.failed(uid, reason); + webrtcPrivateService.failed(uid, reason); return ResultUtils.success(); } @ApiOperation(httpMethod = "POST", value = "挂断") @PostMapping("/handup") public Result handup(@RequestParam Long uid) { - webrtcService.handup(uid); + webrtcPrivateService.handup(uid); return ResultUtils.success(); } @@ -66,7 +66,7 @@ public class WebrtcController { @PostMapping("/candidate") @ApiOperation(httpMethod = "POST", value = "同步candidate") public Result candidate(@RequestParam Long uid, @RequestBody String candidate) { - webrtcService.candidate(uid, candidate); + webrtcPrivateService.candidate(uid, candidate); return ResultUtils.success(); } @@ -74,6 +74,6 @@ public class WebrtcController { @GetMapping("/iceservers") @ApiOperation(httpMethod = "GET", value = "获取iceservers") public Result> iceservers() { - return ResultUtils.success(webrtcService.getIceServers()); + return ResultUtils.success(webrtcPrivateService.getIceServers()); } } diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java new file mode 100644 index 0000000..3f98c4f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java @@ -0,0 +1,31 @@ +package com.bx.implatform.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("回复用户连接请求DTO") +public class WebrtcGroupAnswerDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + + @NotNull(message = "用户id不可为空") + @ApiModelProperty(value = "用户id,代表回复谁的连接请求") + private Long userId; + + @NotEmpty(message = "anwer不可为空") + @ApiModelProperty(value = "用户本地anwer信息") + private String answer; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java new file mode 100644 index 0000000..93cda08 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java @@ -0,0 +1,32 @@ +package com.bx.implatform.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("发起群视频通话DTO") +public class WebrtcGroupCandidateDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + + @NotNull(message = "用户id不可为空") + @ApiModelProperty(value = "用户id") + private Long userId; + + @NotEmpty(message = "candidate信息不可为空") + @ApiModelProperty(value = "candidate信息") + private String candidate; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java new file mode 100644 index 0000000..7ddcf63 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java @@ -0,0 +1,26 @@ +package com.bx.implatform.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("用户设备操作DTO") +public class WebrtcGroupDeviceDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + + @ApiModelProperty(value = "是否开启摄像头") + private Boolean isCamera = false; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java new file mode 100644 index 0000000..e9fa69f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java @@ -0,0 +1,25 @@ +package com.bx.implatform.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("用户通话失败DTO") +public class WebrtcGroupFailedDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + + @ApiModelProperty(value = "失败原因") + private String reason; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java new file mode 100644 index 0000000..e5ea7b8 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java @@ -0,0 +1,29 @@ +package com.bx.implatform.dto; + +import com.bx.implatform.session.WebrtcUserInfo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("邀请用户进入群视频通话DTO") +public class WebrtcGroupInviteDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + + @NotEmpty(message = "参与用户信息不可为空") + @ApiModelProperty(value = "参与用户信息") + private List userInfos; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java new file mode 100644 index 0000000..6d4e212 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("进入群视频通话DTO") +public class WebrtcGroupJoinDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java new file mode 100644 index 0000000..fbf9de5 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java @@ -0,0 +1,31 @@ +package com.bx.implatform.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("回复用户连接请求DTO") +public class WebrtcGroupOfferDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + + @NotNull(message = "用户id不可为空") + @ApiModelProperty(value = "用户id,代表回复谁的连接请求") + private Long userId; + + @NotEmpty(message = "offer不可为空") + @ApiModelProperty(value = "用户offer信息") + private String offer; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java new file mode 100644 index 0000000..9736bba --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java @@ -0,0 +1,29 @@ +package com.bx.implatform.dto; + +import com.bx.implatform.session.WebrtcUserInfo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@ApiModel("发起群视频通话DTO") +public class WebrtcGroupSetupDTO { + + @NotNull(message = "群聊id不可为空") + @ApiModelProperty(value = "群聊id") + private Long groupId; + + @NotEmpty(message = "参与用户信息不可为空") + @ApiModelProperty(value = "参与用户信息") + private List userInfos; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java index 2200387..59f229b 100644 --- a/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java +++ b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java @@ -1,8 +1,6 @@ package com.bx.implatform.enums; import lombok.AllArgsConstructor; -import lombok.Getter; - @AllArgsConstructor public enum MessageType { @@ -54,38 +52,27 @@ public enum MessageType { */ LOADDING(30,"加载中"), - /** - * 语音呼叫 - */ RTC_CALL_VOICE(100, "语音呼叫"), - /** - * 视频呼叫 - */ RTC_CALL_VIDEO(101, "视频呼叫"), - /** - * 接受 - */ RTC_ACCEPT(102, "接受"), - /** - * 拒绝 - */ RTC_REJECT(103, "拒绝"), - /** - * 取消呼叫 - */ RTC_CANCEL(104, "取消呼叫"), - /** - * 呼叫失败 - */ RTC_FAILED(105, "呼叫失败"), - /** - * 挂断 - */ RTC_HANDUP(106, "挂断"), - /** - * 同步candidate - */ - RTC_CANDIDATE(107, "同步candidate"); + RTC_CANDIDATE(107, "同步candidate"), + 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,"推送offer信息"), + RTC_GROUP_ANSWER(209,"推送answer信息"), + RTC_GROUP_CANDIDATE(210,"同步candidate"), + RTC_GROUP_DEVICE(211,"设备操作"), + ; private final Integer code; diff --git a/im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java b/im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java new file mode 100644 index 0000000..1aa50de --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java @@ -0,0 +1,27 @@ +package com.bx.implatform.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Getter +@AllArgsConstructor +public enum WebrtcMode { + + /** + * 视频通话 + */ + VIDEO( "video"), + + /** + * 语音通话 + */ + VOICE( "voice"); + + private final String value; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java b/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java index 7836729..bfa2440 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java @@ -73,4 +73,12 @@ public interface IGroupMemberService extends IService { * @param userId 用户id */ void removeByGroupAndUserId(Long groupId, Long userId); + + /** + * 用户用户是否在群中 + * + * @param groupId 群聊id + * @param userIds 用户id + */ + Boolean isInGroup(Long groupId,List userIds); } 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 new file mode 100644 index 0000000..66264bf --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java @@ -0,0 +1,79 @@ +package com.bx.implatform.service; + +import com.bx.implatform.dto.*; + +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); + + /** + * 通话过程中继续邀请用户加入通话 + */ + void invite(WebrtcGroupInviteDTO dto); + + /** + * 取消通话,仅通话发起人可以取消通话 + */ + void cancel(Long groupId); + + /** + * 退出通话,如果当前没有人在通话中,将取消整个通话 + */ + void quit(Long groupId); + + /** + * 推送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); + + + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java b/im-platform/src/main/java/com/bx/implatform/service/IWebrtcPrivateService.java similarity index 83% rename from im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java rename to im-platform/src/main/java/com/bx/implatform/service/IWebrtcPrivateService.java index b93858f..7eea5db 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/IWebrtcPrivateService.java @@ -1,7 +1,6 @@ package com.bx.implatform.service; import com.bx.implatform.config.ICEServer; -import org.springframework.web.bind.annotation.RequestBody; import java.util.List; @@ -10,7 +9,7 @@ import java.util.List; * * @author */ -public interface IWebrtcService { +public interface IWebrtcPrivateService { void call(Long uid, String mode,String offer); diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java index 3727add..592f761 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java @@ -3,6 +3,7 @@ package com.bx.implatform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bx.implatform.contant.RedisKey; @@ -34,30 +35,26 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = new QueryWrapper<>(); - wrapper.lambda().eq(GroupMember::getGroupId, groupId) - .eq(GroupMember::getUserId, userId); + wrapper.lambda().eq(GroupMember::getGroupId, groupId).eq(GroupMember::getUserId, userId); return this.getOne(wrapper); } @Override public List findByUserId(Long userId) { LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); - memberWrapper.eq(GroupMember::getUserId, userId) - .eq(GroupMember::getQuit, false); + memberWrapper.eq(GroupMember::getUserId, userId).eq(GroupMember::getQuit, false); return this.list(memberWrapper); } @Override public List findQuitInMonth(Long userId) { - Date monthTime = DateTimeUtils.addMonths(new Date(),-1); + Date monthTime = DateTimeUtils.addMonths(new Date(), -1); LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); - memberWrapper.eq(GroupMember::getUserId, userId) - .eq(GroupMember::getQuit, true) - .ge(GroupMember::getQuitTime,monthTime); + memberWrapper.eq(GroupMember::getUserId, userId).eq(GroupMember::getQuit, true) + .ge(GroupMember::getQuitTime, monthTime); return this.list(memberWrapper); } @@ -72,9 +69,8 @@ public class GroupMemberServiceImpl extends ServiceImpl findUserIdsByGroupId(Long groupId) { LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); - memberWrapper.eq(GroupMember::getGroupId, groupId) - .eq(GroupMember::getQuit, false) - .select(GroupMember::getUserId); + memberWrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getQuit, false) + .select(GroupMember::getUserId); List members = this.list(memberWrapper); return members.stream().map(GroupMember::getUserId).collect(Collectors.toList()); } @@ -83,9 +79,8 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaUpdate(); - wrapper.eq(GroupMember::getGroupId, groupId) - .set(GroupMember::getQuit, true) - .set(GroupMember::getQuitTime,new Date()); + wrapper.eq(GroupMember::getGroupId, groupId).set(GroupMember::getQuit, true) + .set(GroupMember::getQuitTime, new Date()); this.update(wrapper); } @@ -93,10 +88,19 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaUpdate(); - wrapper.eq(GroupMember::getGroupId, groupId) - .eq(GroupMember::getUserId, userId) - .set(GroupMember::getQuit, true) - .set(GroupMember::getQuitTime,new Date()); + wrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getUserId, userId).set(GroupMember::getQuit, true) + .set(GroupMember::getQuitTime, new Date()); this.update(wrapper); } + + @Override + public Boolean isInGroup(Long groupId, List userIds) { + if (CollectionUtils.isEmpty(userIds)) { + return true; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getQuit, false) + .in(GroupMember::getUserId, userIds); + return userIds.size() == this.count(wrapper); + } } 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 new file mode 100644 index 0000000..283beb6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java @@ -0,0 +1,445 @@ +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.RedisLock; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.*; +import com.bx.implatform.entity.GroupMember; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.service.IGroupMemberService; +import com.bx.implatform.service.IWebrtcGroupService; +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.vo.GroupMessageVO; +import com.bx.implatform.vo.WebrtcGroupFailedVO; +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.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 IWebrtcGroupService { + + private final IGroupMemberService groupMemberService; + private final RedisTemplate redisTemplate; + private final IMClient imClient; + /** + * 最多支持8路视频 + */ + private final int maxChannel = 9; + + @RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId") + @Override + public void setup(WebrtcGroupSetupDTO dto) { + UserSession userSession = SessionContext.getSession(); + List userIds = getRecvIds(dto.getUserInfos()); + String key = buildWebrtcSessionKey(dto.getGroupId()); + if (redisTemplate.hasKey(key)) { + throw new GlobalException("该群聊已存在一个通话"); + } + if (!groupMemberService.isInGroup(dto.getGroupId(), userIds)) { + throw new GlobalException("存在不在群聊中的用户"); + } + // 离线用户处理 + List userInfos = new LinkedList<>(); + List offlineUserIds = new LinkedList<>(); + for (WebrtcUserInfo userInfo : dto.getUserInfos()) { + if (imClient.isOnline(userInfo.getId())) { + userInfos.add(userInfo); + } else { + offlineUserIds.add(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("用户不在线"); + sendMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), userInfo, JSON.toJSONString(vo)); + } + // 向被邀请的用户广播消息,发起呼叫 + List recvIds = getRecvIds(dto.getUserInfos()); + sendMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), recvIds, JSON.toJSONString(userInfos)); + 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 recvIds = getRecvIds(webrtcSession.getUserInfos()); + sendMessage1(MessageType.RTC_GROUP_ACCEPT, groupId, recvIds, ""); + 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 userInfos = + webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId())) + .collect(Collectors.toList()); + webrtcSession.setUserInfos(userInfos); + saveWebrtcSession(groupId, webrtcSession); + // 广播消息给的所有用户 + List recvIds = getRecvIds(userInfos); + sendMessage1(MessageType.RTC_GROUP_REJECT, groupId, recvIds, ""); + 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 userInfos = + webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId())) + .collect(Collectors.toList()); + webrtcSession.setUserInfos(userInfos); + saveWebrtcSession(dto.getGroupId(), webrtcSession); + // 广播信令 + WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); + vo.setUserIds(Arrays.asList(userSession.getUserId())); + vo.setReason(dto.getReason()); + List recvIds = getRecvIds(userInfos); + sendMessage1(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), recvIds, JSON.toJSONString(vo)); + log.info("群通话失败,userId:{},groupId:{},原因:{}", userSession.getUserId(), dto.getReason()); + } + + @RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId") + @Override + public void join(Long groupId) { + UserSession userSession = SessionContext.getSession(); + WebrtcGroupSession webrtcSession = getWebrtcSession(groupId); + // 校验 + GroupMember member = groupMemberService.findByGroupAndUserId(groupId, userSession.getUserId()); + if (Objects.isNull(member)) { + throw new GlobalException("您不在群里中"); + } + // 防止重复进入 + if (isInchat(webrtcSession, userSession.getUserId())) { + throw new GlobalException("您已在通话中"); + } + WebrtcUserInfo userInfo = new WebrtcUserInfo(); + userInfo.setId(userSession.getUserId()); + userInfo.setNickName(member.getAliasName()); + userInfo.setHeadImage(member.getHeadImage()); + userInfo.setIsCamera(false); + // 将当前用户加入通话用户列表中 + if (!isExist(webrtcSession, userSession.getUserId())) { + webrtcSession.getUserInfos().add(userInfo); + } + webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); + saveWebrtcSession(groupId, webrtcSession); + // 广播信令 + List recvIds = getRecvIds(webrtcSession.getUserInfos()); + sendMessage1(MessageType.RTC_GROUP_JOIN, groupId, recvIds, JSON.toJSONString(userInfo)); + log.info("加入群通话,userId:{},groupId:{}", userSession.getUserId(), groupId); + } + + @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()); + // 过滤掉已经在通话中的用户 + List userInfos = webrtcSession.getUserInfos(); + // 原用户id + List userIds = getRecvIds(userInfos); + // 离线用户id + List offlineUserIds = new LinkedList<>(); + // 新加入的用户 + List newUserInfos = new LinkedList<>(); + for (WebrtcUserInfo userInfo : dto.getUserInfos()) { + if (isExist(webrtcSession, userInfo.getId())) { + // 防止重复进入 + continue; + } + if (imClient.isOnline(userInfo.getId())) { + newUserInfos.add(userInfo); + } else { + offlineUserIds.add(userInfo.getId()); + } + } + // 更新会话信息 + 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()); + 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)); + // 向已在通话中的用户同步新邀请的用户信息 + sendMessage1(MessageType.RTC_GROUP_INVITE, dto.getGroupId(), userIds, JSON.toJSONString(newUserInfos)); + 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); + // 广播消息给的所有用户 + List recvIds = getRecvIds(webrtcSession.getUserInfos()); + sendMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, ""); + 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 inChatUsers = + webrtcSession.getInChatUsers().stream().filter(user -> !user.getId().equals(userSession.getUserId())) + .collect(Collectors.toList()); + 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); + // 广播给还在呼叫中的用户,取消通话 + List recvIds = getRecvIds(webrtcSession.getUserInfos()); + sendMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, ""); + log.info("群通话结束,groupId:{}", groupId); + } else { + // 更新会话信息 + webrtcSession.setInChatUsers(inChatUsers); + webrtcSession.setUserInfos(userInfos); + saveWebrtcSession(groupId, webrtcSession); + // 广播信令 + List recvIds = getRecvIds(userInfos); + sendMessage1(MessageType.RTC_GROUP_QUIT, groupId, recvIds, ""); + 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.info("对方未加入群通话,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(), + dto.getGroupId()); + return; + } + // 推送offer给对方 + sendMessage2(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.info("对方未加入群通话,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(), + dto.getGroupId()); + return; + } + // 推送answer信息给对方 + sendMessage2(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.info("对方未加入群通话,无法同步candidate,userId:{},remoteUserId:{},groupId:{}", userSession.getUserId(), + dto.getUserId(), dto.getGroupId()); + return; + } + // 推送candidate信息给对方 + sendMessage2(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()); + saveWebrtcSession(dto.getGroupId(), webrtcSession); + // 广播信令 + List recvIds = getRecvIds(webrtcSession.getUserInfos()); + sendMessage1(MessageType.RTC_GROUP_DEVICE, dto.getGroupId(), recvIds, JSON.toJSONString(dto)); + log.info("设备操作,userId:{},groupId:{},摄像头:{}", userSession.getUserId(), dto.getGroupId(), + dto.getIsCamera()); + } + + 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, 2, TimeUnit.HOURS); + } + + 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 getRecvIds(List 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 sendMessage1(MessageType messageType, Long groupId, List recvIds, 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 sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); + sendMessage.setRecvIds(recvIds); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + sendMessage.setData(messageInfo); + imClient.sendGroupMessage(sendMessage); + } + + private void sendMessage2(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 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); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java similarity index 87% rename from im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java rename to im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java index 5b2ccad..892f2e9 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java @@ -8,10 +8,10 @@ import com.bx.implatform.config.ICEServerConfig; import com.bx.implatform.contant.RedisKey; import com.bx.implatform.enums.MessageType; import com.bx.implatform.exception.GlobalException; -import com.bx.implatform.service.IWebrtcService; +import com.bx.implatform.service.IWebrtcPrivateService; import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.UserSession; -import com.bx.implatform.session.WebrtcSession; +import com.bx.implatform.session.WebrtcPrivateSession; import com.bx.implatform.vo.PrivateMessageVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,7 +26,7 @@ import java.util.concurrent.TimeUnit; @Slf4j @Service @RequiredArgsConstructor -public class WebrtcServiceImpl implements IWebrtcService { +public class WebrtcPrivateServiceImpl implements IWebrtcPrivateService { private final IMClient imClient; private final RedisTemplate redisTemplate; @@ -39,10 +39,10 @@ public class WebrtcServiceImpl implements IWebrtcService { throw new GlobalException("对方目前不在线"); } // 创建webrtc会话 - WebrtcSession webrtcSession = new WebrtcSession(); + WebrtcPrivateSession webrtcSession = new WebrtcPrivateSession(); webrtcSession.setCallerId(session.getUserId()); webrtcSession.setCallerTerminal(session.getTerminal()); - String key = getSessionKey(session.getUserId(), uid); + String key = getWebRtcSessionKey(session.getUserId(), uid); redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS); // 向对方所有终端发起呼叫 PrivateMessageVO messageInfo = new PrivateMessageVO(); @@ -66,11 +66,11 @@ public class WebrtcServiceImpl implements IWebrtcService { public void accept(Long uid, @RequestBody String answer) { UserSession session = SessionContext.getSession(); // 查询webrtc会话 - WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); // 更新接受者信息 webrtcSession.setAcceptorId(session.getUserId()); webrtcSession.setAcceptorTerminal(session.getTerminal()); - String key = getSessionKey(session.getUserId(), uid); + String key = getWebRtcSessionKey(session.getUserId(), uid); redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS); // 向发起人推送接受通话信令 PrivateMessageVO messageInfo = new PrivateMessageVO(); @@ -94,7 +94,7 @@ public class WebrtcServiceImpl implements IWebrtcService { public void reject(Long uid) { UserSession session = SessionContext.getSession(); // 查询webrtc会话 - WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); // 删除会话信息 removeWebrtcSession(uid, session.getUserId()); // 向发起人推送拒绝通话信令 @@ -139,7 +139,7 @@ public class WebrtcServiceImpl implements IWebrtcService { public void failed(Long uid, String reason) { UserSession session = SessionContext.getSession(); // 查询webrtc会话 - WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); // 删除会话信息 removeWebrtcSession(uid, session.getUserId()); // 向发起方推送通话失败信令 @@ -165,7 +165,7 @@ public class WebrtcServiceImpl implements IWebrtcService { public void handup(Long uid) { UserSession session = SessionContext.getSession(); // 查询webrtc会话 - WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); // 删除会话信息 removeWebrtcSession(uid, session.getUserId()); // 向对方推送挂断通话信令 @@ -190,7 +190,7 @@ public class WebrtcServiceImpl implements IWebrtcService { public void candidate(Long uid, String candidate) { UserSession session = SessionContext.getSession(); // 查询webrtc会话 - WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); // 向发起方推送同步candidate信令 PrivateMessageVO messageInfo = new PrivateMessageVO(); messageInfo.setType(MessageType.RTC_CANDIDATE.code()); @@ -214,9 +214,9 @@ public class WebrtcServiceImpl implements IWebrtcService { return iceServerConfig.getIceServers(); } - private WebrtcSession getWebrtcSession(Long userId, Long uid) { - String key = getSessionKey(userId, uid); - WebrtcSession webrtcSession = (WebrtcSession)redisTemplate.opsForValue().get(key); + private WebrtcPrivateSession getWebrtcSession(Long userId, Long uid) { + String key = getWebRtcSessionKey(userId, uid); + WebrtcPrivateSession webrtcSession = (WebrtcPrivateSession)redisTemplate.opsForValue().get(key); if (webrtcSession == null) { throw new GlobalException("通话已结束"); } @@ -224,17 +224,17 @@ public class WebrtcServiceImpl implements IWebrtcService { } private void removeWebrtcSession(Long userId, Long uid) { - String key = getSessionKey(userId, uid); + String key = getWebRtcSessionKey(userId, uid); redisTemplate.delete(key); } - private String getSessionKey(Long id1, Long id2) { + private String getWebRtcSessionKey(Long id1, Long id2) { Long minId = id1 > id2 ? id2 : id1; Long maxId = id1 > id2 ? id1 : id2; - return String.join(":", RedisKey.IM_WEBRTC_SESSION, minId.toString(), maxId.toString()); + return String.join(":", RedisKey.IM_WEBRTC_PRIVATE_SESSION, minId.toString(), maxId.toString()); } - private Integer getTerminalType(Long uid, WebrtcSession webrtcSession) { + private Integer getTerminalType(Long uid, WebrtcPrivateSession webrtcSession) { if (uid.equals(webrtcSession.getCallerId())) { return webrtcSession.getCallerTerminal(); } diff --git a/im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java b/im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java new file mode 100644 index 0000000..f6343bf --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java @@ -0,0 +1,33 @@ +package com.bx.implatform.session; + +import com.bx.imcommon.model.IMUserInfo; +import lombok.Data; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author: 谢绍许 + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +public class WebrtcGroupSession { + + /** + * 通话发起者 + */ + private IMUserInfo host; + + /** + * 所有被邀请的用户列表 + */ + private List userInfos; + + /** + * 已经进入通话的用户列表 + */ + private List inChatUsers = new LinkedList<>(); + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/session/WebrtcSession.java b/im-platform/src/main/java/com/bx/implatform/session/WebrtcPrivateSession.java similarity index 92% rename from im-platform/src/main/java/com/bx/implatform/session/WebrtcSession.java rename to im-platform/src/main/java/com/bx/implatform/session/WebrtcPrivateSession.java index a26f298..aefea51 100644 --- a/im-platform/src/main/java/com/bx/implatform/session/WebrtcSession.java +++ b/im-platform/src/main/java/com/bx/implatform/session/WebrtcPrivateSession.java @@ -8,7 +8,7 @@ import lombok.Data; * @Date 2022/10/21 */ @Data -public class WebrtcSession { +public class WebrtcPrivateSession { /** * 发起者id */ diff --git a/im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java b/im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java new file mode 100644 index 0000000..fc5ad57 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java @@ -0,0 +1,26 @@ +package com.bx.implatform.session; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * @author: 谢绍许 + * @date: 2024-06-02 + * @version: 1.0 + */ +@Data +@ApiModel("用户信息") +public class WebrtcUserInfo { + @ApiModelProperty(value = "用户id") + private Long id; + + @ApiModelProperty(value = "用户昵称") + private String nickName; + + @ApiModelProperty(value = "用户头像") + private String headImage; + + @ApiModelProperty(value = "是否开启摄像头") + private Boolean isCamera; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java new file mode 100644 index 0000000..78548b0 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.vo; + +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 WebrtcGroupFailedVO { + + @ApiModelProperty(value = "失败用户列表") + private List userIds; + + @ApiModelProperty(value = "失败原因") + private String reason; +} diff --git a/im-uniapp/App.vue b/im-uniapp/App.vue index 9c5ceb2..3fd1604 100644 --- a/im-uniapp/App.vue +++ b/im-uniapp/App.vue @@ -109,9 +109,8 @@ }, insertPrivateMessage(friend, msg) { - // webrtc 信令 - if (msg.type >= enums.MESSAGE_TYPE.RTC_CALL_VOICE && - msg.type <= enums.MESSAGE_TYPE.RTC_CANDIDATE) { + // 单人视频信令 + if (msg.type >= 100 && msg.type <= 199) { // #ifdef MP-WEIXIN // 小程序不支持音视频 return; @@ -122,19 +121,18 @@ 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"){ + if(curPage != "pages/chat/chat-private-video"){ const friendInfo = encodeURIComponent(JSON.stringify(friend)); uni.navigateTo({ - url: `/pages/chat/chat-video?mode=${mode}&friend=${friendInfo}&isHost=false` + url: `/pages/chat/chat-private-video?mode=${mode}&friend=${friendInfo}&isHost=false` }) } } setTimeout(() => { - uni.$emit('WS_RTC',msg); + uni.$emit('WS_RTC_PRIVATE',msg); },500) return; } - let chatInfo = { type: 'PRIVATE', targetId: friend.id, @@ -186,6 +184,35 @@ }, insertGroupMessage(group, msg) { + // 群视频信令 + if (msg.type >= 200 && msg.type <= 299) { + // #ifdef MP-WEIXIN + // 小程序不支持音视频 + return; + // #endif + // 被呼叫,弹出视频页面 + let delayTime = 10; + if(msg.type == enums.MESSAGE_TYPE.RTC_GROUP_SETUP){ + let pages = getCurrentPages(); + let curPage = pages[pages.length-1].route; + if(curPage != "pages/chat/chat-group-video"){ + const userInfos = encodeURIComponent(msg.content); + const inviterId = msg.sendId; + const groupId = msg.groupId + uni.navigateTo({ + url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=false + &inviterId=${inviterId}&userInfos=${userInfos}` + }) + delayTime = 500; + } + } + // 消息转发到chat-group-video页面进行处理 + setTimeout(() => { + uni.$emit('WS_RTC_GROUP',msg); + },delayTime) + return; + } + let chatInfo = { type: 'GROUP', targetId: group.id, diff --git a/im-uniapp/common/enums.js b/im-uniapp/common/enums.js index b695b21..e74b893 100644 --- a/im-uniapp/common/enums.js +++ b/im-uniapp/common/enums.js @@ -20,7 +20,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 USER_STATE = { diff --git a/im-uniapp/components/group-member-selector/group-member-selector.vue b/im-uniapp/components/group-member-selector/group-member-selector.vue new file mode 100644 index 0000000..2addd13 --- /dev/null +++ b/im-uniapp/components/group-member-selector/group-member-selector.vue @@ -0,0 +1,162 @@ + + + + + + + \ No newline at end of file diff --git a/im-uniapp/hybrid/html/index.html b/im-uniapp/hybrid/html/index.html index 6733f82..05ff8e2 100644 --- a/im-uniapp/hybrid/html/index.html +++ b/im-uniapp/hybrid/html/index.html @@ -1,13 +1 @@ - - - - - - - - 视频通话 - - -
音视频通话为付费功能,有需要请联系作者...
- - \ No newline at end of file +web
\ No newline at end of file diff --git a/im-uniapp/pages.json b/im-uniapp/pages.json index 520b23c..c540831 100644 --- a/im-uniapp/pages.json +++ b/im-uniapp/pages.json @@ -17,7 +17,9 @@ }, { "path": "pages/chat/chat-box" },{ - "path": "pages/chat/chat-video" + "path": "pages/chat/chat-private-video" + },{ + "path": "pages/chat/chat-group-video" }, { "path": "pages/friend/friend-add" }, { diff --git a/im-uniapp/pages/chat/chat-box.vue b/im-uniapp/pages/chat/chat-box.vue index 08702a6..84df209 100644 --- a/im-uniapp/pages/chat/chat-box.vue +++ b/im-uniapp/pages/chat/chat-box.vue @@ -6,14 +6,13 @@ - + - + @@ -31,7 +30,7 @@ - + - + @@ -133,6 +133,7 @@ keyboardHeight: 322, atUserIds: [], recordText: "", + needScrollToBottom: false, // 需要滚动到底部 showMinIdx: 0 // 下标小于showMinIdx的消息不显示,否则可能很卡 } }, @@ -174,9 +175,9 @@ }) }, onRtCall(msgInfo) { - if (msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE) { + if (msgInfo.type == this.$enums.MESSAGE_TYPE.ACT_RT_VOICE) { this.onPriviteVoice(); - } else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO) { + } else if (msgInfo.type == this.$enums.MESSAGE_TYPE.ACT_RT_VIDEO) { this.onPriviteVideo(); } }, @@ -692,7 +693,14 @@ messageSize: function(newSize, oldSize) { // 接收到消息时滚动到底部 if (newSize > oldSize) { - this.scrollToBottom(); + console.log("messageSize",newSize,oldSize) + let pages = getCurrentPages(); + let curPage = pages[pages.length-1].route; + if(curPage == "pages/chat/chat-box"){ + this.scrollToBottom(); + }else { + this.needScrollToBottom = true; + } } }, unreadCount: { @@ -723,11 +731,16 @@ this.$store.commit("activeChat", options.chatIdx); // 复位回执消息 this.isReceipt = false; - // 页面滚到底部 - this.scrollToBottom(); }, onUnload() { this.$store.commit("activeChat", -1); + }, + onShow(){ + if(this.needScrollToBottom){ + // 页面滚到底部 + this.scrollToBottom(); + this.needScrollToBottom = false; + } } } diff --git a/im-uniapp/pages/chat/chat-private-video.vue b/im-uniapp/pages/chat/chat-private-video.vue index afb405b..b72d22c 100644 --- a/im-uniapp/pages/chat/chat-private-video.vue +++ b/im-uniapp/pages/chat/chat-private-video.vue @@ -20,16 +20,6 @@ onMessage(e) { this.onWebviewMessage(e.detail.data[0]); }, - onInsertMessage(msgInfo){ - let chat = { - type: 'PRIVATE', - targetId: this.friend.id, - showName: this.friend.nickName, - headImage: this.friend.headImage, - }; - this.$store.commit("openChat",chat); - this.$store.commit("insertMessage", msgInfo); - }, onWebviewMessage(event) { console.log("来自webview的消息:" + JSON.stringify(event)) switch (event.key) { @@ -39,9 +29,6 @@ case "WV_CLOSE": uni.navigateBack(); break; - case "INSERT_MESSAGE": - this.onInsertMessage(event.data); - break; } }, sendMessageToWebView(key, message) { diff --git a/im-uniapp/store/chatStore.js b/im-uniapp/store/chatStore.js index 5356df3..3641d21 100644 --- a/im-uniapp/store/chatStore.js +++ b/im-uniapp/store/chatStore.js @@ -171,9 +171,9 @@ export default { chat.lastContent = "[语音]"; } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) { chat.lastContent = msgInfo.content; - } else if (msgInfo.type == MESSAGE_TYPE.RT_VOICE) { + } else if (msgInfo.type == MESSAGE_TYPE.ACT_RT_VOICE) { chat.lastContent = "[语音通话]"; - } else if (msgInfo.type == MESSAGE_TYPE.RT_VIDEO) { + } else if (msgInfo.type == MESSAGE_TYPE.ACT_RT_VIDEO) { chat.lastContent = "[视频通话]"; } chat.lastSendTime = msgInfo.sendTime; @@ -275,14 +275,14 @@ export default { } this.commit("saveToStorage"); }, - loadingPrivateMsg(state, loadding) { - state.loadingPrivateMsg = loadding; + loadingPrivateMsg(state, loading) { + state.loadingPrivateMsg = loading; if (!this.getters.isLoading()) { this.commit("refreshChats") } }, - loadingGroupMsg(state, loadding) { - state.loadingGroupMsg = loadding; + loadingGroupMsg(state, loading) { + state.loadingGroupMsg = loading; if (!this.getters.isLoading()) { this.commit("refreshChats") } From 8f15c0fa24707523b0fd9884ec176d9d37984519 Mon Sep 17 00:00:00 2001 From: xsx <825657193@qq.com> Date: Sat, 22 Jun 2024 04:20:22 +0800 Subject: [PATCH 17/18] =?UTF-8?q?feat:=20=E5=8D=95=E4=BA=BA=E9=9F=B3?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=8A=9F=E8=83=BD=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + im-ui/src/components/rtc/RtcGroupVideo.vue | 802 +-------------------- 2 files changed, 24 insertions(+), 781 deletions(-) diff --git a/.gitignore b/.gitignore index 2b4b40c..6e07688 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ /im-uniapp/unpackage/ /im-uniapp/hybrid/ /im-uniapp/package-lock.json +/im-ui/src/components/rtc/LocalVideo.vue +/im-ui/src/components/rtc/RemoteVideo.vue +/im-ui/src/components/rtc/RtcGroupAcceptor.vue diff --git a/im-ui/src/components/rtc/RtcGroupVideo.vue b/im-ui/src/components/rtc/RtcGroupVideo.vue index b7e7f27..d5d028d 100644 --- a/im-ui/src/components/rtc/RtcGroupVideo.vue +++ b/im-ui/src/components/rtc/RtcGroupVideo.vue @@ -1,802 +1,42 @@ \ No newline at end of file From e2404b148593450950be6afaf32794e451974b23 Mon Sep 17 00:00:00 2001 From: xsx <825657193@qq.com> Date: Sat, 22 Jun 2024 11:41:25 +0800 Subject: [PATCH 18/18] =?UTF-8?q?feat:=20=E5=A4=9A=E4=BA=BA=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E9=80=9A=E8=AF=9D=E5=8A=9F=E8=83=BD=E4=B8=8A=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - im-ui/src/view/Login.vue | 27 ++++++--------------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d524d93..032bf40 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,5 @@ wsApi.onClose((e) => { 1. 本系统允许用于商业用途,且不收费(自愿投币)。**但切记不要用于任何非法用途** ,本软件作者不会为此承担任何责任 1. 基于本系统二次开发后再次开源的项目,请注明引用出处,以避免引发不必要的误会 -1. 如果您也想体验开源(bei bai piao)的快感,成为本项目的贡献者,欢迎提交PR。开发前最好提前联系作者,避免功能重复开发 1. 作者目前不打算接项目,如果能接受1k/天以上的报价,也可以聊聊 diff --git a/im-ui/src/view/Login.vue b/im-ui/src/view/Login.vue index 5e14eda..2ab0468 100644 --- a/im-ui/src/view/Login.vue +++ b/im-ui/src/view/Login.vue @@ -8,35 +8,20 @@
  • 加入uniapp移动端,支持移动端和web端同时在线,多端消息同步
  • 目前uniapp移动端支持安卓、ios、h5、微信小程序
  • 聊天窗口支持粘贴截图、@群成员、已读未读显示
  • -
  • 支持群聊已读显示(回执消息)
  • -
  • 语雀文档 - 盒子IM详细介绍文档,目前限时免费开放中 +
  • 语雀文档: + 盒子IM详细介绍文档
  • -

    最近更新(2024-03-17):

    +

    最近更新(2024-06-22):

      -
    • web端音视频功能优化:支持语音呼叫、会话中加入通话状态消息
    • -
    • uniapp端支持音视频通话,并与web端打通
    • -
    • uniapp端音视频源码通话源码暂未开源,需付费获取: - uniapp端音视频通源码购买说明 +
    • 群语音通话功能上线,且同时支持web端和uniapp端
    • +
    • 音视频通话部分源码未开源,可付费获取: + 音视频源码购买说明
    -
    -

    最近更新(2024-03-31):

    -
      -
    • uniapp移动端支持发送语音消息
    • -
    -
    -
    -

    最近更新(2024-04-27):

    -
      -
    • uniapp端加载离线消息慢以及卡顿问题优化
    • -
    • web端样式风格调整
    • -
    -

    如果本项目对您有帮助,请在gitee上帮忙点个star