committed by
Gitee
96 changed files with 5030 additions and 2032 deletions
@ -0,0 +1,16 @@ |
|||||
|
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 OnlineCheck { |
||||
|
|
||||
|
} |
||||
@ -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,43 @@ |
|||||
|
package com.bx.implatform.aspect; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.bx.imclient.IMClient; |
||||
|
import com.bx.implatform.annotation.RedisLock; |
||||
|
import com.bx.implatform.exception.GlobalException; |
||||
|
import com.bx.implatform.session.SessionContext; |
||||
|
import com.bx.implatform.session.UserSession; |
||||
|
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.api.RLock; |
||||
|
import org.springframework.core.annotation.Order; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.lang.reflect.Method; |
||||
|
|
||||
|
/** |
||||
|
* @author: blue |
||||
|
* @date: 2024-06-16 |
||||
|
* @version: 1.0 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Aspect |
||||
|
@Component |
||||
|
@RequiredArgsConstructor |
||||
|
public class OnlineCheckAspect { |
||||
|
|
||||
|
private final IMClient imClient; |
||||
|
|
||||
|
@Around("@annotation(com.bx.implatform.annotation.OnlineCheck)") |
||||
|
public Object around(ProceedingJoinPoint joinPoint) throws Throwable { |
||||
|
UserSession session = SessionContext.getSession(); |
||||
|
if(!imClient.isOnline(session.getUserId())){ |
||||
|
throw new GlobalException("您当前的网络连接已断开,请稍后重试"); |
||||
|
} |
||||
|
return joinPoint.proceed(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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,585 @@ |
|||||
|
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.OnlineCheck; |
||||
|
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; |
||||
|
|
||||
|
|
||||
|
@OnlineCheck |
||||
|
@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()); |
||||
|
} |
||||
|
|
||||
|
@OnlineCheck |
||||
|
@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); |
||||
|
} |
||||
|
|
||||
|
@OnlineCheck |
||||
|
@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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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: Blue |
||||
|
* @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() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
if(this.stream){ |
||||
|
this.close() |
||||
|
} |
||||
|
let constraints = { |
||||
|
video: { |
||||
|
with: window.screen.width, |
||||
|
height: window.screen.height |
||||
|
}, |
||||
|
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,40 @@ |
|||||
|
|
||||
|
// 是否普通消息
|
||||
|
let isNormal = function(type){ |
||||
|
return type>=0 && type < 10; |
||||
|
} |
||||
|
|
||||
|
// 是否状态消息
|
||||
|
let isStatus = function(type){ |
||||
|
return type>=10 && type < 20; |
||||
|
} |
||||
|
|
||||
|
// 是否提示消息
|
||||
|
let isTip = function(type){ |
||||
|
return type>=20 && type < 30; |
||||
|
} |
||||
|
|
||||
|
// 操作交互类消息
|
||||
|
let isAction = function(type){ |
||||
|
return type>=40 && type < 50; |
||||
|
} |
||||
|
|
||||
|
// 单人通话信令
|
||||
|
let isRtcPrivate = function(type){ |
||||
|
return type>=100 && type < 300; |
||||
|
} |
||||
|
|
||||
|
// 多人通话信令
|
||||
|
let isRtcGroup = function(type){ |
||||
|
return type>=200 && type < 400; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export { |
||||
|
isNormal, |
||||
|
isStatus, |
||||
|
isTip, |
||||
|
isAction, |
||||
|
isRtcPrivate, |
||||
|
isRtcGroup |
||||
|
} |
||||
@ -0,0 +1,151 @@ |
|||||
|
import http from './httpRequest.js' |
||||
|
|
||||
|
class RtcGroupApi {} |
||||
|
|
||||
|
RtcGroupApi.prototype.setup = function(groupId, userInfos) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userInfos |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/setup', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.accept = function(groupId) { |
||||
|
return http({ |
||||
|
url: '/webrtc/group/accept?groupId='+groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.reject = function(groupId) { |
||||
|
return http({ |
||||
|
url: '/webrtc/group/reject?groupId='+groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.failed = function(groupId,reason) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
reason |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/failed', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcGroupApi.prototype.join = function(groupId) { |
||||
|
return http({ |
||||
|
url: '/webrtc/group/join?groupId='+groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.invite = function(groupId, userInfos) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userInfos |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/invite', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcGroupApi.prototype.offer = function(groupId, userId, offer) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
offer |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/offer', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.answer = function(groupId, userId, answer) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
answer |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/answer', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.quit = function(groupId) { |
||||
|
return http({ |
||||
|
url: '/webrtc/group/quit?groupId=' + groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.cancel = function(groupId) { |
||||
|
return http({ |
||||
|
url: '/webrtc/group/cancel?groupId=' + groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
candidate |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/candidate', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
isCamera, |
||||
|
isMicroPhone |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/device', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) { |
||||
|
let formData = { |
||||
|
groupId, |
||||
|
userId, |
||||
|
candidate |
||||
|
} |
||||
|
return http({ |
||||
|
url: '/webrtc/group/candidate', |
||||
|
method: 'post', |
||||
|
data: formData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcGroupApi.prototype.heartbeat = function(groupId) { |
||||
|
return http({ |
||||
|
url: '/webrtc/group/heartbeat?groupId=' + groupId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export default RtcGroupApi; |
||||
@ -0,0 +1,66 @@ |
|||||
|
import http from './httpRequest.js' |
||||
|
|
||||
|
class RtcPrivateApi { |
||||
|
} |
||||
|
|
||||
|
RtcPrivateApi.prototype.call = function(uid, mode, offer) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/call?uid=${uid}&mode=${mode}`, |
||||
|
method: 'post', |
||||
|
data: JSON.stringify(offer) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcPrivateApi.prototype.accept = function(uid, answer) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/accept?uid=${uid}`, |
||||
|
method: 'post', |
||||
|
data: JSON.stringify(answer) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
RtcPrivateApi.prototype.handup = function(uid) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/handup?uid=${uid}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcPrivateApi.prototype.cancel = function(uid) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/cancel?uid=${uid}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcPrivateApi.prototype.reject = function(uid) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/reject?uid=${uid}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcPrivateApi.prototype.failed = function(uid, reason) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/failed?uid=${uid}&reason=${reason}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
RtcPrivateApi.prototype.sendCandidate = function(uid, candidate) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/candidate?uid=${uid}`, |
||||
|
method: 'post', |
||||
|
data: JSON.stringify(candidate) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
RtcPrivateApi.prototype.heartbeat = function(uid) { |
||||
|
return http({ |
||||
|
url: `/webrtc/private/heartbeat?uid=${uid}`, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export default RtcPrivateApi; |
||||
@ -0,0 +1,120 @@ |
|||||
|
|
||||
|
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) |
||||
|
} |
||||
|
if(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; |
||||
|
this.peerConnection = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default ImWebRtc; |
||||
Binary file not shown.
File diff suppressed because it is too large
@ -1,477 +0,0 @@ |
|||||
<template> |
|
||||
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false" |
|
||||
:visible="isShow" width="50%" height="70%" :before-close="handleClose"> |
|
||||
<div class="chat-video"> |
|
||||
<div v-show="rtcInfo.mode=='video'" class="chat-video-box"> |
|
||||
<div class="chat-video-friend" v-loading="loading" element-loading-text="等待对方接听..." |
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)"> |
|
||||
<head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="80" :name="rtcInfo.friend.nickName" |
|
||||
:url="rtcInfo.friend.headImage"> |
|
||||
</head-image> |
|
||||
<video ref="friendVideo" autoplay=""></video> |
|
||||
</div> |
|
||||
<div class="chat-video-mine"> |
|
||||
<video ref="mineVideo" autoplay=""></video> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div v-show="rtcInfo.mode=='voice'" class="chat-voice-box" v-loading="loading" element-loading-text="等待对方接听..." |
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)"> |
|
||||
<head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="200" :name="rtcInfo.friend.nickName" |
|
||||
:url="rtcInfo.friend.headImage"> |
|
||||
<div class="chat-voice-name">{{rtcInfo.friend.nickName}}</div> |
|
||||
</head-image> |
|
||||
</div> |
|
||||
<div class="chat-video-controllbar"> |
|
||||
<div v-show="isWaiting" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;" |
|
||||
@click="cancel()"></div> |
|
||||
<div v-show="isAccepted" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;" |
|
||||
@click="handup()"></div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</el-dialog> |
|
||||
|
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import HeadImage from '../common/HeadImage.vue'; |
|
||||
|
|
||||
export default { |
|
||||
name: 'chatVideo', |
|
||||
components: { |
|
||||
HeadImage |
|
||||
}, |
|
||||
data() { |
|
||||
return { |
|
||||
isShow: false, |
|
||||
stream: null, |
|
||||
audio: new Audio(), |
|
||||
loading: false, |
|
||||
peerConnection: null, |
|
||||
videoTime: 0, |
|
||||
videoTimer: null, |
|
||||
candidates: [], |
|
||||
configuration: { |
|
||||
iceServers: [] |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
init() { |
|
||||
this.isShow = true; |
|
||||
if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) { |
|
||||
this.$message.error("初始化失败,原因可能是: 1.未部署ssl证书 2.您的浏览器不支持WebRTC"); |
|
||||
this.insertMessage("设备不支持通话"); |
|
||||
if (!this.rtcInfo.isHost) { |
|
||||
this.sendFailed("对方设备不支持通话") |
|
||||
} |
|
||||
return; |
|
||||
} |
|
||||
// 打开摄像头 |
|
||||
this.openCamera((stream) => { |
|
||||
// 初始化webrtc连接 |
|
||||
this.setupPeerConnection(stream); |
|
||||
if (this.rtcInfo.isHost) { |
|
||||
// 发起呼叫 |
|
||||
this.call(); |
|
||||
} else { |
|
||||
// 接受呼叫 |
|
||||
this.accept(this.rtcInfo.offer); |
|
||||
} |
|
||||
}); |
|
||||
}, |
|
||||
openCamera(callback) { |
|
||||
navigator.getUserMedia({ |
|
||||
video: this.isVideo, |
|
||||
audio: true |
|
||||
}, (stream) => { |
|
||||
console.log(this.loading) |
|
||||
this.stream = stream; |
|
||||
this.$refs.mineVideo.srcObject = stream; |
|
||||
this.$refs.mineVideo.muted = true; |
|
||||
callback(stream) |
|
||||
}, (error) => { |
|
||||
let devText = this.isVideo ? "摄像头" : "麦克风" |
|
||||
this.$message.error(`打开${devText}失败:${error}`); |
|
||||
callback() |
|
||||
}) |
|
||||
}, |
|
||||
closeCamera() { |
|
||||
if (this.stream) { |
|
||||
this.stream.getTracks().forEach((track) => { |
|
||||
track.stop(); |
|
||||
}); |
|
||||
this.$refs.mineVideo.srcObject = null; |
|
||||
this.stream = null; |
|
||||
} |
|
||||
}, |
|
||||
setupPeerConnection(stream) { |
|
||||
this.peerConnection = new RTCPeerConnection(this.configuration); |
|
||||
this.peerConnection.ontrack = (e) => { |
|
||||
this.$refs.friendVideo.srcObject = e.streams[0]; |
|
||||
}; |
|
||||
this.peerConnection.onicecandidate = (event) => { |
|
||||
if (event.candidate) { |
|
||||
if (this.isAccepted) { |
|
||||
// 已连接,直接发送 |
|
||||
this.sendCandidate(event.candidate); |
|
||||
} else { |
|
||||
// 未连接,缓存起来,连接后再发送 |
|
||||
this.candidates.push(event.candidate) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
if (stream) { |
|
||||
stream.getTracks().forEach((track) => { |
|
||||
this.peerConnection.addTrack(track, stream); |
|
||||
}); |
|
||||
} |
|
||||
this.peerConnection.oniceconnectionstatechange = (event) => { |
|
||||
let state = event.target.iceConnectionState; |
|
||||
console.log("ICE connection status changed : " + state) |
|
||||
if (state == 'connected') { |
|
||||
this.resetTime(); |
|
||||
} |
|
||||
}; |
|
||||
}, |
|
||||
insertMessage(messageTip) { |
|
||||
// 打开会话,防止消息无法插入 |
|
||||
let chat = { |
|
||||
type: 'PRIVATE', |
|
||||
targetId: this.rtcInfo.friend.id, |
|
||||
showName: this.rtcInfo.friend.nickName, |
|
||||
headImage: this.rtcInfo.friend.headImage, |
|
||||
}; |
|
||||
this.$store.commit("openChat", chat); |
|
||||
// 插入消息 |
|
||||
let MESSAGE_TYPE = this.$enums.MESSAGE_TYPE; |
|
||||
let msgInfo = { |
|
||||
type: this.rtcInfo.mode == "video" ? MESSAGE_TYPE.RT_VIDEO : MESSAGE_TYPE.RT_VOICE, |
|
||||
sendId: this.rtcInfo.sendId, |
|
||||
recvId: this.rtcInfo.recvId, |
|
||||
content: this.isChating ? "通话时长 " + this.currentTime : messageTip, |
|
||||
status: 1, |
|
||||
selfSend: this.rtcInfo.isHost, |
|
||||
sendTime: new Date().getTime() |
|
||||
} |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
}, |
|
||||
onRTCMessage(msg) { |
|
||||
if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) { |
|
||||
// 对方接受了的通话 |
|
||||
this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content))); |
|
||||
// 关闭等待提示 |
|
||||
this.loading = false; |
|
||||
// 状态为聊天中 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING) |
|
||||
// 停止播放语音 |
|
||||
this.audio.pause(); |
|
||||
// 发送candidate |
|
||||
this.candidates.forEach((candidate) => { |
|
||||
this.sendCandidate(candidate); |
|
||||
}) |
|
||||
} else if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) { |
|
||||
// 对方拒绝了通话 |
|
||||
this.$message.error("对方拒绝了您的通话请求"); |
|
||||
// 插入消息 |
|
||||
this.insertMessage("对方已拒绝") |
|
||||
// 状态为空闲 |
|
||||
this.close(); |
|
||||
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) { |
|
||||
// 呼叫失败 |
|
||||
this.$message.error(msg.content) |
|
||||
// 插入消息 |
|
||||
this.insertMessage(msg.content) |
|
||||
this.close(); |
|
||||
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { |
|
||||
// 候选线路信息 |
|
||||
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content))); |
|
||||
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) { |
|
||||
// 对方挂断 |
|
||||
this.$message.success("对方已挂断"); |
|
||||
// 插入消息 |
|
||||
this.insertMessage("对方已挂断") |
|
||||
this.close(); |
|
||||
} |
|
||||
}, |
|
||||
call() { |
|
||||
let offerParam = { |
|
||||
offerToRecieveAudio: 1, |
|
||||
offerToRecieveVideo: this.isVideo ? 1 : 0 |
|
||||
} |
|
||||
this.peerConnection.createOffer(offerParam).then((offer) => { |
|
||||
this.peerConnection.setLocalDescription(offer); |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/call?uid=${this.rtcInfo.friend.id}&mode=${this.rtcInfo.mode}`, |
|
||||
method: 'post', |
|
||||
data: JSON.stringify(offer) |
|
||||
}).then(() => { |
|
||||
this.loading = true; |
|
||||
// 状态为聊天中 |
|
||||
this.audio.play(); |
|
||||
}) |
|
||||
}, (error) => { |
|
||||
this.insertMessage("未接通") |
|
||||
this.$message.error(error); |
|
||||
}); |
|
||||
|
|
||||
}, |
|
||||
accept(offer) { |
|
||||
let offerParam = { |
|
||||
offerToRecieveAudio: 1, |
|
||||
offerToRecieveVideo: this.isVideo ? 1 : 0 |
|
||||
} |
|
||||
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); |
|
||||
this.peerConnection.createAnswer(offerParam).then((answer) => { |
|
||||
this.peerConnection.setLocalDescription(answer); |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/accept?uid=${this.rtcInfo.friend.id}`, |
|
||||
method: 'post', |
|
||||
data: JSON.stringify(answer) |
|
||||
}).then(() => { |
|
||||
// 聊天中状态 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING) |
|
||||
}) |
|
||||
|
|
||||
}, |
|
||||
(error) => { |
|
||||
this.$message.error(error); |
|
||||
}); |
|
||||
|
|
||||
}, |
|
||||
handup() { |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/handup?uid=${this.rtcInfo.friend.id}`, |
|
||||
method: 'post' |
|
||||
}) |
|
||||
this.insertMessage("已挂断") |
|
||||
this.close(); |
|
||||
this.$message.success("您已挂断,通话结束") |
|
||||
}, |
|
||||
cancel() { |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/cancel?uid=${this.rtcInfo.friend.id}`, |
|
||||
method: 'post' |
|
||||
}) |
|
||||
this.insertMessage("已取消") |
|
||||
this.close(); |
|
||||
this.$message.success("已取消呼叫,通话结束") |
|
||||
}, |
|
||||
sendFailed(reason) { |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`, |
|
||||
method: 'post' |
|
||||
}) |
|
||||
}, |
|
||||
sendCandidate(candidate) { |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/candidate?uid=${this.rtcInfo.friend.id}`, |
|
||||
method: 'post', |
|
||||
data: JSON.stringify(candidate) |
|
||||
}) |
|
||||
}, |
|
||||
close() { |
|
||||
this.isShow = false; |
|
||||
this.closeCamera(); |
|
||||
this.loading = false; |
|
||||
this.videoTime = 0; |
|
||||
this.videoTimer && clearInterval(this.videoTimer); |
|
||||
this.audio.pause(); |
|
||||
this.candidates = []; |
|
||||
if (this.peerConnection) { |
|
||||
this.peerConnection.close(); |
|
||||
this.peerConnection.onicecandidate = null; |
|
||||
this.peerConnection.onaddstream = null; |
|
||||
} |
|
||||
if (this.$refs.friendVideo) { |
|
||||
this.$refs.friendVideo.srcObject = null; |
|
||||
} |
|
||||
// 状态置为空闲 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE); |
|
||||
}, |
|
||||
resetTime() { |
|
||||
this.videoTime = 0; |
|
||||
this.videoTimer && clearInterval(this.videoTimer); |
|
||||
this.videoTimer = setInterval(() => { |
|
||||
this.videoTime++; |
|
||||
}, 1000) |
|
||||
}, |
|
||||
handleClose() { |
|
||||
if (this.isAccepted) { |
|
||||
this.handup() |
|
||||
} else if (this.isWaiting) { |
|
||||
this.cancel(); |
|
||||
} else { |
|
||||
this.close(); |
|
||||
} |
|
||||
}, |
|
||||
hasUserMedia() { |
|
||||
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator |
|
||||
.mozGetUserMedia || |
|
||||
navigator.msGetUserMedia; |
|
||||
return !!navigator.getUserMedia; |
|
||||
}, |
|
||||
hasRTCPeerConnection() { |
|
||||
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; |
|
||||
}, |
|
||||
initAudio() { |
|
||||
let url = require(`@/assets/audio/call.wav`); |
|
||||
this.audio.src = url; |
|
||||
this.audio.loop = true; |
|
||||
}, |
|
||||
initICEServers() { |
|
||||
this.$http({ |
|
||||
url: '/webrtc/private/iceservers', |
|
||||
method: 'get' |
|
||||
}).then((servers) => { |
|
||||
this.configuration.iceServers = servers; |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
}, |
|
||||
watch: { |
|
||||
rtcState: { |
|
||||
handler(newState, oldState) { |
|
||||
// WAIT_CALL是主动呼叫弹出,ACCEPTED是被呼叫接受后弹出 |
|
||||
if (newState == this.$enums.RTC_STATE.WAIT_CALL || |
|
||||
newState == this.$enums.RTC_STATE.ACCEPTED) { |
|
||||
this.init(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
computed: { |
|
||||
title() { |
|
||||
let strTitle = `${this.modeText}通话-${this.rtcInfo.friend.nickName}`; |
|
||||
if (this.isChating) { |
|
||||
strTitle += `(${this.currentTime})`; |
|
||||
} else if (this.isWaiting) { |
|
||||
strTitle += `(呼叫中)`; |
|
||||
} |
|
||||
return strTitle; |
|
||||
}, |
|
||||
currentTime() { |
|
||||
let min = Math.floor(this.videoTime / 60); |
|
||||
let sec = this.videoTime % 60; |
|
||||
let strTime = min < 10 ? "0" : ""; |
|
||||
strTime += min; |
|
||||
strTime += ":" |
|
||||
strTime += sec < 10 ? "0" : ""; |
|
||||
strTime += sec; |
|
||||
return strTime; |
|
||||
}, |
|
||||
rtcInfo() { |
|
||||
return this.$store.state.userStore.rtcInfo; |
|
||||
}, |
|
||||
rtcState() { |
|
||||
return this.rtcInfo.state; |
|
||||
}, |
|
||||
isVideo() { |
|
||||
return this.rtcInfo.mode == "video" |
|
||||
}, |
|
||||
modeText() { |
|
||||
return this.isVideo ? "视频" : "语音"; |
|
||||
}, |
|
||||
isAccepted() { |
|
||||
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING || |
|
||||
this.rtcInfo.state == this.$enums.RTC_STATE.ACCEPTED |
|
||||
}, |
|
||||
isWaiting() { |
|
||||
return this.rtcInfo.state == this.$enums.RTC_STATE.WAIT_CALL; |
|
||||
}, |
|
||||
isChating() { |
|
||||
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING; |
|
||||
} |
|
||||
}, |
|
||||
mounted() { |
|
||||
this.initAudio(); |
|
||||
this.initICEServers(); |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style lang="scss"> |
|
||||
.chat-video { |
|
||||
position: relative; |
|
||||
|
|
||||
.el-loading-text { |
|
||||
color: white !important; |
|
||||
font-size: 16px !important; |
|
||||
} |
|
||||
|
|
||||
.el-icon-loading { |
|
||||
color: white !important; |
|
||||
font-size: 30px !important; |
|
||||
} |
|
||||
|
|
||||
.chat-video-box { |
|
||||
position: relative; |
|
||||
border: #4880b9 solid 1px; |
|
||||
background-color: #eeeeee; |
|
||||
|
|
||||
.chat-video-friend { |
|
||||
height: 70vh; |
|
||||
|
|
||||
.friend-head-image { |
|
||||
position: absolute; |
|
||||
} |
|
||||
|
|
||||
video { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
object-fit: cover; |
|
||||
transform: rotateY(180deg); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.chat-video-mine { |
|
||||
position: absolute; |
|
||||
z-index: 99999; |
|
||||
width: 25vh; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
box-shadow: 0px 0px 5px #ccc; |
|
||||
background-color: #cccccc; |
|
||||
|
|
||||
video { |
|
||||
width: 100%; |
|
||||
object-fit: cover; |
|
||||
transform: rotateY(180deg); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.chat-voice-box { |
|
||||
position: relative; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
border: #4880b9 solid 1px; |
|
||||
width: 100%; |
|
||||
height: 50vh; |
|
||||
padding-top: 10vh; |
|
||||
background-color: aliceblue; |
|
||||
|
|
||||
.chat-voice-name { |
|
||||
text-align: center; |
|
||||
font-size: 22px; |
|
||||
font-weight: 600; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.chat-video-controllbar { |
|
||||
display: flex; |
|
||||
justify-content: space-around; |
|
||||
padding: 10px; |
|
||||
|
|
||||
.icon { |
|
||||
font-size: 50px; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,277 +0,0 @@ |
|||||
<template> |
|
||||
<div v-show="isShow" class="chat-video-acceptor"> |
|
||||
<head-image :id="rtcInfo.friend.id" :name="rtcInfo.friend.nickName" :url="rtcInfo.friend.headImage" :size="100"></head-image> |
|
||||
<div class="acceptor-text"> |
|
||||
{{tip}} |
|
||||
</div> |
|
||||
<div class="acceptor-btn-group"> |
|
||||
<div class="icon iconfont icon-phone-accept accept" @click="accpet()" title="接受"></div> |
|
||||
<div class="icon iconfont icon-phone-reject reject" @click="reject()" title="拒绝"></div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import HeadImage from '../common/HeadImage.vue'; |
|
||||
|
|
||||
export default { |
|
||||
name: "videoAcceptor", |
|
||||
components: { |
|
||||
HeadImage |
|
||||
}, |
|
||||
data() { |
|
||||
return { |
|
||||
isShow: false, |
|
||||
audio: new Audio() |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
accpet() { |
|
||||
// 状态置为已接受 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.ACCEPTED); |
|
||||
// 关闭 |
|
||||
this.close(); |
|
||||
}, |
|
||||
reject() { |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/reject?uid=${this.rtcInfo.friend.id}`, |
|
||||
method: 'post' |
|
||||
}) |
|
||||
// 插入消息到会话中 |
|
||||
this.insertMessage("已拒绝"); |
|
||||
// 状态置为空闲 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE); |
|
||||
// 关闭 |
|
||||
this.close(); |
|
||||
}, |
|
||||
failed(reason) { |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`, |
|
||||
method: 'post' |
|
||||
}) |
|
||||
// 插入消息到会话中 |
|
||||
this.insertMessage("未接听"); |
|
||||
// 状态置为空闲 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE); |
|
||||
// 关闭 |
|
||||
this.close(); |
|
||||
}, |
|
||||
onRtcCall(msgInfo, friend, mode) { |
|
||||
console.log("onRtcCall") |
|
||||
// 只要不是空闲状态,都是繁忙 |
|
||||
if (this.rtcInfo.state != this.$enums.RTC_STATE.FREE) { |
|
||||
// 已经在跟别人聊天了,不能让其他会话进来 |
|
||||
let reason = "对方忙,无法与您通话"; |
|
||||
this.$http({ |
|
||||
url: `/webrtc/private/failed?uid=${msgInfo.sendId}&reason=${reason}`, |
|
||||
method: 'post' |
|
||||
}) |
|
||||
return; |
|
||||
} |
|
||||
// 显示呼叫 |
|
||||
this.isShow = true; |
|
||||
// 初始化RTC会话信息 |
|
||||
let rtcInfo = { |
|
||||
mode: mode, |
|
||||
isHost: false, |
|
||||
friend: friend, |
|
||||
sendId: msgInfo.sendId, |
|
||||
recvId: msgInfo.recvId, |
|
||||
offer: JSON.parse(msgInfo.content), |
|
||||
state: this.$enums.RTC_STATE.WAIT_ACCEPT |
|
||||
} |
|
||||
this.$store.commit("setRtcInfo", rtcInfo); |
|
||||
// 播放呼叫音频 |
|
||||
this.audio.play(); |
|
||||
// 超时未接听 |
|
||||
this.timer && clearTimeout(this.timer); |
|
||||
this.timer = setTimeout(() => { |
|
||||
this.failed("对方无应答"); |
|
||||
}, 30000) |
|
||||
|
|
||||
}, |
|
||||
onRtcCancel(msgInfo) { |
|
||||
// 防止被其他用户的操作干扰 |
|
||||
if (msgInfo.sendId != this.rtcInfo.friend.id) { |
|
||||
return; |
|
||||
} |
|
||||
// 取消视频通话请求 |
|
||||
this.$message.success("对方取消了呼叫"); |
|
||||
// 插入消息到会话中 |
|
||||
this.insertMessage("对方已取消"); |
|
||||
// 状态置为空闲 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE); |
|
||||
// 关闭 |
|
||||
this.close(); |
|
||||
}, |
|
||||
onRtcAccept(msgInfo) { |
|
||||
// 这里处理的时自己在其他设备接听了的情况 |
|
||||
if (msgInfo.selfSend) { |
|
||||
this.$message.success("已在其他设备接听"); |
|
||||
// 插入消息到会话中 |
|
||||
this.insertMessage("已在其他设备接听") |
|
||||
// 状态置为空闲 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE); |
|
||||
// 关闭 |
|
||||
this.close(); |
|
||||
} |
|
||||
}, |
|
||||
onRtcReject(msgInfo){ |
|
||||
// 我在其他终端拒绝了对方的通话 |
|
||||
if (msgInfo.selfSend) { |
|
||||
this.$message.success("已在其他设备拒绝通话"); |
|
||||
// 插入消息到会话中 |
|
||||
this.insertMessage("已在其他设备拒绝") |
|
||||
// 状态置为空闲 |
|
||||
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE); |
|
||||
// 关闭 |
|
||||
this.close(); |
|
||||
} |
|
||||
}, |
|
||||
onRTCMessage(msgInfo, friend) { |
|
||||
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE) { |
|
||||
this.onRtcCall(msgInfo, friend, "voice"); |
|
||||
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO) { |
|
||||
this.onRtcCall(msgInfo, friend, "video"); |
|
||||
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) { |
|
||||
this.onRtcCancel(msgInfo); |
|
||||
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) { |
|
||||
this.onRtcAccept(msgInfo); |
|
||||
}else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) { |
|
||||
this.onRtcReject(msgInfo); |
|
||||
} |
|
||||
}, |
|
||||
insertMessage(messageTip) { |
|
||||
// 先打开会话,防止会话存在导致无法插入消息 |
|
||||
let chat = { |
|
||||
type: 'PRIVATE', |
|
||||
targetId: this.rtcInfo.friend.id, |
|
||||
showName: this.rtcInfo.friend.nickName, |
|
||||
headImage: this.rtcInfo.friend.headImageThumb, |
|
||||
}; |
|
||||
this.$store.commit("openChat", chat); |
|
||||
// 插入消息 |
|
||||
let MESSAGE_TYPE = this.$enums.MESSAGE_TYPE; |
|
||||
let msgInfo = { |
|
||||
type: this.rtcInfo.mode == "video" ? MESSAGE_TYPE.RT_VIDEO : MESSAGE_TYPE.RT_VOICE, |
|
||||
sendId: this.rtcInfo.sendId, |
|
||||
recvId: this.rtcInfo.recvId, |
|
||||
content: messageTip, |
|
||||
status: 1, |
|
||||
selfSend: this.rtcInfo.isHost, |
|
||||
sendTime: new Date().getTime() |
|
||||
} |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
}, |
|
||||
close() { |
|
||||
this.timer && clearTimeout(this.timer); |
|
||||
this.audio.pause(); |
|
||||
this.isShow = false; |
|
||||
}, |
|
||||
initAudio() { |
|
||||
let url = require(`@/assets/audio/call.wav`); |
|
||||
this.audio.src = url; |
|
||||
this.audio.loop = true; |
|
||||
} |
|
||||
}, |
|
||||
computed: { |
|
||||
tip() { |
|
||||
let modeText = this.mode == "video" ? "视频" : "语音" |
|
||||
return `${this.rtcInfo.friend.nickName} 请求和您进行${modeText}通话...` |
|
||||
}, |
|
||||
rtcInfo(){ |
|
||||
return this.$store.state.userStore.rtcInfo; |
|
||||
} |
|
||||
}, |
|
||||
mounted() { |
|
||||
// 初始化语音 |
|
||||
this.initAudio(); |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style scoped lang="scss"> |
|
||||
.chat-video-acceptor { |
|
||||
position: absolute; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: center; |
|
||||
right: 5px; |
|
||||
bottom: 5px; |
|
||||
width: 250px; |
|
||||
height: 250px; |
|
||||
padding: 20px; |
|
||||
background-color: #eeeeee; |
|
||||
border: #dddddd solid 5px; |
|
||||
border-radius: 3%; |
|
||||
|
|
||||
.acceptor-text { |
|
||||
padding: 10px; |
|
||||
text-align: center; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
|
|
||||
.acceptor-btn-group { |
|
||||
display: flex; |
|
||||
justify-content: space-around; |
|
||||
margin-top: 20px; |
|
||||
width: 100%; |
|
||||
|
|
||||
.icon { |
|
||||
font-size: 60px; |
|
||||
cursor: pointer; |
|
||||
border-radius: 50%; |
|
||||
|
|
||||
&.accept { |
|
||||
color: green; |
|
||||
animation: anim 2s ease-in infinite, vibration 2s ease-in infinite; |
|
||||
|
|
||||
@keyframes anim { |
|
||||
0% { |
|
||||
box-shadow: 0 1px 0 4px #ffffff; |
|
||||
} |
|
||||
|
|
||||
10% { |
|
||||
box-shadow: 0 1px 0 8px rgba(255, 165, 0, 1); |
|
||||
} |
|
||||
|
|
||||
25% { |
|
||||
box-shadow: 0 1px 0 12px rgba(255, 210, 128, 1), 0 1px 0 16px rgba(255, 201, 102, 1); |
|
||||
} |
|
||||
|
|
||||
50% { |
|
||||
box-shadow: 0 2px 5px 10px rgba(255, 184, 51, 1), 0 2px 5px 23px rgba(248, 248, 255, 1); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@keyframes vibration { |
|
||||
0% { |
|
||||
transform: rotate(0deg); |
|
||||
} |
|
||||
|
|
||||
25% { |
|
||||
transform: rotate(20deg); |
|
||||
} |
|
||||
|
|
||||
50% { |
|
||||
transform: rotate(0deg); |
|
||||
} |
|
||||
|
|
||||
75% { |
|
||||
transform: rotate(-15deg); |
|
||||
} |
|
||||
|
|
||||
100% { |
|
||||
transform: rotate(0deg); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
&.reject { |
|
||||
color: red; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</style> |
|
||||
@ -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,126 @@ |
|||||
|
<template> |
||||
|
<div class="rtc-private-acceptor"> |
||||
|
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"></head-image> |
||||
|
<div class="acceptor-text"> |
||||
|
{{tip}} |
||||
|
</div> |
||||
|
<div class="acceptor-btn-group"> |
||||
|
<div class="icon iconfont icon-phone-accept accept" @click="$emit('accept')" title="接受"></div> |
||||
|
<div class="icon iconfont icon-phone-reject reject" @click="$emit('reject')" title="拒绝"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import HeadImage from '../common/HeadImage.vue'; |
||||
|
|
||||
|
export default { |
||||
|
name: "rtcPrivateAcceptor", |
||||
|
components: { |
||||
|
HeadImage |
||||
|
}, |
||||
|
data() { |
||||
|
return {} |
||||
|
}, |
||||
|
props: { |
||||
|
mode:{ |
||||
|
type: String |
||||
|
}, |
||||
|
friend:{ |
||||
|
type: Object |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
tip() { |
||||
|
let modeText = this.mode == "video" ? "视频" : "语音" |
||||
|
return `${this.friend.nickName} 请求和您进行${modeText}通话...` |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
.rtc-private-acceptor { |
||||
|
position: absolute; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
right: 5px; |
||||
|
bottom: 5px; |
||||
|
width: 250px; |
||||
|
height: 250px; |
||||
|
padding: 20px; |
||||
|
background-color: #eeeeee; |
||||
|
border: #dddddd solid 5px; |
||||
|
border-radius: 3%; |
||||
|
|
||||
|
.acceptor-text { |
||||
|
padding: 10px; |
||||
|
text-align: center; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.acceptor-btn-group { |
||||
|
display: flex; |
||||
|
justify-content: space-around; |
||||
|
margin-top: 20px; |
||||
|
width: 100%; |
||||
|
|
||||
|
.icon { |
||||
|
font-size: 60px; |
||||
|
cursor: pointer; |
||||
|
border-radius: 50%; |
||||
|
|
||||
|
&.accept { |
||||
|
color: green; |
||||
|
animation: anim 2s ease-in infinite, vibration 2s ease-in infinite; |
||||
|
|
||||
|
@keyframes anim { |
||||
|
0% { |
||||
|
box-shadow: 0 1px 0 4px #ffffff; |
||||
|
} |
||||
|
|
||||
|
10% { |
||||
|
box-shadow: 0 1px 0 8px rgba(255, 165, 0, 1); |
||||
|
} |
||||
|
|
||||
|
25% { |
||||
|
box-shadow: 0 1px 0 12px rgba(255, 210, 128, 1), 0 1px 0 16px rgba(255, 201, 102, 1); |
||||
|
} |
||||
|
|
||||
|
50% { |
||||
|
box-shadow: 0 2px 5px 10px rgba(255, 184, 51, 1), 0 2px 5px 23px rgba(248, 248, 255, 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes vibration { |
||||
|
0% { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
|
||||
|
25% { |
||||
|
transform: rotate(20deg); |
||||
|
} |
||||
|
|
||||
|
50% { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
|
||||
|
75% { |
||||
|
transform: rotate(-15deg); |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
&.reject { |
||||
|
color: red; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,496 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false" |
||||
|
:visible.sync="showRoom" width="50%" height="70%" :before-close="onQuit"> |
||||
|
<div class="rtc-private-video"> |
||||
|
<div v-show="isVideo" class="rtc-video-box"> |
||||
|
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..." |
||||
|
element-loading-background="rgba(0, 0, 0, 0.3)" > |
||||
|
<head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName" |
||||
|
:url="friend.headImage"> |
||||
|
</head-image> |
||||
|
<video ref="remoteVideo" autoplay=""></video> |
||||
|
</div> |
||||
|
<div class="rtc-video-mine"> |
||||
|
<video ref="localVideo" autoplay=""></video> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..." |
||||
|
element-loading-background="rgba(0, 0, 0, 0.3)"> |
||||
|
<head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName" |
||||
|
:url="friend.headImage"> |
||||
|
<div class="rtc-voice-name">{{friend.nickName}}</div> |
||||
|
</head-image> |
||||
|
</div> |
||||
|
<div class="rtc-control-bar"> |
||||
|
<div title="取消" class="icon iconfont icon-phone-reject reject" |
||||
|
style="color: red;" @click="onQuit()"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
<rtc-private-acceptor v-if="!isHost&&isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept" |
||||
|
@reject="onReject"></rtc-private-acceptor> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import HeadImage from '../common/HeadImage.vue'; |
||||
|
import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue'; |
||||
|
import ImWebRtc from '@/api/webrtc'; |
||||
|
import ImCamera from '@/api/camera'; |
||||
|
import RtcPrivateApi from '@/api/rtcPrivateApi' |
||||
|
|
||||
|
export default { |
||||
|
name: 'rtcPrivateVideo', |
||||
|
components: { |
||||
|
HeadImage, |
||||
|
RtcPrivateAcceptor |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
camera: new ImCamera(), // 摄像头和麦克风 |
||||
|
webrtc: new ImWebRtc(), // webrtc相关 |
||||
|
API: new RtcPrivateApi(), // API |
||||
|
audio: new Audio(), // 呼叫音频 |
||||
|
showRoom: false, |
||||
|
friend: {}, |
||||
|
isHost: false, // 是否发起人 |
||||
|
state: "CLOSE", // CLOSE:关闭 WAITING:等待呼叫或接听 CHATING:聊天中 ERROR:出现异常 |
||||
|
mode: 'video', // 模式 video:视频聊 voice:语音聊天 |
||||
|
localStream: null, // 本地视频流 |
||||
|
remoteStream: null, // 对方视频流 |
||||
|
videoTime: 0, |
||||
|
videoTimer: null, |
||||
|
heartbeatTimer: null, |
||||
|
candidates: [], |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
open(rtcInfo) { |
||||
|
this.showRoom = true; |
||||
|
this.mode = rtcInfo.mode; |
||||
|
this.isHost = rtcInfo.isHost; |
||||
|
this.friend = rtcInfo.friend; |
||||
|
if (this.isHost) { |
||||
|
this.onCall(); |
||||
|
} |
||||
|
}, |
||||
|
initAudio() { |
||||
|
let url = require(`@/assets/audio/call.wav`); |
||||
|
this.audio.src = url; |
||||
|
this.audio.loop = true; |
||||
|
}, |
||||
|
initRtc() { |
||||
|
this.webrtc.init(this.configuration) |
||||
|
this.webrtc.setupPeerConnection((stream) => { |
||||
|
this.$refs.remoteVideo.srcObject = stream; |
||||
|
this.remoteStream = stream; |
||||
|
}) |
||||
|
// 监听候选信息 |
||||
|
this.webrtc.onIcecandidate((candidate) => { |
||||
|
if (this.state == "CHATING") { |
||||
|
// 连接已就绪,直接发送 |
||||
|
this.API.sendCandidate(this.friend.id, candidate); |
||||
|
} else { |
||||
|
// 连接未就绪,缓存起来,连接后再发送 |
||||
|
this.candidates.push(candidate) |
||||
|
} |
||||
|
}) |
||||
|
// 监听连接成功状态 |
||||
|
this.webrtc.onStateChange((state) => { |
||||
|
if (state == "connected") { |
||||
|
console.log("webrtc连接成功") |
||||
|
} else if (state == "disconnected") { |
||||
|
console.log("webrtc连接断开") |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
onCall() { |
||||
|
if (!this.checkDevEnable()) { |
||||
|
this.close(); |
||||
|
} |
||||
|
// 初始化webrtc |
||||
|
this.initRtc(); |
||||
|
// 启动心跳 |
||||
|
this.startHeartBeat(); |
||||
|
// 打开摄像头 |
||||
|
this.openStream().finally(() => { |
||||
|
this.webrtc.setStream(this.localStream); |
||||
|
this.webrtc.createOffer().then((offer) => { |
||||
|
// 发起呼叫 |
||||
|
this.API.call(this.friend.id, this.mode, offer).then(() => { |
||||
|
// 直接进入聊天状态 |
||||
|
this.state = "WAITING"; |
||||
|
// 播放呼叫铃声 |
||||
|
this.audio.play(); |
||||
|
}).catch(()=>{ |
||||
|
this.close(); |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}, |
||||
|
onAccept() { |
||||
|
if (!this.checkDevEnable()) { |
||||
|
this.API.failed(this.friend.id, "对方设备不支持通话") |
||||
|
this.close(); |
||||
|
return; |
||||
|
} |
||||
|
// 进入房间 |
||||
|
this.showRoom = true; |
||||
|
this.state = "CHATING"; |
||||
|
// 停止呼叫铃声 |
||||
|
this.audio.pause(); |
||||
|
// 初始化webrtc |
||||
|
this.initRtc(); |
||||
|
// 打开摄像头 |
||||
|
this.openStream().finally(() => { |
||||
|
this.webrtc.setStream(this.localStream); |
||||
|
this.webrtc.createAnswer(this.offer).then((answer) => { |
||||
|
this.API.accept(this.friend.id, answer); |
||||
|
// 记录时长 |
||||
|
this.startChatTime(); |
||||
|
// 清理定时器 |
||||
|
this.waitTimer && clearTimeout(this.waitTimer); |
||||
|
}) |
||||
|
}) |
||||
|
}, |
||||
|
onReject() { |
||||
|
console.log("onReject") |
||||
|
// 退出通话 |
||||
|
this.API.reject(this.friend.id); |
||||
|
// 退出 |
||||
|
this.close(); |
||||
|
}, |
||||
|
onHandup() { |
||||
|
this.API.handup(this.friend.id) |
||||
|
this.$message.success("您已挂断,通话结束") |
||||
|
this.close(); |
||||
|
}, |
||||
|
onCancel() { |
||||
|
this.API.cancel(this.friend.id) |
||||
|
this.$message.success("已取消呼叫,通话结束") |
||||
|
this.close(); |
||||
|
}, |
||||
|
onRTCMessage(msg) { |
||||
|
// 除了发起通话,如果在关闭状态就无需处理 |
||||
|
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE && |
||||
|
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO && |
||||
|
this.isClose) { |
||||
|
return; |
||||
|
} |
||||
|
// RTC信令处理 |
||||
|
switch (msg.type) { |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE: |
||||
|
this.onRTCCall(msg, 'voice') |
||||
|
break; |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO: |
||||
|
this.onRTCCall(msg, 'video') |
||||
|
break; |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_ACCEPT: |
||||
|
this.onRTCAccept(msg) |
||||
|
break; |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_REJECT: |
||||
|
this.onRTCReject(msg) |
||||
|
break; |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_CANCEL: |
||||
|
this.onRTCCancel(msg) |
||||
|
break; |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_FAILED: |
||||
|
this.onRTCFailed(msg) |
||||
|
break; |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_HANDUP: |
||||
|
this.onRTCHandup(msg) |
||||
|
break; |
||||
|
case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE: |
||||
|
this.onRTCCandidate(msg) |
||||
|
break; |
||||
|
} |
||||
|
}, |
||||
|
onRTCCall(msg, mode) { |
||||
|
this.offer = JSON.parse(msg.content); |
||||
|
this.isHost = false; |
||||
|
this.mode = mode; |
||||
|
this.$http({ |
||||
|
url: `/friend/find/${msg.sendId}`, |
||||
|
method: 'get' |
||||
|
}).then((friend) => { |
||||
|
this.friend = friend; |
||||
|
this.state = "WAITING"; |
||||
|
this.audio.play(); |
||||
|
this.startHeartBeat(); |
||||
|
// 30s未接听自动挂掉 |
||||
|
this.waitTimer = setTimeout(() => { |
||||
|
this.API.failed(this.friend.id,"对方无应答"); |
||||
|
this.$message.error("您未接听"); |
||||
|
this.close(); |
||||
|
}, 30000) |
||||
|
}) |
||||
|
}, |
||||
|
onRTCAccept(msg) { |
||||
|
if (msg.selfSend) { |
||||
|
// 在其他设备接听 |
||||
|
this.$message.success("已在其他设备接听"); |
||||
|
this.close(); |
||||
|
} else { |
||||
|
// 对方接受了的通话 |
||||
|
let offer = JSON.parse(msg.content); |
||||
|
this.webrtc.setRemoteDescription(offer); |
||||
|
// 状态为聊天中 |
||||
|
this.state = 'CHATING' |
||||
|
// 停止播放语音 |
||||
|
this.audio.pause(); |
||||
|
// 发送candidate |
||||
|
this.candidates.forEach((candidate) => { |
||||
|
this.API.sendCandidate(this.friend.id, candidate); |
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
}, |
||||
|
onRTCReject(msg) { |
||||
|
if (msg.selfSend) { |
||||
|
this.$message.success("已在其他设备拒绝"); |
||||
|
this.close(); |
||||
|
} else { |
||||
|
this.$message.error("对方拒绝了您的通话请求"); |
||||
|
this.close(); |
||||
|
} |
||||
|
}, |
||||
|
onRTCFailed(msg) { |
||||
|
// 呼叫失败 |
||||
|
this.$message.error(msg.content) |
||||
|
this.close(); |
||||
|
}, |
||||
|
onRTCCancel() { |
||||
|
// 对方取消通话 |
||||
|
this.$message.success("对方取消了呼叫"); |
||||
|
this.close(); |
||||
|
}, |
||||
|
onRTCHandup() { |
||||
|
// 对方挂断 |
||||
|
this.$message.success("对方已挂断"); |
||||
|
this.close(); |
||||
|
}, |
||||
|
onRTCCandidate(msg) { |
||||
|
let candidate = JSON.parse(msg.content); |
||||
|
this.webrtc.addIceCandidate(candidate); |
||||
|
}, |
||||
|
|
||||
|
openStream() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
if (this.isVideo) { |
||||
|
// 打开摄像头+麦克风 |
||||
|
this.camera.openVideo().then((stream) => { |
||||
|
this.localStream = stream; |
||||
|
this.$nextTick(() => { |
||||
|
this.$refs.localVideo.srcObject = stream; |
||||
|
this.$refs.localVideo.muted = true; |
||||
|
}) |
||||
|
resolve(stream); |
||||
|
}).catch((e) => { |
||||
|
this.$message.error("打开摄像头失败") |
||||
|
console.log("本摄像头打开失败:" + e.message) |
||||
|
reject(e); |
||||
|
}) |
||||
|
} else { |
||||
|
// 打开麦克风 |
||||
|
this.camera.openAudio().then((stream) => { |
||||
|
this.localStream = stream; |
||||
|
this.$refs.localVideo.srcObject = stream; |
||||
|
resolve(stream); |
||||
|
}).catch((e) => { |
||||
|
this.$message.error("打开麦克风失败") |
||||
|
console.log("打开麦克风失败:" + e.message) |
||||
|
reject(e); |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
startChatTime() { |
||||
|
this.videoTime = 0; |
||||
|
this.videoTimer && clearInterval(this.videoTimer); |
||||
|
this.videoTimer = setInterval(() => { |
||||
|
this.videoTime++; |
||||
|
}, 1000) |
||||
|
}, |
||||
|
checkDevEnable() { |
||||
|
// 检测摄像头 |
||||
|
if (!this.camera.isEnable()) { |
||||
|
this.message.error("访问摄像头失败"); |
||||
|
return false; |
||||
|
} |
||||
|
// 检测webrtc |
||||
|
if (!this.webrtc.isEnable()) { |
||||
|
this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC"); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
}, |
||||
|
startHeartBeat() { |
||||
|
// 每15s推送一次心跳 |
||||
|
this.heartbeatTimer && clearInterval(this.heartbeatTimer); |
||||
|
this.heartbeatTimer = setInterval(() => { |
||||
|
this.API.heartbeat(this.friend.id); |
||||
|
}, 15000) |
||||
|
}, |
||||
|
close() { |
||||
|
this.showRoom = false; |
||||
|
this.camera.close(); |
||||
|
this.webrtc.close(); |
||||
|
this.audio.pause(); |
||||
|
this.videoTime = 0; |
||||
|
this.videoTimer && clearInterval(this.videoTimer); |
||||
|
this.heartbeatTimer && clearInterval(this.heartbeatTimer); |
||||
|
this.waitTimer && clearTimeout(this.waitTimer); |
||||
|
this.videoTimer = null; |
||||
|
this.heartbeatTimer = null; |
||||
|
this.waitTimer = null; |
||||
|
this.state = 'CLOSE'; |
||||
|
this.candidates = []; |
||||
|
}, |
||||
|
onQuit() { |
||||
|
if (this.isChating) { |
||||
|
this.onHandup() |
||||
|
} else if (this.isWaiting) { |
||||
|
this.onCancel(); |
||||
|
} else { |
||||
|
this.close(); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
title() { |
||||
|
let strTitle = `${this.modeText}通话-${this.friend.nickName}`; |
||||
|
if (this.isChating) { |
||||
|
strTitle += `(${this.currentTime})`; |
||||
|
} else if (this.isWaiting) { |
||||
|
strTitle += `(呼叫中)`; |
||||
|
} |
||||
|
return strTitle; |
||||
|
}, |
||||
|
currentTime() { |
||||
|
let min = Math.floor(this.videoTime / 60); |
||||
|
let sec = this.videoTime % 60; |
||||
|
let strTime = min < 10 ? "0" : ""; |
||||
|
strTime += min; |
||||
|
strTime += ":" |
||||
|
strTime += sec < 10 ? "0" : ""; |
||||
|
strTime += sec; |
||||
|
return strTime; |
||||
|
}, |
||||
|
configuration() { |
||||
|
const iceServers = this.$store.state.configStore.webrtc.iceServers; |
||||
|
return { |
||||
|
iceServers: iceServers |
||||
|
} |
||||
|
}, |
||||
|
isVideo() { |
||||
|
return this.mode == "video" |
||||
|
}, |
||||
|
modeText() { |
||||
|
return this.isVideo ? "视频" : "语音"; |
||||
|
}, |
||||
|
isChating() { |
||||
|
return this.state == "CHATING"; |
||||
|
}, |
||||
|
isWaiting() { |
||||
|
return this.state == "WAITING"; |
||||
|
}, |
||||
|
isClose() { |
||||
|
return this.state == "CLOSE"; |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
// 初始化音频文件 |
||||
|
this.initAudio(); |
||||
|
}, |
||||
|
created() { |
||||
|
// 监听页面刷新事件 |
||||
|
window.addEventListener('beforeunload', () => { |
||||
|
this.onQuit(); |
||||
|
}); |
||||
|
}, |
||||
|
beforeUnmount() { |
||||
|
this.onQuit(); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.rtc-private-video { |
||||
|
position: relative; |
||||
|
|
||||
|
.el-loading-text { |
||||
|
color: white !important; |
||||
|
font-size: 16px !important; |
||||
|
} |
||||
|
|
||||
|
.path { |
||||
|
stroke: white !important; |
||||
|
} |
||||
|
|
||||
|
.rtc-video-box { |
||||
|
position: relative; |
||||
|
border: #4880b9 solid 1px; |
||||
|
background-color: #eeeeee; |
||||
|
|
||||
|
.rtc-video-friend { |
||||
|
height: 70vh; |
||||
|
|
||||
|
.friend-head-image { |
||||
|
position: absolute; |
||||
|
} |
||||
|
|
||||
|
video { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: cover; |
||||
|
transform: rotateY(180deg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rtc-video-mine { |
||||
|
position: absolute; |
||||
|
z-index: 99999; |
||||
|
width: 25vh; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
box-shadow: 0px 0px 5px #ccc; |
||||
|
background-color: #cccccc; |
||||
|
|
||||
|
video { |
||||
|
width: 100%; |
||||
|
object-fit: cover; |
||||
|
transform: rotateY(180deg); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rtc-voice-box { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
border: #4880b9 solid 1px; |
||||
|
width: 100%; |
||||
|
height: 50vh; |
||||
|
padding-top: 10vh; |
||||
|
background-color: aliceblue; |
||||
|
|
||||
|
.rtc-voice-name { |
||||
|
text-align: center; |
||||
|
font-size: 22px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rtc-control-bar { |
||||
|
display: flex; |
||||
|
justify-content: space-around; |
||||
|
padding: 10px; |
||||
|
|
||||
|
.icon { |
||||
|
font-size: 50px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</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,40 @@ |
|||||
|
|
||||
|
// 是否普通消息
|
||||
|
let isNormal = function(type){ |
||||
|
return type>=0 && type < 10; |
||||
|
} |
||||
|
|
||||
|
// 是否状态消息
|
||||
|
let isStatus = function(type){ |
||||
|
return type>=10 && type < 20; |
||||
|
} |
||||
|
|
||||
|
// 是否提示消息
|
||||
|
let isTip = function(type){ |
||||
|
return type>=20 && type < 30; |
||||
|
} |
||||
|
|
||||
|
// 操作交互类消息
|
||||
|
let isAction = function(type){ |
||||
|
return type>=40 && type < 50; |
||||
|
} |
||||
|
|
||||
|
// 单人通话信令
|
||||
|
let isRtcPrivate = function(type){ |
||||
|
return type>=100 && type < 300; |
||||
|
} |
||||
|
|
||||
|
// 多人通话信令
|
||||
|
let isRtcGroup = function(type){ |
||||
|
return type>=200 && type < 400; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export { |
||||
|
isNormal, |
||||
|
isStatus, |
||||
|
isTip, |
||||
|
isAction, |
||||
|
isRtcPrivate, |
||||
|
isRtcGroup |
||||
|
} |
||||
@ -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> |
||||
@ -1,118 +0,0 @@ |
|||||
<template> |
|
||||
<!-- for wx audit --> |
|
||||
<view class="page user-search"> |
|
||||
<view class="search-bar"> |
|
||||
<uni-search-bar v-model="searchText" :focus="true" @confirm="onSearch()" can |
|
||||
cancelButton="none" ceholder="用户名/昵称"></uni-search-bar> |
|
||||
</view> |
|
||||
<view class="user-items"> |
|
||||
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> |
|
||||
<view v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id"> |
|
||||
<view class="user-item"> |
|
||||
<head-image :id="user.id" :name="user.nickName" |
|
||||
:online="user.online" :url="user.headImage" |
|
||||
:size="100"></head-image> |
|
||||
<view class="user-name">{{ user.nickName}}</view> |
|
||||
<view class="user-btns"> |
|
||||
<button type="primary" v-show="!isFriend(user.id)" size="mini" |
|
||||
@click.stop="onAddFriend(user)">加为好友</button> |
|
||||
<button type="default" v-show="isFriend(user.id)" size="mini" disabled>已添加</button> |
|
||||
</view> |
|
||||
</view> |
|
||||
</view> |
|
||||
</scroll-view> |
|
||||
</view> |
|
||||
</view> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
searchText: "", |
|
||||
users: [] |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
onSearch() { |
|
||||
this.$http({ |
|
||||
url: "/user/findByName?name=" + this.searchText, |
|
||||
method: "GET" |
|
||||
}).then((data) => { |
|
||||
this.users = data; |
|
||||
}) |
|
||||
}, |
|
||||
onAddFriend(user) { |
|
||||
this.$http({ |
|
||||
url: "/friend/add?friendId=" + user.id, |
|
||||
method: "POST" |
|
||||
}).then((data) => { |
|
||||
let friend = { |
|
||||
id: user.id, |
|
||||
nickName: user.nickName, |
|
||||
headImage: user.headImage, |
|
||||
online: user.online |
|
||||
} |
|
||||
this.$store.commit("addFriend", friend); |
|
||||
uni.showToast({ |
|
||||
title: "添加成功,对方已成为您的好友", |
|
||||
icon: "none" |
|
||||
}) |
|
||||
}) |
|
||||
}, |
|
||||
onShowUserInfo(user) { |
|
||||
uni.navigateTo({ |
|
||||
url: "/pages/common/user-info?id=" + user.id |
|
||||
}) |
|
||||
}, |
|
||||
isFriend(userId) { |
|
||||
let friends = this.$store.state.friendStore.friends; |
|
||||
let friend = friends.find((f) => f.id == userId); |
|
||||
return friend&&!friend.delete; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style scoped lang="scss"> |
|
||||
.user-search { |
|
||||
position: relative; |
|
||||
border: #dddddd solid 1px; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
|
|
||||
.search-bar { |
|
||||
background: white; |
|
||||
} |
|
||||
.user-items{ |
|
||||
position: relative; |
|
||||
flex: 1; |
|
||||
overflow: hidden; |
|
||||
.user-item { |
|
||||
height: 120rpx; |
|
||||
display: flex; |
|
||||
margin-bottom: 1rpx; |
|
||||
position: relative; |
|
||||
padding: 0 30rpx ; |
|
||||
align-items: center; |
|
||||
background-color: white; |
|
||||
white-space: nowrap; |
|
||||
|
|
||||
.user-name { |
|
||||
flex:1; |
|
||||
padding-left: 20rpx; |
|
||||
font-size: 30rpx; |
|
||||
font-weight: 600; |
|
||||
line-height: 60rpx; |
|
||||
white-space: nowrap; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.scroll-bar { |
|
||||
height: 100%; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
</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); |
||||
|
}); |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue