build: change file name
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -9,10 +9,10 @@
|
|||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.ttstd</groupId>
|
<groupId>com.ttstd</groupId>
|
||||||
<artifactId>OneKeyCallWebRTCSignaling</artifactId>
|
<artifactId>WebRTCSignaling</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>OneKeyCallWebRTCSignaling</name>
|
<name>WebRTCSignaling</name>
|
||||||
<description>OneKeyCallWebRTCSignaling</description>
|
<description>WebRTCSignaling</description>
|
||||||
<url/>
|
<url/>
|
||||||
<licenses>
|
<licenses>
|
||||||
<license/>
|
<license/>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class OneKeyCallWebRtcSignalingApplication {
|
public class WebRtcSignalingApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(OneKeyCallWebRtcSignalingApplication.class, args);
|
SpringApplication.run(WebRtcSignalingApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,19 +15,35 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebRTC 信令中继服务。
|
||||||
|
*
|
||||||
|
* <p>职责:作为 Client(观看端)与 Service(推流端)之间的无状态消息中转站。
|
||||||
|
* 收到消息后解析出目标用户,在线则直接转发,离线则交由离线缓冲(Redis)暂存,
|
||||||
|
* 待对端上线后由 {@link #onOpen} 补发,从而保证信令不丢失、不出现死锁。</p>
|
||||||
|
*
|
||||||
|
* <p>协议:外层信封 {@link WebRTCMessage}{type, target, data},
|
||||||
|
* 内层载荷 {@link SignalingMessage}{type, senderId, targetId, payload}。</p>
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
@ServerEndpoint("/signaling/{userId}")
|
@ServerEndpoint("/signaling/{userId}")
|
||||||
public class WebRTCSignalingServer {
|
public class WebRTCSignalingServer {
|
||||||
|
|
||||||
Logger logger = LoggerFactory.getLogger(WebRTCSignalingServer.class);
|
private static final Logger logger = LoggerFactory.getLogger(WebRTCSignalingServer.class);
|
||||||
|
|
||||||
// 用于存储在线用户的 Session
|
/** 在线用户 Session 表 */
|
||||||
private static final Map<String, Session> clients = new ConcurrentHashMap<>();
|
private static final Map<String, Session> clients = new ConcurrentHashMap<>();
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
// RedisTemplate for storing and retrieving messages for offline users
|
/** 需要中转的信令类型白名单(其余类型由服务器内部处理或不转发) */
|
||||||
|
private static final Set<String> RELAY_TYPES = Set.of(
|
||||||
|
"init", "ask", "offer", "answer", "accept", "reject",
|
||||||
|
"sdp", "iceCandidate", "call", "hangup"
|
||||||
|
);
|
||||||
|
|
||||||
private static RedisTemplate<String, Object> redisTemplate;
|
private static RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -38,15 +54,14 @@ public class WebRTCSignalingServer {
|
|||||||
@OnOpen
|
@OnOpen
|
||||||
public void onOpen(Session session, @PathParam("userId") String userId) {
|
public void onOpen(Session session, @PathParam("userId") String userId) {
|
||||||
clients.put(userId, session);
|
clients.put(userId, session);
|
||||||
logger.info("onOpen: 用户连接: " + userId);
|
logger.info("onOpen: 用户连接: {}", userId);
|
||||||
|
|
||||||
// Check for pending messages in Redis for the newly connected user
|
// 用户上线后补发其离线期间暂存的消息
|
||||||
String redisKey = "webrtc:pending_messages:" + userId;
|
String redisKey = "webrtc:pending_messages:" + userId;
|
||||||
if (redisTemplate != null) {
|
if (redisTemplate != null) {
|
||||||
Long size = redisTemplate.opsForList().size(redisKey);
|
Long size = redisTemplate.opsForList().size(redisKey);
|
||||||
if (size != null && size > 0) {
|
if (size != null && size > 0) {
|
||||||
logger.info("onOpen: 用户 {} 上线,发现 {} 条待发送消息。", userId, size);
|
logger.info("onOpen: 用户 {} 上线,发现 {} 条待发送消息。", userId, size);
|
||||||
// Retrieve and send all pending messages
|
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
WebRTCMessage pendingMessage = (WebRTCMessage) redisTemplate.opsForList().leftPop(redisKey);
|
WebRTCMessage pendingMessage = (WebRTCMessage) redisTemplate.opsForList().leftPop(redisKey);
|
||||||
if (pendingMessage != null) {
|
if (pendingMessage != null) {
|
||||||
@@ -57,7 +72,7 @@ public class WebRTCSignalingServer {
|
|||||||
logger.error("onOpen: 序列化待发送消息失败: {}", e.getMessage());
|
logger.error("onOpen: 序列化待发送消息失败: {}", e.getMessage());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("onOpen: 发送待发送消息给用户 {} 失败: {}", userId, e.getMessage());
|
logger.error("onOpen: 发送待发送消息给用户 {} 失败: {}", userId, e.getMessage());
|
||||||
// If sending fails (e.g., network issue), push it back to Redis
|
// 发送失败(如网络抖动)重新入队,等待下次补发
|
||||||
redisTemplate.opsForList().rightPush(redisKey, pendingMessage);
|
redisTemplate.opsForList().rightPush(redisKey, pendingMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,47 +89,27 @@ public class WebRTCSignalingServer {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
WebRTCMessage webRTCMessage = objectMapper.readValue(message, WebRTCMessage.class);
|
WebRTCMessage webRTCMessage = objectMapper.readValue(message, WebRTCMessage.class);
|
||||||
if (webRTCMessage == null) {
|
if (webRTCMessage == null || webRTCMessage.getType() == null) {
|
||||||
logger.warn("onMessage: 无法解析消息,请检查消息格式。");
|
logger.warn("onMessage: 无法解析消息或缺少 type 字段。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String type = webRTCMessage.getType();
|
String type = webRTCMessage.getType();
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "init":
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "ask":
|
|
||||||
// Handle ask message
|
|
||||||
case "offer":
|
|
||||||
// Handle offer message
|
|
||||||
case "answer":
|
|
||||||
// Handle answer messages
|
|
||||||
case "reject":
|
|
||||||
// Handle reject message
|
|
||||||
case "accept":
|
|
||||||
// Handle accept message
|
|
||||||
handleWebRTCMessage(session, webRTCMessage, userId);
|
|
||||||
break;
|
|
||||||
case "sdp":
|
|
||||||
// Handle SessionDescription messages
|
|
||||||
case "iceCandidate":
|
|
||||||
// Handle iceCandidate messages
|
|
||||||
handleSdpMessage(session, webRTCMessage, userId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "call":
|
|
||||||
// Handle call message
|
|
||||||
break;
|
|
||||||
case "hangup":
|
|
||||||
// Handle hangup message
|
|
||||||
break;
|
|
||||||
case "ping":
|
case "ping":
|
||||||
// Handle ping message
|
// 应用层心跳保活,无需转发
|
||||||
|
logger.debug("onMessage: 收到来自 {} 的心跳,忽略。", userId);
|
||||||
break;
|
break;
|
||||||
case "unknown":
|
case "unknown":
|
||||||
|
case "error":
|
||||||
|
logger.warn("onMessage: 收到 {} 消息,不进行处理。", type);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Handle unknown message type
|
if (RELAY_TYPES.contains(type)) {
|
||||||
|
relayMessage(session, webRTCMessage, userId);
|
||||||
|
} else {
|
||||||
|
logger.warn("onMessage: 未知消息类型 {},已忽略。", type);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
@@ -126,103 +121,66 @@ public class WebRTCSignalingServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleWebRTCMessage(Session session, WebRTCMessage webRTCMessage, String userId) throws JsonProcessingException {
|
/**
|
||||||
logger.info("handleWebRTCMessage: 收到来自用户 {} 的消息: {}", userId, webRTCMessage);
|
* 统一中继:将信令转发给目标用户。
|
||||||
String type = webRTCMessage.getType();
|
* <ul>
|
||||||
|
* <li>目标在线 → 直接异步发送;</li>
|
||||||
SignalingMessage signalingMessage;
|
* <li>目标离线且有 Redis → 暂存,待其上线补发(避免状态丢失);</li>
|
||||||
if (webRTCMessage.getData() instanceof String) {
|
* <li>目标离线且无 Redis → 向发送方返回错误。</li>
|
||||||
signalingMessage = objectMapper.readValue((String) webRTCMessage.getData(), SignalingMessage.class);
|
* </ul>
|
||||||
} else {
|
*/
|
||||||
signalingMessage = objectMapper.convertValue(webRTCMessage.getData(), SignalingMessage.class);
|
private void relayMessage(Session session, WebRTCMessage webRTCMessage, String fromUserId) {
|
||||||
|
String targetId = extractTargetId(webRTCMessage);
|
||||||
|
if (targetId == null || targetId.isEmpty()) {
|
||||||
|
logger.warn("relayMessage: 缺少目标用户ID,无法转发。消息类型: {}", webRTCMessage.getType());
|
||||||
|
sendErrorMessage(session, fromUserId, "消息格式错误,缺少目标用户ID。");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String targetId = signalingMessage.getTargetId();
|
|
||||||
|
|
||||||
if (targetId != null && !targetId.isEmpty()) {
|
|
||||||
if (clients.containsKey(targetId)) {
|
|
||||||
// Target user is online, send message directly
|
|
||||||
Session targetSession = clients.get(targetId);
|
Session targetSession = clients.get(targetId);
|
||||||
if (targetSession != null && targetSession.isOpen()) {
|
if (targetSession != null && targetSession.isOpen()) {
|
||||||
// targetSession.getAsyncRemote().sendText(objectMapper.writeValueAsString(webRTCMessage));
|
try {
|
||||||
logger.info("onMessage: 消息从 {} 发送给在线用户 {}", userId, targetId);
|
|
||||||
switch (type) {
|
|
||||||
case "ask":// Handle ask message
|
|
||||||
targetSession.getAsyncRemote().sendText(objectMapper.writeValueAsString(webRTCMessage));
|
targetSession.getAsyncRemote().sendText(objectMapper.writeValueAsString(webRTCMessage));
|
||||||
break;
|
logger.info("relayMessage: 消息 {} 已从 {} 转发给在线用户 {}", webRTCMessage.getType(), fromUserId, targetId);
|
||||||
case "offer":// Handle offer message
|
} catch (JsonProcessingException e) {
|
||||||
targetSession.getAsyncRemote().sendText(objectMapper.writeValueAsString(webRTCMessage));
|
logger.error("relayMessage: 序列化消息失败: {}", e.getMessage());
|
||||||
break;
|
sendErrorMessage(session, fromUserId, "消息序列化失败。");
|
||||||
case "answer":// Handle answer messages
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "reject":// Handle reject message
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "accept":// Handle accept message
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 目标离线:优先离线缓冲,保证信令不丢
|
||||||
|
if (redisTemplate != null) {
|
||||||
|
storeMessageInRedis(targetId, webRTCMessage);
|
||||||
|
logger.info("relayMessage: 目标 {} 离线,消息 {} 已暂存,待其上线补发。", targetId, webRTCMessage.getType());
|
||||||
} else {
|
} else {
|
||||||
clients.remove(targetId);
|
clients.remove(targetId);
|
||||||
logger.warn("onMessage: 目标用户 {} 的会话已关闭", targetId);
|
logger.info("relayMessage: 目标用户 {} 不在线(无 Redis 缓冲)", targetId);
|
||||||
|
sendErrorMessage(session, fromUserId, "目标用户 {" + targetId + "} 不在线");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
sendErrorMessage(session, userId, "onMessage: 目标用户 {" + targetId + "} 不在线");
|
|
||||||
logger.info("onMessage: 目标用户 {} 不在线", targetId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn("onMessage: 收到消息但缺少 targetId 或 targetId 为空,无法转发或存储。消息内容: {}", webRTCMessage);
|
|
||||||
sendErrorMessage(session, userId, "消息格式错误,缺少目标用户ID。");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSdpMessage(Session session, WebRTCMessage webRTCMessage, String userId) throws JsonProcessingException {
|
/** 从消息中解析目标用户ID:优先取内层 SignalingMessage.targetId,回退到外层 target */
|
||||||
logger.info("handleSdpMessage: 收到来自用户 {} 的消息: {}", userId, webRTCMessage);
|
private String extractTargetId(WebRTCMessage message) {
|
||||||
String type = webRTCMessage.getType();
|
Object data = message.getData();
|
||||||
|
if (data != null) {
|
||||||
SignalingMessage signalingMessage;
|
try {
|
||||||
if (webRTCMessage.getData() instanceof String) {
|
SignalingMessage sm = (data instanceof String)
|
||||||
signalingMessage = objectMapper.readValue((String) webRTCMessage.getData(), SignalingMessage.class);
|
? objectMapper.readValue((String) data, SignalingMessage.class)
|
||||||
} else {
|
: objectMapper.convertValue(data, SignalingMessage.class);
|
||||||
signalingMessage = objectMapper.convertValue(webRTCMessage.getData(), SignalingMessage.class);
|
if (sm != null && sm.getTargetId() != null && !sm.getTargetId().isEmpty()) {
|
||||||
|
return sm.getTargetId();
|
||||||
}
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
String targetId = signalingMessage.getTargetId();
|
// 解析失败时回退到外层 target
|
||||||
|
|
||||||
if (targetId != null && !targetId.isEmpty()) {
|
|
||||||
if (clients.containsKey(targetId)) {
|
|
||||||
// Target user is online, send message directly
|
|
||||||
Session targetSession = clients.get(targetId);
|
|
||||||
if (targetSession != null && targetSession.isOpen()) {
|
|
||||||
// targetSession.getAsyncRemote().sendText(objectMapper.writeValueAsString(webRTCMessage));
|
|
||||||
logger.info("onMessage: 消息从 {} 发送给在线用户 {}", userId, targetId);
|
|
||||||
switch (type) {
|
|
||||||
case "sdp":// Handle SessionDescription message
|
|
||||||
targetSession.getAsyncRemote().sendText(objectMapper.writeValueAsString(webRTCMessage));
|
|
||||||
break;
|
|
||||||
case "iceCandidate":// Handle iceCandidate message
|
|
||||||
targetSession.getAsyncRemote().sendText(objectMapper.writeValueAsString(webRTCMessage));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
clients.remove(targetId);
|
|
||||||
logger.warn("onMessage: 目标用户 {} 的会话已关闭", targetId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sendErrorMessage(session, userId, "onMessage: 目标用户 {" + targetId + "} 不在线");
|
|
||||||
logger.info("onMessage: 目标用户 {} 不在线", targetId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn("onMessage: 收到消息但缺少 targetId 或 targetId 为空,无法转发或存储。消息内容: {}", webRTCMessage);
|
|
||||||
sendErrorMessage(session, userId, "消息格式错误,缺少目标用户ID。");
|
|
||||||
}
|
}
|
||||||
|
return (message.getTarget() != null && !message.getTarget().isEmpty()) ? message.getTarget() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClose
|
@OnClose
|
||||||
public void onClose(@PathParam("userId") String userId) {
|
public void onClose(@PathParam("userId") String userId) {
|
||||||
clients.remove(userId);
|
clients.remove(userId);
|
||||||
logger.info("onClose: 用户离开: " + userId);
|
logger.info("onClose: 用户离开: {}", userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnError
|
@OnError
|
||||||
@@ -235,7 +193,7 @@ public class WebRTCSignalingServer {
|
|||||||
WebRTCMessage errorMessage = new WebRTCMessage();
|
WebRTCMessage errorMessage = new WebRTCMessage();
|
||||||
errorMessage.setType("error");
|
errorMessage.setType("error");
|
||||||
errorMessage.setTarget(userId);
|
errorMessage.setTarget(userId);
|
||||||
errorMessage.setData(errorMsg); // Include the error message in data
|
errorMessage.setData(errorMsg);
|
||||||
session.getAsyncRemote().sendText(objectMapper.writeValueAsString(errorMessage));
|
session.getAsyncRemote().sendText(objectMapper.writeValueAsString(errorMessage));
|
||||||
logger.info("sendErrorMessage: 发送错误消息: {}", errorMsg);
|
logger.info("sendErrorMessage: 发送错误消息: {}", errorMsg);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
@@ -244,7 +202,6 @@ public class WebRTCSignalingServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void storeMessageInRedis(String targetId, WebRTCMessage message) {
|
private void storeMessageInRedis(String targetId, WebRTCMessage message) {
|
||||||
if (redisTemplate != null) {
|
|
||||||
try {
|
try {
|
||||||
String redisKey = "webrtc:pending_messages:" + targetId;
|
String redisKey = "webrtc:pending_messages:" + targetId;
|
||||||
redisTemplate.opsForList().rightPush(redisKey, message);
|
redisTemplate.opsForList().rightPush(redisKey, message);
|
||||||
@@ -252,8 +209,5 @@ public class WebRTCSignalingServer {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("storeMessageInRedis: 存储消息到 Redis 失败: {}", e.getMessage(), e);
|
logger.error("storeMessageInRedis: 存储消息到 Redis 失败: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.warn("RedisTemplate is not initialized. Cannot store message for user {}.", targetId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
spring.application.name=OneKeyCallWebRTCSignaling
|
spring.application.name=WebRTCSignaling
|
||||||
server.port=2310
|
server.port=2310
|
||||||
|
|
||||||
spring.data.redis.database=5
|
spring.data.redis.database=5
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
class OneKeyCallWebRtcSignalingApplicationTests {
|
class WebRtcSignalingApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
||||||
Reference in New Issue
Block a user