[+] 连接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服务器")
|
@Schema(title = "turn服务器", description = "turn服务器")
|
||||||
private String[] 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 = "信令配置")
|
@Schema(title = "信令配置", description = "信令配置")
|
||||||
private SignalProperties signal;
|
private SignalProperties signal;
|
||||||
|
/**
|
||||||
|
* Mediasoup配置
|
||||||
|
*/
|
||||||
|
@Schema(title = "Mediasoup配置", description = "Mediasoup配置")
|
||||||
|
private MediasoupProperties mediasoup;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
# 媒体
|
# 媒体
|
||||||
|
|
||||||
|
## 媒体信令
|
||||||
|
|
||||||
|
###
|
||||||
|
|||||||
@@ -1,12 +1,223 @@
|
|||||||
package com.acgist.taoyao.mediasoup;
|
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客户端
|
* Mediasoup客户端
|
||||||
*
|
*
|
||||||
* @author acgist
|
* @author acgist
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
public class MediasoupClient {
|
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 java.util.List;
|
||||||
|
|
||||||
import com.acgist.taoyao.mediasoup.client.ClientStream;
|
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 {
|
public final class Transport {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终端
|
||||||
|
*/
|
||||||
|
private ClientSession clientSession;
|
||||||
/**
|
/**
|
||||||
* 生产者列表
|
* 生产者列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ taoyao:
|
|||||||
# 架构模式
|
# 架构模式
|
||||||
framework: MESH
|
framework: MESH
|
||||||
# 媒体端口范围
|
# 媒体端口范围
|
||||||
min-port: 45535
|
min-port: 40000
|
||||||
max-port: 65535
|
max-port: 49999
|
||||||
# 公共服务
|
# 公共服务
|
||||||
stun:
|
stun:
|
||||||
- stun:stun1.l.google.com:19302
|
- 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
|
- 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:
|
signal:
|
||||||
host: 192.168.1.100
|
host: 192.168.1.100
|
||||||
port: ${server.port:8888}
|
port: ${server.port:8888}
|
||||||
schema: wss
|
schema: wss
|
||||||
websocket: /websocket.signal
|
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:
|
record:
|
||||||
storage: /data/record
|
storage: /data/record
|
||||||
security:
|
security:
|
||||||
|
|||||||
Reference in New Issue
Block a user