feat: 增加websocket

This commit is contained in:
2026-06-02 21:15:18 +08:00
parent 369e222b50
commit fa52638105
11 changed files with 309 additions and 40 deletions

View File

@@ -51,6 +51,12 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,53 @@
package com.ttstd.signaling.bean;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import java.io.Serializable;
public class IceCandidate implements Serializable {
String sdpMid;
int sdpMLineIndex;
String sdp;
public IceCandidate() {
}
public IceCandidate(String sdpMid, int sdpMLineIndex, String sdp) {
this.sdpMid = sdpMid;
this.sdpMLineIndex = sdpMLineIndex;
this.sdp = sdp;
}
public String getSdpMid() {
return sdpMid;
}
public void setSdpMid(String sdpMid) {
this.sdpMid = sdpMid;
}
public int getSdpMLineIndex() {
return sdpMLineIndex;
}
public void setSdpMLineIndex(int sdpMLineIndex) {
this.sdpMLineIndex = sdpMLineIndex;
}
public String getSdp() {
return sdp;
}
public void setSdp(String sdp) {
this.sdp = sdp;
}
@Override
public String toString() {
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
}
}

View File

@@ -0,0 +1,41 @@
package com.ttstd.signaling.bean;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import java.io.Serializable;
public class SessionDescription implements Serializable {
String type;
String description;
public SessionDescription() {
}
public SessionDescription(String type, String description) {
this.type = type;
this.description = description;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
}
}

View File

@@ -1,6 +1,5 @@
package com.ttstd.signaling.controller;
import com.ttstd.signaling.model.SignalingMessage;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.*;

View File

@@ -0,0 +1,13 @@
package com.ttstd.signaling.model;
import lombok.Data;
@Data
public class ClientIceMessage {
private String sdpMid;
private int sdpMLineIndex;
private String sdp;
private String fromUser;
private String toUser;
private long timestamp;
}

View File

@@ -0,0 +1,12 @@
package com.ttstd.signaling.model;
import lombok.Data;
@Data
public class ClientOfferMessage {
private String type; // 类型: "offer", "answer", "ice", "reject", "accept"
private String fromUser;
private String toUser;
private long timestamp;
}

View File

@@ -0,0 +1,14 @@
package com.ttstd.signaling.model;
import lombok.Data;
@Data
public class ClientSdpMessage {
private String type; // 类型: "offer", "answer", "ice", "reject", "accept"
private String sdp;
private String fromUser;
private String toUser;
private long timestamp;
}

View File

@@ -0,0 +1,12 @@
package com.ttstd.signaling.model;
import lombok.Data;
@Data
public class ServiceIceMessage {
private String sdpMid;
private int sdpMLineIndex;
private String sdp;
private String fromUser;
long timestamp;
}

View File

@@ -0,0 +1,11 @@
package com.ttstd.signaling.model;
import lombok.Data;
@Data
public class ServiceSdpMessage {
private String type; // 类型: "offer", "answer", "ice", "reject", "accept"
private String sdp;
private String fromUser;
long timestamp;
}

View File

@@ -1,15 +0,0 @@
package com.ttstd.signaling.model;
import lombok.Data;
import java.util.Map;
@Data
public class SignalingMessage {
private String type; // 消息类型: offer, answer, candidate, join, leave
private String from; // 发送者ID
private String to; // 接收者ID
private String roomId; // 房间ID
private Object sdp; // SDP描述
private Object candidate; // ICE候选
private Map<String, Object> payload; // 其他负载数据
}

View File

