38 changed files with 1696 additions and 131 deletions
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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<>(); |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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> |
||||
@ -1,13 +1 @@ |
|||||
<!DOCTYPE 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> |
||||
<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> |
|
||||
@ -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) { |
||||
|
// 如果webview还没初始化好,则延迟100ms再推送 |
||||
|
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 |
||||
|
// APP的webview |
||||
|
this.wv = this.$scope.$getAppWebview().children()[0] |
||||
|
// #endif |
||||
|
// #ifdef H5 |
||||
|
// H5的webview就是iframe |
||||
|
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> |
||||
@ -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----- |
||||
@ -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----- |
||||
Loading…
Reference in new issue