[*] 结构调整

This commit is contained in:
acgist
2023-02-02 19:43:26 +08:00
parent 1de076ae16
commit d09a9dbc1f
201 changed files with 4 additions and 4 deletions

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

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,3 @@
taoyao:
security:
permit: /favicon.ico,/error

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -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;
};
/** 设置媒体 */
// TODOstream判断
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);
};
};

View File

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

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -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 {};
}

View File

@@ -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();
}
}

View File

@@ -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() {
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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);
});
}
}

View File

@@ -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);
});
}
}