[*] 结构调整
This commit is contained in:
49
taoyao-signal-server/taoyao-server/pom.xml
Normal file
49
taoyao-signal-server/taoyao-server/pom.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.acgist</groupId>
|
||||
<artifactId>taoyao</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>taoyao-server</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>taoyao-server</name>
|
||||
<description>启动服务:启动服务</description>
|
||||
|
||||
<properties>
|
||||
<taoyao.maven.basedir>${project.parent.basedir}</taoyao.maven.basedir>
|
||||
<taoyao.maven.skip.assembly>false</taoyao.maven.skip.assembly>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.acgist</groupId>
|
||||
<artifactId>taoyao-media</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.acgist.taoyao.main.TaoyaoApplication</mainClass>
|
||||
<addClasspath>true</addClasspath>
|
||||
<classpathPrefix>./</classpathPrefix>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.acgist.taoyao.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.acgist.taoyao.interceptor.SecurityInterceptor;
|
||||
import com.acgist.taoyao.interceptor.SlowInterceptor;
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Configuration
|
||||
public class TaoyaoAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SlowInterceptor slowInterceptor() {
|
||||
return new SlowInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "taoyao.security", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@ConditionalOnMissingBean
|
||||
public SecurityInterceptor securityInterceptor() {
|
||||
return new SecurityInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.acgist.taoyao.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.acgist.taoyao.boot.model.Message;
|
||||
import com.acgist.taoyao.boot.property.MediaProperties;
|
||||
import com.acgist.taoyao.boot.property.WebrtcProperties;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Tag(name = "配置", description = "配置管理")
|
||||
@RestController
|
||||
@RequestMapping("/config")
|
||||
public class ConfigController {
|
||||
|
||||
@Autowired
|
||||
private MediaProperties mediaProperties;
|
||||
@Autowired
|
||||
private WebrtcProperties webrtcProperties;
|
||||
|
||||
@Operation(summary = "媒体配置", description = "媒体配置")
|
||||
@GetMapping("/media")
|
||||
@ApiResponse(content = @Content(schema = @Schema(implementation = MediaProperties.class)))
|
||||
public Message media() {
|
||||
return Message.success(this.mediaProperties);
|
||||
}
|
||||
|
||||
@Operation(summary = "WebRTC配置", description = "WebRTC配置")
|
||||
@GetMapping("/webrtc")
|
||||
@ApiResponse(content = @Content(schema = @Schema(implementation = WebrtcProperties.class)))
|
||||
public Message webrtc() {
|
||||
return Message.success(this.webrtcProperties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.acgist.taoyao.interceptor;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.acgist.taoyao.boot.interceptor.InterceptorAdapter;
|
||||
import com.acgist.taoyao.boot.property.SecurityProperties;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 安全拦截
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Slf4j
|
||||
public class SecurityInterceptor extends InterceptorAdapter {
|
||||
|
||||
@Autowired
|
||||
private SecurityProperties securityProperties;
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "安全拦截";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] pathPattern() {
|
||||
return new String[] { "/**" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Integer.MIN_VALUE + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
if(this.permit(request) || this.authorization(request)) {
|
||||
return true;
|
||||
}
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic Realm=\"" + this.securityProperties.getRealm() + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request 请求
|
||||
*
|
||||
* @return 是否许可请求
|
||||
*/
|
||||
private boolean permit(HttpServletRequest request) {
|
||||
final String uri = request.getRequestURI();
|
||||
if(ArrayUtils.isEmpty(this.securityProperties.getPermit())) {
|
||||
return false;
|
||||
}
|
||||
for (String permit : this.securityProperties.getPermit()) {
|
||||
if(StringUtils.startsWith(uri, permit)) {
|
||||
log.debug("授权成功(许可请求):{}-{}", uri, permit);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request 请求
|
||||
*
|
||||
* @return 是否授权成功
|
||||
*/
|
||||
private boolean authorization(HttpServletRequest request) {
|
||||
final String uri = request.getRequestURI();
|
||||
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if(StringUtils.isEmpty(authorization)) {
|
||||
return false;
|
||||
}
|
||||
if(!StringUtils.startsWithIgnoreCase(authorization, SecurityProperties.BASIC)) {
|
||||
return false;
|
||||
}
|
||||
authorization = authorization.substring(SecurityProperties.BASIC.length()).strip();
|
||||
authorization = new String(Base64.getDecoder().decode(authorization));
|
||||
if(!authorization.equals(this.securityProperties.getAuthorization())) {
|
||||
return false;
|
||||
}
|
||||
log.debug("授权成功(Basic):{}-{}", uri, authorization);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.acgist.taoyao.interceptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.acgist.taoyao.boot.interceptor.InterceptorAdapter;
|
||||
import com.acgist.taoyao.boot.property.TaoyaoProperties;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 过慢请求统计拦截
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Slf4j
|
||||
public class SlowInterceptor extends InterceptorAdapter {
|
||||
|
||||
/**
|
||||
* 时间
|
||||
*/
|
||||
private ThreadLocal<Long> local = new ThreadLocal<>();
|
||||
|
||||
@Autowired
|
||||
private TaoyaoProperties taoyaoProperties;
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "过慢请求统计拦截";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] pathPattern() {
|
||||
return new String[] { "/**" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
this.local.set(System.currentTimeMillis());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) throws Exception {
|
||||
final long duration;
|
||||
final Long last = this.local.get();
|
||||
if(last != null && (duration = System.currentTimeMillis() - last) > this.taoyaoProperties.getTimeout()) {
|
||||
log.info("请求执行时间过慢:{}-{}", request.getRequestURI(), duration);
|
||||
}
|
||||
this.local.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.acgist.taoyao.main;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
@ComponentScan(basePackages = "com.acgist.taoyao")
|
||||
@SpringBootApplication
|
||||
public class TaoyaoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TaoyaoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
taoyao:
|
||||
security:
|
||||
permit: /favicon.ico,/error
|
||||
@@ -0,0 +1,139 @@
|
||||
server:
|
||||
port: 8888
|
||||
http2:
|
||||
enabled: true
|
||||
ssl:
|
||||
key-alias: taoyao
|
||||
key-store: classpath:taoyao.jks
|
||||
key-store-password: 123456
|
||||
key-password: 123456
|
||||
tomcat:
|
||||
thread:
|
||||
max: 128
|
||||
min-spare: 4
|
||||
remoteip:
|
||||
host-header: X-Forwarded-Host
|
||||
port-header: X-Forwarded-Port
|
||||
protocol-header: X-Forwarded-Proto
|
||||
remote-ip-header: X-Forwarded-For
|
||||
# servlet:
|
||||
# context-path: /taoyao
|
||||
spring:
|
||||
profiles:
|
||||
active: dev
|
||||
application:
|
||||
name: taoyao-server
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 256MB
|
||||
max-request-size: 256MB
|
||||
task:
|
||||
execution:
|
||||
pool:
|
||||
core-size: 8
|
||||
max-size: 128
|
||||
keep-alive: 60s
|
||||
queue-capacity: 100000
|
||||
allow-core-thread-timeout: true
|
||||
shutdown:
|
||||
await-termination: true
|
||||
await-termination-period: 60s
|
||||
thread-name-prefix: ${spring.application.name}-
|
||||
scheduling:
|
||||
pool:
|
||||
size: 4
|
||||
shutdown:
|
||||
await-termination: true
|
||||
await-termination-period: 60s
|
||||
thread-name-prefix: ${spring.application.name}-scheduling-
|
||||
taoyao:
|
||||
url: https://gitee.com/acgist/taoyao
|
||||
name: 桃夭
|
||||
timeout: 5000
|
||||
version: 1.0.0
|
||||
description: WebRTC信令服务
|
||||
server-id: 1
|
||||
id:
|
||||
sn: 0
|
||||
max-index: 999999
|
||||
# 媒体配置
|
||||
media:
|
||||
audio:
|
||||
format: OPUS
|
||||
sample-size: 16
|
||||
sample-rate: 32000
|
||||
video:
|
||||
format: H264
|
||||
bitrate: 1200
|
||||
frame-rate: 24
|
||||
resolution: 1920*1080
|
||||
# 高清视频
|
||||
high-video:
|
||||
format: H264
|
||||
bitrate: 1000
|
||||
frame-rate: 18
|
||||
resolution: 1280*720
|
||||
# 标清视频
|
||||
norm-video:
|
||||
format: H264
|
||||
bitrate: 800
|
||||
frame-rate: 16
|
||||
resolution: 720*480
|
||||
# 流畅视频
|
||||
flow-video:
|
||||
format: H264
|
||||
bitrate: 600
|
||||
frame-rate: 16
|
||||
resolution: 640*480
|
||||
# WebRTC配置
|
||||
webrtc:
|
||||
# 架构模式
|
||||
framework: MESH
|
||||
# 媒体端口范围
|
||||
min-port: 45535
|
||||
max-port: 65535
|
||||
# 公共服务
|
||||
stun:
|
||||
- stun:stun1.l.google.com:19302
|
||||
- stun:stun2.l.google.com:19302
|
||||
- stun:stun3.l.google.com:19302
|
||||
- stun:stun4.l.google.com:19302
|
||||
# 自己搭建:coturn
|
||||
turn:
|
||||
- turn:127.0.0.1:8888
|
||||
- turn:127.0.0.1:8888
|
||||
- turn:127.0.0.1:8888
|
||||
- turn:127.0.0.1:8888
|
||||
# KMS服务配置:可以部署多个简单实现负载均衡
|
||||
kms:
|
||||
host: 192.168.1.100
|
||||
port: 18888
|
||||
schema: wss
|
||||
websocket: /websocket.signal
|
||||
username: taoyao
|
||||
password: taoyao
|
||||
# Moon架构配置
|
||||
moon:
|
||||
audio-mix: true
|
||||
# 信令服务配置
|
||||
signal:
|
||||
host: 192.168.1.100
|
||||
port: ${server.port:8888}
|
||||
schema: wss
|
||||
websocket: /websocket.signal
|
||||
record:
|
||||
storage: /data/record
|
||||
security:
|
||||
enabled: true
|
||||
realm: taoyao
|
||||
permit: /v3/api-docs/,/swagger-ui/,/swagger-ui.html,/favicon.ico,/error
|
||||
username: taoyao
|
||||
password: taoyao
|
||||
scheduled:
|
||||
session: 0 * * * * ?
|
||||
node-list:
|
||||
- host: localhost
|
||||
port: 8888
|
||||
username: ${taoyao.security.username:taoyao}
|
||||
password: ${taoyao.security.username:taoyao}
|
||||
|
||||
1
taoyao-signal-server/taoyao-server/src/main/resources/static/css/font.min.css
vendored
Normal file
1
taoyao-signal-server/taoyao-server/src/main/resources/static/css/font.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
@charset "UTF-8";
|
||||
/**文本选择*/
|
||||
::selection{background:#222;color:#fff;}
|
||||
/**字体大小*/
|
||||
@media screen and (min-width:800px){html{font-size:16px;}}
|
||||
@media screen and (min-width:1200px){html{font-size:18px;}}
|
||||
@media screen and (min-width:1600px){html{font-size:20px;}}
|
||||
/**默认样式*/
|
||||
*{margin:0;padding:0;border:none;outline:none;box-sizing:content-box;}
|
||||
html{background:#EBEBEB;}
|
||||
html,body{font-family:Arial,Consolas,SimSun,"宋体";color:#222;font-weight:normal;}
|
||||
body{width:100%;height:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);font-size:1rem;line-height:1.4em;}
|
||||
a{color:#1155AA;text-decoration:none;}
|
||||
a:link{text-decoration:none;}
|
||||
a:hover{color:#4477EE;text-decoration:none;}
|
||||
a:visited{text-decoration:none;}
|
||||
img{border:0;}
|
||||
ol,ul,li{list-style:none;}
|
||||
input[type=text],textarea{box-shadow:0px 0px 3px 0px rgba(0,0,0,0.1) inset;border:1px solid rgba(0,0,0,0.1)!important;}
|
||||
input[type=text]:focus,textarea:focus,input[type=text]:hover,textarea:hover{border:1px solid #1155AA!important;}
|
||||
input::-webkit-calendar-picker-indicator{color:#1155AA;background:none;}
|
||||
/**容器*/
|
||||
.taoyao{text-align:center;}
|
||||
/**直播*/
|
||||
.taoyao .live > .video{width:100%;height:100%;}
|
||||
.taoyao .live .handler{position:fixed;width:100%;bottom:2rem;font-size:2rem;}
|
||||
/**会议*/
|
||||
.taoyao .handler a{cursor:pointer;}
|
||||
.taoyao > .handler{font-size:2rem;padding:1rem 0;width:100%;}
|
||||
.taoyao .list{width:90vw;margin:auto;}
|
||||
.taoyao .meeting{float:left;overflow:hidden;position:relative;width:calc(25% - 2rem);border:1rem solid #fff;}
|
||||
.taoyao .me,.taoyao .meeting:hover{border-color:#060;}
|
||||
.taoyao .meeting > .video{height:15vw;}
|
||||
.taoyao .meeting > .video video{width:100%;height:100%;}
|
||||
.taoyao .meeting .handler{position:absolute;bottom:0rem;text-align:center;width:100%;background:rgba(0,0,0,0.2);padding:0.2rem 0;}
|
||||
.taoyao .meeting .handler a{color:#fff;}
|
||||
.taoyao .meeting .handler a:hover{color:#060!important;}
|
||||
.taoyao .meeting .handler a.expel:hover{color:#C00!important;}
|
||||
.taoyao .meeting .handler a.record.active{color:#C00;}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 297 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>桃夭</title>
|
||||
<link rel="stylesheet" type="text/css" href="./css/style.css" />
|
||||
<script type="text/javascript" src="./javascript/taoyao.js"></script>
|
||||
<style type="text/css">
|
||||
a{width:50%;height:100%;position:fixed;text-align:center;line-height:100%;font-size:4rem;display:flex;align-items:center;justify-content:center;}
|
||||
a:last-child{left:50%;}
|
||||
a:hover{color:#fff;background:#060;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="./live.html">直播</a>
|
||||
<a href="./meeting.html">会议</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,835 @@
|
||||
/**
|
||||
* 桃夭WebRTC终端核心功能
|
||||
*
|
||||
* 代码注意:
|
||||
* 1. undefined判断使用两个等号,其他情况使用三个。
|
||||
*/
|
||||
/** 兼容 */
|
||||
const RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
|
||||
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
||||
const RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
|
||||
/** 默认音频配置 */
|
||||
const defaultAudioConfig = {
|
||||
// 设备
|
||||
// deviceId : '',
|
||||
// 音量:0~1
|
||||
volume: 0.5,
|
||||
// 延迟大小(单位毫秒):500毫秒以内较好
|
||||
latency: 0.4,
|
||||
// 采样数:16
|
||||
sampleSize: 16,
|
||||
// 采样率:8000|16000|32000|48000
|
||||
sampleRate: 32000,
|
||||
// 声道数量:1|2
|
||||
channelCount : 1,
|
||||
// 是否开启自动增益:true|false
|
||||
autoGainControl: false,
|
||||
// 是否开启降噪功能:true|false
|
||||
noiseSuppression: true,
|
||||
// 是否开启回音消除:true|false
|
||||
echoCancellation: true,
|
||||
// 消除回音方式:system|browser
|
||||
echoCancellationType: 'system'
|
||||
};
|
||||
/** 默认视频配置 */
|
||||
const defaultVideoConfig = {
|
||||
// 设备
|
||||
// deviceId: '',
|
||||
// 宽度
|
||||
width: 1280,
|
||||
// 高度
|
||||
height: 720,
|
||||
// 帧率
|
||||
frameRate: 24,
|
||||
// 选摄像头:user|left|right|environment
|
||||
facingMode: 'environment'
|
||||
}
|
||||
/** 默认RTCPeerConnection配置 */
|
||||
const defaultRPCConfig = {
|
||||
// ICE代理的服务器
|
||||
iceServers: null,
|
||||
// 传输通道绑定策略:balanced|max-compat|max-bundle
|
||||
bundlePolicy: 'balanced',
|
||||
// RTCP多路复用策略:require|negotiate
|
||||
rtcpMuxPolicy: 'require',
|
||||
// ICE传输策略:all|relay
|
||||
iceTransportPolicy: 'all'
|
||||
// ICE候选个数
|
||||
// iceCandidatePoolSize: 8
|
||||
}
|
||||
/** 信令配置 */
|
||||
const signalConfig = {
|
||||
/** 当前终端SN */
|
||||
sn: 'taoyao',
|
||||
/** 当前版本 */
|
||||
version: '1.0.0',
|
||||
// 信令授权
|
||||
username: 'taoyao',
|
||||
password: 'taoyao'
|
||||
};
|
||||
/** 信令协议 */
|
||||
const signalProtocol = {
|
||||
/** 直播信令 */
|
||||
live: {
|
||||
},
|
||||
/** 媒体信令 */
|
||||
media: {
|
||||
/** 发布 */
|
||||
publish: 5000,
|
||||
/** 订阅 */
|
||||
subscribe: 5002,
|
||||
/** 候选 */
|
||||
offer: 5997,
|
||||
/** Answer */
|
||||
answer: 5998,
|
||||
/** 候选 */
|
||||
candidate: 5999
|
||||
},
|
||||
/** 终端信令 */
|
||||
client: {
|
||||
/** 注册 */
|
||||
register: 2000,
|
||||
/** 下发配置 */
|
||||
config: 2004,
|
||||
/** 心跳 */
|
||||
heartbeat: 2005,
|
||||
/** 重启终端 */
|
||||
reboot: 2997,
|
||||
},
|
||||
/** 会议信令 */
|
||||
meeting: {
|
||||
/** 创建会议信令 */
|
||||
create: 4000,
|
||||
/** 进入会议信令 */
|
||||
enter: 4002,
|
||||
},
|
||||
/** 平台信令 */
|
||||
platform: {
|
||||
/** 异常 */
|
||||
error: 1999
|
||||
},
|
||||
/** 当前索引 */
|
||||
index: 100000,
|
||||
/** 最小索引 */
|
||||
minIndex: 100000,
|
||||
/** 最大索引 */
|
||||
maxIndex: 999999,
|
||||
/** 生成索引 */
|
||||
buildId: function() {
|
||||
if(this.index++ >= this.maxIndex) {
|
||||
this.index = this.minIndex;
|
||||
}
|
||||
return Date.now() + '' + this.index;
|
||||
},
|
||||
/** 生成信令消息 */
|
||||
buildProtocol: function(pid, body, id) {
|
||||
let message = {
|
||||
header: {
|
||||
v: signalConfig.version,
|
||||
id: id || this.buildId(),
|
||||
sn: signalConfig.sn,
|
||||
pid: pid,
|
||||
},
|
||||
'body': body
|
||||
};
|
||||
return message;
|
||||
}
|
||||
};
|
||||
/** 信令通道 */
|
||||
const signalChannel = {
|
||||
/** 桃夭 */
|
||||
taoyao: null,
|
||||
/** 通道 */
|
||||
channel: null,
|
||||
/** 地址 */
|
||||
address: null,
|
||||
/** 回调 */
|
||||
callback: null,
|
||||
/** 回调事件 */
|
||||
callbackMapping: new Map(),
|
||||
/** 心跳时间 */
|
||||
heartbeatTime: 30 * 1000,
|
||||
/** 心跳定时器 */
|
||||
heartbeatTimer: null,
|
||||
/** 重连定时器 */
|
||||
reconnectTimer: null,
|
||||
/** 防止重复重连 */
|
||||
lockReconnect: false,
|
||||
/** 当前重连时间 */
|
||||
connectionTimeout: 5 * 1000,
|
||||
/** 最小重连时间 */
|
||||
minReconnectionDelay: 5 * 1000,
|
||||
/** 最大重连时间 */
|
||||
maxReconnectionDelay: 60 * 1000,
|
||||
/** 重连失败时间增长倍数 */
|
||||
reconnectionDelayGrowFactor: 2,
|
||||
/** 心跳 */
|
||||
heartbeat: function() {
|
||||
let self = this;
|
||||
if(self.heartbeatTimer) {
|
||||
clearTimeout(self.heartbeatTimer);
|
||||
}
|
||||
self.heartbeatTimer = setTimeout(function() {
|
||||
// 电池:navigator.getBattery()
|
||||
if (self.channel && self.channel.readyState === WebSocket.OPEN) {
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.client.heartbeat,
|
||||
{
|
||||
signal: 100,
|
||||
battery: 100
|
||||
}
|
||||
));
|
||||
self.heartbeat();
|
||||
} else {
|
||||
console.warn('发送心跳失败', self.channel);
|
||||
}
|
||||
}, self.heartbeatTime);
|
||||
},
|
||||
/** 连接 */
|
||||
connect: function(address, callback, reconnection = true) {
|
||||
let self = this;
|
||||
self.address = address;
|
||||
self.callback = callback;
|
||||
return new Promise((resolve, reject) => {
|
||||
console.debug('连接信令通道', address);
|
||||
self.channel = new WebSocket(address);
|
||||
self.channel.onopen = function(e) {
|
||||
console.debug('打开信令通道', e);
|
||||
// 注册终端
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.client.register,
|
||||
{
|
||||
ip: null,
|
||||
mac: null,
|
||||
signal: 100,
|
||||
battery: 100,
|
||||
username: signalConfig.username,
|
||||
password: signalConfig.password
|
||||
}
|
||||
));
|
||||
// 重置时间
|
||||
self.connectionTimeout = self.minReconnectionDelay
|
||||
// 开始心跳
|
||||
self.heartbeat();
|
||||
// 成功回调
|
||||
resolve(e);
|
||||
};
|
||||
self.channel.onclose = function(e) {
|
||||
console.error('信令通道关闭', self.channel, e);
|
||||
if(reconnection) {
|
||||
self.reconnect();
|
||||
}
|
||||
reject(e);
|
||||
};
|
||||
self.channel.onerror = function(e) {
|
||||
console.error('信令通道异常', self.channel, e);
|
||||
if(reconnection) {
|
||||
self.reconnect();
|
||||
}
|
||||
reject(e);
|
||||
};
|
||||
/**
|
||||
* 回调策略:
|
||||
* 1. 如果注册请求回调,同时执行结果返回true不再执行后面所有回调。
|
||||
* 2. 如果注册全局回调,同时执行结果返回true不再执行后面所有回调。
|
||||
* 3. 如果前面所有回调没有返回true执行默认回调。
|
||||
*/
|
||||
self.channel.onmessage = function(e) {
|
||||
console.debug('信令通道消息', e.data);
|
||||
let done = false;
|
||||
let data = JSON.parse(e.data);
|
||||
// 请求回调
|
||||
if(self.callbackMapping.has(data.header.id)) {
|
||||
try {
|
||||
done = self.callbackMapping.get(data.header.id)(data);
|
||||
} finally {
|
||||
self.callbackMapping.delete(data.header.id);
|
||||
}
|
||||
}
|
||||
// 全局回调
|
||||
if(self.callback) {
|
||||
done = self.callback(data);
|
||||
}
|
||||
// 默认回调
|
||||
if(!done) {
|
||||
self.defaultCallback(data);
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
/** 重连 */
|
||||
reconnect: function() {
|
||||
let self = this;
|
||||
if (self.lockReconnect) {
|
||||
return;
|
||||
}
|
||||
self.lockReconnect = true;
|
||||
// 关闭旧的通道
|
||||
if(self.channel && self.channel.readyState === WebSocket.OPEN) {
|
||||
self.channel.close();
|
||||
self.channel = null;
|
||||
}
|
||||
if(self.reconnectTimer) {
|
||||
clearTimeout(self.reconnectTimer);
|
||||
}
|
||||
// 打开定时重连
|
||||
self.reconnectTimer = setTimeout(function() {
|
||||
console.info('信令通道重连', self.address, new Date());
|
||||
self.connect(self.address, self.callback, true);
|
||||
self.lockReconnect = false;
|
||||
}, self.connectionTimeout);
|
||||
if (self.connectionTimeout >= self.maxReconnectionDelay) {
|
||||
self.connectionTimeout = self.maxReconnectionDelay;
|
||||
} else {
|
||||
self.connectionTimeout = self.connectionTimeout * self.reconnectionDelayGrowFactor
|
||||
}
|
||||
},
|
||||
/** 发送消息 */
|
||||
push: function(data, callback) {
|
||||
// 注册回调
|
||||
if(data && callback) {
|
||||
this.callbackMapping.set(data.header.id, callback);
|
||||
}
|
||||
// 发送消息
|
||||
if(data && data.header) {
|
||||
this.channel.send(JSON.stringify(data));
|
||||
} else {
|
||||
this.channel.send(data);
|
||||
}
|
||||
},
|
||||
/** 关闭通道 */
|
||||
close: function() {
|
||||
clearTimeout(this.heartbeatTimer);
|
||||
},
|
||||
/** 默认回调 */
|
||||
defaultCallback: function(data) {
|
||||
console.debug('没有适配信令消息默认处理', data);
|
||||
switch(data.header.pid) {
|
||||
case signalProtocol.media.publish:
|
||||
this.defaultMediaPublish(data);
|
||||
break;
|
||||
case signalProtocol.media.subscribe:
|
||||
this.defaultMediaSubscribe(data);
|
||||
break;
|
||||
case signalProtocol.media.offer:
|
||||
this.defaultMediaOffer(data);
|
||||
break;
|
||||
case signalProtocol.media.answer:
|
||||
this.defaultMediaAnswer(data);
|
||||
break;
|
||||
case signalProtocol.media.candidate:
|
||||
this.defaultMediaCandidate(data);
|
||||
break;
|
||||
case signalProtocol.client.register:
|
||||
break;
|
||||
case signalProtocol.client.config:
|
||||
this.defaultClientConfig(data);
|
||||
break;
|
||||
case signalProtocol.client.heartbeat:
|
||||
break;
|
||||
case signalProtocol.client.reboot:
|
||||
this.defaultClientReboot(data);
|
||||
break;
|
||||
case signalProtocol.meeting.create:
|
||||
break;
|
||||
case signalProtocol.meeting.enter:
|
||||
this.defaultMeetingEnter(data);
|
||||
break;
|
||||
case signalProtocol.platform.error:
|
||||
console.error('信令发生错误', data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
/** 终端默认回调 */
|
||||
defaultClientConfig: function(data) {
|
||||
let self = this;
|
||||
// 配置终端
|
||||
self.taoyao
|
||||
.configMedia(data.body.media.audio, data.body.media.video)
|
||||
.configWebrtc(data.body.webrtc);
|
||||
// 打开媒体通道
|
||||
let videoId = self.taoyao.videoId;
|
||||
if(videoId) {
|
||||
self.taoyao.buildLocalMedia()
|
||||
.then(stream => {
|
||||
self.taoyao.buildMediaChannel(videoId, stream);
|
||||
})
|
||||
.catch(e => console.error('打开终端媒体失败', e));
|
||||
console.debug('自动打开媒体通道', videoId);
|
||||
} else {
|
||||
console.debug('没有配置本地媒体信息跳过自动打开媒体通道');
|
||||
}
|
||||
},
|
||||
defaultClientReboot: function(data) {
|
||||
console.info('重启终端');
|
||||
location.reload();
|
||||
},
|
||||
/** 默认媒体回调 */
|
||||
defaultMediaPublish: function(data) {
|
||||
},
|
||||
defaultMediaSubscribe: function(data) {
|
||||
let self = this;
|
||||
const from = data.body.from;
|
||||
const remote = this.taoyao.remoteClientFilter(from, true);
|
||||
remote.localMediaChannel.createOffer().then(description => {
|
||||
console.debug('Local Create Offer', description);
|
||||
remote.localMediaChannel.setLocalDescription(description);
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.media.offer,
|
||||
{
|
||||
to: from,
|
||||
sdp: {
|
||||
sdp: description.sdp,
|
||||
type: description.type
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
},
|
||||
defaultMediaOffer: function(data) {
|
||||
let self = this;
|
||||
const from = data.body.from;
|
||||
const remote = this.taoyao.remoteClientFilter(from, true);
|
||||
remote.remoteMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body.sdp));
|
||||
remote.remoteMediaChannel.createAnswer().then(description => {
|
||||
console.debug('Remote Create Answer', description);
|
||||
remote.remoteMediaChannel.setLocalDescription(description);
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.media.answer,
|
||||
{
|
||||
to: from,
|
||||
sdp: {
|
||||
sdp: description.sdp,
|
||||
type: description.type
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
},
|
||||
defaultMediaAnswer: function(data) {
|
||||
const from = data.body.from;
|
||||
const remote = this.taoyao.remoteClientFilter(from, true);
|
||||
remote.localMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body.sdp));
|
||||
},
|
||||
defaultMediaCandidate: function(data) {
|
||||
if(!this.taoyao.checkCandidate(data.body.candidate)) {
|
||||
console.debug('候选缺失要素', data);
|
||||
return;
|
||||
}
|
||||
console.debug('Set ICE Candidate', data.body);
|
||||
const from = data.body.from;
|
||||
const remote = this.taoyao.remoteClientFilter(from, true);
|
||||
if(data.body.type === 'local') {
|
||||
remote.remoteMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
|
||||
} else if(data.body.type === 'remote'){
|
||||
remote.localMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
|
||||
} else if(data.body.type === 'mesh') {
|
||||
remote.localMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
|
||||
// remote.remoteMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
|
||||
} else {
|
||||
console.warn('不支持的候选类型', data.body.type);
|
||||
}
|
||||
},
|
||||
/** 会议默认回调 */
|
||||
defaultMeetingEnter: function(data) {
|
||||
this.taoyao.mediaSubscribe(data.body.sn);
|
||||
}
|
||||
};
|
||||
/** 终端 */
|
||||
function TaoyaoClient(
|
||||
taoyao,
|
||||
sn,
|
||||
shareMediaChannel,
|
||||
audioEnabled,
|
||||
videoEnabled
|
||||
) {
|
||||
/** 桃夭 */
|
||||
this.taoyao = taoyao;
|
||||
/** 终端标识 */
|
||||
this.sn = sn;
|
||||
/** 视频对象 */
|
||||
this.video = null;
|
||||
/** 媒体信息 */
|
||||
this.stream = null;
|
||||
this.audioTrack = [];
|
||||
this.videoTrack = [];
|
||||
/** 媒体状态:是否含有 */
|
||||
this.audioStatus = false;
|
||||
this.videoStatus = false;
|
||||
this.recordStatus = false;
|
||||
/** 本地媒体通道 */
|
||||
this.localMediaChannel = null;
|
||||
/** 远程媒体通道 */
|
||||
this.remoteMediaChannel = null;
|
||||
/** 是否共享媒体通道 */
|
||||
this.shareMediaChannel = shareMediaChannel;
|
||||
/** 媒体状态:是否播放 */
|
||||
this.audioEnabled = audioEnabled == undefined ? true : audioEnabled;
|
||||
this.videoEnabled = videoEnabled == undefined ? true : videoEnabled;
|
||||
/** 重置 */
|
||||
this.reset = function() {
|
||||
}
|
||||
/** 播放视频 */
|
||||
this.play = async function() {
|
||||
await this.video.play();
|
||||
return this;
|
||||
};
|
||||
/** 重新加载 */
|
||||
this.load = async function() {
|
||||
await this.video.load();
|
||||
return this;
|
||||
}
|
||||
/** 暂停视频 */
|
||||
this.pause = async function() {
|
||||
await this.video.pause();
|
||||
return this;
|
||||
};
|
||||
/** 关闭视频 */
|
||||
this.close = async function() {
|
||||
await this.video.close();
|
||||
// TODO:释放连接
|
||||
return this;
|
||||
};
|
||||
/** 设置媒体 */
|
||||
// TODO:stream判断
|
||||
this.buildStream = async function(videoId, stream, track) {
|
||||
if(!this.video && videoId) {
|
||||
this.video = document.getElementById(videoId);
|
||||
}
|
||||
if(!this.video) {
|
||||
throw new Error('视频对象无效:' + videoId);
|
||||
}
|
||||
if(!this.stream) {
|
||||
this.stream = new MediaStream();
|
||||
this.video.srcObject = this.stream;
|
||||
}
|
||||
if(track) {
|
||||
if(track.kind === 'audio') {
|
||||
this.buildAudioTrack(track);
|
||||
}
|
||||
if(track.kind === 'video') {
|
||||
this.buildVideoTrack(track);
|
||||
}
|
||||
} else if(stream) {
|
||||
let audioTrack = stream.getAudioTracks();
|
||||
let videoTrack = stream.getVideoTracks();
|
||||
// TODO:验证API试试修改媒体
|
||||
// audioTrack.getSettings
|
||||
// audioTrack.getCapabilities
|
||||
// audioTrack.applyCapabilities
|
||||
if(audioTrack && audioTrack.length) {
|
||||
audioTrack.forEach(v => this.buildAudioTrack(v));
|
||||
}
|
||||
if(videoTrack && videoTrack.length) {
|
||||
videoTrack.forEach(v => this.buildVideoTrack(v));
|
||||
}
|
||||
} else {
|
||||
throw new Error('无效媒体信息');
|
||||
}
|
||||
console.debug('设置媒体', this.video, this.stream, this.audioTrack, this.videoTrack);
|
||||
await this.load();
|
||||
await this.play();
|
||||
return this;
|
||||
};
|
||||
/** 设置音频流 */
|
||||
this.buildAudioTrack = function(track) {
|
||||
// 关闭旧的
|
||||
// 创建新的
|
||||
track.sn = this.sn;
|
||||
this.audioStatus = true;
|
||||
this.audioTrack.push(track);
|
||||
if(this.audioEnabled) {
|
||||
this.stream.addTrack(track);
|
||||
}
|
||||
};
|
||||
/** 设置视频流 */
|
||||
this.buildVideoTrack = function(track) {
|
||||
// 关闭旧的
|
||||
// 创建新的
|
||||
track.sn = this.sn;
|
||||
this.videoStatus = true;
|
||||
this.videoTrack.push(track);
|
||||
if(this.videoEnabled) {
|
||||
this.stream.addTrack(track);
|
||||
}
|
||||
};
|
||||
/** 打开媒体通道 */
|
||||
this.openMediaChannel = function() {
|
||||
if(this.shareMediaChannel) {
|
||||
this.localMediaChannel = this.taoyao.localMediaChannel;
|
||||
this.remoteMediaChannel = this.taoyao.remoteMediaChannel;
|
||||
} else {
|
||||
let self = this;
|
||||
// 本地通道
|
||||
let mediaChannel = new RTCPeerConnection(defaultRPCConfig);
|
||||
self.taoyao.localClient.audioTrack.forEach(v => mediaChannel.addTrack(v, self.taoyao.localClient.stream));
|
||||
self.taoyao.localClient.videoTrack.forEach(v => mediaChannel.addTrack(v, self.taoyao.localClient.stream));
|
||||
mediaChannel.ontrack = function(e) {
|
||||
console.debug('Mesh Media Track', self.sn, e);
|
||||
let remote = self.taoyao.remoteClientFilter(self.sn);
|
||||
// TODO:判断数量
|
||||
remote.buildStream(remote.sn, e.streams[0], e.track);
|
||||
};
|
||||
mediaChannel.onicecandidate = function(e) {
|
||||
// TODO:判断给谁
|
||||
let to = self.taoyao.remoteClient.map(v => v.sn)[0];
|
||||
if(!self.taoyao.checkCandidate(e.candidate)) {
|
||||
console.debug('Send Mesh ICE Candidate Fail', e);
|
||||
return;
|
||||
}
|
||||
console.debug('Send Mesh ICE Candidate', to, e);
|
||||
self.taoyao.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.media.candidate,
|
||||
{
|
||||
to: to,
|
||||
type: 'mesh',
|
||||
candidate: e.candidate
|
||||
}
|
||||
));
|
||||
};
|
||||
this.localMediaChannel = mediaChannel;
|
||||
this.remoteMediaChannel = mediaChannel;
|
||||
}
|
||||
};
|
||||
}
|
||||
/** 桃夭 */
|
||||
function Taoyao(
|
||||
videoId,
|
||||
webSocket,
|
||||
localClientAudioEnabled,
|
||||
localClientVideoEnabled
|
||||
) {
|
||||
/** 本地视频ID */
|
||||
this.videoId = videoId;
|
||||
/** WebRTC配置 */
|
||||
this.webrtc = null;
|
||||
/** WebSocket地址 */
|
||||
// "wss://192.168.1.100:8888/websocket.signal"
|
||||
this.webSocket = webSocket || 'wss://' + location.host + '/websocket.signal';
|
||||
/** 设备状态 */
|
||||
this.audioEnabled = true;
|
||||
this.videoEnabled = true;
|
||||
/** 媒体配置 */
|
||||
this.audioConfig = defaultAudioConfig;
|
||||
this.videoConfig = defaultVideoConfig;
|
||||
/** 发送信令 */
|
||||
this.push = null;
|
||||
/** 本地终端 */
|
||||
this.localClient = null;
|
||||
this.localMediaChannel = null;
|
||||
/** 本地媒体状态 */
|
||||
this.localClientAudioEnabled = localClientAudioEnabled == undefined ? false : localClientAudioEnabled;
|
||||
this.localClientVideoEnabled = localClientVideoEnabled == undefined ? true : localClientVideoEnabled;
|
||||
/** 远程终端 */
|
||||
this.remoteClient = [];
|
||||
this.remoteMediaChannel = null;
|
||||
/** 是否共享媒体通道 */
|
||||
this.shareMediaChannel = true;
|
||||
/** 信令通道 */
|
||||
this.signalChannel = null;
|
||||
/** 媒体配置 */
|
||||
this.configMedia = function(audio = {}, video = {}) {
|
||||
this.audioConfig = {...this.audioConfig, ...audio};
|
||||
this.videoConfig = {...this.videoConfig, ...video};
|
||||
console.debug('终端媒体配置', this.audioConfig, this.videoConfig);
|
||||
return this;
|
||||
};
|
||||
/** WebRTC配置 */
|
||||
this.configWebrtc = function(config = {}) {
|
||||
this.webrtc = config;
|
||||
this.webSocket = this.webrtc.signal.address;
|
||||
this.shareMediaChannel = this.webrtc.framework === 'MOON';
|
||||
defaultRPCConfig.iceServers = this.webrtc.stun.map(v => ({'urls': v}));
|
||||
console.debug('WebRTC配置', this.webrtc, defaultRPCConfig);
|
||||
return this;
|
||||
};
|
||||
/** 打开信令通道 */
|
||||
this.buildChannel = function(callback) {
|
||||
signalChannel.taoyao = this;
|
||||
this.signalChannel = signalChannel;
|
||||
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
|
||||
this.push = function(data, pushCallback) {
|
||||
this.signalChannel.push(data, pushCallback);
|
||||
};
|
||||
return this.signalChannel.connect(this.webSocket, callback);
|
||||
};
|
||||
/** 打开本地媒体 */
|
||||
this.buildLocalMedia = function() {
|
||||
let self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
if(
|
||||
navigator.mediaDevices &&
|
||||
navigator.mediaDevices.getUserMedia &&
|
||||
navigator.mediaDevices.enumerateDevices
|
||||
) {
|
||||
navigator.mediaDevices.enumerateDevices()
|
||||
.then(list => {
|
||||
let audioEnabled = false;
|
||||
let videoEnabled = false;
|
||||
list.forEach(v => {
|
||||
console.debug('终端媒体设备', v, v.kind, v.label);
|
||||
switch(v.kind) {
|
||||
case 'audioinput':
|
||||
audioEnabled = true;
|
||||
break;
|
||||
case 'videoinput':
|
||||
videoEnabled = true;
|
||||
break;
|
||||
default:
|
||||
console.debug('没有适配设备', v.kind, v.label);
|
||||
break;
|
||||
}
|
||||
});
|
||||
if(!audioEnabled) {
|
||||
console.warn('终端没有音频输入设备');
|
||||
self.audioEnabled = false;
|
||||
}
|
||||
if(!videoEnabled) {
|
||||
console.warn('终端没有视频输入设备');
|
||||
self.videoEnabled = false;
|
||||
}
|
||||
console.debug('打开终端媒体', self.audioEnabled, self.videoEnabled, self.audioConfig, self.videoConfig);
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: self.audioEnabled ? self.audioConfig : false,
|
||||
video: self.videoEnabled ? self.videoConfig : false
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('检查终端设备异常', e);
|
||||
self.videoEnabled = false;
|
||||
self.videoEnabled = false;
|
||||
});
|
||||
} else {
|
||||
throw new Error('不支持的终端设备');
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 远程终端过滤 */
|
||||
this.remoteClientFilter = function(sn, autoBuild) {
|
||||
if(sn === signalConfig.sn) {
|
||||
console.warn('远程终端等于本地终端');
|
||||
return this.localClient;
|
||||
}
|
||||
let array = this.remoteClient.filter(v => v.sn === sn);
|
||||
let remote = null;
|
||||
if(array.length > 0) {
|
||||
remote = array[0];
|
||||
} else if(autoBuild) {
|
||||
remote = new TaoyaoClient(this, sn, this.shareMediaChannel);
|
||||
remote.openMediaChannel();
|
||||
this.remoteClient.push(remote);
|
||||
}
|
||||
return remote;
|
||||
};
|
||||
/** 关闭:关闭媒体 */
|
||||
this.close = function() {
|
||||
// TODO:释放资源
|
||||
};
|
||||
/** 关机:关闭媒体、关闭信令 */
|
||||
this.shutdown = function() {
|
||||
this.close();
|
||||
};
|
||||
/** 打开媒体通道 */
|
||||
this.buildMediaChannel = async function(localVideoId, stream) {
|
||||
let self = this;
|
||||
// 本地视频
|
||||
this.localClient = new TaoyaoClient(this, signalConfig.sn, this.shareMediaChannel, this.localClientAudioEnabled, this.localClientVideoEnabled);
|
||||
await this.localClient.buildStream(localVideoId, stream);
|
||||
if(this.shareMediaChannel) {
|
||||
// 本地通道
|
||||
this.localMediaChannel = new RTCPeerConnection(defaultRPCConfig);
|
||||
this.localClient.audioTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
|
||||
this.localClient.videoTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
|
||||
this.localMediaChannel.ontrack = function(e) {
|
||||
console.debug('Local Media Track', e);
|
||||
};
|
||||
this.localMediaChannel.onicecandidate = function(e) {
|
||||
// TODO:判断给谁
|
||||
let to = self.remoteClient.map(v => v.sn)[0];
|
||||
if(!self.checkCandidate(e.candidate)) {
|
||||
console.debug('Send Local ICE Candidate Fail', e);
|
||||
return;
|
||||
}
|
||||
console.debug('Send Local ICE Candidate', to, e);
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.media.candidate,
|
||||
{
|
||||
to: to,
|
||||
type: 'local',
|
||||
candidate: e.candidate
|
||||
}
|
||||
));
|
||||
};
|
||||
// 远程通道
|
||||
this.remoteMediaChannel = new RTCPeerConnection(defaultRPCConfig);
|
||||
this.remoteMediaChannel.ontrack = function(e) {
|
||||
console.debug('Remote Media Track', e.track.sn, e);
|
||||
let remote = self.taoyao.remoteClientFilter(e.track.sn);
|
||||
// TODO:判断数量
|
||||
remote.buildStream(remote.sn, e.streams[0], e.track);
|
||||
};
|
||||
this.remoteMediaChannel.onicecandidate = function(e) {
|
||||
// TODO:判断给谁
|
||||
let to = self.remoteClient.map(v => v.sn)[0];
|
||||
if(!self.checkCandidate(e.candidate)) {
|
||||
console.debug('Send Remote ICE Candidate Fail', e);
|
||||
return;
|
||||
}
|
||||
console.debug('Send Remote ICE Candidate', to, e);
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.media.candidate,
|
||||
{
|
||||
to: to,
|
||||
type: 'remote',
|
||||
candidate: e.candidate
|
||||
}
|
||||
));
|
||||
};
|
||||
console.debug('打开共享媒体通道', this.localMediaChannel, this.remoteMediaChannel);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
/** 校验candidate */
|
||||
this.checkCandidate = function(candidate) {
|
||||
if(
|
||||
!candidate ||
|
||||
!candidate.candidate ||
|
||||
candidate.sdpMid === null ||
|
||||
candidate.sdpMid === null ||
|
||||
candidate.sdpMLineIndex === undefined ||
|
||||
candidate.sdpMLineIndex === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/** 媒体信令 */
|
||||
this.mediaSubscribe = function(sn, callback) {
|
||||
let self = this;
|
||||
self.remoteClientFilter(sn, true);
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.media.subscribe,
|
||||
{
|
||||
to: sn
|
||||
}
|
||||
), callback);
|
||||
};
|
||||
/** 会议信令 */
|
||||
this.meetingCreate = function(callback) {
|
||||
let self = this;
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.meeting.create
|
||||
), callback);
|
||||
}
|
||||
this.meetingEnter = function(id, callback) {
|
||||
let self = this;
|
||||
self.push(signalProtocol.buildProtocol(
|
||||
signalProtocol.meeting.enter,
|
||||
{
|
||||
id: id
|
||||
}
|
||||
), callback);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>直播</title>
|
||||
<link rel="stylesheet" type="text/css" href="./css/font.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./css/style.css" />
|
||||
<script type="text/javascript" src="./javascript/taoyao.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="taoyao" id="taoyao">
|
||||
<div class="live">
|
||||
<div class="video">
|
||||
</div>
|
||||
<div class="handler">
|
||||
<a class="audio icon-volume-medium" title="音频状态"></a>
|
||||
<a class="video icon-play2" title="视频状态"></a>
|
||||
<a class="record icon-radio-checked" title="录制视频"></a>
|
||||
<a class="kick icon-cancel-circle" title="退出直播"></a>
|
||||
<a class="close icon-switch" title="关闭直播"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
const live = document.getElementById('taoyao');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>会议</title>
|
||||
<link rel="stylesheet" type="text/css" href="./css/font.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./css/style.css" />
|
||||
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
|
||||
<script type="text/javascript" src="./javascript/taoyao.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="taoyao" id="app">
|
||||
<div class="handler">
|
||||
<a class="create icon-make-group" title="创建会议" @click="create"></a>
|
||||
<a class="invite icon-address-book" title="邀请会议" @click="invite"></a>
|
||||
<a class="enter icon-enter" title="进入会议" @click="enter"></a>
|
||||
<a class="leave icon-exit" title="离开会议" @click="leave"></a>
|
||||
<a class="close icon-switch" title="关闭会议" @click="close"></a>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="meeting me">
|
||||
<div class="video">
|
||||
<video id="local"></video>
|
||||
</div>
|
||||
<div class="handler">
|
||||
<a class="audio icon-volume-medium" title="音频状态" @click="audioSelf"></a>
|
||||
<a class="video icon-play2" title="视频状态" @click="videoSelf"></a>
|
||||
<a class="record icon-radio-checked" title="录制视频" @click="recordSelf"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meeting" v-for="client in this.remoteClient" :key="client.sn">
|
||||
<div class="video">
|
||||
<video v-bind:id="client.sn"></video>
|
||||
</div>
|
||||
<div class="handler">
|
||||
<a class="audio" title="音频状态" v-bind:class="client.audioStatus?'icon-volume-medium':'icon-volume-mute2'" @click="audio(client.sn)"></a>
|
||||
<a class="video" title="视频状态" v-bind:class="client.videoStatus?'icon-play2':'icon-stop'" @click="video(client.sn)"></a>
|
||||
<a class="record icon-radio-checked" title="录制视频" v-bind:class="client.recordStatus?'active':''" @click="record(client.sn)"></a>
|
||||
<a class="expel icon-cancel-circle" title="踢出会议" @click="expel(client.sn)"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
taoyao: null,
|
||||
remoteClient: [],
|
||||
meetingId: null
|
||||
},
|
||||
mounted() {
|
||||
// 设置帐号
|
||||
let sn = prompt('你的账号', signalConfig.sn);
|
||||
signalConfig.sn = sn;
|
||||
// 加载桃夭
|
||||
let self = this;
|
||||
this.taoyao = new Taoyao('local');
|
||||
this.remoteClient = this.taoyao.remoteClient;
|
||||
// 打开信令通道
|
||||
this.taoyao
|
||||
.buildChannel(self.callback)
|
||||
.then(e => console.debug('信令通道连接成功'));
|
||||
},
|
||||
beforeDestroy() {
|
||||
},
|
||||
methods: {
|
||||
// 信令回调:true表示已经处理;false表示没有处理;
|
||||
callback: function(data) {
|
||||
let self = this;
|
||||
switch(data.header.pid) {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// 创建会议
|
||||
create: function(event) {
|
||||
let self = this;
|
||||
this.taoyao.meetingCreate(data => {
|
||||
self.taoyao.meetingEnter(data.body.id);
|
||||
return true;
|
||||
});
|
||||
},
|
||||
// 会议邀请
|
||||
invite: function(sn) {
|
||||
},
|
||||
// 进入会议
|
||||
enter: function(sn) {
|
||||
let id = prompt('房间标识');
|
||||
let password = prompt('房间密码');
|
||||
if(id) {
|
||||
this.taoyao.meetingEnter(id);
|
||||
}
|
||||
},
|
||||
// 离开会议
|
||||
leave: function(sn) {
|
||||
},
|
||||
// 关闭会议
|
||||
close: function(sn) {
|
||||
},
|
||||
// 控制音频
|
||||
audio: function(sn) {
|
||||
this.client(sn).audio = !this.client(sn).audio;
|
||||
},
|
||||
// 控制视频
|
||||
video: function(sn) {
|
||||
this.client(sn).video = !this.client(sn).video;
|
||||
},
|
||||
// 录制视频
|
||||
record: function(sn) {
|
||||
this.client(sn).record = !this.client(sn).record;
|
||||
},
|
||||
// 踢出会议
|
||||
expel: function(sn) {
|
||||
},
|
||||
// 控制音频
|
||||
audioSelf: function(sn) {
|
||||
},
|
||||
// 控制视频
|
||||
videoSelf: function(sn) {
|
||||
},
|
||||
// 录制视频
|
||||
recordSelf: function(sn) {
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
taoyao-signal-server/taoyao-server/src/main/resources/taoyao.jks
Normal file
BIN
taoyao-signal-server/taoyao-server/src/main/resources/taoyao.jks
Normal file
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
package com.acgist.taoyao.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 多线程测试
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface CostedTest {
|
||||
|
||||
/**
|
||||
* @return 执行次数
|
||||
*/
|
||||
int count() default 1;
|
||||
|
||||
/**
|
||||
* @return 线程数量
|
||||
*/
|
||||
int thread() default 1;
|
||||
|
||||
/**
|
||||
* @return 超时时间
|
||||
*/
|
||||
long timeout() default 1000;
|
||||
|
||||
/**
|
||||
* @return 超时时间单位
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.acgist.taoyao.annotation;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.test.context.TestContext;
|
||||
import org.springframework.test.context.TestExecutionListener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 多线程测试监听
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Slf4j
|
||||
public class CostedTestTestExecutionListener implements TestExecutionListener {
|
||||
|
||||
@Override
|
||||
public void afterTestMethod(TestContext testContext) throws Exception {
|
||||
final CostedTest costedTest = testContext.getTestMethod().getDeclaredAnnotation(CostedTest.class);
|
||||
if(costedTest == null) {
|
||||
return;
|
||||
}
|
||||
final int count = costedTest.count();
|
||||
final int thread = costedTest.thread();
|
||||
final long timeout = costedTest.timeout();
|
||||
final TimeUnit timeUnit = costedTest.timeUnit();
|
||||
final long aTime = System.currentTimeMillis();
|
||||
if(thread == 1) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
testContext.getTestMethod().invoke(testContext.getTestInstance());
|
||||
}
|
||||
} else {
|
||||
final CountDownLatch countDownLatch = new CountDownLatch(count);
|
||||
final ExecutorService executor = Executors.newFixedThreadPool(thread);
|
||||
for (int index = 0; index < count; index++) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
testContext.getTestMethod().invoke(testContext.getTestInstance());
|
||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
log.error("多线程测试异常", e);
|
||||
} finally {
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
countDownLatch.await(timeout, timeUnit);
|
||||
}
|
||||
final long zTime = System.currentTimeMillis();
|
||||
final long costed = zTime - aTime;
|
||||
log.info("多线程测试消耗时间:{}", costed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.acgist.taoyao.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.TestExecutionListeners.MergeMode;
|
||||
|
||||
/**
|
||||
* 测试启动
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
@TestExecutionListeners(listeners = CostedTestTestExecutionListener.class, mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
|
||||
public @interface TaoyaoTest {
|
||||
|
||||
@AliasFor(annotation = SpringBootTest.class)
|
||||
Class<?>[] classes() default {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.acgist.taoyao.boot.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.acgist.taoyao.annotation.CostedTest;
|
||||
import com.acgist.taoyao.annotation.TaoyaoTest;
|
||||
import com.acgist.taoyao.main.TaoyaoApplication;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@TaoyaoTest(classes = TaoyaoApplication.class)
|
||||
//@SpringBootTest(classes = TaoyaoApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
class IdServiceTest {
|
||||
|
||||
@Autowired
|
||||
private IdService idService;
|
||||
|
||||
@Test
|
||||
// @Timeout(value = 1000, unit = TimeUnit.MILLISECONDS)
|
||||
// @Rollback()
|
||||
// @RepeatedTest(10)
|
||||
void testId() {
|
||||
final long id = this.idService.buildId();
|
||||
log.info("生成ID:{}", id);
|
||||
}
|
||||
|
||||
@Test
|
||||
@CostedTest(count = 100000, thread = 10)
|
||||
void testIdCosted() {
|
||||
this.idService.buildId();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.acgist.taoyao.main;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class TaoyaoApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.acgist.taoyao.signal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.net.http.WebSocket;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.acgist.taoyao.annotation.TaoyaoTest;
|
||||
import com.acgist.taoyao.main.TaoyaoApplication;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@TaoyaoTest(classes = TaoyaoApplication.class)
|
||||
class SignalTest {
|
||||
|
||||
@Test
|
||||
void testSignal() throws InterruptedException {
|
||||
final WebSocket clientA = WebSocketClient.build("wss://localhost:8888/websocket.signal", "clientA");
|
||||
final WebSocket clientB = WebSocketClient.build("wss://localhost:8888/websocket.signal", "clientB");
|
||||
clientA.sendText("""
|
||||
{"header":{"pid":1000,"v":"1.0.0","id":"1","sn":"clientA"},"body":{}}
|
||||
""", true).join();
|
||||
assertNotNull(clientA);
|
||||
assertNotNull(clientB);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testThread() throws InterruptedException {
|
||||
final int total = 1000;
|
||||
final CountDownLatch count = new CountDownLatch(total);
|
||||
final WebSocket clientA = WebSocketClient.build("wss://localhost:8888/websocket.signal", "clientA", count);
|
||||
final long aTime = System.currentTimeMillis();
|
||||
for (int index = 0; index < total; index++) {
|
||||
clientA.sendText("""
|
||||
{"header":{"pid":2999,"v":"1.0.0","id":"1","sn":"clientA"},"body":{}}
|
||||
""", true).join();
|
||||
}
|
||||
// final ExecutorService executor = Executors.newFixedThreadPool(10);
|
||||
// for (int index = 0; index < total; index++) {
|
||||
// executor.execute(() -> {
|
||||
// synchronized (clientA) {
|
||||
// clientA.sendText("""
|
||||
// {"header":{"pid":2999,"v":"1.0.0","id":"1","sn":"clientA"},"body":{}}
|
||||
// """, true).join();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
count.await();
|
||||
final long zTime = System.currentTimeMillis();
|
||||
log.info("执行时间:{}", zTime - aTime);
|
||||
Thread.sleep(1000);
|
||||
assertNotNull(clientA);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.acgist.taoyao.signal;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.WebSocket;
|
||||
import java.net.http.WebSocket.Listener;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class WebSocketClient {
|
||||
|
||||
public static final WebSocket build(String uri, String sn) throws InterruptedException {
|
||||
return build(uri, sn, null);
|
||||
}
|
||||
|
||||
public static final WebSocket build(String uri, String sn, CountDownLatch count) throws InterruptedException {
|
||||
final Object lock = new Object();
|
||||
try {
|
||||
return HttpClient
|
||||
.newBuilder()
|
||||
.sslContext(newSSLContext())
|
||||
.build()
|
||||
.newWebSocketBuilder()
|
||||
.buildAsync(URI.create(uri), new Listener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket) {
|
||||
webSocket.sendText(String.format("""
|
||||
{"header":{"pid":2000,"v":"1.0.0","id":"1","sn":"%s"},"body":{"username":"taoyao","password":"taoyao","ip":"127.0.0.1","mac":"00:00:00:00:00:00"}}
|
||||
""", sn), true);
|
||||
Listener.super.onOpen(webSocket);
|
||||
}
|
||||
@Override
|
||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
||||
synchronized (lock) {
|
||||
lock.notifyAll();
|
||||
}
|
||||
if(count == null) {
|
||||
log.debug("收到WebSocket消息:{}", data);
|
||||
} else {
|
||||
count.countDown();
|
||||
log.debug("收到WebSocket消息:{}-{}", count.getCount(), data);
|
||||
}
|
||||
return Listener.super.onText(webSocket, data, last);
|
||||
}
|
||||
})
|
||||
.join();
|
||||
} finally {
|
||||
synchronized (lock) {
|
||||
lock.wait(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final SSLContext newSSLContext() {
|
||||
SSLContext sslContext = null;
|
||||
try {
|
||||
// SSL协议:SSL、SSLv2、SSLv3、TLS、TLSv1、TLSv1.1、TLSv1.2、TLSv1.3
|
||||
sslContext = SSLContext.getInstance("TLSv1.2");
|
||||
sslContext.init(null, TRUST_ALL_CERT_MANAGER, new SecureRandom());
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException e) {
|
||||
log.error("新建SSLContext异常", e);
|
||||
try {
|
||||
sslContext = SSLContext.getDefault();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
log.error("新建默认SSLContext异常", ex);
|
||||
}
|
||||
}
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
private static final TrustManager[] TRUST_ALL_CERT_MANAGER = new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
}
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
}
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.acgist.taoyao.signal.protocol;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.acgist.taoyao.annotation.TaoyaoTest;
|
||||
import com.acgist.taoyao.main.TaoyaoApplication;
|
||||
import com.acgist.taoyao.signal.protocol.platform.ScriptProtocol;
|
||||
|
||||
@TaoyaoTest(classes = TaoyaoApplication.class)
|
||||
class ScriptProtocolTest {
|
||||
|
||||
@Autowired
|
||||
private ScriptProtocol scriptProtocol;
|
||||
|
||||
@Test
|
||||
void testScript() {
|
||||
assertDoesNotThrow(() -> {
|
||||
this.scriptProtocol.execute(null, Map.of("script", "netstat -ano"), null, null);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.acgist.taoyao.signal.protocol;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.acgist.taoyao.annotation.TaoyaoTest;
|
||||
import com.acgist.taoyao.main.TaoyaoApplication;
|
||||
import com.acgist.taoyao.signal.protocol.platform.ShutdownProtocol;
|
||||
|
||||
@TaoyaoTest(classes = TaoyaoApplication.class)
|
||||
class ShutdownProtocolTest {
|
||||
|
||||
@Autowired
|
||||
private ShutdownProtocol shutdownProtocol;
|
||||
|
||||
@Test
|
||||
void testShutdown() {
|
||||
assertDoesNotThrow(() -> {
|
||||
this.shutdownProtocol.execute("taoyao", null, null);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user