Browse Source

feat:多人视频-开发中

master
xsx 2 years ago
parent
commit
3453788448
  1. 6
      im-platform/pom.xml
  2. 37
      im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java
  3. 82
      im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java
  4. 42
      im-platform/src/main/java/com/bx/implatform/config/RedissonConfig.java
  5. 18
      im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java
  6. 111
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcGroupController.java
  7. 22
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java
  8. 31
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java
  9. 32
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java
  10. 26
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java
  11. 25
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java
  12. 29
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java
  13. 23
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java
  14. 31
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java
  15. 29
      im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java
  16. 41
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  17. 27
      im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java
  18. 8
      im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java
  19. 79
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcGroupService.java
  20. 3
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcPrivateService.java
  21. 44
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java
  22. 445
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcGroupServiceImpl.java
  23. 36
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java
  24. 33
      im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java
  25. 2
      im-platform/src/main/java/com/bx/implatform/session/WebrtcPrivateSession.java
  26. 26
      im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java
  27. 23
      im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java
  28. 41
      im-uniapp/App.vue
  29. 14
      im-uniapp/common/enums.js
  30. 162
      im-uniapp/components/group-member-selector/group-member-selector.vue
  31. 14
      im-uniapp/hybrid/html/index.html
  32. 4
      im-uniapp/pages.json
  33. 74
      im-uniapp/pages/chat/chat-box.vue
  34. 143
      im-uniapp/pages/chat/chat-group-video.vue
  35. 8
      im-uniapp/pages/chat/chat-private-video.vue
  36. 19
      im-uniapp/ssl/cert.crt
  37. 27
      im-uniapp/ssl/cert.key
  38. 10
      im-uniapp/vite.config.js

6
im-platform/pom.xml

@ -110,7 +110,11 @@
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.3</version>
</dependency>
</dependencies>
<build>

37
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;
}

82
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);
}
}

42
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);
}
}

18
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";
}

111
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();
}
}

22
im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java → 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<List<ICEServer>> iceservers() {
return ResultUtils.success(webrtcService.getIceServers());
return ResultUtils.success(webrtcPrivateService.getIceServers());
}
}

31
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;
}

32
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;
}

26
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;
}

25
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;
}

29
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<WebrtcUserInfo> userInfos;
}

23
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;
}

31
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;
}

29
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<WebrtcUserInfo> userInfos;
}

41
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;

27
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;
}

8
im-platform/src/main/java/com/bx/implatform/service/IGroupMemberService.java

@ -73,4 +73,12 @@ public interface IGroupMemberService extends IService<GroupMember> {
* @param userId 用户id
*/
void removeByGroupAndUserId(Long groupId, Long userId);
/**
* 用户用户是否在群中
*
* @param groupId 群聊id
* @param userIds 用户id
*/
Boolean isInGroup(Long groupId,List<Long> userIds);
}

79
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);
}

3
im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java → 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);

44
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<GroupMemberMapper, Group
return super.saveOrUpdateBatch(members);
}
@Override
public GroupMember findByGroupAndUserId(Long groupId, Long userId) {
public GroupMember findByGroupAndUserId(Long groupId, Long userId) {
QueryWrapper<GroupMember> 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<GroupMember> findByUserId(Long userId) {
LambdaQueryWrapper<GroupMember> 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<GroupMember> findQuitInMonth(Long userId) {
Date monthTime = DateTimeUtils.addMonths(new Date(),-1);
Date monthTime = DateTimeUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMember> 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<GroupMemberMapper, Group
@Override
public List<Long> findUserIdsByGroupId(Long groupId) {
LambdaQueryWrapper<GroupMember> 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<GroupMember> members = this.list(memberWrapper);
return members.stream().map(GroupMember::getUserId).collect(Collectors.toList());
}
@ -83,9 +79,8 @@ public class GroupMemberServiceImpl extends ServiceImpl<GroupMemberMapper, Group
@Override
public void removeByGroupId(Long groupId) {
LambdaUpdateWrapper<GroupMember> 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<GroupMemberMapper, Group
@Override
public void removeByGroupAndUserId(Long groupId, Long userId) {
LambdaUpdateWrapper<GroupMember> 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<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return true;
}
LambdaQueryWrapper<GroupMember> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getQuit, false)
.in(GroupMember::getUserId, userIds);
return userIds.size() == this.count(wrapper);
}
}

445
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<String, Object> 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<Long> 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<WebrtcUserInfo> userInfos = new LinkedList<>();
List<Long> 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<Long> 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<Long> 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<WebrtcUserInfo> userInfos =
webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId()))
.collect(Collectors.toList());
webrtcSession.setUserInfos(userInfos);
saveWebrtcSession(groupId, webrtcSession);
// 广播消息给的所有用户
List<Long> 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<WebrtcUserInfo> 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<Long> 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<Long> 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<WebrtcUserInfo> userInfos = webrtcSession.getUserInfos();
// 原用户id
List<Long> userIds = getRecvIds(userInfos);
// 离线用户id
List<Long> offlineUserIds = new LinkedList<>();
// 新加入的用户
List<WebrtcUserInfo> 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<Long> 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<Long> 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<IMUserInfo> inChatUsers =
webrtcSession.getInChatUsers().stream().filter(user -> !user.getId().equals(userSession.getUserId()))
.collect(Collectors.toList());
List<WebrtcUserInfo> userInfos =
webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId()))
.collect(Collectors.toList());
// 如果群聊中没有人已经接受了通话,则直接取消整个通话
if (inChatUsers.isEmpty() || userInfos.isEmpty()) {
// 移除rtc session
String key = buildWebrtcSessionKey(groupId);
redisTemplate.delete(key);
// 广播给还在呼叫中的用户,取消通话
List<Long> 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<Long> 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<Long> 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<Long> getRecvIds(List<WebrtcUserInfo> userInfos) {
UserSession userSession = SessionContext.getSession();
return userInfos.stream().map(WebrtcUserInfo::getId).filter(id -> !id.equals(userSession.getUserId()))
.collect(Collectors.toList());
}
private Boolean isInchat(WebrtcGroupSession webrtcSession, Long userId) {
return webrtcSession.getInChatUsers().stream().anyMatch(user -> user.getId().equals(userId));
}
private Boolean isExist(WebrtcGroupSession webrtcSession, Long userId) {
return webrtcSession.getUserInfos().stream().anyMatch(user -> user.getId().equals(userId));
}
private void sendMessage1(MessageType messageType, Long groupId, List<Long> 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<GroupMessageVO> 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<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal()));
sendMessage.setRecvIds(Arrays.asList(receiver.getId()));
sendMessage.setRecvTerminals(Arrays.asList(receiver.getTerminal()));
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
sendMessage.setData(messageInfo);
imClient.sendGroupMessage(sendMessage);
}
}

36
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java → 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<String, Object> 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();
}

33
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<WebrtcUserInfo> userInfos;
/**
* 已经进入通话的用户列表
*/
private List<IMUserInfo> inChatUsers = new LinkedList<>();
}

2
im-platform/src/main/java/com/bx/implatform/session/WebrtcSession.java → 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
*/

26
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;
}

23
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<Long> userIds;
@ApiModelProperty(value = "失败原因")
private String reason;
}

41
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,

