committed by
Gitee
73 changed files with 4003 additions and 1024 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,35 @@ |
|||||
|
package com.bx.implatform.controller; |
||||
|
|
||||
|
import com.bx.implatform.config.WebrtcConfig; |
||||
|
import com.bx.implatform.dto.PrivateMessageDTO; |
||||
|
import com.bx.implatform.result.Result; |
||||
|
import com.bx.implatform.result.ResultUtils; |
||||
|
import com.bx.implatform.vo.SystemConfigVO; |
||||
|
import io.swagger.annotations.Api; |
||||
|
import io.swagger.annotations.ApiOperation; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* @author: blue |
||||
|
* @date: 2024-06-10 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Api(tags = "系统相关") |
||||
|
@RestController |
||||
|
@RequestMapping("/system") |
||||
|
@RequiredArgsConstructor |
||||
|
public class SystemController { |
||||
|
|
||||
|
private final WebrtcConfig webrtcConfig; |
||||
|
|
||||
|
@GetMapping("/config") |
||||
|
@ApiOperation(value = "加载系统配置", notes = "加载系统配置") |
||||
|
public Result<SystemConfigVO> loadConfig() { |
||||
|
return ResultUtils.success(new SystemConfigVO(webrtcConfig)); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
package com.bx.implatform.controller; |
||||
|
|
||||
|
import com.bx.implatform.config.WebrtcConfig; |
||||
|
import com.bx.implatform.dto.*; |
||||
|
import com.bx.implatform.result.Result; |
||||
|
import com.bx.implatform.result.ResultUtils; |
||||
|
import com.bx.implatform.service.IWebrtcGroupService; |
||||
|
import com.bx.implatform.vo.WebrtcGroupInfoVO; |
||||
|
import io.swagger.annotations.Api; |
||||
|
import io.swagger.annotations.ApiOperation; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
import javax.validation.Valid; |
||||
|
|
||||
|
/** |
||||
|
* @author: 谢绍许 |
||||
|
* @date: 2024-06-01 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Api(tags = "webrtc视频多人通话") |
||||
|
@RestController |
||||
|
@RequestMapping("/webrtc/group") |
||||
|
@RequiredArgsConstructor |
||||
|
public class WebrtcGroupController { |
||||
|
|
||||
|
private final IWebrtcGroupService webrtcGroupService; |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "发起群视频通话") |
||||
|
@PostMapping("/setup") |
||||
|
public Result setup(@Valid @RequestBody WebrtcGroupSetupDTO dto) { |
||||
|
webrtcGroupService.setup(dto); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "接受通话") |
||||
|
@PostMapping("/accept") |
||||
|
public Result accept(@RequestParam Long groupId) { |
||||
|
webrtcGroupService.accept(groupId); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "拒绝通话") |
||||
|
@PostMapping("/reject") |
||||
|
public Result reject(@RequestParam Long groupId) { |
||||
|
webrtcGroupService.reject(groupId); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "通话失败") |
||||
|
@PostMapping("/failed") |
||||
|
public Result failed(@Valid @RequestBody WebrtcGroupFailedDTO dto) { |
||||
|
webrtcGroupService.failed(dto); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "进入视频通话") |
||||
|
@PostMapping("/join") |
||||
|
public Result join(@RequestParam Long groupId) { |
||||
|
webrtcGroupService.join(groupId); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "取消通话") |
||||
|
@PostMapping("/cancel") |
||||
|
public Result cancel(@RequestParam Long groupId) { |
||||
|
webrtcGroupService.cancel(groupId); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "离开视频通话") |
||||
|
@PostMapping("/quit") |
||||
|
public Result quit(@RequestParam Long groupId) { |
||||
|
webrtcGroupService.quit(groupId); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "推送offer信息") |
||||
|
@PostMapping("/offer") |
||||
|
public Result offer(@Valid @RequestBody WebrtcGroupOfferDTO dto) { |
||||
|
webrtcGroupService.offer(dto); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "推送answer信息") |
||||
|
@PostMapping("/answer") |
||||
|
public Result answer(@Valid @RequestBody WebrtcGroupAnswerDTO dto) { |
||||
|
webrtcGroupService.answer(dto); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "邀请用户进入视频通话") |
||||
|
@PostMapping("/invite") |
||||
|
public Result invite(@Valid @RequestBody WebrtcGroupInviteDTO dto) { |
||||
|
webrtcGroupService.invite(dto); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "同步candidate") |
||||
|
@PostMapping("/candidate") |
||||
|
public Result candidate(@Valid @RequestBody WebrtcGroupCandidateDTO dto) { |
||||
|
webrtcGroupService.candidate(dto); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "设备操作") |
||||
|
@PostMapping("/device") |
||||
|
public Result device(@Valid @RequestBody WebrtcGroupDeviceDTO dto) { |
||||
|
webrtcGroupService.device(dto); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "GET", value = "获取通话信息") |
||||
|
@GetMapping("/info") |
||||
|
public Result<WebrtcGroupInfoVO> info(@RequestParam Long groupId) { |
||||
|
return ResultUtils.success(webrtcGroupService.info(groupId)); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(httpMethod = "POST", value = "获取通话信息") |
||||
|
@PostMapping("/heartbeat") |
||||
|
public Result heartbeat(@RequestParam Long groupId) { |
||||
|
webrtcGroupService.heartbeat(groupId); |
||||
|
return ResultUtils.success(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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,29 @@ |
|||||
|
package com.bx.implatform.dto; |
||||
|
|
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import javax.validation.constraints.NotEmpty; |
||||
|
import javax.validation.constraints.NotNull; |
||||
|
|
||||
|
/** |
||||
|
* @author: 谢绍许 |
||||
|
* @date: 2024-06-01 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Data |
||||
|
@ApiModel("用户设备操作DTO") |
||||
|
public class WebrtcGroupDeviceDTO { |
||||
|
|
||||
|
@NotNull(message = "群聊id不可为空") |
||||
|
@ApiModelProperty(value = "群聊id") |
||||
|
private Long groupId; |
||||
|
|
||||
|
@ApiModelProperty(value = "是否开启摄像头") |
||||
|
private Boolean isCamera; |
||||
|
|
||||
|
@ApiModelProperty(value = "是否开启麦克风") |
||||
|
private Boolean isMicroPhone; |
||||
|
|
||||
|
} |
||||
@ -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.config.WebrtcConfig; |
||||
|
import com.bx.implatform.dto.*; |
||||
|
import com.bx.implatform.vo.WebrtcGroupInfoVO; |
||||
|
|
||||
|
public interface IWebrtcGroupService { |
||||
|
|
||||
|
/** |
||||
|
* 发起通话 |
||||
|
*/ |
||||
|
void setup(WebrtcGroupSetupDTO dto); |
||||
|
|
||||
|
/** |
||||
|
* 接受通话 |
||||
|
*/ |
||||
|
void accept(Long groupId); |
||||
|
|
||||
|
/** |
||||
|
* 拒绝通话 |
||||
|
*/ |
||||
|
void reject(Long groupId); |
||||
|
|
||||
|
/** |
||||
|
* 通话失败,如设备不支持、用户忙等(此接口为系统自动调用,无需用户操作,所以不抛异常) |
||||
|
*/ |
||||
|
void failed(WebrtcGroupFailedDTO dto); |
||||
|
|
||||
|
/** |
||||
|
* 主动加入通话 |
||||
|
*/ |
||||
|
void join(Long groupId); |
||||
|
|
||||
|
/** |
||||
|
* 通话过程中继续邀请用户加入通话 |
||||
|
*/ |
||||
|
void invite(WebrtcGroupInviteDTO dto); |
||||
|
|
||||
|
/** |
||||
|
* 取消通话,仅通话发起人可以取消通话 |
||||
|
*/ |
||||
|
void cancel(Long groupId); |
||||
|
|
||||
|
/** |
||||
|
* 退出通话,如果当前没有人在通话中,将取消整个通话 |
||||
|
*/ |
||||
|
void quit(Long groupId); |
||||
|
|
||||
|
/** |
||||
|
* 推送offer信息给对方 |
||||
|
*/ |
||||
|
void offer(WebrtcGroupOfferDTO dto); |
||||
|
|
||||
|
/** |
||||
|
* 推送answer信息给对方 |
||||
|
*/ |
||||
|
void answer(WebrtcGroupAnswerDTO dto); |
||||
|
|
||||
|
/** |
||||
|
* 推送candidate信息给对方 |
||||
|
*/ |
||||
|
void candidate(WebrtcGroupCandidateDTO dto); |
||||
|
|
||||
|
/** |
||||
|
* 用户进行了设备操作,如果关闭摄像头 |
||||
|
*/ |
||||
|
void device(WebrtcGroupDeviceDTO dto); |
||||
|
|
||||
|
/** |
||||
|
* 查询通话信息 |
||||
|
*/ |
||||
|
WebrtcGroupInfoVO info(Long groupId); |
||||
|
|
||||
|
/** |
||||
|
* 心跳保持, 用户每15s上传一次心跳 |
||||
|
*/ |
||||
|
void heartbeat(Long groupId); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,581 @@ |
|||||
|
package com.bx.implatform.service.impl; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.alibaba.fastjson.JSON; |
||||
|
import com.bx.imclient.IMClient; |
||||
|
import com.bx.imcommon.model.IMGroupMessage; |
||||
|
import com.bx.imcommon.model.IMUserInfo; |
||||
|
import com.bx.implatform.annotation.RedisLock; |
||||
|
import com.bx.implatform.config.WebrtcConfig; |
||||
|
import com.bx.implatform.contant.RedisKey; |
||||
|
import com.bx.implatform.dto.*; |
||||
|
import com.bx.implatform.entity.GroupMember; |
||||
|
import com.bx.implatform.entity.GroupMessage; |
||||
|
import com.bx.implatform.enums.MessageStatus; |
||||
|
import com.bx.implatform.enums.MessageType; |
||||
|
import com.bx.implatform.exception.GlobalException; |
||||
|
import com.bx.implatform.service.IGroupMemberService; |
||||
|
import com.bx.implatform.service.IGroupMessageService; |
||||
|
import com.bx.implatform.service.IWebrtcGroupService; |
||||
|
import com.bx.implatform.session.SessionContext; |
||||
|
import com.bx.implatform.session.UserSession; |
||||
|
import com.bx.implatform.session.WebrtcGroupSession; |
||||
|
import com.bx.implatform.session.WebrtcUserInfo; |
||||
|
import com.bx.implatform.util.BeanUtils; |
||||
|
import com.bx.implatform.util.UserStateUtils; |
||||
|
import com.bx.implatform.vo.GroupMessageVO; |
||||
|
import com.bx.implatform.vo.WebrtcGroupFailedVO; |
||||
|
import com.bx.implatform.vo.WebrtcGroupInfoVO; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.util.*; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* 群语音通话服务类,所有涉及修改webtcSession的方法都要挂分布式锁 |
||||
|
* |
||||
|
* @author: blue |
||||
|
* @date: 2024-06-01 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class WebrtcGroupServiceImpl implements IWebrtcGroupService { |
||||
|
|
||||
|
private final IGroupMemberService groupMemberService; |
||||
|
private final IGroupMessageService groupMessageService; |
||||
|
private final RedisTemplate<String, Object> redisTemplate; |
||||
|
private final IMClient imClient; |
||||
|
private final UserStateUtils userStateUtils; |
||||
|
private final WebrtcConfig webrtcConfig; |
||||
|
|
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId") |
||||
|
@Override |
||||
|
public void setup(WebrtcGroupSetupDTO dto) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
if(!imClient.isOnline(userSession.getUserId())){ |
||||
|
throw new GlobalException("您已断开连接,请重新登陆"); |
||||
|
} |
||||
|
if (dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) { |
||||
|
throw new GlobalException("最多支持" + webrtcConfig.getMaxChannel() + "人进行通话"); |
||||
|
} |
||||
|
List<Long> userIds = getRecvIds(dto.getUserInfos()); |
||||
|
if (!groupMemberService.isInGroup(dto.getGroupId(), userIds)) { |
||||
|
throw new GlobalException("部分用户不在群聊中"); |
||||
|
} |
||||
|
String key = buildWebrtcSessionKey(dto.getGroupId()); |
||||
|
if (redisTemplate.hasKey(key)) { |
||||
|
throw new GlobalException("该群聊已存在一个通话"); |
||||
|
} |
||||
|
// 有效用户
|
||||
|
List<WebrtcUserInfo> userInfos = new LinkedList<>(); |
||||
|
// 离线用户
|
||||
|
List<Long> offlineUserIds = new LinkedList<>(); |
||||
|
// 忙线用户
|
||||
|
List<Long> busyUserIds = new LinkedList<>(); |
||||
|
for (WebrtcUserInfo userInfo : dto.getUserInfos()) { |
||||
|
if (!imClient.isOnline(userInfo.getId())) { |
||||
|
userInfos.add(userInfo); |
||||
|
//offlineUserIds.add(userInfo.getId());
|
||||
|
} else if (userStateUtils.isBusy(userInfo.getId())) { |
||||
|
busyUserIds.add(userInfo.getId()); |
||||
|
} else { |
||||
|
userInfos.add(userInfo); |
||||
|
// 设置用户忙线状态
|
||||
|
userStateUtils.setBusy(userInfo.getId()); |
||||
|
} |
||||
|
} |
||||
|
// 创建通话session
|
||||
|
WebrtcGroupSession webrtcSession = new WebrtcGroupSession(); |
||||
|
IMUserInfo userInfo = new IMUserInfo(userSession.getUserId(), userSession.getTerminal()); |
||||
|
webrtcSession.setHost(userInfo); |
||||
|
webrtcSession.setUserInfos(userInfos); |
||||
|
webrtcSession.getInChatUsers().add(userInfo); |
||||
|
saveWebrtcSession(dto.getGroupId(), webrtcSession); |
||||
|
// 向发起邀请者推送邀请失败消息
|
||||
|
if (!offlineUserIds.isEmpty()) { |
||||
|
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); |
||||
|
vo.setUserIds(offlineUserIds); |
||||
|
vo.setReason("用户当前不在线"); |
||||
|
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), userInfo, JSON.toJSONString(vo)); |
||||
|
} |
||||
|
if (!busyUserIds.isEmpty()) { |
||||
|
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); |
||||
|
vo.setUserIds(busyUserIds); |
||||
|
vo.setReason("用户正忙"); |
||||
|
IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal()); |
||||
|
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo)); |
||||
|
} |
||||
|
// 向被邀请的用户广播消息,发起呼叫
|
||||
|
List<Long> recvIds = getRecvIds(userInfos); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), recvIds, JSON.toJSONString(userInfos),false); |
||||
|
// 发送文字提示信息
|
||||
|
WebrtcUserInfo mineInfo = findUserInfo(webrtcSession,userSession.getUserId()); |
||||
|
String content = mineInfo.getNickName() + " 发起了语音通话"; |
||||
|
sendTipMessage(dto.getGroupId(),content); |
||||
|
log.info("发起群通话,userId:{},groupId:{}", userSession.getUserId(), dto.getGroupId()); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId") |
||||
|
@Override |
||||
|
public void accept(Long groupId) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId); |
||||
|
// 校验
|
||||
|
if (!isExist(webrtcSession, userSession.getUserId())) { |
||||
|
throw new GlobalException("您未被邀请通话"); |
||||
|
} |
||||
|
// 防止重复进入
|
||||
|
if (isInchat(webrtcSession, userSession.getUserId())) { |
||||
|
throw new GlobalException("您已在通话中"); |
||||
|
} |
||||
|
// 将当前用户加入通话用户列表中
|
||||
|
webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); |
||||
|
saveWebrtcSession(groupId, webrtcSession); |
||||
|
// 广播信令
|
||||
|
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos()); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_ACCEPT, groupId, recvIds, "",true); |
||||
|
log.info("加入群通话,userId:{},groupId:{}", userSession.getUserId(), groupId); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId") |
||||
|
@Override |
||||
|
public void reject(Long groupId) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId); |
||||
|
// 校验
|
||||
|
if (!isExist(webrtcSession, userSession.getUserId())) { |
||||
|
throw new GlobalException("您未被邀请通话"); |
||||
|
} |
||||
|
// 防止重复进入
|
||||
|
if (isInchat(webrtcSession, userSession.getUserId())) { |
||||
|
throw new GlobalException("您已在通话中"); |
||||
|
} |
||||
|
// 将用户从列表中移除
|
||||
|
List<WebrtcUserInfo> userInfos = |
||||
|
webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId())) |
||||
|
.collect(Collectors.toList()); |
||||
|
webrtcSession.setUserInfos(userInfos); |
||||
|
saveWebrtcSession(groupId, webrtcSession); |
||||
|
// 进入空闲状态
|
||||
|
userStateUtils.setFree(userSession.getUserId()); |
||||
|
// 广播消息给的所有用户
|
||||
|
List<Long> recvIds = getRecvIds(userInfos); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_REJECT, groupId, recvIds, "",true); |
||||
|
log.info("拒绝群通话,userId:{},groupId:{}", userSession.getUserId(), groupId); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId") |
||||
|
@Override |
||||
|
public void failed(WebrtcGroupFailedDTO dto) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId()); |
||||
|
// 校验
|
||||
|
if (!isExist(webrtcSession, userSession.getUserId())) { |
||||
|
return; |
||||
|
} |
||||
|
if (isInchat(webrtcSession, userSession.getUserId())) { |
||||
|
return; |
||||
|
} |
||||
|
// 将用户从列表中移除
|
||||
|
List<WebrtcUserInfo> userInfos = |
||||
|
webrtcSession.getUserInfos().stream().filter(user -> !user.getId().equals(userSession.getUserId())) |
||||
|
.collect(Collectors.toList()); |
||||
|
webrtcSession.setUserInfos(userInfos); |
||||
|
saveWebrtcSession(dto.getGroupId(), webrtcSession); |
||||
|
// 进入空闲状态
|
||||
|
userStateUtils.setFree(userSession.getUserId()); |
||||
|
// 广播信令
|
||||
|
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); |
||||
|
vo.setUserIds(Arrays.asList(userSession.getUserId())); |
||||
|
vo.setReason(dto.getReason()); |
||||
|
List<Long> recvIds = getRecvIds(userInfos); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), recvIds, JSON.toJSONString(vo),false); |
||||
|
log.info("群通话失败,userId:{},groupId:{},原因:{}", userSession.getUserId(), dto.getReason()); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId") |
||||
|
@Override |
||||
|
public void join(Long groupId) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId); |
||||
|
if (webrtcSession.getUserInfos().size() >= webrtcConfig.getMaxChannel()) { |
||||
|
throw new GlobalException("人员已满,无法进入通话"); |
||||
|
} |
||||
|
GroupMember member = groupMemberService.findByGroupAndUserId(groupId, userSession.getUserId()); |
||||
|
if (Objects.isNull(member) || member.getQuit()) { |
||||
|
throw new GlobalException("您不在群里中"); |
||||
|
} |
||||
|
IMUserInfo mine = findInChatUser(webrtcSession, userSession.getUserId()); |
||||
|
if(!Objects.isNull(mine) && mine.getTerminal() != userSession.getTerminal()){ |
||||
|
throw new GlobalException("已在其他设备加入通话"); |
||||
|
} |
||||
|
WebrtcUserInfo userInfo = new WebrtcUserInfo(); |
||||
|
userInfo.setId(userSession.getUserId()); |
||||
|
userInfo.setNickName(member.getAliasName()); |
||||
|
userInfo.setHeadImage(member.getHeadImage()); |
||||
|
// 默认是开启麦克风,关闭摄像头
|
||||
|
userInfo.setIsCamera(false); |
||||
|
userInfo.setIsMicroPhone(true); |
||||
|
// 将当前用户加入通话用户列表中
|
||||
|
if (!isExist(webrtcSession, userSession.getUserId())) { |
||||
|
webrtcSession.getUserInfos().add(userInfo); |
||||
|
} |
||||
|
if (!isInchat(webrtcSession, userSession.getUserId())) { |
||||
|
webrtcSession.getInChatUsers().add(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); |
||||
|
} |
||||
|
saveWebrtcSession(groupId, webrtcSession); |
||||
|
// 进入忙线状态
|
||||
|
userStateUtils.setBusy(userSession.getUserId()); |
||||
|
// 广播信令
|
||||
|
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos()); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_JOIN, groupId, recvIds, JSON.toJSONString(userInfo),false); |
||||
|
log.info("加入群通话,userId:{},groupId:{}", userSession.getUserId(), groupId); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId") |
||||
|
@Override |
||||
|
public void invite(WebrtcGroupInviteDTO dto) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId()); |
||||
|
if (webrtcSession.getUserInfos().size() + dto.getUserInfos().size() > webrtcConfig.getMaxChannel()) { |
||||
|
throw new GlobalException("最多支持" + webrtcConfig.getMaxChannel() + "人进行通话"); |
||||
|
} |
||||
|
if (!groupMemberService.isInGroup(dto.getGroupId(), getRecvIds(dto.getUserInfos()))) { |
||||
|
throw new GlobalException("部分用户不在群聊中"); |
||||
|
} |
||||
|
// 过滤掉已经在通话中的用户
|
||||
|
List<WebrtcUserInfo> userInfos = webrtcSession.getUserInfos(); |
||||
|
// 原用户id
|
||||
|
List<Long> userIds = getRecvIds(userInfos); |
||||
|
// 离线用户id
|
||||
|
List<Long> offlineUserIds = new LinkedList<>(); |
||||
|
// 忙线用户
|
||||
|
List<Long> busyUserIds = new LinkedList<>(); |
||||
|
// 新加入的用户
|
||||
|
List<WebrtcUserInfo> newUserInfos = new LinkedList<>(); |
||||
|
for (WebrtcUserInfo userInfo : dto.getUserInfos()) { |
||||
|
if (isExist(webrtcSession, userInfo.getId())) { |
||||
|
// 防止重复进入
|
||||
|
continue; |
||||
|
} |
||||
|
if (!imClient.isOnline(userInfo.getId())) { |
||||
|
// offlineUserIds.add(userInfo.getId());
|
||||
|
userStateUtils.setBusy(userInfo.getId()); |
||||
|
newUserInfos.add(userInfo); |
||||
|
} else if (userStateUtils.isBusy(userInfo.getId())) { |
||||
|
busyUserIds.add(userInfo.getId()); |
||||
|
} else { |
||||
|
// 进入忙线状态
|
||||
|
userStateUtils.setBusy(userInfo.getId()); |
||||
|
newUserInfos.add(userInfo); |
||||
|
} |
||||
|
} |
||||
|
// 更新会话信息
|
||||
|
userInfos.addAll(newUserInfos); |
||||
|
saveWebrtcSession(dto.getGroupId(), webrtcSession); |
||||
|
// 向发起邀请者推送邀请失败消息
|
||||
|
if (!offlineUserIds.isEmpty()) { |
||||
|
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); |
||||
|
vo.setUserIds(offlineUserIds); |
||||
|
vo.setReason("用户当前不在线"); |
||||
|
IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal()); |
||||
|
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo)); |
||||
|
} |
||||
|
if (!busyUserIds.isEmpty()) { |
||||
|
WebrtcGroupFailedVO vo = new WebrtcGroupFailedVO(); |
||||
|
vo.setUserIds(busyUserIds); |
||||
|
vo.setReason("用户正在忙"); |
||||
|
IMUserInfo reciver = new IMUserInfo(userSession.getUserId(), userSession.getTerminal()); |
||||
|
sendRtcMessage2(MessageType.RTC_GROUP_FAILED, dto.getGroupId(), reciver, JSON.toJSONString(vo)); |
||||
|
} |
||||
|
// 向被邀请的发起呼叫
|
||||
|
List<Long> newUserIds = getRecvIds(newUserInfos); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_SETUP, dto.getGroupId(), newUserIds, JSON.toJSONString(userInfos),false); |
||||
|
// 向已在通话中的用户同步新邀请的用户信息
|
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_INVITE, dto.getGroupId(), userIds, JSON.toJSONString(newUserInfos),false); |
||||
|
log.info("邀请加入群通话,userId:{},groupId:{},邀请用户:{}", userSession.getUserId(), dto.getGroupId(), |
||||
|
newUserIds); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId") |
||||
|
@Override |
||||
|
public void cancel(Long groupId) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId); |
||||
|
if (!userSession.getUserId().equals(webrtcSession.getHost().getId())) { |
||||
|
throw new GlobalException("只有发起人可以取消通话"); |
||||
|
} |
||||
|
// 移除rtc session
|
||||
|
String key = buildWebrtcSessionKey(groupId); |
||||
|
redisTemplate.delete(key); |
||||
|
// 进入空闲状态
|
||||
|
webrtcSession.getUserInfos().forEach(user -> userStateUtils.setFree(user.getId())); |
||||
|
// 广播消息给的所有用户
|
||||
|
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos()); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, "",false); |
||||
|
// 发送文字提示信息
|
||||
|
sendTipMessage(groupId,"通话结束"); |
||||
|
log.info("发起人取消群通话,userId:{},groupId:{}", userSession.getUserId(), groupId); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#groupId") |
||||
|
@Override |
||||
|
public void quit(Long groupId) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(groupId); |
||||
|
// 将用户从列表中移除
|
||||
|
List<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); |
||||
|
// 进入空闲状态
|
||||
|
webrtcSession.getUserInfos().forEach(user -> userStateUtils.setFree(user.getId())); |
||||
|
// 广播给还在呼叫中的用户,取消通话
|
||||
|
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos()); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_CANCEL, groupId, recvIds, "",false); |
||||
|
// 发送文字提示信息
|
||||
|
sendTipMessage(groupId,"通话结束"); |
||||
|
log.info("群通话结束,groupId:{}", groupId); |
||||
|
} else { |
||||
|
// 更新会话信息
|
||||
|
webrtcSession.setInChatUsers(inChatUsers); |
||||
|
webrtcSession.setUserInfos(userInfos); |
||||
|
saveWebrtcSession(groupId, webrtcSession); |
||||
|
// 进入空闲状态
|
||||
|
userStateUtils.setFree(userSession.getUserId()); |
||||
|
// 广播信令
|
||||
|
List<Long> recvIds = getRecvIds(userInfos); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_QUIT, groupId, recvIds, "",false); |
||||
|
log.info("用户退出群通话,userId:{},groupId:{}", userSession.getUserId(), groupId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void offer(WebrtcGroupOfferDTO dto) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId()); |
||||
|
IMUserInfo userInfo = findInChatUser(webrtcSession, dto.getUserId()); |
||||
|
if (Objects.isNull(userInfo)) { |
||||
|
log.info("对方未加入群通话,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(), |
||||
|
dto.getGroupId()); |
||||
|
return; |
||||
|
} |
||||
|
// 推送offer给对方
|
||||
|
sendRtcMessage2(MessageType.RTC_GROUP_OFFER, dto.getGroupId(), userInfo, dto.getOffer()); |
||||
|
log.info("推送offer信息,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(), |
||||
|
dto.getGroupId()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void answer(WebrtcGroupAnswerDTO dto) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId()); |
||||
|
IMUserInfo userInfo = findInChatUser(webrtcSession, dto.getUserId()); |
||||
|
if (Objects.isNull(userInfo)) { |
||||
|
// 对方未加入群通话
|
||||
|
log.info("对方未加入群通话,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(), |
||||
|
dto.getGroupId()); |
||||
|
return; |
||||
|
} |
||||
|
// 推送answer信息给对方
|
||||
|
sendRtcMessage2(MessageType.RTC_GROUP_ANSWER, dto.getGroupId(), userInfo, dto.getAnswer()); |
||||
|
log.info("回复answer信息,userId:{},对方id:{},groupId:{}", userSession.getUserId(), dto.getUserId(), |
||||
|
dto.getGroupId()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void candidate(WebrtcGroupCandidateDTO dto) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId()); |
||||
|
IMUserInfo userInfo = findInChatUser(webrtcSession, dto.getUserId()); |
||||
|
if (Objects.isNull(userInfo)) { |
||||
|
// 对方未加入群通话
|
||||
|
log.info("对方未加入群通话,无法同步candidate,userId:{},remoteUserId:{},groupId:{}", userSession.getUserId(), |
||||
|
dto.getUserId(), dto.getGroupId()); |
||||
|
return; |
||||
|
} |
||||
|
// 推送candidate信息给对方
|
||||
|
sendRtcMessage2(MessageType.RTC_GROUP_CANDIDATE, dto.getGroupId(), userInfo, dto.getCandidate()); |
||||
|
log.info("同步candidate信息,userId:{},groupId:{}", userSession.getUserId(), dto.getGroupId()); |
||||
|
} |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_RTC_GROUP, key = "#dto.groupId") |
||||
|
@Override |
||||
|
public void device(WebrtcGroupDeviceDTO dto) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
// 查询会话信息
|
||||
|
WebrtcGroupSession webrtcSession = getWebrtcSession(dto.getGroupId()); |
||||
|
WebrtcUserInfo userInfo = findUserInfo(webrtcSession, userSession.getUserId()); |
||||
|
if (Objects.isNull(userInfo)) { |
||||
|
throw new GlobalException("您已不在通话中"); |
||||
|
} |
||||
|
// 更新设备状态
|
||||
|
userInfo.setIsCamera(dto.getIsCamera()); |
||||
|
userInfo.setIsMicroPhone(dto.getIsMicroPhone()); |
||||
|
saveWebrtcSession(dto.getGroupId(), webrtcSession); |
||||
|
// 广播信令
|
||||
|
List<Long> recvIds = getRecvIds(webrtcSession.getUserInfos()); |
||||
|
sendRtcMessage1(MessageType.RTC_GROUP_DEVICE, dto.getGroupId(), recvIds, JSON.toJSONString(dto),false); |
||||
|
log.info("设备操作,userId:{},groupId:{},摄像头:{}", userSession.getUserId(), dto.getGroupId(), |
||||
|
dto.getIsCamera()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public WebrtcGroupInfoVO info(Long groupId) { |
||||
|
WebrtcGroupInfoVO vo = new WebrtcGroupInfoVO(); |
||||
|
String key = buildWebrtcSessionKey(groupId); |
||||
|
WebrtcGroupSession webrtcSession = (WebrtcGroupSession)redisTemplate.opsForValue().get(key); |
||||
|
if (Objects.isNull(webrtcSession)) { |
||||
|
// 群聊当前没有通话
|
||||
|
vo.setIsChating(false); |
||||
|
} else { |
||||
|
// 群聊正在通话中
|
||||
|
vo.setIsChating(true); |
||||
|
vo.setUserInfos(webrtcSession.getUserInfos()); |
||||
|
Long hostId = webrtcSession.getHost().getId(); |
||||
|
WebrtcUserInfo host = findUserInfo(webrtcSession, hostId); |
||||
|
if (Objects.isNull(host)) { |
||||
|
// 如果发起人已经退出了通话,则从数据库查询发起人数据
|
||||
|
GroupMember member = groupMemberService.findByGroupAndUserId(groupId, hostId); |
||||
|
host = new WebrtcUserInfo(); |
||||
|
host.setId(hostId); |
||||
|
host.setNickName(member.getAliasName()); |
||||
|
host.setHeadImage(member.getHeadImage()); |
||||
|
} |
||||
|
vo.setHost(host); |
||||
|
} |
||||
|
return vo; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void heartbeat(Long groupId) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
// 给通话session续命
|
||||
|
String key = buildWebrtcSessionKey(groupId); |
||||
|
redisTemplate.expire(key, 30, TimeUnit.SECONDS); |
||||
|
// 用户忙线状态续命
|
||||
|
userStateUtils.expire(userSession.getUserId()); |
||||
|
} |
||||
|
|
||||
|
private WebrtcGroupSession getWebrtcSession(Long groupId) { |
||||
|
String key = buildWebrtcSessionKey(groupId); |
||||
|
WebrtcGroupSession webrtcSession = (WebrtcGroupSession)redisTemplate.opsForValue().get(key); |
||||
|
if (Objects.isNull(webrtcSession)) { |
||||
|
throw new GlobalException("通话已结束"); |
||||
|
} |
||||
|
return webrtcSession; |
||||
|
} |
||||
|
|
||||
|
private void saveWebrtcSession(Long groupId, WebrtcGroupSession webrtcSession) { |
||||
|
String key = buildWebrtcSessionKey(groupId); |
||||
|
redisTemplate.opsForValue().set(key, webrtcSession, 30, TimeUnit.SECONDS); |
||||
|
} |
||||
|
|
||||
|
private String buildWebrtcSessionKey(Long groupId) { |
||||
|
return StrUtil.join(":", RedisKey.IM_WEBRTC_GROUP_SESSION, groupId); |
||||
|
} |
||||
|
|
||||
|
private IMUserInfo findInChatUser(WebrtcGroupSession webrtcSession, Long userId) { |
||||
|
for (IMUserInfo userInfo : webrtcSession.getInChatUsers()) { |
||||
|
if (userInfo.getId().equals(userId)) { |
||||
|
return userInfo; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private WebrtcUserInfo findUserInfo(WebrtcGroupSession webrtcSession, Long userId) { |
||||
|
for (WebrtcUserInfo userInfo : webrtcSession.getUserInfos()) { |
||||
|
if (userInfo.getId().equals(userId)) { |
||||
|
return userInfo; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private List<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 sendRtcMessage1(MessageType messageType, Long groupId, List<Long> recvIds, String content,Boolean sendSelf) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
GroupMessageVO messageInfo = new GroupMessageVO(); |
||||
|
messageInfo.setType(messageType.code()); |
||||
|
messageInfo.setGroupId(groupId); |
||||
|
messageInfo.setSendId(userSession.getUserId()); |
||||
|
messageInfo.setContent(content); |
||||
|
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); |
||||
|
sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); |
||||
|
sendMessage.setRecvIds(recvIds); |
||||
|
sendMessage.setSendToSelf(sendSelf); |
||||
|
sendMessage.setSendResult(false); |
||||
|
sendMessage.setData(messageInfo); |
||||
|
imClient.sendGroupMessage(sendMessage); |
||||
|
} |
||||
|
|
||||
|
private void sendRtcMessage2(MessageType messageType, Long groupId, IMUserInfo receiver, String content) { |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
GroupMessageVO messageInfo = new GroupMessageVO(); |
||||
|
messageInfo.setType(messageType.code()); |
||||
|
messageInfo.setGroupId(groupId); |
||||
|
messageInfo.setSendId(userSession.getUserId()); |
||||
|
messageInfo.setContent(content); |
||||
|
IMGroupMessage<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); |
||||
|
} |
||||
|
|
||||
|
private void sendTipMessage(Long groupId,String content){ |
||||
|
UserSession userSession = SessionContext.getSession(); |
||||
|
// 群聊成员列表
|
||||
|
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId); |
||||
|
// 保存消息
|
||||
|
GroupMessage msg = new GroupMessage(); |
||||
|
msg.setGroupId(groupId); |
||||
|
msg.setContent(content); |
||||
|
msg.setSendId(userSession.getUserId()); |
||||
|
msg.setSendTime(new Date()); |
||||
|
msg.setStatus(MessageStatus.UNSEND.code()); |
||||
|
msg.setSendNickName(userSession.getNickName()); |
||||
|
msg.setType(MessageType.TIP_TEXT.code()); |
||||
|
groupMessageService.save(msg); |
||||
|
// 群发罅隙
|
||||
|
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class); |
||||
|
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); |
||||
|
sendMessage.setSender(new IMUserInfo(userSession.getUserId(), userSession.getTerminal())); |
||||
|
sendMessage.setRecvIds(userIds); |
||||
|
sendMessage.setSendResult(false); |
||||
|
sendMessage.setData(msgInfo); |
||||
|
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,29 @@ |
|||||
|
package com.bx.implatform.session; |
||||
|
|
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* @author: 谢绍许 |
||||
|
* @date: 2024-06-02 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Data |
||||
|
@ApiModel("用户信息") |
||||
|
public class WebrtcUserInfo { |
||||
|
@ApiModelProperty(value = "用户id") |
||||
|
private Long id; |
||||
|
|
||||
|
@ApiModelProperty(value = "用户昵称") |
||||
|
private String nickName; |
||||
|
|
||||
|
@ApiModelProperty(value = "用户头像") |
||||
|
private String headImage; |
||||
|
|
||||
|
@ApiModelProperty(value = "是否开启摄像头") |
||||
|
private Boolean isCamera; |
||||
|
|
||||
|
@ApiModelProperty(value = "是否开启麦克风") |
||||
|
private Boolean isMicroPhone; |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
package com.bx.implatform.util; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.bx.implatform.contant.RedisKey; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
/** |
||||
|
* @author: 谢绍许 |
||||
|
* @date: 2024-06-10 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Component |
||||
|
@RequiredArgsConstructor |
||||
|
public class UserStateUtils { |
||||
|
|
||||
|
private final RedisTemplate<String, Object> redisTemplate; |
||||
|
|
||||
|
public void setBusy(Long userId){ |
||||
|
String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); |
||||
|
redisTemplate.opsForValue().set(key,1,30, TimeUnit.SECONDS); |
||||
|
} |
||||
|
|
||||
|
public void expire(Long userId){ |
||||
|
String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); |
||||
|
redisTemplate.expire(key,30, TimeUnit.SECONDS); |
||||
|
} |
||||
|
|
||||
|
public void setFree(Long userId){ |
||||
|
String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); |
||||
|
redisTemplate.delete(key); |
||||
|
} |
||||
|
|
||||
|
public Boolean isBusy(Long userId){ |
||||
|
String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); |
||||
|
return redisTemplate.hasKey(key); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
package com.bx.implatform.vo; |
||||
|
|
||||
|
import com.bx.implatform.config.WebrtcConfig; |
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* @author: blue |
||||
|
* @date: 2024-06-10 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Data |
||||
|
@ApiModel("系统配置VO") |
||||
|
@AllArgsConstructor |
||||
|
public class SystemConfigVO { |
||||
|
|
||||
|
@ApiModelProperty(value = "webrtc配置") |
||||
|
private WebrtcConfig webrtc; |
||||
|
|
||||
|
} |
||||
@ -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,28 @@ |
|||||
|
package com.bx.implatform.vo; |
||||
|
|
||||
|
import com.bx.implatform.session.WebrtcUserInfo; |
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* @author: 谢绍许 |
||||
|
* @date: 2024-06-09 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Data |
||||
|
@ApiModel("群通话信息VO") |
||||
|
public class WebrtcGroupInfoVO { |
||||
|
|
||||
|
|
||||
|
@ApiModelProperty(value = "是否在通话中") |
||||
|
private Boolean isChating; |
||||
|
|
||||
|
@ApiModelProperty(value = "通话发起人") |
||||
|
WebrtcUserInfo host; |
||||
|
|
||||
|
@ApiModelProperty(value = "通话用户列表") |
||||
|
private List<WebrtcUserInfo> userInfos; |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
|
||||
|
class ImCamera { |
||||
|
constructor() { |
||||
|
this.stream = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ImCamera.prototype.isEnable = function() { |
||||
|
return !!navigator && !!navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia; |
||||
|
} |
||||
|
|
||||
|
ImCamera.prototype.openVideo = function(isFacing) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
if(this.stream){ |
||||
|
this.close() |
||||
|
} |
||||
|
let facingMode = isFacing ? "user" : "environment"; |
||||
|
let constraints = { |
||||
|
video: { |
||||
|
facingMode: facingMode |
||||
|
}, |
||||
|
audio: { |
||||
|
echoCancellation: true, //音频开启回音消除
|
||||
|
noiseSuppression: true // 开启降噪
|
||||
|
} |
||||
|
} |
||||
|
console.log("getUserMedia") |
||||
|
navigator.mediaDevices.getUserMedia(constraints).then((stream) => { |
||||
|
console.log("摄像头打开") |
||||
|
this.stream = stream; |
||||
|
resolve(stream); |
||||
|
}).catch((e) => { |
||||
|
console.log(e) |
||||
|
console.log("摄像头未能正常打开") |
||||
|
reject({ |
||||
|
code: 0, |
||||
|
message: "摄像头未能正常打开" |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ImCamera.prototype.openAudio = function() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
let constraints = { |
||||
|
video: false, |
||||
|
audio: { |
||||
|
echoCancellation: true, //音频开启回音消除
|
||||
|
noiseSuppression: true // 开启降噪
|
||||
|
} |
||||
|
} |
||||
|
navigator.mediaDevices.getUserMedia(constraints).then((stream) => { |
||||
|
this.stream = stream; |
||||
|
resolve(stream); |
||||
|
}).catch(() => { |
||||
|
console.log("麦克风未能正常打开") |
||||
|
reject({ |
||||
|
code: 0, |
||||
|
message: "麦克风未能正常打开" |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
ImCamera.prototype.close = function() { |
||||
|
// 停止流
|
||||
|
if (this.stream) { |
||||
|
this.stream.getTracks().forEach((track) => { |
||||
|
track.stop(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default ImCamera; |
||||
@ -0,0 +1,3 @@ |
|||||
|
import Vue from 'vue'; |
||||
|
|
||||
|
export default new Vue(); |
||||
@ -0,0 +1,156 @@ |
|||||
|
import http from './httpRequest.js' |
||||
|
|
||||
|
class RtcGroupApi { |
||||
|
constructor() { |
||||
|
this.http = http; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcGroupApi.prototype.setup = function(groupId, userInfos) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userInfos |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/setup', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.accept = function(groupId) { |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/accept?groupId='+groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.reject = function(groupId) { |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/reject?groupId='+groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.failed = function(groupId,reason) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
reason |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/failed', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcGroupApi.prototype.join = function(groupId) { |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/join?groupId='+groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.invite = function(groupId, userInfos) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userInfos |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/invite', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcGroupApi.prototype.offer = function(groupId, userId, offer) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
offer |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/offer', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.answer = function(groupId, userId, answer) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
answer |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/answer', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.quit = function(groupId) { |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/quit?groupId=' + groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.cancel = function(groupId) { |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/cancel?groupId=' + groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
candidate |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/candidate', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
isCamera, |
||||
|
isMicroPhone |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/device', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
candidate |
||||
|
} |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/candidate', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.heartbeat = function(groupId) { |
||||
|
return this.http({ |
||||
|
url: '/webrtc/group/heartbeat?groupId=' + groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export default RtcGroupApi; |
||||
@ -0,0 +1,117 @@ |
|||||
|
|
||||
|
class ImWebRtc { |
||||
|
constructor() { |
||||
|
this.configuration = {} |
||||
|
this.stream = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.isEnable = function() { |
||||
|
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window |
||||
|
.mozRTCPeerConnection; |
||||
|
window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window |
||||
|
.mozRTCSessionDescription; |
||||
|
window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window |
||||
|
.mozRTCIceCandidate; |
||||
|
return !!window.RTCPeerConnection; |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.init = function(configuration) { |
||||
|
this.configuration = configuration; |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.setupPeerConnection = function(callback) { |
||||
|
this.peerConnection = new RTCPeerConnection(this.configuration); |
||||
|
this.peerConnection.ontrack = (e) => { |
||||
|
// 对方的视频流
|
||||
|
callback(e.streams[0]); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ImWebRtc.prototype.setStream = function(stream) { |
||||
|
if(this.stream){ |
||||
|
this.peerConnection.removeStream(this.stream) |
||||
|
} |
||||
|
stream.getTracks().forEach((track) => { |
||||
|
this.peerConnection.addTrack(track, stream); |
||||
|
}); |
||||
|
this.stream = stream; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ImWebRtc.prototype.onIcecandidate = function(callback) { |
||||
|
this.peerConnection.onicecandidate = (event) => { |
||||
|
// 追踪到候选信息
|
||||
|
if (event.candidate) { |
||||
|
callback(event.candidate) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.onStateChange = function(callback) { |
||||
|
// 监听连接状态
|
||||
|
this.peerConnection.oniceconnectionstatechange = (event) => { |
||||
|
let state = event.target.iceConnectionState; |
||||
|
console.log("ICE连接状态变化: : " + state) |
||||
|
callback(state) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.createOffer = function() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const offerParam = {}; |
||||
|
offerParam.offerToRecieveAudio = 1; |
||||
|
offerParam.offerToRecieveVideo = 1; |
||||
|
// 创建本地sdp信息
|
||||
|
this.peerConnection.createOffer(offerParam).then((offer) => { |
||||
|
// 设置本地sdp信息
|
||||
|
this.peerConnection.setLocalDescription(offer); |
||||
|
// 发起呼叫请求
|
||||
|
resolve(offer) |
||||
|
}).catch((e) => { |
||||
|
reject(e) |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
ImWebRtc.prototype.createAnswer = function(offer) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
// 设置远端的sdp
|
||||
|
this.setRemoteDescription(offer); |
||||
|
// 创建本地dsp
|
||||
|
const offerParam = {}; |
||||
|
offerParam.offerToRecieveAudio = 1; |
||||
|
offerParam.offerToRecieveVideo = 1; |
||||
|
this.peerConnection.createAnswer(offerParam).then((answer) => { |
||||
|
// 设置本地sdp信息
|
||||
|
this.peerConnection.setLocalDescription(answer); |
||||
|
// 接受呼叫请求
|
||||
|
resolve(answer) |
||||
|
}).catch((e) => { |
||||
|
reject(e) |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.setRemoteDescription = function(offer) { |
||||
|
// 设置对方的sdp信息
|
||||
|
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.addIceCandidate = function(candidate) { |
||||
|
// 添加对方的候选人信息
|
||||
|
this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); |
||||
|
} |
||||
|
|
||||
|
ImWebRtc.prototype.close = function(uid) { |
||||
|
// 关闭RTC连接
|
||||
|
if (this.peerConnection) { |
||||
|
this.peerConnection.close(); |
||||
|
this.peerConnection.onicecandidate = null; |
||||
|
this.peerConnection.onaddstream = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default ImWebRtc; |
||||
Binary file not shown.
File diff suppressed because it is too large
@ -0,0 +1,70 @@ |
|||||
|
<template> |
||||
|
<div class="group-member-item" :style="{'height':height+'px'}"> |
||||
|
<div class="member-avatar"> |
||||
|
<head-image :size="headImageSize" :name="member.aliasName" |
||||
|
:url="member.headImage" :online="member.online"> </head-image> |
||||
|
</div> |
||||
|
<div class="member-name" :style="{'line-height':height+'px'}"> |
||||
|
<div>{{ member.aliasName }}</div> |
||||
|
</div> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import HeadImage from "../common/HeadImage.vue"; |
||||
|
export default { |
||||
|
name: "groupMember", |
||||
|
components: { HeadImage }, |
||||
|
data() { |
||||
|
return {}; |
||||
|
}, |
||||
|
props: { |
||||
|
member: { |
||||
|
type: Object, |
||||
|
required: true |
||||
|
}, |
||||
|
height:{ |
||||
|
type: Number, |
||||
|
default: 50 |
||||
|
} |
||||
|
}, |
||||
|
computed:{ |
||||
|
headImageSize(){ |
||||
|
return Math.ceil(this.height * 0.75) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.group-member-item { |
||||
|
display: flex; |
||||
|
margin-bottom: 1px; |
||||
|
position: relative; |
||||
|
padding: 0 15px; |
||||
|
align-items: center; |
||||
|
background-color: #fafafa; |
||||
|
white-space: nowrap; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: #eeeeee; |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
background-color: #eeeeee; |
||||
|
} |
||||
|
|
||||
|
.member-name { |
||||
|
flex:1; |
||||
|
padding-left: 10px; |
||||
|
height: 100%; |
||||
|
text-align: left; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,161 @@ |
|||||
|
<template> |
||||
|
<el-dialog title="选择成员" :visible.sync="isShow" width="50%"> |
||||
|
<div class="group-member-selector"> |
||||
|
<div class="left-box"> |
||||
|
<el-input placeholder="搜索" v-model="searchText"> |
||||
|
<i class="el-icon-search el-input__icon" slot="suffix"> </i> |
||||
|
</el-input> |
||||
|
<el-scrollbar style="height:400px;"> |
||||
|
<div v-for="m in members" :key="m.userId"> |
||||
|
<group-member-item v-show="!m.quit&&m.aliasName.startsWith(searchText)" |
||||
|
:member="m" @click.native="onClickMember(m)"> |
||||
|
<el-checkbox :disabled="m.locked" v-model="m.checked" @change="onChange(m)" |
||||
|
@click.native.stop=""></el-checkbox> |
||||
|
</group-member-item> |
||||
|
</div> |
||||
|
</el-scrollbar> |
||||
|
</div> |
||||
|
<div class="arrow el-icon-d-arrow-right"></div> |
||||
|
<div class="right-box"> |
||||
|
<div class="select-tip"> 已勾选{{checkedMembers.length}}位成员</div> |
||||
|
<div class="checked-member-list"> |
||||
|
<div v-for="m in members" :key="m.userId"> |
||||
|
<group-member class="member-item" v-if="m.checked" :member="m"></group-member> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<span slot="footer" class="dialog-footer"> |
||||
|
<el-button @click="close()">取 消</el-button> |
||||
|
<el-button type="primary" @click="ok()">确 定</el-button> |
||||
|
</span> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import GroupMemberItem from './GroupMemberItem.vue'; |
||||
|
import GroupMember from './GroupMember.vue'; |
||||
|
|
||||
|
export default { |
||||
|
name: "addGroupMember", |
||||
|
components: { |
||||
|
GroupMemberItem, |
||||
|
GroupMember |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
isShow: false, |
||||
|
searchText: "", |
||||
|
maxSize: -1, |
||||
|
members: [] |
||||
|
} |
||||
|
}, |
||||
|
props: { |
||||
|
groupId: { |
||||
|
type: Number |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
open(maxSize, checkedIds, lockedIds) { |
||||
|
this.maxSize = maxSize; |
||||
|
this.isShow = true; |
||||
|
this.loadGroupMembers(checkedIds, lockedIds); |
||||
|
}, |
||||
|
loadGroupMembers(checkedIds, lockedIds) { |
||||
|
this.$http({ |
||||
|
url: `/group/members/${this.groupId}`, |
||||
|
method: 'get' |
||||
|
}).then((members) => { |
||||
|
members.forEach((m) => { |
||||
|
// 默认选择和锁定的用户 |
||||
|
m.checked = checkedIds.indexOf(m.userId) >= 0; |
||||
|
m.locked = lockedIds.indexOf(m.userId) >= 0; |
||||
|
}); |
||||
|
this.members = members; |
||||
|
}); |
||||
|
}, |
||||
|
onClickMember(m) { |
||||
|
if (!m.locked) { |
||||
|
m.checked = !m.checked; |
||||
|
} |
||||
|
if (this.checkedMembers.length > this.maxSize) { |
||||
|
this.$message.error(`最多选择${this.maxSize}位成员`) |
||||
|
m.checked = false; |
||||
|
} |
||||
|
}, |
||||
|
onChange(m) { |
||||
|
if (this.checkedMembers.length > this.maxSize) { |
||||
|
this.$message.error(`最多选择${this.maxSize}位成员`) |
||||
|
m.checked = false; |
||||
|
} |
||||
|
}, |
||||
|
ok() { |
||||
|
this.$emit("complete", this.checkedMembers); |
||||
|
this.isShow = false; |
||||
|
}, |
||||
|
close() { |
||||
|
this.isShow = false; |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
checkedMembers() { |
||||
|
let ids = []; |
||||
|
this.members.forEach((m) => { |
||||
|
if (m.checked) { |
||||
|
ids.push(m); |
||||
|
} |
||||
|
}) |
||||
|
return ids; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.group-member-selector { |
||||
|
display: flex; |
||||
|
|
||||
|
.left-box { |
||||
|
width: 48%; |
||||
|
border: #587FF0 solid 1px; |
||||
|
border-radius: 5px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.arrow { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
font-size: 20px; |
||||
|
padding: 10px; |
||||
|
font-weight: 600; |
||||
|
color: #687Ff0; |
||||
|
} |
||||
|
|
||||
|
.right-box { |
||||
|
|
||||
|
width: 48%; |
||||
|
border: #587FF0 solid 1px; |
||||
|
border-radius: 5px; |
||||
|
|
||||
|
.select-tip { |
||||
|
text-align: left; |
||||
|
height: 40px; |
||||
|
line-height: 40px; |
||||
|
text-indent: 5px; |
||||
|
} |
||||
|
|
||||
|
.checked-member-list { |
||||
|
padding: 10px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
flex-wrap: wrap; |
||||
|
|
||||
|
.member-item { |
||||
|
padding: 2px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,116 @@ |
|||||
|
<template> |
||||
|
<el-dialog title="是否加入通话?" :visible.sync="isShow" width="400px"> |
||||
|
<div class="rtc-group-join"> |
||||
|
<div class="host-info"> |
||||
|
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image> |
||||
|
<div class="host-text">{{'发起人:'+rtcInfo.host.nickName}}</div> |
||||
|
</div> |
||||
|
<div class="users-info"> |
||||
|
<div>{{rtcInfo.userInfos.length+'人正在通话中'}}</div> |
||||
|
<div class="user-list"> |
||||
|
<div class="user-item" v-for="user in rtcInfo.userInfos" :key="user.id"> |
||||
|
<head-image :url="user.headImage" :name="user.nickName" :size="40"> |
||||
|
</head-image> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<span slot="footer" class="dialog-footer"> |
||||
|
<el-button @click="onCancel()">取 消</el-button> |
||||
|
<el-button type="primary" @click="onOk()">确 定</el-button> |
||||
|
</span> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import HeadImage from '@/components/common/HeadImage' |
||||
|
|
||||
|
export default{ |
||||
|
name: "rtcGroupJoin", |
||||
|
components:{ |
||||
|
HeadImage |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
isShow: false, |
||||
|
rtcInfo: { |
||||
|
host:{}, |
||||
|
userInfos:[] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
props: { |
||||
|
groupId: { |
||||
|
type: Number |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
open(rtcInfo) { |
||||
|
this.rtcInfo = rtcInfo; |
||||
|
this.isShow = true; |
||||
|
}, |
||||
|
onOk() { |
||||
|
this.isShow = false; |
||||
|
let userInfos = this.rtcInfo.userInfos; |
||||
|
let mine = this.$store.state.userStore.userInfo; |
||||
|
if(!userInfos.find((user)=>user.id==mine.id)){ |
||||
|
// 加入自己的信息 |
||||
|
userInfos.push({ |
||||
|
id: mine.id, |
||||
|
nickName: mine.nickName, |
||||
|
headImage: mine.headImageThumb, |
||||
|
isCamera: false, |
||||
|
isMicroPhone: true |
||||
|
}) |
||||
|
} |
||||
|
let rtcInfo = { |
||||
|
isHost: false, |
||||
|
groupId: this.groupId, |
||||
|
inviterId: mine.id, |
||||
|
userInfos: userInfos |
||||
|
} |
||||
|
// 通过home.vue打开多人视频窗口 |
||||
|
this.$eventBus.$emit("openGroupVideo", rtcInfo); |
||||
|
|
||||
|
}, |
||||
|
onCancel(){ |
||||
|
this.isShow = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.rtc-group-join { |
||||
|
height: 260px; |
||||
|
padding: 10px; |
||||
|
.host-info { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
font-size: 16px; |
||||
|
padding: 10px; |
||||
|
height: 100px; |
||||
|
align-items: center; |
||||
|
|
||||
|
.host-text{ |
||||
|
margin-top: 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.users-info { |
||||
|
font-size: 16px; |
||||
|
margin-top: 20px; |
||||
|
.user-list { |
||||
|
display: flex; |
||||
|
padding: 5px 5px; |
||||
|
height: 90px; |
||||
|
flex-wrap: wrap; |
||||
|
justify-content: center; |
||||
|
|
||||
|
.user-item{ |
||||
|
padding: 2px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,42 @@ |
|||||
|
<template> |
||||
|
<el-dialog v-dialogDrag top="5vh" title="语音通话" :close-on-click-modal="false" :close-on-press-escape="false" |
||||
|
:visible.sync="isShow" width="50%"> |
||||
|
<div class='rtc-group-video'> |
||||
|
<div style="padding-top:30px;font-weight: 600; text-align: center;font-size: 16px;"> |
||||
|
多人音视频通话为付费功能,有需要请联系作者... |
||||
|
</div> |
||||
|
<div style="padding-top:50px; text-align: center;font-size: 16px;"> |
||||
|
点击下方文档了解详细信息: |
||||
|
</div> |
||||
|
<div style="padding-top:10px; text-align: center;font-size: 16px;"> |
||||
|
<a href="https://www.yuque.com/u1475064/mufu2a/vi7engzluty594s2" target="_blank"> |
||||
|
付费-音视频通话源码 |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "rtcGroupVideo", |
||||
|
data() { |
||||
|
return { |
||||
|
isShow: false |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
open() { |
||||
|
this.isShow = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.rtc-group-video { |
||||
|
height: 300px; |
||||
|
background-color: #E8F2FF; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,32 @@ |
|||||
|
import http from '../api/httpRequest.js' |
||||
|
|
||||
|
export default { |
||||
|
state: { |
||||
|
webrtc: {} |
||||
|
}, |
||||
|
mutations: { |
||||
|
setConfig(state, config) { |
||||
|
state.webrtc = config.webrtc; |
||||
|
}, |
||||
|
clear(state){ |
||||
|
state.webrtc = {}; |
||||
|
} |
||||
|
}, |
||||
|
actions:{ |
||||
|
loadConfig(context){ |
||||
|
return new Promise((resolve, reject) => { |
||||
|
http({ |
||||
|
url: '/system/config', |
||||
|
method: 'GET' |
||||
|
}).then((config) => { |
||||
|
console.log("系统配置",config) |
||||
|
context.commit("setConfig",config); |
||||
|
resolve(); |
||||
|
}).catch((res)=>{ |
||||
|
reject(res); |
||||
|
}); |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,174 @@ |
|||||
|
<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 |
||||
|
}, |
||||
|
maxSize: { |
||||
|
type: Number, |
||||
|
default: -1 |
||||
|
} |
||||
|
}, |
||||
|
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; |
||||
|
} |
||||
|
// 达到选择上限 |
||||
|
if (this.maxSize > 0 && this.checkedIds.length > this.maxSize) { |
||||
|
m.checked = false; |
||||
|
uni.showToast({ |
||||
|
title: `最多选择${this.maxSize}位用户`, |
||||
|
icon: "none" |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
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> |
||||
@ -0,0 +1,89 @@ |
|||||
|
<template> |
||||
|
<uni-popup ref="popup" type="center"> |
||||
|
<uni-popup-dialog mode="base" message="成功消息" :duration="2000" title="是否加入通话?" confirmText="加入" |
||||
|
@confirm="onOk"> |
||||
|
<div class="group-rtc-join"> |
||||
|
<div class="host-info"> |
||||
|
<div>发起人</div> |
||||
|
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image> |
||||
|
</div> |
||||
|
<div class="user-info"> |
||||
|
<div>{{rtcInfo.userInfos.length+'人正在通话中'}}</div> |
||||
|
<scroll-view scroll-x="true" scroll-left="120"> |
||||
|
<view class="user-list"> |
||||
|
<view v-for="user in rtcInfo.userInfos" class="user-item"> |
||||
|
<head-image :name="user.nickName" :url="user.headImage" :size="80"></head-image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
</div> |
||||
|
</div> |
||||
|
</uni-popup-dialog> |
||||
|
</uni-popup> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
data() { |
||||
|
return { |
||||
|
rtcInfo: {} |
||||
|
} |
||||
|
}, |
||||
|
props: { |
||||
|
groupId: { |
||||
|
type: Number |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
open(rtcInfo) { |
||||
|
this.rtcInfo = rtcInfo; |
||||
|
this.$refs.popup.open(); |
||||
|
}, |
||||
|
onOk() { |
||||
|
let users = this.rtcInfo.userInfos; |
||||
|
let mine = this.$store.state.userStore.userInfo; |
||||
|
// 加入自己的信息 |
||||
|
if(!users.find((user)=>user.id==mine.id)){ |
||||
|
users.push({ |
||||
|
id: mine.id, |
||||
|
nickName: mine.nickName, |
||||
|
headImage: mine.headImageThumb, |
||||
|
isCamera: false, |
||||
|
isMicroPhone: true |
||||
|
}) |
||||
|
} |
||||
|
const userInfos = encodeURIComponent(JSON.stringify(users)); |
||||
|
uni.navigateTo({ |
||||
|
url: `/pages/chat/chat-group-video?groupId=${this.groupId}&isHost=false |
||||
|
&inviterId=${mine.id}&userInfos=${userInfos}` |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.group-rtc-join { |
||||
|
width: 100%; |
||||
|
|
||||
|
.host-info { |
||||
|
font-size: 16px; |
||||
|
padding: 10px; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
font-size: 16px; |
||||
|
padding: 10px; |
||||
|
} |
||||
|
|
||||
|
.user-list { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
height: 90rpx; |
||||
|
|
||||
|
.user-item { |
||||
|
padding: 3rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<!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> |
||||
@ -0,0 +1,144 @@ |
|||||
|
<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); |
||||
|
this.url += "&config=" + JSON.stringify(this.$store.state.configStore.webrtc); |
||||
|
}, |
||||
|
}, |
||||
|
onBackPress() { |
||||
|
console.log("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----- |
||||
@ -0,0 +1,32 @@ |
|||||
|
import http from '../common/request' |
||||
|
|
||||
|
export default { |
||||
|
state: { |
||||
|
webrtc: {} |
||||
|
}, |
||||
|
mutations: { |
||||
|
setConfig(state, config) { |
||||
|
state.webrtc = config.webrtc; |
||||
|
}, |
||||
|
clear(state){ |
||||
|
state.webrtc = {}; |
||||
|
} |
||||
|
}, |
||||
|
actions:{ |
||||
|
loadConfig(context){ |
||||
|
return new Promise((resolve, reject) => { |
||||
|
http({ |
||||
|
url: '/system/config', |
||||
|
method: 'GET' |
||||
|
}).then((config) => { |
||||
|
console.log("系统配置",config) |
||||
|
context.commit("setConfig",config); |
||||
|
resolve(); |
||||
|
}).catch((res)=>{ |
||||
|
reject(res); |
||||
|
}); |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
After Width: | Height: | Size: 132 KiB |
Loading…
Reference in new issue