diff --git a/README.md b/README.md index 00b9bab..d524d93 100644 --- a/README.md +++ b/README.md @@ -242,8 +242,9 @@ wsApi.onClose((e) => { ![输入图片说明](%E6%88%AA%E5%9B%BE/wx-mp/%E5%85%B6%E4%BB%96.jpg) #### 加入交流群 +1群目前已满员,扫码进入2群: -![输入图片说明](%E6%88%AA%E5%9B%BE/%E4%BA%A4%E6%B5%81%E7%BE%A4.png) +![输入图片说明](%E6%88%AA%E5%9B%BE/%E4%BA%A4%E6%B5%81%E7%BE%A42.png) 欢迎进群与小伙们一起交流, **申请加群前请务必先star哦** 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/config/ICEServerConfig.java b/im-platform/src/main/java/com/bx/implatform/config/WebrtcConfig.java similarity index 84% rename from im-platform/src/main/java/com/bx/implatform/config/ICEServerConfig.java rename to im-platform/src/main/java/com/bx/implatform/config/WebrtcConfig.java index 967a298..633d640 100644 --- a/im-platform/src/main/java/com/bx/implatform/config/ICEServerConfig.java +++ b/im-platform/src/main/java/com/bx/implatform/config/WebrtcConfig.java @@ -10,7 +10,9 @@ import java.util.List; @Data @Component @ConfigurationProperties(prefix = "webrtc") -public class ICEServerConfig { +public class WebrtcConfig { + + private Integer maxChannel = 9; private List iceServers = new ArrayList<>(); 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..026e3c9 100644 --- a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java +++ b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java @@ -2,17 +2,22 @@ package com.bx.implatform.contant; public final class RedisKey { - private RedisKey() { - } - + /** + * 用户状态 无值:空闲 1:正在忙 + */ + public static final String IM_USER_STATE = "im:user:state"; /** * 已读群聊消息位置(已读最大id) */ 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 +35,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/SystemController.java b/im-platform/src/main/java/com/bx/implatform/controller/SystemController.java new file mode 100644 index 0000000..d3cd30b --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/SystemController.java @@ -0,0 +1,35 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.config.WebrtcConfig; +import com.bx.implatform.dto.PrivateMessageDTO; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.vo.SystemConfigVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + + + +/** + * @author: blue + * @date: 2024-06-10 + * @version: 1.0 + */ +@Api(tags = "系统相关") +@RestController +@RequestMapping("/system") +@RequiredArgsConstructor +public class SystemController { + + private final WebrtcConfig webrtcConfig; + + @GetMapping("/config") + @ApiOperation(value = "加载系统配置", notes = "加载系统配置") + public Result loadConfig() { + return ResultUtils.success(new SystemConfigVO(webrtcConfig)); + } + + +} 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..03361be --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java @@ -0,0 +1,126 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.config.WebrtcConfig; +import com.bx.implatform.dto.*; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.IWebrtcGroupService; +import com.bx.implatform.vo.WebrtcGroupInfoVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +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(); + } + + @ApiOperation(httpMethod = "GET", value = "获取通话信息") + @GetMapping("/info") + public Result info(@RequestParam Long groupId) { + return ResultUtils.success(webrtcGroupService.info(groupId)); + } + + @ApiOperation(httpMethod = "POST", value = "获取通话信息") + @PostMapping("/heartbeat") + public Result heartbeat(@RequestParam Long groupId) { + webrtcGroupService.heartbeat(groupId); + return ResultUtils.success(); + } + + } diff --git a/im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java similarity index 73% 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..40cca08 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,14 +66,14 @@ 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(); } - - @GetMapping("/iceservers") - @ApiOperation(httpMethod = "GET", value = "获取iceservers") - public Result> iceservers() { - return ResultUtils.success(webrtcService.getIceServers()); + @ApiOperation(httpMethod = "POST", value = "获取通话信息") + @PostMapping("/heartbeat") + public Result heartbeat(@RequestParam Long uid) { + webrtcPrivateService.heartbeat(uid); + return ResultUtils.success(); } } 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..bc8ec79 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java @@ -0,0 +1,29 @@ +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; + + @ApiModelProperty(value = "是否开启麦克风") + private Boolean isMicroPhone; + +} 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..edef9e6 --- /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.config.WebrtcConfig; +import com.bx.implatform.dto.*; +import com.bx.implatform.vo.WebrtcGroupInfoVO; + +public interface IWebrtcGroupService { + + /** + * 发起通话 + */ + 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); + +} 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 76% 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..04dd4ad 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); @@ -26,7 +25,6 @@ public interface IWebrtcService { void candidate(Long uid, String candidate); - List getIceServers(); - + void heartbeat(Long uid); } 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..423cefc --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java @@ -0,0 +1,581 @@ +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.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.IGroupMemberService; +import com.bx.implatform.service.IGroupMessageService; +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.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 IWebrtcGroupService { + + private final IGroupMemberService groupMemberService; + private final IGroupMessageService groupMessageService; + private final RedisTemplate redisTemplate; + private final IMClient imClient; + private final UserStateUtils userStateUtils; + private final WebrtcConfig webrtcConfig; + + + @RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId") + @Override + public void setup(WebrtcGroupSetupDTO dto) { + UserSession userSession = SessionContext.getSession(); + if(!imClient.isOnline(userSession.getUserId())){ + throw new GlobalException("您已断开连接,请重新登陆"); + } + if (dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) { + throw new GlobalException("最多支持" + webrtcConfig.getMaxChannel() + "人进行通话"); + } + List userIds = getRecvIds(dto.getUserInfos()); + if (!groupMemberService.isInGroup(dto.getGroupId(), userIds)) { + throw new GlobalException("部分用户不在群聊中"); + } + String key = buildWebrtcSessionKey(dto.getGroupId()); + if (redisTemplate.hasKey(key)) { + throw new GlobalException("该群聊已存在一个通话"); + } + // 有效用户 + List userInfos = new LinkedList<>(); + // 离线用户 + List offlineUserIds = new LinkedList<>(); + // 忙线用户 + List busyUserIds = new LinkedList<>(); + for (WebrtcUserInfo userInfo : dto.getUserInfos()) { + if (!imClient.isOnline(userInfo.getId())) { + userInfos.add(userInfo); + //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 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 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 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 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 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 recvIds = getRecvIds(userInfos); + sendRtcMessage1(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), recvIds, JSON.toJSONString(vo),false); + 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); + 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() != userSession.getTerminal()){ + throw new GlobalException("已在其他设备加入通话"); + } + WebrtcUserInfo userInfo = new WebrtcUserInfo(); + userInfo.setId(userSession.getUserId()); + userInfo.setNickName(member.getAliasName()); + userInfo.setHeadImage(member.getHeadImage()); + // 默认是开启麦克风,关闭摄像头 + userInfo.setIsCamera(false); + userInfo.setIsMicroPhone(true); + // 将当前用户加入通话用户列表中 + if (!isExist(webrtcSession, userSession.getUserId())) { + webrtcSession.getUserInfos().add(userInfo); + } + if (!isInchat(webrtcSession, userSession.getUserId())) { + webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); + } + saveWebrtcSession(groupId, webrtcSession); + // 进入忙线状态 + userStateUtils.setBusy(userSession.getUserId()); + // 广播信令 + List recvIds = getRecvIds(webrtcSession.getUserInfos()); + sendRtcMessage1(MessageType.RTC_GROUP_JOIN, groupId, recvIds, JSON.toJSONString(userInfo),false); + 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()); + 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 userInfos = webrtcSession.getUserInfos(); + // 原用户id + List userIds = getRecvIds(userInfos); + // 离线用户id + List offlineUserIds = new LinkedList<>(); + // 忙线用户 + List busyUserIds = new LinkedList<>(); + // 新加入的用户 + List newUserInfos = new LinkedList<>(); + for (WebrtcUserInfo userInfo : dto.getUserInfos()) { + if (isExist(webrtcSession, userInfo.getId())) { + // 防止重复进入 + continue; + } + if (!imClient.isOnline(userInfo.getId())) { + // offlineUserIds.add(userInfo.getId()); + userStateUtils.setBusy(userInfo.getId()); + newUserInfos.add(userInfo); + } 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 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 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 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); + // 进入空闲状态 + webrtcSession.getUserInfos().forEach(user -> userStateUtils.setFree(user.getId())); + // 广播给还在呼叫中的用户,取消通话 + List 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 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.info("对方未加入群通话,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.info("对方未加入群通话,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.info("对方未加入群通话,无法同步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 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.getAliasName()); + 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 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 sendRtcMessage1(MessageType messageType, Long groupId, List 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 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 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 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 sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); + sendMessage.setRecvIds(userIds); + sendMessage.setSendResult(false); + sendMessage.setData(msgInfo); + 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 76% 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..b05fc6c 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 @@ -4,14 +4,15 @@ import com.bx.imclient.IMClient; import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMUserInfo; import com.bx.implatform.config.ICEServer; -import com.bx.implatform.config.ICEServerConfig; +import com.bx.implatform.config.WebrtcConfig; 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.util.UserStateUtils; import com.bx.implatform.vo.PrivateMessageVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,11 +27,12 @@ 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; - private final ICEServerConfig iceServerConfig; + private final WebrtcConfig iceServerConfig; + private final UserStateUtils userStateUtils; @Override public void call(Long uid, String mode, String offer) { @@ -38,12 +40,18 @@ public class WebrtcServiceImpl implements IWebrtcService { if (!imClient.isOnline(uid)) { throw new GlobalException("对方目前不在线"); } + if(userStateUtils.isBusy(uid)){ + 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); - redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS); + String key = getWebRtcSessionKey(session.getUserId(), uid); + redisTemplate.opsForValue().set(key, webrtcSession, 60, TimeUnit.SECONDS); + // 设置用户忙线状态 + userStateUtils.setBusy(uid); + userStateUtils.setBusy(session.getUserId()); // 向对方所有终端发起呼叫 PrivateMessageVO messageInfo = new PrivateMessageVO(); MessageType messageType = mode.equals("video") ? MessageType.RTC_CALL_VIDEO : MessageType.RTC_CALL_VOICE; @@ -66,12 +74,12 @@ 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); - redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS); + String key = getWebRtcSessionKey(session.getUserId(), uid); + redisTemplate.opsForValue().set(key, webrtcSession, 60, TimeUnit.SECONDS); // 向发起人推送接受通话信令 PrivateMessageVO messageInfo = new PrivateMessageVO(); messageInfo.setType(MessageType.RTC_ACCEPT.code()); @@ -94,9 +102,12 @@ 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()); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); // 向发起人推送拒绝通话信令 PrivateMessageVO messageInfo = new PrivateMessageVO(); messageInfo.setType(MessageType.RTC_REJECT.code()); @@ -119,6 +130,9 @@ public class WebrtcServiceImpl implements IWebrtcService { UserSession session = SessionContext.getSession(); // 删除会话信息 removeWebrtcSession(session.getUserId(), uid); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); // 向对方所有终端推送取消通话信令 PrivateMessageVO messageInfo = new PrivateMessageVO(); messageInfo.setType(MessageType.RTC_CANCEL.code()); @@ -139,9 +153,12 @@ 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()); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); // 向发起方推送通话失败信令 PrivateMessageVO messageInfo = new PrivateMessageVO(); messageInfo.setType(MessageType.RTC_FAILED.code()); @@ -165,9 +182,12 @@ 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()); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); // 向对方推送挂断通话信令 PrivateMessageVO messageInfo = new PrivateMessageVO(); messageInfo.setType(MessageType.RTC_HANDUP.code()); @@ -190,7 +210,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()); @@ -210,13 +230,18 @@ public class WebrtcServiceImpl implements IWebrtcService { } @Override - public List getIceServers() { - return iceServerConfig.getIceServers(); + public void heartbeat(Long uid) { + UserSession session = SessionContext.getSession(); + // 会话续命 + String key = getWebRtcSessionKey(session.getUserId(), uid); + redisTemplate.expire(key,60,TimeUnit.SECONDS); + // 用户状态续命 + userStateUtils.expire(session.getUserId()); } - 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 +249,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..75b16fb --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java @@ -0,0 +1,29 @@ +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; + + @ApiModelProperty(value = "是否开启麦克风") + private Boolean isMicroPhone; +} diff --git a/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java b/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java new file mode 100644 index 0000000..40678b2 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java @@ -0,0 +1,44 @@ +package com.bx.implatform.util; + +import cn.hutool.core.util.StrUtil; +import com.bx.implatform.contant.RedisKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * @author: 谢绍许 + * @date: 2024-06-10 + * @version: 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class UserStateUtils { + + private final RedisTemplate redisTemplate; + + public void setBusy(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.opsForValue().set(key,1,30, TimeUnit.SECONDS); + } + + public void expire(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.expire(key,30, TimeUnit.SECONDS); + } + + public void setFree(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.delete(key); + } + + public Boolean isBusy(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + return redisTemplate.hasKey(key); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/SystemConfigVO.java b/im-platform/src/main/java/com/bx/implatform/vo/SystemConfigVO.java new file mode 100644 index 0000000..f8e936e --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/SystemConfigVO.java @@ -0,0 +1,22 @@ +package com.bx.implatform.vo; + +import com.bx.implatform.config.WebrtcConfig; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * @author: blue + * @date: 2024-06-10 + * @version: 1.0 + */ +@Data +@ApiModel("系统配置VO") +@AllArgsConstructor +public class SystemConfigVO { + + @ApiModelProperty(value = "webrtc配置") + private WebrtcConfig webrtc; + +} 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-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java new file mode 100644 index 0000000..8706b41 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java @@ -0,0 +1,28 @@ +package com.bx.implatform.vo; + +import com.bx.implatform.session.WebrtcUserInfo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +/** + * @author: 谢绍许 + * @date: 2024-06-09 + * @version: 1.0 + */ +@Data +@ApiModel("群通话信息VO") +public class WebrtcGroupInfoVO { + + + @ApiModelProperty(value = "是否在通话中") + private Boolean isChating; + + @ApiModelProperty(value = "通话发起人") + WebrtcUserInfo host; + + @ApiModelProperty(value = "通话用户列表") + private List userInfos; +} diff --git a/im-platform/src/main/resources/application.yml b/im-platform/src/main/resources/application.yml index b195ce1..8e0cbe8 100644 --- a/im-platform/src/main/resources/application.yml +++ b/im-platform/src/main/resources/application.yml @@ -39,6 +39,7 @@ minio: videoPath: video webrtc: + max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 iceServers: - urls: stun:stun.l.google.com:19302 diff --git a/im-ui/src/App.vue b/im-ui/src/App.vue index b60f612..60aa2b0 100644 --- a/im-ui/src/App.vue +++ b/im-ui/src/App.vue @@ -85,4 +85,31 @@ .el-button { padding: 8px 15px !important; } + +.el-checkbox { + display: flex; + align-items: center; + + //修改选中框的大小 + .el-checkbox__inner { + width: 20px; + height: 20px; + + //修改选中框中的对勾的大小和位置 + &::after { + height: 12px; + left: 7px; + } + } + + //修改点击文字颜色不变 + .el-checkbox__input.is-checked+.el-checkbox__label { + color: #333333; + } + + .el-checkbox__label { + line-height: 20px; + padding-left: 8px; + } +} \ No newline at end of file diff --git a/im-ui/src/api/camera.js b/im-ui/src/api/camera.js new file mode 100644 index 0000000..be3a1ae --- /dev/null +++ b/im-ui/src/api/camera.js @@ -0,0 +1,76 @@ + +class ImCamera { + constructor() { + this.stream = null; + } +} + +ImCamera.prototype.isEnable = function() { + return !!navigator && !!navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia; +} + +ImCamera.prototype.openVideo = function(isFacing) { + return new Promise((resolve, reject) => { + if(this.stream){ + this.close() + } + let facingMode = isFacing ? "user" : "environment"; + let constraints = { + video: { + facingMode: facingMode + }, + audio: { + echoCancellation: true, //音频开启回音消除 + noiseSuppression: true // 开启降噪 + } + } + console.log("getUserMedia") + navigator.mediaDevices.getUserMedia(constraints).then((stream) => { + console.log("摄像头打开") + this.stream = stream; + resolve(stream); + }).catch((e) => { + console.log(e) + console.log("摄像头未能正常打开") + reject({ + code: 0, + message: "摄像头未能正常打开" + }) + }) + }) +} + + +ImCamera.prototype.openAudio = function() { + return new Promise((resolve, reject) => { + let constraints = { + video: false, + audio: { + echoCancellation: true, //音频开启回音消除 + noiseSuppression: true // 开启降噪 + } + } + navigator.mediaDevices.getUserMedia(constraints).then((stream) => { + this.stream = stream; + resolve(stream); + }).catch(() => { + console.log("麦克风未能正常打开") + reject({ + code: 0, + message: "麦克风未能正常打开" + }) + }) + + }) +} + +ImCamera.prototype.close = function() { + // 停止流 + if (this.stream) { + this.stream.getTracks().forEach((track) => { + track.stop(); + }); + } +} + +export default ImCamera; \ No newline at end of file diff --git a/im-ui/src/api/enums.js b/im-ui/src/api/enums.js index 0e6a1b1..0157962 100644 --- a/im-ui/src/api/enums.js +++ b/im-ui/src/api/enums.js @@ -1,18 +1,17 @@ - const MESSAGE_TYPE = { TEXT: 0, - IMAGE:1, - FILE:2, - AUDIO:3, - VIDEO:4, - RT_VOICE:5, - RT_VIDEO:6, - RECALL:10, - READED:11, - RECEIPT:12, - TIP_TIME:20, - TIP_TEXT:21, - LOADDING:30, + IMAGE: 1, + FILE: 2, + AUDIO: 3, + VIDEO: 4, + RT_VOICE: 5, + RT_VIDEO: 6, + RECALL: 10, + READED: 11, + RECEIPT: 12, + TIP_TIME: 20, + TIP_TEXT: 21, + LOADDING: 30, RTC_CALL_VOICE: 100, RTC_CALL_VIDEO: 101, RTC_ACCEPT: 102, @@ -20,7 +19,19 @@ const MESSAGE_TYPE = { RTC_CANCEL: 104, RTC_FAILED: 105, RTC_HANDUP: 106, - RTC_CANDIDATE: 107 + RTC_CANDIDATE: 107, + RTC_GROUP_SETUP: 200, + RTC_GROUP_ACCEPT: 201, + RTC_GROUP_REJECT: 202, + RTC_GROUP_FAILED: 203, + RTC_GROUP_CANCEL: 204, + RTC_GROUP_QUIT: 205, + RTC_GROUP_INVITE: 206, + RTC_GROUP_JOIN: 207, + RTC_GROUP_OFFER: 208, + RTC_GROUP_ANSWER: 209, + RTC_GROUP_CANDIDATE: 210, + RTC_GROUP_DEVICE: 211 } const RTC_STATE = { @@ -28,7 +39,7 @@ const RTC_STATE = { WAIT_CALL: 1, // 呼叫后等待 WAIT_ACCEPT: 2, // 被呼叫后等待 ACCEPTED: 3, // 已接受聊天,等待建立连接 - CHATING:4 // 聊天中 + CHATING: 4 // 聊天中 } const TERMINAL_TYPE = { @@ -39,8 +50,8 @@ const TERMINAL_TYPE = { const MESSAGE_STATUS = { UNSEND: 0, SENDED: 1, - RECALL:2, - READED:3 + RECALL: 2, + READED: 3 } @@ -49,4 +60,4 @@ export { RTC_STATE, TERMINAL_TYPE, MESSAGE_STATUS -} +} \ No newline at end of file diff --git a/im-ui/src/api/eventBus.js b/im-ui/src/api/eventBus.js new file mode 100644 index 0000000..a72b416 --- /dev/null +++ b/im-ui/src/api/eventBus.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); \ No newline at end of file diff --git a/im-ui/src/api/rtcGroupApi.js b/im-ui/src/api/rtcGroupApi.js new file mode 100644 index 0000000..58e8409 --- /dev/null +++ b/im-ui/src/api/rtcGroupApi.js @@ -0,0 +1,156 @@ +import http from './httpRequest.js' + +class RtcGroupApi { + constructor() { + this.http = http; + } +} + + +RtcGroupApi.prototype.setup = function(groupId, userInfos) { + let formData = { + groupId, + userInfos + } + return this.http({ + url: '/webrtc/group/setup', + method: 'post', + data: formData + }) +} + +RtcGroupApi.prototype.accept = function(groupId) { + return this.http({ + url: '/webrtc/group/accept?groupId='+groupId, + method: 'post' + }) +} + +RtcGroupApi.prototype.reject = function(groupId) { + return this.http({ + url: '/webrtc/group/reject?groupId='+groupId, + method: 'post' + }) +} + +RtcGroupApi.prototype.failed = function(groupId,reason) { + let formData = { + groupId, + reason + } + return this.http({ + url: '/webrtc/group/failed', + method: 'post', + data: formData + }) +} + + +RtcGroupApi.prototype.join = function(groupId) { + return this.http({ + url: '/webrtc/group/join?groupId='+groupId, + method: 'post' + }) +} + +RtcGroupApi.prototype.invite = function(groupId, userInfos) { + let formData = { + groupId, + userInfos + } + return this.http({ + url: '/webrtc/group/invite', + method: 'post', + data: formData + }) +} + + +RtcGroupApi.prototype.offer = function(groupId, userId, offer) { + let formData = { + groupId, + userId, + offer + } + return this.http({ + url: '/webrtc/group/offer', + method: 'post', + data: formData + }) +} + +RtcGroupApi.prototype.answer = function(groupId, userId, answer) { + let formData = { + groupId, + userId, + answer + } + return this.http({ + url: '/webrtc/group/answer', + method: 'post', + data: formData + }) +} + +RtcGroupApi.prototype.quit = function(groupId) { + return this.http({ + url: '/webrtc/group/quit?groupId=' + groupId, + method: 'post' + }) +} + +RtcGroupApi.prototype.cancel = function(groupId) { + return this.http({ + url: '/webrtc/group/cancel?groupId=' + groupId, + method: 'post' + }) +} + +RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) { + let formData = { + groupId, + userId, + candidate + } + return this.http({ + url: '/webrtc/group/candidate', + method: 'post', + data: formData + }) +} + +RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) { + let formData = { + groupId, + isCamera, + isMicroPhone + } + return this.http({ + url: '/webrtc/group/device', + method: 'post', + data: formData + }) +} + + +RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) { + let formData = { + groupId, + userId, + candidate + } + return this.http({ + url: '/webrtc/group/candidate', + method: 'post', + data: formData + }) +} + +RtcGroupApi.prototype.heartbeat = function(groupId) { + return this.http({ + url: '/webrtc/group/heartbeat?groupId=' + groupId, + method: 'post' + }) +} + +export default RtcGroupApi; \ No newline at end of file diff --git a/im-ui/src/api/webrtc.js b/im-ui/src/api/webrtc.js new file mode 100644 index 0000000..359118b --- /dev/null +++ b/im-ui/src/api/webrtc.js @@ -0,0 +1,117 @@ + +class ImWebRtc { + constructor() { + this.configuration = {} + this.stream = null; + } +} + +ImWebRtc.prototype.isEnable = function() { + window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window + .mozRTCPeerConnection; + window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window + .mozRTCSessionDescription; + window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window + .mozRTCIceCandidate; + return !!window.RTCPeerConnection; +} + +ImWebRtc.prototype.init = function(configuration) { + this.configuration = configuration; +} + +ImWebRtc.prototype.setupPeerConnection = function(callback) { + this.peerConnection = new RTCPeerConnection(this.configuration); + this.peerConnection.ontrack = (e) => { + // 对方的视频流 + callback(e.streams[0]); + }; +} + + +ImWebRtc.prototype.setStream = function(stream) { + if(this.stream){ + this.peerConnection.removeStream(this.stream) + } + stream.getTracks().forEach((track) => { + this.peerConnection.addTrack(track, stream); + }); + this.stream = stream; +} + + +ImWebRtc.prototype.onIcecandidate = function(callback) { + this.peerConnection.onicecandidate = (event) => { + // 追踪到候选信息 + if (event.candidate) { + callback(event.candidate) + } + } +} + +ImWebRtc.prototype.onStateChange = function(callback) { + // 监听连接状态 + this.peerConnection.oniceconnectionstatechange = (event) => { + let state = event.target.iceConnectionState; + console.log("ICE连接状态变化: : " + state) + callback(state) + }; +} + +ImWebRtc.prototype.createOffer = function() { + return new Promise((resolve, reject) => { + const offerParam = {}; + offerParam.offerToRecieveAudio = 1; + offerParam.offerToRecieveVideo = 1; + // 创建本地sdp信息 + this.peerConnection.createOffer(offerParam).then((offer) => { + // 设置本地sdp信息 + this.peerConnection.setLocalDescription(offer); + // 发起呼叫请求 + resolve(offer) + }).catch((e) => { + reject(e) + }) + }); +} + + +ImWebRtc.prototype.createAnswer = function(offer) { + return new Promise((resolve, reject) => { + // 设置远端的sdp + this.setRemoteDescription(offer); + // 创建本地dsp + const offerParam = {}; + offerParam.offerToRecieveAudio = 1; + offerParam.offerToRecieveVideo = 1; + this.peerConnection.createAnswer(offerParam).then((answer) => { + // 设置本地sdp信息 + this.peerConnection.setLocalDescription(answer); + // 接受呼叫请求 + resolve(answer) + }).catch((e) => { + reject(e) + }) + }); +} + +ImWebRtc.prototype.setRemoteDescription = function(offer) { + // 设置对方的sdp信息 + this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); +} + +ImWebRtc.prototype.addIceCandidate = function(candidate) { + // 添加对方的候选人信息 + this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); +} + +ImWebRtc.prototype.close = function(uid) { + // 关闭RTC连接 + if (this.peerConnection) { + this.peerConnection.close(); + this.peerConnection.onicecandidate = null; + this.peerConnection.onaddstream = null; + } +} + +export default ImWebRtc; \ No newline at end of file diff --git a/im-ui/src/assets/iconfont/iconfont.css b/im-ui/src/assets/iconfont/iconfont.css index ff4113e..8c5b819 100644 --- a/im-ui/src/assets/iconfont/iconfont.css +++ b/im-ui/src/assets/iconfont/iconfont.css @@ -1,6 +1,6 @@ @font-face { font-family: "iconfont"; /* Project id 3791506 */ - src: url('iconfont.ttf?t=1714220334746') format('truetype'); + src: url('iconfont.ttf?t=1718373714629') format('truetype'); } .iconfont { @@ -11,6 +11,38 @@ -moz-osx-font-smoothing: grayscale; } +.icon-invite-rtc:before { + content: "\e65f"; +} + +.icon-quit:before { + content: "\e606"; +} + +.icon-camera-off:before { + content: "\e6b5"; +} + +.icon-speaker-off:before { + content: "\ea3c"; +} + +.icon-microphone-on:before { + content: "\e63b"; +} + +.icon-speaker-on:before { + content: "\e6a4"; +} + +.icon-camera-on:before { + content: "\e627"; +} + +.icon-microphone-off:before { + content: "\efe5"; +} + .icon-chat:before { content: "\e600"; } diff --git a/im-ui/src/assets/iconfont/iconfont.ttf b/im-ui/src/assets/iconfont/iconfont.ttf index 63ec967..1697727 100644 Binary files a/im-ui/src/assets/iconfont/iconfont.ttf and b/im-ui/src/assets/iconfont/iconfont.ttf differ diff --git a/im-ui/src/components/chat/ChatBox.vue b/im-ui/src/components/chat/ChatBox.vue index 6e4a766..e9ea6b0 100644 --- a/im-ui/src/components/chat/ChatBox.vue +++ b/im-ui/src/components/chat/ChatBox.vue @@ -13,11 +13,11 @@
  • - +
@@ -36,8 +36,8 @@
- +
@@ -47,19 +47,23 @@
+ @click="showPrivateVideo('voice')"> +
+
+ @click="showPrivateVideo('video')">
x + @compositionstart="onEditorCompositionStart" + @compositionend="onEditorCompositionEnd" @input="onEditorInput" + :placeholder="placeholder" @blur="onEditBoxBlur()" @keydown.down="onKeyDown" + @keydown.up="onKeyUp" @keydown.enter.prevent="onKeyEnter">x
@@ -85,853 +89,905 @@ - + + +
\ No newline at end of file + \ No newline at end of file diff --git a/im-ui/src/components/common/HeadImage.vue b/im-ui/src/components/common/HeadImage.vue index eccba60..91eec68 100644 --- a/im-ui/src/components/common/HeadImage.vue +++ b/im-ui/src/components/common/HeadImage.vue @@ -28,6 +28,16 @@ type: Number, default: 50 }, + width: { + type: Number + }, + height: { + type: Number + }, + radius:{ + type: String, + default: "10%" + }, url: { type: String }, @@ -54,12 +64,18 @@ } }, computed:{ - avatarImageStyle(){ - return `width:${this.size}px; height:${this.size}px;` + avatarImageStyle() { + let w = this.width ? this.width : this.size; + let h = this.height ? this.height : this.size; + return `width:${w}px; height:${h}px; + border-radius: ${this.radius};` }, - avatarTextStyle(){ - return `width: ${this.size}px;height:${this.size}px; - color:${this.textColor};font-size:${this.size*0.6}px;` + avatarTextStyle() { + let w = this.width ? this.width : this.size; + let h = this.height ? this.height : this.size; + return `width: ${w}px;height:${h}px; + color:${this.textColor};font-size:${w*0.6}px; + border-radius: ${this.radius};` }, textColor(){ let hash = 0; @@ -79,7 +95,7 @@ .avatar-image { position: relative; overflow: hidden; - border-radius: 10%; + display: block; } .avatar-text{ diff --git a/im-ui/src/components/group/AddGroupMember.vue b/im-ui/src/components/group/AddGroupMember.vue index d60be28..d41f815 100644 --- a/im-ui/src/components/group/AddGroupMember.vue +++ b/im-ui/src/components/group/AddGroupMember.vue @@ -136,33 +136,7 @@ border-radius: 5px; overflow: hidden; - .el-checkbox { - display: flex; - align-items: center; - - //修改选中框的大小 - .el-checkbox__inner { - width: 20px; - height: 20px; - - //修改选中框中的对勾的大小和位置 - &::after { - height: 12px; - left: 7px; - } - } - - //修改点击文字颜色不变 - .el-checkbox__input.is-checked+.el-checkbox__label { - color: #333333; - } - - .el-checkbox__label { - line-height: 20px; - padding-left: 8px; - } - } - + .agm-friend-checkbox { margin-right: 20px; } diff --git a/im-ui/src/components/group/GroupMemberItem.vue b/im-ui/src/components/group/GroupMemberItem.vue new file mode 100644 index 0000000..1af7943 --- /dev/null +++ b/im-ui/src/components/group/GroupMemberItem.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/im-ui/src/components/group/GroupMemberSelector.vue b/im-ui/src/components/group/GroupMemberSelector.vue new file mode 100644 index 0000000..138d79b --- /dev/null +++ b/im-ui/src/components/group/GroupMemberSelector.vue @@ -0,0 +1,161 @@ + + + + + \ No newline at end of file diff --git a/im-ui/src/components/rtc/RtcGroupJoin.vue b/im-ui/src/components/rtc/RtcGroupJoin.vue new file mode 100644 index 0000000..7d09b10 --- /dev/null +++ b/im-ui/src/components/rtc/RtcGroupJoin.vue @@ -0,0 +1,116 @@ + + + + + \ No newline at end of file diff --git a/im-ui/src/components/rtc/RtcGroupVideo.vue b/im-ui/src/components/rtc/RtcGroupVideo.vue new file mode 100644 index 0000000..ef83b3c --- /dev/null +++ b/im-ui/src/components/rtc/RtcGroupVideo.vue @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/im-ui/src/components/chat/ChatVideoAcceptor.vue b/im-ui/src/components/rtc/RtcPrivateAcceptor.vue similarity index 98% rename from im-ui/src/components/chat/ChatVideoAcceptor.vue rename to im-ui/src/components/rtc/RtcPrivateAcceptor.vue index 6216e23..95d5d52 100644 --- a/im-ui/src/components/chat/ChatVideoAcceptor.vue +++ b/im-ui/src/components/rtc/RtcPrivateAcceptor.vue @@ -1,5 +1,5 @@