[+] 连接Mediasoup
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
package com.acgist.taoyao.boot.property;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* KMS配置
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Schema(title = "KMS配置", description = "KMS配置")
|
||||
public class KmsProperties {
|
||||
|
||||
/**
|
||||
* KMS主机
|
||||
*/
|
||||
@Schema(title = "KMS主机", description = "KMS主机")
|
||||
private String host;
|
||||
/**
|
||||
* KMS端口
|
||||
*/
|
||||
@Schema(title = "KMS端口", description = "KMS端口")
|
||||
private Integer port;
|
||||
/**
|
||||
* KMS协议
|
||||
*/
|
||||
@Schema(title = "KMS协议", description = "KMS协议")
|
||||
private String schema;
|
||||
/**
|
||||
* KMS地址
|
||||
*/
|
||||
@Schema(title = "KMS地址", description = "KMS地址")
|
||||
private String websocket;
|
||||
/**
|
||||
* KMS用户
|
||||
*/
|
||||
@Schema(title = "KMS用户", description = "KMS用户")
|
||||
@JsonIgnore
|
||||
private String username;
|
||||
/**
|
||||
* KMS密码
|
||||
*/
|
||||
@Schema(title = "KMS密码", description = "KMS密码")
|
||||
@JsonIgnore
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* @return 完整KMS地址
|
||||
*/
|
||||
@Schema(title = "完整KMS地址", description = "完整KMS地址")
|
||||
public String getAddress() {
|
||||
return this.schema + "://" + this.host + ":" + this.port + this.websocket;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.acgist.taoyao.boot.property;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Mediasoup配置
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Schema(title = "Mediasoup配置", description = "Mediasoup配置")
|
||||
public class MediasoupProperties {
|
||||
|
||||
/**
|
||||
* Mediasoup主机
|
||||
*/
|
||||
@Schema(title = "Mediasoup主机", description = "Mediasoup主机")
|
||||
private String host;
|
||||
/**
|
||||
* Mediasoup端口
|
||||
*/
|
||||
@Schema(title = "Mediasoup端口", description = "Mediasoup端口")
|
||||
private Integer port;
|
||||
/**
|
||||
* Mediasoup协议
|
||||
*/
|
||||
@Schema(title = "Mediasoup协议", description = "Mediasoup协议")
|
||||
private String schema;
|
||||
/**
|
||||
* Mediasoup地址
|
||||
*/
|
||||
@Schema(title = "Mediasoup地址", description = "Mediasoup地址")
|
||||
private String websocket;
|
||||
/**
|
||||
* Mediasoup用户
|
||||
*/
|
||||
@Schema(title = "Mediasoup用户", description = "Mediasoup用户")
|
||||
@JsonIgnore
|
||||
private String username;
|
||||
/**
|
||||
* Mediasoup密码
|
||||
*/
|
||||
@Schema(title = "Mediasoup密码", description = "Mediasoup密码")
|
||||
@JsonIgnore
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* @return 完整Mediasoup地址
|
||||
*/
|
||||
@Schema(title = "完整Mediasoup地址", description = "完整Mediasoup地址")
|
||||
public String getAddress() {
|
||||
return this.schema + "://" + this.host + ":" + this.port + this.websocket;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.acgist.taoyao.boot.property;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Moon架构配置
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Schema(title = "Moon架构配置", description = "Moon架构配置")
|
||||
public class MoonProperties {
|
||||
|
||||
/**
|
||||
* 是否混音
|
||||
*/
|
||||
@Schema(title = "是否混音", description = "是否混音")
|
||||
private Boolean audioMix;
|
||||
|
||||
}
|
||||
@@ -60,20 +60,15 @@ public class WebrtcProperties {
|
||||
*/
|
||||
@Schema(title = "turn服务器", description = "turn服务器")
|
||||
private String[] turn;
|
||||
/**
|
||||
* KMS配置
|
||||
*/
|
||||
@Schema(title = "KMS配置", description = "KMS配置")
|
||||
private KmsProperties kms;
|
||||
/**
|
||||
* Moon架构配置
|
||||
*/
|
||||
@Schema(title = "Moon架构配置", description = "Moon架构配置")
|
||||
private MoonProperties moon;
|
||||
/**
|
||||
* 信令配置
|
||||
*/
|
||||
@Schema(title = "信令配置", description = "信令配置")
|
||||
private SignalProperties signal;
|
||||
/**
|
||||
* Mediasoup配置
|
||||
*/
|
||||
@Schema(title = "Mediasoup配置", description = "Mediasoup配置")
|
||||
private MediasoupProperties mediasoup;
|
||||
|
||||
}
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
# 媒体
|
||||
|
||||
## 媒体信令
|
||||
|
||||
###
|
||||
|
||||
@@ -1,12 +1,223 @@
|
||||
package com.acgist.taoyao.mediasoup;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.WebSocket;
|
||||
import java.net.http.WebSocket.Listener;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.acgist.taoyao.boot.property.MediasoupProperties;
|
||||
import com.acgist.taoyao.boot.property.TaoyaoProperties;
|
||||
import com.acgist.taoyao.boot.property.WebrtcProperties;
|
||||
import com.acgist.taoyao.boot.utils.JSONUtils;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Mediasoup客户端
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MediasoupClient {
|
||||
|
||||
/**
|
||||
* Mediasoup WebSocket通道
|
||||
*/
|
||||
private WebSocket webSocket;
|
||||
/**
|
||||
* Mediasoup配置
|
||||
*/
|
||||
private MediasoupProperties mediasoupProperties;
|
||||
|
||||
@Autowired
|
||||
private TaskScheduler taskSchedulerl;
|
||||
@Autowired
|
||||
private TaoyaoProperties taoyaoProperties;
|
||||
@Autowired
|
||||
private WebrtcProperties webrtcProperties;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.mediasoupProperties = this.webrtcProperties.getMediasoup();
|
||||
this.buildClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接Mediasoup WebSocket通道
|
||||
*/
|
||||
public void buildClient() {
|
||||
final URI uri = URI.create(this.mediasoupProperties.getAddress());
|
||||
log.info("开始连接Mediasoup:{}", uri);
|
||||
try {
|
||||
HttpClient
|
||||
.newBuilder()
|
||||
.sslContext(buildSSLContext())
|
||||
.build()
|
||||
.newWebSocketBuilder()
|
||||
.connectTimeout(Duration.ofMillis(this.taoyaoProperties.getTimeout()))
|
||||
.buildAsync(uri, new MessageListener())
|
||||
.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("连接Mediasoup异常:{}", uri, e);
|
||||
this.taskSchedulerl.schedule(this::buildClient, Instant.now().plusSeconds(5));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param message 消息
|
||||
*/
|
||||
public void send(Object message) {
|
||||
while(this.webSocket == null) {
|
||||
Thread.yield();
|
||||
}
|
||||
this.webSocket.sendText(JSONUtils.toJSON(message), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息监听
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
public class MessageListener implements Listener {
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket) {
|
||||
log.info("Mediasoup通道打开:{}", webSocket);
|
||||
Listener.super.onOpen(webSocket);
|
||||
// 关闭旧的通道
|
||||
if(MediasoupClient.this.webSocket != null && !(MediasoupClient.this.webSocket.isInputClosed() && MediasoupClient.this.webSocket.isOutputClosed())) {
|
||||
MediasoupClient.this.webSocket.abort();
|
||||
}
|
||||
// 设置新的通道
|
||||
MediasoupClient.this.webSocket = webSocket;
|
||||
// 发送授权消息
|
||||
MediasoupClient.this.send(Map.of(
|
||||
"username", MediasoupClient.this.mediasoupProperties.getUsername(),
|
||||
"password", MediasoupClient.this.mediasoupProperties.getPassword()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
|
||||
log.debug("Mediasoup收到消息(binary):{}", webSocket);
|
||||
return Listener.super.onBinary(webSocket, data, last);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
||||
log.debug("Mediasoup收到消息(text):{}-{}", webSocket, data);
|
||||
return Listener.super.onText(webSocket, data, last);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
||||
log.warn("Mediasoup通道关闭:{}-{}-{}", webSocket, statusCode, reason);
|
||||
try {
|
||||
return Listener.super.onClose(webSocket, statusCode, reason);
|
||||
} finally {
|
||||
MediasoupClient.this.taskSchedulerl.schedule(MediasoupClient.this::buildClient, Instant.now().plusSeconds(5));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(WebSocket webSocket, Throwable error) {
|
||||
log.error("Mediasoup通道异常:{}", webSocket, error);
|
||||
try {
|
||||
Listener.super.onError(webSocket, error);
|
||||
} finally {
|
||||
MediasoupClient.this.taskSchedulerl.schedule(MediasoupClient.this::buildClient, Instant.now().plusSeconds(5));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message) {
|
||||
log.debug("Mediasoup收到消息(ping):{}", webSocket);
|
||||
return Listener.super.onPing(webSocket, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) {
|
||||
log.debug("Mediasoup收到消息(pong):{}", webSocket);
|
||||
return Listener.super.onPong(webSocket, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SSLContext
|
||||
*
|
||||
* @return {@link SSLContext}
|
||||
*/
|
||||
private static final SSLContext buildSSLContext() {
|
||||
try {
|
||||
// SSL协议:SSL、SSLv2、SSLv3、TLS、TLSv1、TLSv1.1、TLSv1.2、TLSv1.3
|
||||
final SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
|
||||
sslContext.init(null, new X509TrustManager[] { TaoyaoTrustManager.INSTANCE }, new SecureRandom());
|
||||
return sslContext;
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException e) {
|
||||
log.error("新建SSLContext异常", e);
|
||||
}
|
||||
try {
|
||||
return SSLContext.getDefault();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("新建SSLContext异常", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 证书验证
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
public static class TaoyaoTrustManager implements X509TrustManager {
|
||||
|
||||
private static final TaoyaoTrustManager INSTANCE = new TaoyaoTrustManager();
|
||||
|
||||
private TaoyaoTrustManager() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if(chain == null) {
|
||||
throw new CertificateException("证书验证失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if(chain == null) {
|
||||
throw new CertificateException("证书验证失败");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.acgist.taoyao.mediasoup.listener;
|
||||
|
||||
/**
|
||||
* Mediasoup事件监听
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
public abstract class Listener {
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.acgist.taoyao.mediasoup.transport;
|
||||
import java.util.List;
|
||||
|
||||
import com.acgist.taoyao.mediasoup.client.ClientStream;
|
||||
import com.acgist.taoyao.signal.client.ClientSession;
|
||||
|
||||
/**
|
||||
* 传输通道
|
||||
@@ -11,6 +12,10 @@ import com.acgist.taoyao.mediasoup.client.ClientStream;
|
||||
*/
|
||||
public final class Transport {
|
||||
|
||||
/**
|
||||
* 终端
|
||||
*/
|
||||
private ClientSession clientSession;
|
||||
/**
|
||||
* 生产者列表
|
||||
*/
|
||||
|
||||
@@ -90,8 +90,8 @@ taoyao:
|
||||
# 架构模式
|
||||
framework: MESH
|
||||
# 媒体端口范围
|
||||
min-port: 45535
|
||||
max-port: 65535
|
||||
min-port: 40000
|
||||
max-port: 49999
|
||||
# 公共服务
|
||||
stun:
|
||||
- stun:stun1.l.google.com:19302
|
||||
@@ -104,23 +104,21 @@ taoyao:
|
||||
- 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
|
||||
# Mediasoup服务配置:可以部署多个简单实现负载均衡
|
||||
mediasoup:
|
||||
host: 127.0.0.1
|
||||
#host: 192.168.8.110
|
||||
port: 4443
|
||||
schema: wss
|
||||
websocket: /websocket.signal
|
||||
username: taoyao
|
||||
password: taoyao
|
||||
record:
|
||||
storage: /data/record
|
||||
security:
|
||||
|
||||
Reference in New Issue
Block a user