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