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 @@
+
+
+
+
+ 选择成员
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ m.aliasName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
-
+
语音通话
+
+
+ 语音通话
+
@@ -101,8 +104,12 @@
+
+
+
@@ -130,11 +137,11 @@
methods: {
onRecorderInput() {
this.showRecord = true;
- this.switchChatTabBox('none',true);
+ this.switchChatTabBox('none', true);
},
onKeyboardInput() {
this.showRecord = false;
- this.switchChatTabBox('none',false);
+ this.switchChatTabBox('none', false);
},
onSendRecord(data) {
let msgInfo = {
@@ -161,7 +168,7 @@
// 滚动到底部
this.scrollToBottom();
this.isReceipt = false;
-
+
})
},
onRtCall(msgInfo) {
@@ -174,13 +181,38 @@
onVideoCall() {
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
- url: `/pages/chat/chat-video?mode=video&friend=${friendInfo}&isHost=true`
+ url: `/pages/chat/chat-private-video?mode=video&friend=${friendInfo}&isHost=true`
})
},
onVoiceCall() {
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
- url: `/pages/chat/chat-video?mode=voice&friend=${friendInfo}&isHost=true`
+ url: `/pages/chat/chat-private-video?mode=voice&friend=${friendInfo}&isHost=true`
+ })
+ },
+ onGroupVideo() {
+ let ids = [this.mine.id];
+ this.$refs.selBox.init(ids, ids);
+ this.$refs.selBox.open();
+ },
+ onSelectMember(ids) {
+ let users = [];
+ ids.forEach(id => {
+ let m = this.groupMembers.find(m => m.userId == id);
+ // 只取部分字段,压缩url长度
+ users.push({
+ id: m.userId,
+ nickName: m.aliasName,
+ headImage: m.headImage,
+ isCamera: false
+ })
+ })
+ const groupId = this.group.id;
+ const inviterId = this.mine.id;
+ const userInfos = encodeURIComponent(JSON.stringify(users));
+ uni.navigateTo({
+ url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=true
+ &inviterId=${inviterId}&userInfos=${userInfos}`
})
},
moveChatToTop() {
@@ -302,13 +334,13 @@
});
},
- onShowEmoChatTab(){
+ onShowEmoChatTab() {
this.showRecord = false;
- this.switchChatTabBox('emo',true)
+ this.switchChatTabBox('emo', true)
},
- onShowToolsChatTab(){
+ onShowToolsChatTab() {
this.showRecord = false;
- this.switchChatTabBox('tools',true)
+ this.switchChatTabBox('tools', true)
},
switchChatTabBox(chatTabBox, hideKeyBoard) {
this.chatTabBox = chatTabBox;
@@ -496,11 +528,11 @@
});
},
onScrollToTop() {
- if(this.showMinIdx==0){
+ if (this.showMinIdx == 0) {
console.log("消息已滚动到顶部")
return;
}
-
+
// #ifndef H5
// 防止滚动条定格在顶部,不能一直往上滚
this.scrollToMsgIdx(this.showMinIdx);
@@ -541,7 +573,8 @@
});
},
readedMessage() {
- if(this.unreadCount == 0){
+ console.log("readedMessage")
+ if (this.unreadCount == 0) {
return;
}
let url = ""
@@ -718,7 +751,6 @@
}
}
-
.chat-msg {
flex: 1;
padding: 0;
diff --git a/im-uniapp/pages/chat/chat-group-video.vue b/im-uniapp/pages/chat/chat-group-video.vue
new file mode 100644
index 0000000..6056d42
--- /dev/null
+++ b/im-uniapp/pages/chat/chat-group-video.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/im-uniapp/pages/chat/chat-video.vue b/im-uniapp/pages/chat/chat-private-video.vue
similarity index 93%
rename from im-uniapp/pages/chat/chat-video.vue
rename to im-uniapp/pages/chat/chat-private-video.vue
index ab8b2e7..801d17b 100644
--- a/im-uniapp/pages/chat/chat-video.vue
+++ b/im-uniapp/pages/chat/chat-private-video.vue
@@ -1,6 +1,6 @@
-
-
+
+
@@ -85,7 +85,7 @@
this.sendMessageToWebView("NAV_BACK",{})
},
onLoad(options) {
- uni.$on('WS_RTC', msg => {
+ uni.$on('WS_RTC_PRIVATE', msg => {
// 推送给web-view处理
this.sendMessageToWebView("RTC_MESSAGE", msg);
})
@@ -104,7 +104,7 @@
this.initUrl();
},
onUnload() {
- uni.$off('WS_RTC')
+ uni.$off('WS_RTC_PRIVATE')
}
}
diff --git a/im-uniapp/ssl/cert.crt b/im-uniapp/ssl/cert.crt
new file mode 100644
index 0000000..12f90bf
--- /dev/null
+++ b/im-uniapp/ssl/cert.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIjCCAgoCCQCw2aUcFWVX4jANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJj
+bjEMMAoGA1UECAwDZ3oIMQswCQYDVQQHDAJnejEcMBoGA1UECgwTRGVmYXVsdCBD
+b21wYW55IEx0ZDELMAkGA1UEAwwCYngwHhcNMjQwNDI4MTQzNTIzWhcNMzQwNDI2
+MTQzNTIzWjBTMQswCQYDVQQGEwJjbjEMMAoGA1UECAwDZ3oIMQswCQYDVQQHDAJn
+ejEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDELMAkGA1UEAwwCYngwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqZXvAXwH0xA5TSposclmZXxox
+pfT5F0eSOaxRE2NFfUbHoCrCHYV8pPAIy9S6vbG5Bbh4eprv4smH4lHWfa+81nI8
+sKizmJ3jdFSzCRrIHzbdlQsY0Vg+VasyoWjyjVDJeDzz/G/vQUeb19+kXlHVDETt
+J7sZEqNyDxewsiDBUf2f+fvsgtIWakuD7CGe/P9e6gHz0D++GezOUKgUtL3eUkCa
+pI8+ecoAG1ud/3MtRvGyq9FwwsQwsscu1YVmt7fRhuGbcM3/bog1VXe/to/msKUC
+gCZjWS82D9sw0ikEAn7jagKJu1ezybmN9/JljhpC8UgZnqPT01LzfFvDECN7AgMB
+AAEwDQYJKoZIhvcNAQELBQADggEBAMnjP0ANnPSTbwSCufVXwJgX5tWcSGjezFAY
+Du+rbdUipn2O4/NCkTTPpDbrDKRET2zDUrxJOXu/UZBS8lreowtUQCk8lX7kH5oj
+72lmcOFgWUyk8ULTPzrl0sdaQ8mhsvf+vHO9Ww/+RqQlzlr+eMuamMm1wDrbczWK
+z1tq2QQuIhxf1pIznHag5eWui6Z0RIRQaozbXWU6VuSf703CNixxdZsdNWHpdiJW
+vj8LewFaSmGp6HzwMX6/Kx/kocqpeeCZ6CharePv2C5bC5Kd5KVFCHnp5xbcZKUq
+8Q7CSH5WKV3QkoFKGPz1qh17qeryxgoqLQXLapptNKOS76QBivM=
+-----END CERTIFICATE-----
diff --git a/im-uniapp/ssl/cert.key b/im-uniapp/ssl/cert.key
new file mode 100644
index 0000000..ec189b5
--- /dev/null
+++ b/im-uniapp/ssl/cert.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA6mV7wF8B9MQOU0qaLHJZmV8aMaX0+RdHkjmsURNjRX1Gx6Aq
+wh2FfKTwCMvUur2xuQW4eHqa7+LJh+JR1n2vvNZyPLCos5id43RUswkayB823ZUL
+GNFYPlWrMqFo8o1QyXg88/xv70FHm9ffpF5R1QxE7Se7GRKjcg8XsLIgwVH9n/n7
+7ILSFmpLg+whnvz/XuoB89A/vhnszlCoFLS93lJAmqSPPnnKABtbnf9zLUbxsqvR
+cMLEMLLHLtWFZre30Ybhm3DN/26INVV3v7aP5rClAoAmY1kvNg/bMNIpBAJ+42oC
+ibtXs8m5jffyZY4aQvFIGZ6j09NS83xbwxAjewIDAQABAoIBAQC8uyPunFE15Rrn
+w9zpxtUQIjw0F71tR2pQefGegm7fR+TS3csv65dM6n1/h6ftCMpuAQYstAPG/aNp
+rzhX7XGwKjdnWJMtsMgImeWNFtqiokeKGPULcZyM7GvhY4feLR0Ma60gg3UZf0WK
+XUJs1aksUym4jtIeeRxzvWVE19h57uEG1fJM3Rf0OFhb1iUYVjtW4iW+RTtc1mpb
+hoda+8P3Tua33WuhSYtFusDx35ZM2WDgYlgeMxm94JUFUUOIhiggasYNsu1YmQl4
+AqhRncGn6p/gZVQsjkeCtmTIyD+igqulI/OkqI3DmFCzFSoSXLFE7HZ4pL500Vxn
+aOvOYRCZAoGBAP1Aopr0scpLzt7Lei+dbLc3wxziCyeNtVDvswFS93Lx1bnSJw4m
+0PAvQGoOdeiPI1vmsdDJdV5R0Vmbybyz7JPiTyUyti4p909s5AtpPqdLptjuO2ge
+2b1YD/HnubL0omlejKu5fKg3zaPqhr/Z8f6WfYSsm1dV10arSBj4JdvVAoGBAOzw
+epHXXnAfaC/cEOUOOVe5o/MNxIYYfJkG6VtmB3v+oY0/C+SyUbkY3Qu5yjCDBYhP
+rLVr1+TiLE3Sqj+ndRvICy8T6Iv+hA2ijvJiNVAjtqkwM5YOMJdFYI6fem1N+Hkv
+ipOQUWFmwUBAKQm4BSGtNdbL89KTTV1tMubH4joPAoGBANRGmkWSd3gepO8A1ZEV
+vmuw1N3f5wOnd2S5Fm00su9pIAGa0lu9U4MPyEldh52AZV4B9+gPBU8i+3zF5YpD
+sjifCEIgyK3XRVIQ7vFVrUujUN4iii8TNOXN68eTuYb0ITJ7KyRB3OhPphIQYhRr
+xbjlQZ6045yH+mNk7JDpZyplAoGAVF4sxtGRZwtH5gLOYUF3Wa1Ym6tDVxxRAYxc
+e5cRAy3gCJNygLSeNPKNgydcv3ln9umn7dHAxldivzNMO+483O+WS+Ui4PZ3vwMr
+M1OU+Dw/Rm9LbxsOYk7p2t8ekN06pKwxA+pXj/8uwNoXwsYrzZoHmbx1zX12BtZj
+UZnLDDECgYBpvFK+cntSzE+qpsvxYnosSswcJvmGoOzBCE2aWebwXp0QOwjg/Zh/
+VR5Mc8L8xHpcpUJZXaTmyeouwc2XPfBvvbWlGZFh7zBn2dKCNxT62fPXKFX2rBgE
+k4f033ToXD6Lv0JT94JfjS0GB+zzHjfcS/K8Lr3d3lUmkiI1LFD5GA==
+-----END RSA PRIVATE KEY-----
diff --git a/im-uniapp/vite.config.js b/im-uniapp/vite.config.js
index 0c46ed6..c866fc9 100644
--- a/im-uniapp/vite.config.js
+++ b/im-uniapp/vite.config.js
@@ -1,6 +1,7 @@
import { defineConfig } from "vite"
import uni from "@dcloudio/vite-plugin-uni";
-
+const path = require('path')
+const fs = require('fs')
export default defineConfig({
plugins: [
uni()
@@ -14,6 +15,11 @@ export default defineConfig({
changeOrigin: true
},
- }
+ },
+ // 音视频功能需要ssl证书,如需调试请打开注释
+ // https: {
+ // cert: fs.readFileSync(path.join(__dirname, 'ssl/cert.crt')),
+ // key: fs.readFileSync(path.join(__dirname, 'ssl/cert.key'))
+ // }
}
})
\ No newline at end of file