@@ -1,25 +1,24 @@
package com.ttstd.signaling.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ttstd.signaling.config.HttpSessionConfigurator;
import com.ttstd.signaling.model.SignalMessage;
import com.ttstd.signaling.model.SignalingMessage;
import jakarta.servlet.http.HttpSession;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.ttstd.signaling.bean.IceCandidate;
import com.ttstd.signaling.bean.SessionDescription;
import com.ttstd.signaling.model.*;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -29,10 +28,17 @@ public class WebRTCSignalingServer {
Logger logger = LoggerFactory.getLogger(WebRTCSignalingServer.class);
private Gson gson = new Gson();
// 用于存储在线用户的 Session
private static final Map<String, Session> clients = new ConcurrentHashMap<>();
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final Map<String, SessionDescription> serviceSessionDescriptionMap = new ConcurrentHashMap<>();
private static final Map<String, IceCandidate> serviceIceCandidateMap = new ConcurrentHashMap<>();
private static final Map<String, SessionDescription> clientSessionDescriptionMap = new ConcurrentHashMap<>();
private static final Map<String, IceCandidate> clientIceCandidateMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
clients.put(userId, session);
@@ -41,24 +47,141 @@ public class WebRTCSignalingServer {
@OnMessage
public void onMessage(String message, Session session, @PathParam("userId") String userId) {
try {
// 解析收到的 JSON 消息
SignalMessage signal = objectMapper.readValue(message, SignalMessage.class);
String toUser = signal.getToUser();
Session targetSession = clients.get(toUser);
logger.info("onMessage: message = " + message);
if (targetSession != null && targetSession.isOpen()) {
// 转发信令SDP, ICE, 拒绝/接受)
targetSession.getAsyncRemote().sendText(message);
logger.info("onMessage: 信令类型 [" + signal.getType() + "] 从 " + userId + " 发往 " + toUser);
} else {
sendErrorMessage(session, "目标用户不在线: " + toUser);
try {
JsonNode jsonNode = objectMapper.readTree(message);
int msgType = jsonNode.get("msg_type").asInt();
logger.info("onMessage: msgType = " + msgType);
String content = jsonNode.get("content").toString();
logger.info("onMessage: content = " + content);
switch (msgType) {
case 1:
receiverServiceSdp(content);
break;
case 2:
receiverServiceIce(content);
break;
case 3:
receiverClientOfferMessage(content);
break;
case 4:
receiverClientSdpMessage(content);
break;
case 5:
receiverClientIceMessage(content);
break;
case -99:
// 解析收到的 JSON 消息
SignalMessage signal = objectMapper.readValue(message, SignalMessage.class);
String toUser = signal.getToUser();
Session targetSession = clients.get(toUser);
if (targetSession != null && targetSession.isOpen()) {
// 转发信令SDP, ICE, 拒绝/接受)
targetSession.getAsyncRemote().sendText(message);
logger.info("onMessage: 信令类型 [" + signal.getType() + "] 从 " + userId + " 发往 " + toUser);
} else {
sendErrorMessage(session, "目标用户不在线: " + toUser);
}
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 接收并存储被控端sdp信息
* @param jsonString
* @throws JsonProcessingException
*/
private void receiverServiceSdp(String jsonString) throws JsonProcessingException {
// 解析收到的 JSON 消息
ServiceSdpMessage serviceSdpMessage = objectMapper.readValue(jsonString, ServiceSdpMessage.class);
String fromUser = serviceSdpMessage.getFromUser();
Session targetSession = clients.get(fromUser);
logger.info("receiverServiceSdp: fromUser = " + fromUser);
logger.info("receiverServiceSdp: targetSession = " + targetSession);
SessionDescription sessionDescription = new SessionDescription(serviceSdpMessage.getType(), serviceSdpMessage.getSdp());
serviceSessionDescriptionMap.put(fromUser, sessionDescription);
logger.info("receiverServiceSdp: serviceSessionDescriptionMap size = " + serviceSessionDescriptionMap.size());
}
/**
* 接收并存储被控端ice信息
* @param jsonString
* @throws JsonProcessingException
*/
private void receiverServiceIce(String jsonString) throws JsonProcessingException {
ServiceIceMessage serviceIceMessage = objectMapper.readValue(jsonString, ServiceIceMessage.class);
String fromUser = serviceIceMessage.getFromUser();
Session targetSession = clients.get(fromUser);
logger.info("receiverServiceIce: fromUser = " + fromUser);
logger.info("receiverServiceIce: targetSession = " + targetSession);
IceCandidate iceCandidate = new IceCandidate(serviceIceMessage.getSdpMid(), serviceIceMessage.getSdpMLineIndex(), serviceIceMessage.getSdp());
serviceIceCandidateMap.put(fromUser, iceCandidate);
logger.info("receiverServiceIce: serviceIceCandidateMap size = " + serviceIceCandidateMap.size());
}
/**
* 接收并存储控制端sdp信息
* @param jsonString
* @throws JsonProcessingException
*/
private void receiverClientOfferMessage(String jsonString) throws JsonProcessingException {
ClientOfferMessage clientOfferMessage = objectMapper.readValue(jsonString, ClientOfferMessage.class);
String fromUser = clientOfferMessage.getFromUser();
logger.info("receiverClientOfferMessage: fromUser = " + fromUser);
String toUser = clientOfferMessage.getToUser();
logger.info("receiverClientOfferMessage: toUser = " + toUser);
logger.info("receiverClientOfferMessage: serviceSessionDescriptionMap size = " + serviceSessionDescriptionMap.size());
logger.info("receiverClientOfferMessage: serviceIceCandidateMap size = " + serviceIceCandidateMap.size());
Session session = clients.get(fromUser);
SessionDescription sessionDescription = serviceSessionDescriptionMap.get(toUser);
JsonObject sdpJsonObject = new JsonObject();
sdpJsonObject.addProperty("msg_type", "11");
sdpJsonObject.addProperty("sdp", sessionDescription.toString());
String sdpJsonString = gson.toJson(sdpJsonObject);
logger.info("receiverClientOfferMessage: sdpJsonString = " + sdpJsonString);
session.getAsyncRemote().sendText(gson.toJson(sdpJsonObject));
IceCandidate iceCandidate = serviceIceCandidateMap.get(toUser);
JsonObject iceJsonObject = new JsonObject();
iceJsonObject.addProperty("msg_type", "22");
iceJsonObject.addProperty("ice", iceCandidate.toString());
String iceJsonString = gson.toJson(iceJsonObject);
logger.info("receiverClientOfferMessage: iceJsonString = " + iceJsonString);
session.getAsyncRemote().sendText(gson.toJson(iceJsonObject));
}
/**
* 接收并存储控制端ice信息
* @param jsonString
* @throws JsonProcessingException
*/
private void receiverClientSdpMessage(String jsonString) throws JsonProcessingException {
ClientSdpMessage clientSdpMessage = objectMapper.readValue(jsonString, ClientSdpMessage.class);
String fromUser = clientSdpMessage.getFromUser();
String toUser = clientSdpMessage.getToUser();
SessionDescription description = new SessionDescription(clientSdpMessage.getType(), clientSdpMessage.getSdp());
clientSessionDescriptionMap.put(fromUser, description);
}
private void receiverClientIceMessage(String jsonString) throws JsonProcessingException {
ClientIceMessage clientIceMessage = objectMapper.readValue(jsonString, ClientIceMessage.class);
String fromUser = clientIceMessage.getFromUser();
String toUser = clientIceMessage.getToUser();
IceCandidate iceCandidate = new IceCandidate(clientIceMessage.getSdpMid(), clientIceMessage.getSdpMLineIndex(), clientIceMessage.getSdp());
clientIceCandidateMap.put(fromUser, iceCandidate);
}
@OnClose
public void onClose(@PathParam("userId") String userId) {
clients.remove(userId);