14
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 = {

162
im-uniapp/components/group-member-selector/group-member-selector.vue

@ -0,0 +1,162 @@
<template>
<uni-popup ref="popup" type="bottom">
<view class="chat-group-member-choose">
<view class="top-bar">
<view class="top-tip">选择成员</view>
<button class="top-btn" type="warn" size="mini" @click="onClean()">清空 </button>
<button class="top--btn" type="primary" size="mini" @click="onOk()">确定({{checkedIds.length}})
</button>
</view>
<scroll-view v-show="checkedIds.length>0" scroll-x="true" scroll-left="120">
<view class="checked-users">
<view v-for="m in members" v-show="m.checked" class="user-item">
<head-image :name="m.aliasName" :url="m.headImage" :size="60"></head-image>
</view>
</view>
</scroll-view>
<view class="search-bar">
<uni-search-bar v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar>
</view>
<view class="member-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="m in members" v-show="!m.quit && m.aliasName.startsWith(searchText)" :key="m.userId">
<view class="member-item" @click="onSwitchChecked(m)">
<head-image :name="m.aliasName" :online="m.online" :url="m.headImage"
:size="90"></head-image>
<view class="member-name">{{ m.aliasName}}</view>
<view class="member-checked">
<radio :checked="m.checked" :disabled="m.locked" @click.stop="onSwitchChecked(m)" />
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: "chat-group-member-choose",
props: {
members: {
type: Array
}
},
data() {
return {
searchText: "",
};
},
methods: {
init(checkedIds, lockedIds) {
this.members.forEach((m) => {
m.checked = checkedIds.indexOf(m.userId) >= 0;
m.locked = lockedIds.indexOf(m.userId) >= 0;
});
},
open() {
this.$refs.popup.open();
},
onSwitchChecked(m) {
if(!m.locked){
m.checked = !m.checked;
}
},
onClean() {
this.members.forEach((m) => {
if (!m.locked) {
m.checked = false;
}
})
},
onOk() {
this.$refs.popup.close();
this.$emit("complete", this.checkedIds)
},
isChecked(m) {
return this.checkedIds.indexOf(m.userId) >= 0;
}
},
computed: {
checkedIds() {
let ids = [];
this.members.forEach((m) => {
if (m.checked) {
ids.push(m.userId);
}
})
return ids;
}
}
}
</script>
<style lang="scss" scoped>
.chat-group-member-choose {
position: relative;
border: #dddddd solid 1rpx;
display: flex;
flex-direction: column;
background-color: white;
padding: 10rpx;
border-radius: 15rpx;
.top-bar {
display: flex;
align-items: center;
height: 70rpx;
padding: 10rpx;
.top-tip {
flex: 1;
}
.top-btn {
margin-left: 10rpx;
}
}
.checked-users {
display: flex;
align-items: center;
height: 90rpx;
.user-item {
padding: 3rpx;
}
}
.member-items {
position: relative;
flex: 1;
overflow: hidden;
.member-item {
height: 120rpx;
display: flex;
position: relative;
padding: 0 30rpx;
align-items: center;
background-color: white;
white-space: nowrap;
.member-name {
flex: 1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
.scroll-bar {
height: 800rpx;
}
}
}
</style>

14
im-uniapp/hybrid/html/index.html

@ -1,13 +1 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="favicon.ico">
<title>视频通话</title>
</head>
<body>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话为付费功能,有需要请联系作者...</div>
</body>
</html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>web</title><link href="css/app.51459a25.css" rel="preload" as="style"><link href="js/app.8c902b8a.js" rel="preload" as="script"><link href="js/chunk-vendors.f7e74caf.js" rel="preload" as="script"><link href="css/app.51459a25.css" rel="stylesheet"></head><body style="margin: 0;"><div id="app"></div><script src="static/uni/uni.webview.1.5.5.js"></script><script src="js/chunk-vendors.f7e74caf.js"></script><script src="js/app.8c902b8a.js"></script></body></html>

4
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"
}, {

74
im-uniapp/pages/chat/chat-box.vue

@ -6,14 +6,13 @@
<uni-icons class="btn-side right" type="more-filled" size="30" @click="onShowMore()"></uni-icons>
</view>
<view class="chat-msg" @click="switchChatTabBox('none',true)">
<scroll-view class="scroll-box" scroll-y="true"
upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-'+scrollMsgIdx">
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx&&!msgInfo.delete" :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx"
:msgInfo="msgInfo" :groupMembers="groupMembers">
<chat-message-item v-if="idx>=showMinIdx&&!msgInfo.delete" :headImage="headImage(msgInfo)"
@call="onRtCall(msgInfo)" :showName="showName(msgInfo)" @recall="onRecallMessage"
@delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile"
:id="'chat-item-'+idx" :msgInfo="msgInfo" :groupMembers="groupMembers">
</chat-message-item>
</view>
</scroll-view>
@ -31,7 +30,7 @@
<view class="send-bar">
<view v-if="!showRecord" class="iconfont icon-voice-circle" @click="onRecorderInput()"></view>
<view v-else class="iconfont icon-keyboard" @click="onKeyboardInput()"></view>
<chat-record v-if="showRecord" class="chat-record" @send="onSendRecord" ></chat-record>
<chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record>
<view v-else class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt?'[回执消息]':''" :adjust-position="false" @confirm="sendTextMessage()"
@ -90,6 +89,10 @@
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">语音通话</view>
</view>
<view v-if="chat.type == 'GROUP'" class="chat-tools-item" @click="onGroupVideo()">
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">语音通话</view>
</view>
<!-- #endif -->
</view>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
@ -101,8 +104,12 @@
</scroll-view>
<view v-if="showKeyBoard"></view>
</view>
<!-- @用户时选择成员 -->
<chat-at-box ref="atBox" :ownerId="group.ownerId" :members="groupMembers"
@complete="onAtComplete"></chat-at-box>
<!-- 群语音通话时选择成员 -->
<group-member-selector ref="selBox" :members="groupMembers"
@complete="onSelectMember"></group-member-selector>
</view>
</template>
@ -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;

143
im-uniapp/pages/chat/chat-group-video.vue

@ -0,0 +1,143 @@
<template>
<view class="page chat-group-video">
<view>
<web-view id="chat-video-wv" @message="onMessage" :src="url"></web-view>
</view>
</view>
</template>
<script>
import UNI_APP from '@/.env.js'
export default {
data() {
return {
url: "",
wv: '',
isHost: false,
groupId: null,
inviterId: null,
userInfos: []
}
},
methods: {
onMessage(e) {
this.onWebviewMessage(e.detail.data[0]);
},
onInsertMessage(msgInfo) {
},
onWebviewMessage(event) {
console.log("来自webview的消息:" + JSON.stringify(event))
switch (event.key) {
case "WV_READY":
this.initWebView();
break;
case "WV_CLOSE":
uni.navigateBack();
break;
case "INSERT_MESSAGE":
this.onInsertMessage(event.data);
break;
}
},
sendMessageToWebView(key, message) {
// webview100ms
if (!this.wv) {
setTimeout(() => this.sendMessageToWebView(key, message), 100)
return;
}
let event = {
key: key,
data: message
}
// #ifdef APP-PLUS
this.wv.evalJS(`onEvent('${encodeURIComponent(JSON.stringify(event))}')`)
// #endif
// #ifdef H5
this.wv.postMessage(event, '*');
// #endif
},
initWebView() {
// #ifdef APP-PLUS
// APPwebview
this.wv = this.$scope.$getAppWebview().children()[0]
// #endif
// #ifdef H5
// H5webviewiframe
this.wv = document.getElementById('chat-video-wv').contentWindow
// #endif
},
initUrl() {
this.url = "/hybrid/html/rtc-group/index.html?";
this.url += "baseUrl=" + UNI_APP.BASE_URL;
this.url += "&groupId=" + this.groupId;
this.url += "&userId=" + this.$store.state.userStore.userInfo.id;
this.url += "&inviterId=" + this.inviterId;
this.url += "&isHost=" + this.isHost;
this.url += "&loginInfo=" + JSON.stringify(uni.getStorageSync("loginInfo"));
this.url += "&userInfos=" + JSON.stringify(this.userInfos);
console.log(this.url)
},
},
onBackPress() {
this.sendMessageToWebView("NAV_BACK", {})
},
onLoad(options) {
uni.$on('WS_RTC_GROUP', msg => {
// web-view
this.sendMessageToWebView("RTC_MESSAGE", msg);
})
// #ifdef H5
window.onmessage = (e) => {
this.onWebviewMessage(e.data.data.arg);
}
// #endif
//
this.isHost = JSON.parse(options.isHost);
//
this.inviterId = options.inviterId;
//
this.groupId = options.groupId;
//
this.userInfos = JSON.parse(decodeURIComponent(options.userInfos));
// url
this.initUrl();
},
onUnload() {
uni.$off('WS_RTC_GROUP')
}
}
</script>
<style lang="scss" scoped>
.chat-group-video {
.header {
display: flex;
justify-content: center;
align-items: center;
height: 60rpx;
padding: 5px;
background-color: white;
line-height: 50px;
font-size: 40rpx;
font-weight: 600;
border: #dddddd solid 1px;
.btn-side {
position: absolute;
line-height: 60rpx;
font-size: 28rpx;
cursor: pointer;
&.left {
left: 30rpx;
}
&.right {
right: 30rpx;
}
}
}
}
</style>

8
im-uniapp/pages/chat/chat-video.vue → im-uniapp/pages/chat/chat-private-video.vue

@ -1,6 +1,6 @@
<template>
<view class="page chat-video">
<web-view id="chat-video-wv" @message="onMessage" :src="url"></web-view>
<view class="page chat-private-video">
<web-view id="chat-video-wv" @message="onMessage" :src="url"></web-view>
</view>
</template>
@ -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')
}
}
</script>

19
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-----

27
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-----

10
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'))
// }
}
})
Loading…
Cancel
Save