[+] 终端信令连接

This commit is contained in:
acgist
2022-11-13 12:14:10 +08:00
parent b8f243c64c
commit 8d1392a6da
21 changed files with 412 additions and 70 deletions

View File

@@ -0,0 +1,30 @@
package com.acgist.taoyao.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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
@ConditionalOnMissingBean
public SecurityInterceptor securityInterceptor() {
return new SecurityInterceptor();
}
}

View File

@@ -0,0 +1,97 @@
package com.acgist.taoyao.interceptor;
import java.util.Base64;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.config.SecurityProperties;
import com.acgist.taoyao.boot.interceptor.InterceptorAdapter;
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,60 @@
package com.acgist.taoyao.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import com.acgist.taoyao.boot.config.TaoyaoProperties;
import com.acgist.taoyao.boot.interceptor.InterceptorAdapter;
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

@@ -74,6 +74,10 @@ taoyao:
- stun:stun4.l.google.com:19302
- stun:stun.stunprotocol.org:3478
turn:
host: localhost
port: ${server.port:8888}
schema: wss
websocket: /websocket.signal
record:
storage: /data/record
security:

View File

@@ -1,6 +1,14 @@
/**
* 桃夭WebRTC终端示例
*/
/** 配置 */
const config = {
// 当前终端SN
sn: 'taoyao',
// 信令授权
username: 'taoyao',
password: 'taoyao'
};
/** 音频配置 */
const defaultAudioConfig = {
// 音量0~1
@@ -33,29 +41,45 @@ const defaultVideoConfig = {
// 设备
// deviceId: '',
// 帧率
frameRate: 30,
frameRate: 24,
// 裁切
// resizeMode: '',
// 选摄像头user|left|right|environment
facingMode: 'environment'
}
/** 兼容 */
const XMLHttpRequest = window.XMLHttpRequest;
const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
/** 桃夭 */
function Taoyao(
webSocket,
iceServer
iceServer,
audioConfig,
videoConfig
) {
this.webSocket = webSocket;
this.iceServer = iceServer;
/** 媒体状态 */
this.audioStatus = true;
this.videoStatus = true;
/** 设备状态 */
this.audioEnabled = true;
this.videoEnabled = true;
/** 媒体信息 */
this.audioStreamId = null;
this.videoStreamId = null;
this.audioConfig = defaultAudioConfig;
this.videoConfig = defaultVideoConfig;
/** 媒体配置 */
this.audioConfig = audioConfig || defaultAudioConfig;
this.videoConfig = videoConfig || defaultVideoConfig;
/** 本地视频 */
this.localVideo = null;
/** 终端媒体 */
this.clientMedia = {};
/** 信令通道 */
this.signalChannel = null;
/** 初始 */
this.init = function() {
let self = this;
if(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
navigator.mediaDevices.enumerateDevices()
.then(list => {
@@ -71,57 +95,247 @@ function Taoyao(
});
if(!audioDevice) {
console.log('终端没有音频输入设备');
this.audioConfig = false;
self.audioEnabled = false;
}
if(!videoDevice) {
console.log('终端没有视频输入设备');
this.videoConfig = false;
self.videoEnabled = false;
}
})
.catch(e => console.log('获取终端设备失败', e));
.catch(e => {
console.log('获取终端设备失败', e);
self.videoEnabled = false;
self.videoEnabled = false;
});
}
return this;
};
/** 媒体 */
this.buildUserMedia = function() {
/** 请求 */
this.request = function(url, data = {}, method = 'GET', async = true, timeout = 5000, mime = 'json') {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open(method, url, async);
if(async) {
xhr.timeout = timeout;
xhr.responseType = mime;
xhr.send(data);
xhr.onload = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
resolve(xhr.response);
} else {
reject(xhr.response);
}
}
xhr.onerror = reject;
} else {
xhr.send(data);
if(xhr.readyState === 4 && xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else {
reject(JSON.parse(xhr.response));
}
}
});
};
/** 媒体配置 */
this.configMedia = function(audio = {}, video = {}) {
this.audioConfig = {...this.audioConfig, ...audio};
this.videoCofnig = {...this.videoCofnig, ...video};
console.log('终端媒体配置', this.audioConfig, this.videoConfig);
};
/** WebRTC配置 */
this.configWebrtc = function(config = {}) {
this.webSocket = config.signalAddress;
this.iceServer = config.stun;
console.log('WebRTC配置', this.webSocket, this.iceServer);
};
/** 信令通道 */
this.buildChannel = function(callback) {
this.signalChannel = signalChannel;
this.signalChannel.connect(this.webSocket, callback);
};
/** 本地媒体 */
this.buildLocalMedia = function() {
console.log("获取终端媒体:", this.audioConfig, this.videoConfig);
let self = this;
return new Promise((resolve, reject) => {
console.log("获取终端媒体:", this.audioConfig, this.videoConfig);
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({
audio: this.audioConfig,
video: this.videoConfig
audio: self.audioConfig,
video: self.videoConfig
})
.then(resolve)
.catch(reject);
} else if(navigator.getUserMedia) {
navigator.getUserMedia({
audio: this.audioConfig,
video: this.videoConfig
audio: self.audioConfig,
video: self.videoConfig
}, resolve, reject);
} else {
reject("获取终端媒体失败");
}
});
};
/** 本地 */
this.local = async function(localVideoId, stream) {
const localVideo = document.getElementById(localVideoId);
if ('srcObject' in localVideo) {
localVideo.srcObject = stream;
/** 本地媒体 */
this.localMedia = async function(localVideoId, stream) {
this.localVideo = document.getElementById(localVideoId);
if ('srcObject' in this.localVideo) {
this.localVideo.srcObject = stream;
} else {
localVideo.src = URL.createObjectURL(stream);;
this.localVideo.src = URL.createObjectURL(stream);;
}
await localVideo.play();
await this.localVideo.play();
};
/** 连接 */
this.connect = function() {
};
/** 重连 */
/** 定时 */
/** 媒体 */
/** 视频 */
};
/** 信令协议 */
const protocol = {
pid: {
/** 心跳 */
heartbeat: 1000,
/** 注册 */
register: 2000
},
/** 当前索引 */
index: 100000,
/** 最小索引 */
minIndex: 100000,
/** 最大索引 */
maxIndex: 999999,
/** 生成ID */
buildId: function() {
if(this.index++ >= this.maxIndex) {
this.index = this.minIndex;
}
return Date.now() + '' + this.index;
},
/** 生成协议 */
buildProtocol: function(sn, pid, body) {
let message = {
header: {
v: '1.0.0',
id: this.buildId(),
sn: sn,
pid: pid,
},
"body": body
};
return JSON.stringify(message);
}
};
/** 信令消息 */
/** 信令通道 */
const signalChannel = {
/** 通道 */
channel: null,
/** 地址 */
address: null,
/** 回调 */
callback: null,
/** 心跳时间 */
heartbeatTime: 10 * 1000,
/** 心跳定时器 */
heartbeatTimer: null,
/** 防止重连 */
lockReconnect: false,
/** 重连时间 */
connectionTimeout: 5 * 1000,
/** 最小重连时间 */
minReconnectionDelay: 5 * 1000,
/** 最大重连时间 */
maxReconnectionDelay: 5 * 60 * 1000,
/** 自动重连失败后重连时间增长倍数 */
reconnectionDelayGrowFactor: 1.5,
/** 关闭 */
close: function() {
clearTimeout(this.heartbeatTimer);
},
/** 心跳 */
}
heartbeat: function() {
let self = this;
self.heartbeatTimer = setTimeout(function() {
if (self.channel && self.channel.readyState == WebSocket.OPEN) {
self.channel.send(protocol.buildProtocol(config.sn, protocol.pid.heartbeat));
self.heartbeat();
} else {
console.log('发送心跳失败', self.channel);
}
}, self.heartbeatTime);
},
/** 重连 */
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;
}
// 打开定时重连
setTimeout(function() {
console.log('信令通道重连', self.address);
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
}
},
/** 连接 */
connect: function(address, callback, reconnection = true) {
let self = this;
this.address = address;
this.callback = callback;
console.log("连接信令通道", address);
return new Promise((resolve, reject) => {
self.channel = new WebSocket(address);
self.channel.onopen = function(e) {
console.log('信令通道打开', e);
self.channel.send(protocol.buildProtocol(
config.sn,
protocol.pid.register,
{
ip: null,
mac: null,
signal: 100,
battery: 100,
username: config.username,
password: config.password
}
));
self.connectionTimeout = self.minReconnectionDelay
self.heartbeat();
resolve(e);
};
self.channel.onclose = function(e) {
console.log('信令通道关闭', 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);
};
self.channel.onmessage = function(e) {
console.log('信令消息', e.data);
if(callback) {
callback(JSON.parse(e.data));
}
};
});
}
};
/*
var peer;
var socket; // WebSocket

View File

@@ -50,11 +50,35 @@
list.appendChild(child);
}
const taoyao = new Taoyao();
taoyao
.init()
.buildUserMedia()
.then(stream => taoyao.local('local', stream))
.catch((e) => alert('获取终端媒体失败:' + e));
// 初始
taoyao.init();
// 配置媒体
taoyao.request('/config/media', {}, 'GET', false)
.then(response => {
taoyao.configMedia(response.audio, response.video);
})
.catch(e => console.error('获取媒体配置失败', e));
// 配置WebRTC
taoyao.request('/config/webrtc', {}, 'GET', false)
.then(response => {
taoyao.configWebrtc(response);
taoyao.buildChannel(callback);
})
.catch(e => console.error('获取WebRTC配置失败', e));
// 信令回调
function callback(data) {
switch(data.header.pid) {
case 1000:
// 心跳
break;
}
}
// 信令通道
/*
taoyao.buildLocalMedia()
.then(stream => taoyao.localMedia('local', stream))
.catch((e) => alert('获取终端媒体失败:' + e));
*/
</script>
</body>
</html>

View File

@@ -22,14 +22,14 @@ class IdServiceTest {
// @Rollback()
// @RepeatedTest(10)
void testId() {
final long id = this.idService.id();
final long id = this.idService.buildId();
log.info("生成ID{}", id);
}
@Test
@CostedTest(count = 100000, thread = 10)
void testIdCosted() {
this.idService.id();
this.idService.buildId();
}
}