init
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.ttstd.signaling;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class OneKeyCallWebRtcSignalingApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(OneKeyCallWebRtcSignalingApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ttstd.signaling.config;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.websocket.HandshakeResponse;
|
||||
import jakarta.websocket.server.HandshakeRequest;
|
||||
import jakarta.websocket.server.ServerEndpointConfig;
|
||||
|
||||
public class HttpSessionConfigurator extends ServerEndpointConfig.Configurator {
|
||||
|
||||
@Override
|
||||
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
|
||||
HttpSession httpSession = (HttpSession) request.getHttpSession();
|
||||
config.getUserProperties().put(HttpSession.class.getName(), httpSession);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ttstd.signaling.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
||||
|
||||
@Configuration
|
||||
public class WebSocketConfig {
|
||||
|
||||
@Bean
|
||||
public ServerEndpointExporter serverEndpointExporter() {
|
||||
return new ServerEndpointExporter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ttstd.signaling.controller;
|
||||
|
||||
import com.ttstd.signaling.model.SignalingMessage;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
//@RestController
|
||||
//@RequestMapping("/api")
|
||||
//public class AuthController {
|
||||
//
|
||||
// @PostMapping("/login")
|
||||
// public SignalingMessage login(@RequestParam String username, HttpSession session) {
|
||||
// // 简单的用户认证,实际项目应使用更安全的认证方式
|
||||
// session.setAttribute("userId", username);
|
||||
//
|
||||
// SignalingMessage response = new SignalingMessage();
|
||||
// response.setType("auth-success");
|
||||
// response.setFrom("system");
|
||||
// response.setPayload(java.util.Map.of("userId", username));
|
||||
// return response;
|
||||
// }
|
||||
//
|
||||
// @GetMapping("/rooms/{roomId}/users")
|
||||
// public java.util.List<String> getRoomUsers(@PathVariable String roomId) {
|
||||
// // 获取房间用户列表(需要将roomUsers改为public或提供访问方法)
|
||||
// return java.util.Collections.emptyList();
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,15 @@
|
||||
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; // 其他负载数据
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.ttstd.signaling.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ttstd.signaling.config.HttpSessionConfigurator;
|
||||
import com.ttstd.signaling.model.SignalingMessage;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.websocket.*;
|
||||
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;
|
||||
|
||||
@Component
|
||||
@ServerEndpoint(value = "/signaling", configurator = HttpSessionConfigurator.class)
|
||||
public class WebRTCSignalingServer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebRTCSignalingServer.class);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 存储在线用户会话:key=userId, value=WebSocket会话
|
||||
private static ConcurrentHashMap<String, Session> onlineUsers = new ConcurrentHashMap<>();
|
||||
// 存储用户房间关系:key=userId, value=roomId
|
||||
private static ConcurrentHashMap<String, String> userRooms = new ConcurrentHashMap<>();
|
||||
// 存储房间用户:key=roomId, value=用户ID集合
|
||||
private static ConcurrentHashMap<String, ConcurrentHashMap<String, String>> roomUsers = new ConcurrentHashMap<>();
|
||||
|
||||
private Session session;
|
||||
private String userId;
|
||||
private HttpSession httpSession;
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(Session session, EndpointConfig config) {
|
||||
this.session = session;
|
||||
this.httpSession = (HttpSession) config.getUserProperties()
|
||||
.get(HttpSession.class.getName());
|
||||
|
||||
// 从HTTP会话获取用户ID(实际项目中应从认证信息获取)
|
||||
this.userId = (String) this.httpSession.getAttribute("userId");
|
||||
if (this.userId == null) {
|
||||
this.userId = "user_" + session.getId().substring(0, 8);
|
||||
}
|
||||
|
||||
onlineUsers.put(this.userId, session);
|
||||
logger.info("用户 {} 连接成功, 当前在线用户数: {}", this.userId, onlineUsers.size());
|
||||
|
||||
// 发送连接成功消息
|
||||
sendMessage(createSignalingMessage("connected", this.userId, null, null, null, null));
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onMessage(String message, Session session) {
|
||||
try {
|
||||
SignalingMessage signalingMessage = objectMapper.readValue(message, SignalingMessage.class);
|
||||
signalingMessage.setFrom(this.userId);
|
||||
processSignalingMessage(signalingMessage);
|
||||
} catch (Exception e) {
|
||||
logger.error("消息处理错误: {}", e.getMessage());
|
||||
sendError("消息格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(Session session) {
|
||||
leaveRoom();
|
||||
onlineUsers.remove(this.userId);
|
||||
logger.info("用户 {} 断开连接, 剩余在线用户数: {}", this.userId, onlineUsers.size());
|
||||
}
|
||||
|
||||
@OnError
|
||||
public void onError(Session session, Throwable error) {
|
||||
logger.error("WebSocket错误: {}", error.getMessage());
|
||||
onlineUsers.remove(this.userId);
|
||||
leaveRoom();
|
||||
}
|
||||
|
||||
private void processSignalingMessage(SignalingMessage message) {
|
||||
switch (message.getType()) {
|
||||
case "join":
|
||||
handleJoinRoom(message);
|
||||
break;
|
||||
case "offer":
|
||||
case "answer":
|
||||
case "candidate":
|
||||
forwardMessageToTarget(message);
|
||||
break;
|
||||
case "leave":
|
||||
handleLeaveRoom(message);
|
||||
break;
|
||||
default:
|
||||
sendError("不支持的消息类型: " + message.getType());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleJoinRoom(SignalingMessage message) {
|
||||
String roomId = message.getRoomId();
|
||||
if (roomId == null || roomId.trim().isEmpty()) {
|
||||
sendError("房间ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 离开之前的房间
|
||||
leaveRoom();
|
||||
|
||||
// 加入新房间
|
||||
userRooms.put(this.userId, roomId);
|
||||
roomUsers.computeIfAbsent(roomId, k -> new ConcurrentHashMap<>())
|
||||
.put(this.userId, this.userId);
|
||||
|
||||
logger.info("用户 {} 加入房间 {}", this.userId, roomId);
|
||||
|
||||
// 通知房间内其他用户
|
||||
broadcastToRoom(roomId, createSignalingMessage("user-joined", this.userId, null, roomId, null, null), this.userId);
|
||||
|
||||
// 发送加入成功消息
|
||||
sendMessage(createSignalingMessage("joined", this.userId, null, roomId, null, null));
|
||||
}
|
||||
|
||||
private void handleLeaveRoom(SignalingMessage message) {
|
||||
leaveRoom();
|
||||
sendMessage(createSignalingMessage("left", this.userId, null, null, null, null));
|
||||
}
|
||||
|
||||
private void leaveRoom() {
|
||||
String roomId = userRooms.get(this.userId);
|
||||
if (roomId != null) {
|
||||
userRooms.remove(this.userId);
|
||||
ConcurrentHashMap<String, String> users = roomUsers.get(roomId);
|
||||
if (users != null) {
|
||||
users.remove(this.userId);
|
||||
if (users.isEmpty()) {
|
||||
roomUsers.remove(roomId);
|
||||
} else {
|
||||
// 通知房间内其他用户
|
||||
broadcastToRoom(roomId, createSignalingMessage("user-left", this.userId, null, roomId, null, null), this.userId);
|
||||
}
|
||||
}
|
||||
logger.info("用户 {} 离开房间 {}", this.userId, roomId);
|
||||
}
|
||||
}
|
||||
|
||||
private void forwardMessageToTarget(SignalingMessage message) {
|
||||
String targetUserId = message.getTo();
|
||||
if (targetUserId == null) {
|
||||
sendError("目标用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
Session targetSession = onlineUsers.get(targetUserId);
|
||||
if (targetSession != null && targetSession.isOpen()) {
|
||||
try {
|
||||
targetSession.getBasicRemote().sendText(objectMapper.writeValueAsString(message));
|
||||
logger.debug("消息从 {} 转发到 {}", this.userId, targetUserId);
|
||||
} catch (IOException e) {
|
||||
logger.error("消息转发失败: {}", e.getMessage());
|
||||
onlineUsers.remove(targetUserId);
|
||||
}
|
||||
} else {
|
||||
sendError("目标用户不在线");
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastToRoom(String roomId, SignalingMessage message, String excludeUserId) {
|
||||
ConcurrentHashMap<String, String> users = roomUsers.get(roomId);
|
||||
if (users != null) {
|
||||
users.keySet().forEach(userId -> {
|
||||
if (!userId.equals(excludeUserId)) {
|
||||
Session userSession = onlineUsers.get(userId);
|
||||
if (userSession != null && userSession.isOpen()) {
|
||||
try {
|
||||
userSession.getBasicRemote().sendText(objectMapper.writeValueAsString(message));
|
||||
} catch (IOException e) {
|
||||
logger.error("广播消息失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(SignalingMessage message) {
|
||||
try {
|
||||
this.session.getBasicRemote().sendText(objectMapper.writeValueAsString(message));
|
||||
} catch (IOException e) {
|
||||
logger.error("发送消息失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(String errorMessage) {
|
||||
SignalingMessage errorMsg = createSignalingMessage("error", "system", this.userId, null, null, null);
|
||||
errorMsg.setPayload(java.util.Map.of("message", errorMessage));
|
||||
sendMessage(errorMsg);
|
||||
}
|
||||
|
||||
private SignalingMessage createSignalingMessage(String type, String from, String to,
|
||||
String roomId, Object sdp, Object candidate) {
|
||||
SignalingMessage message = new SignalingMessage();
|
||||
message.setType(type);
|
||||
message.setFrom(from);
|
||||
message.setTo(to);
|
||||
message.setRoomId(roomId);
|
||||
message.setSdp(sdp);
|
||||
message.setCandidate(candidate);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
2
src/main/resources/application.properties
Normal file
2
src/main/resources/application.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
spring.application.name=OneKeyCallWebRTCSignaling
|
||||
server.port=2310
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ttstd.signaling;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class OneKeyCallWebRtcSignalingApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user