WebRTC enables real-time peer-to-peer communication, but requires a signaling server to exchange session information between peers. Here's a comprehensive Java implementation of a WebRTC signaling server.
WebRTC Signaling Architecture
Key Components
- Signaling Server: Exchanges SDP offers/answers and ICE candidates
- WebSocket Protocol: Real-time bidirectional communication
- Room Management: Manages peer connections and sessions
- STUN/TURN Servers: NAT traversal assistance
Signaling Flow
- Join Room → Peer connects to a room
- Offer → Peer A sends SDP offer
- Answer → Peer B sends SDP answer
- ICE Candidates → Exchange network information
- Data Channels → Establish direct communication
Basic WebSocket Signaling Server
Example 1: Basic WebRTC Signaling Server
import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; import org.json.JSONObject; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class WebRTCSignalingServer extends WebSocketServer { private final Map<String, Room> rooms = new ConcurrentHashMap<>(); private final Map<WebSocket, Peer> peers = new ConcurrentHashMap<>(); public WebRTCSignalingServer(int port) { super(new InetSocketAddress(port)); } @Override public void onStart() { System.out.println("WebRTC Signaling Server started on port: " + getPort()); setConnectionLostTimeout(0); setConnectionLostTimeout(100); } @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { System.out.println("New connection: " + conn.getRemoteSocketAddress()); Peer peer = new Peer(conn); peers.put(conn, peer); } @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { System.out.println("Connection closed: " + conn.getRemoteSocketAddress() + " - " + reason); Peer peer = peers.remove(conn); if (peer != null && peer.roomId != null) { leaveRoom(peer); } } @Override public void onMessage(WebSocket conn, String message) { try { JSONObject json = new JSONObject(message); String type = json.getString("type"); Peer peer = peers.get(conn); if (peer == null) { sendError(conn, "Peer not initialized"); return; } switch (type) { case "join": handleJoin(peer, json); break; case "offer": handleOffer(peer, json); break; case "answer": handleAnswer(peer, json); break; case "ice-candidate": handleIceCandidate(peer, json); break; case "leave": handleLeave(peer); break; default: sendError(conn, "Unknown message type: " + type); } } catch (Exception e) { System.err.println("Error processing message: " + e.getMessage()); sendError(conn, "Invalid message format"); } } @Override public void onError(WebSocket conn, Exception ex) { System.err.println("WebSocket error: " + ex.getMessage()); if (conn != null) { Peer peer = peers.get(conn); if (peer != null && peer.roomId != null) { leaveRoom(peer); } } } private void handleJoin(Peer peer, JSONObject message) { String roomId = message.getString("roomId"); String peerId = message.optString("peerId", generatePeerId()); peer.peerId = peerId; peer.roomId = roomId; Room room = rooms.computeIfAbsent(roomId, k -> new Room(roomId)); synchronized (room) { if (room.peers.size() >= 2) { // Limit to 2 peers for simplicity sendError(peer.conn, "Room is full"); return; } room.peers.put(peerId, peer); peer.roomId = roomId; // Notify existing peers about new peer for (Peer existingPeer : room.peers.values()) { if (!existingPeer.peerId.equals(peerId)) { sendMessage(existingPeer.conn, createPeerJoinedMessage(peerId)); } } // Send room info to new peer JSONObject response = new JSONObject(); response.put("type", "joined"); response.put("roomId", roomId); response.put("peerId", peerId); response.put("peers", new JSONObject(room.getPeerIds())); sendMessage(peer.conn, response); System.out.println("Peer " + peerId + " joined room " + roomId); } } private void handleOffer(Peer peer, JSONObject message) { if (peer.roomId == null) { sendError(peer.conn, "Not in a room"); return; } Room room = rooms.get(peer.roomId); if (room == null) { sendError(peer.conn, "Room not found"); return; } String targetPeerId = message.getString("targetPeerId"); JSONObject offer = message.getJSONObject("offer"); Peer targetPeer = room.peers.get(targetPeerId); if (targetPeer == null) { sendError(peer.conn, "Target peer not found"); return; } JSONObject relayMessage = new JSONObject(); relayMessage.put("type", "offer"); relayMessage.put("offer", offer); relayMessage.put("senderPeerId", peer.peerId); sendMessage(targetPeer.conn, relayMessage); System.out.println("Offer relayed from " + peer.peerId + " to " + targetPeerId); } private void handleAnswer(Peer peer, JSONObject message) { if (peer.roomId == null) { sendError(peer.conn, "Not in a room"); return; } Room room = rooms.get(peer.roomId); if (room == null) { sendError(peer.conn, "Room not found"); return; } String targetPeerId = message.getString("targetPeerId"); JSONObject answer = message.getJSONObject("answer"); Peer targetPeer = room.peers.get(targetPeerId); if (targetPeer == null) { sendError(peer.conn, "Target peer not found"); return; } JSONObject relayMessage = new JSONObject(); relayMessage.put("type", "answer"); relayMessage.put("answer", answer); relayMessage.put("senderPeerId", peer.peerId); sendMessage(targetPeer.conn, relayMessage); System.out.println("Answer relayed from " + peer.peerId + " to " + targetPeerId); } private void handleIceCandidate(Peer peer, JSONObject message) { if (peer.roomId == null) { sendError(peer.conn, "Not in a room"); return; } Room room = rooms.get(peer.roomId); if (room == null) { sendError(peer.conn, "Room not found"); return; } String targetPeerId = message.getString("targetPeerId"); JSONObject candidate = message.getJSONObject("candidate"); Peer targetPeer = room.peers.get(targetPeerId); if (targetPeer == null) { sendError(peer.conn, "Target peer not found"); return; } JSONObject relayMessage = new JSONObject(); relayMessage.put("type", "ice-candidate"); relayMessage.put("candidate", candidate); relayMessage.put("senderPeerId", peer.peerId); sendMessage(targetPeer.conn, relayMessage); } private void handleLeave(Peer peer) { leaveRoom(peer); } private void leaveRoom(Peer peer) { if (peer.roomId == null) return; Room room = rooms.get(peer.roomId); if (room == null) return; synchronized (room) { room.peers.remove(peer.peerId); // Notify other peers for (Peer otherPeer : room.peers.values()) { JSONObject message = new JSONObject(); message.put("type", "peer-left"); message.put("peerId", peer.peerId); sendMessage(otherPeer.conn, message); } // Clean up empty rooms if (room.peers.isEmpty()) { rooms.remove(peer.roomId); } System.out.println("Peer " + peer.peerId + " left room " + peer.roomId); peer.roomId = null; } } private JSONObject createPeerJoinedMessage(String peerId) { JSONObject message = new JSONObject(); message.put("type", "peer-joined"); message.put("peerId", peerId); return message; } private void sendMessage(WebSocket conn, JSONObject message) { if (conn.isOpen()) { conn.send(message.toString()); } } private void sendError(WebSocket conn, String error) { JSONObject message = new JSONObject(); message.put("type", "error"); message.put("message", error); sendMessage(conn, message); } private String generatePeerId() { return "peer_" + System.currentTimeMillis() + "_" + Math.abs(new Random().nextInt(1000)); } // Data classes private static class Peer { WebSocket conn; String peerId; String roomId; Peer(WebSocket conn) { this.conn = conn; } } private static class Room { String roomId; Map<String, Peer> peers = new ConcurrentHashMap<>(); Room(String roomId) { this.roomId = roomId; } List<String> getPeerIds() { return new ArrayList<>(peers.keySet()); } } public static void main(String[] args) { int port = 8887; if (args.length > 0) { port = Integer.parseInt(args[0]); } WebRTCSignalingServer server = new WebRTCSignalingServer(port); server.start(); System.out.println("WebRTC Signaling Server running on port " + port); System.out.println("Press Ctrl+C to stop the server"); } } Maven Dependencies
<dependencies> <!-- WebSocket Server --> <dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.5.3</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20231013</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> </dependencies>
Advanced Signaling Server with Multiple Rooms
Example 2: Advanced Signaling Server with Room Management
import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; import org.json.JSONObject; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.*; public class AdvancedSignalingServer extends WebSocketServer { private final Map<String, Room> rooms = new ConcurrentHashMap<>(); private final Map<WebSocket, Peer> peers = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final StatisticsCollector statistics = new StatisticsCollector(); public AdvancedSignalingServer(int port) { super(new InetSocketAddress(port)); } @Override public void onStart() { System.out.println("Advanced WebRTC Signaling Server started on port: " + getPort()); // Start room cleanup task scheduler.scheduleAtFixedRate(this::cleanupEmptyRooms, 1, 1, TimeUnit.HOURS); // Start statistics reporting scheduler.scheduleAtFixedRate(statistics::reportStatistics, 5, 5, TimeUnit.MINUTES); } @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { String clientIp = getClientIp(conn, handshake); System.out.println("New connection from: " + clientIp); Peer peer = new Peer(conn, clientIp); peers.put(conn, peer); statistics.connectionOpened(); } @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { Peer peer = peers.remove(conn); if (peer != null) { if (peer.roomId != null) { leaveRoom(peer); } statistics.connectionClosed(); } System.out.println("Connection closed: " + (peer != null ? peer.peerId : "unknown")); } @Override public void onMessage(WebSocket conn, String message) { statistics.messageReceived(); try { JSONObject json = new JSONObject(message); String type = json.getString("type"); Peer peer = peers.get(conn); if (peer == null) { sendError(conn, "Peer not initialized"); return; } long startTime = System.currentTimeMillis(); switch (type) { case "join": handleJoin(peer, json); break; case "create-room": handleCreateRoom(peer, json); break; case "list-rooms": handleListRooms(peer); break; case "offer": handleOffer(peer, json); break; case "answer": handleAnswer(peer, json); break; case "ice-candidate": handleIceCandidate(peer, json); break; case "data": handleDataMessage(peer, json); break; case "leave": handleLeave(peer); break; case "ping": handlePing(peer); break; default: sendError(conn, "Unknown message type: " + type); } long processingTime = System.currentTimeMillis() - startTime; statistics.recordProcessingTime(processingTime); } catch (Exception e) { System.err.println("Error processing message: " + e.getMessage()); sendError(conn, "Invalid message format: " + e.getMessage()); } } private void handleJoin(Peer peer, JSONObject message) { String roomId = message.getString("roomId"); String peerId = message.optString("peerId", generatePeerId()); String displayName = message.optString("displayName", peerId); Room room = rooms.get(roomId); if (room == null) { sendError(peer.conn, "Room not found: " + roomId); return; } synchronized (room) { if (room.isFull()) { sendError(peer.conn, "Room is full"); return; } if (room.peers.containsKey(peerId)) { sendError(peer.conn, "Peer ID already exists in room"); return; } peer.peerId = peerId; peer.displayName = displayName; peer.roomId = roomId; room.peers.put(peerId, peer); room.joinTime = System.currentTimeMillis(); // Notify existing peers JSONObject peerJoinedMessage = new JSONObject(); peerJoinedMessage.put("type", "peer-joined"); peerJoinedMessage.put("peerId", peerId); peerJoinedMessage.put("displayName", displayName); broadcastToRoom(room, peerJoinedMessage, peerId); // Send room info to new peer JSONObject response = new JSONObject(); response.put("type", "joined"); response.put("roomId", roomId); response.put("peerId", peerId); response.put("roomConfig", room.getConfig()); response.put("peers", room.getPeerInfo()); sendMessage(peer.conn, response); statistics.roomJoined(); System.out.println("Peer " + peerId + " joined room " + roomId); } } private void handleCreateRoom(Peer peer, JSONObject message) { String roomId = message.getString("roomId"); String roomName = message.optString("roomName", roomId); int maxPeers = message.optInt("maxPeers", 10); boolean isPublic = message.optBoolean("isPublic", true); if (rooms.containsKey(roomId)) { sendError(peer.conn, "Room already exists"); return; } Room room = new Room(roomId, roomName, maxPeers, isPublic); rooms.put(roomId, room); JSONObject response = new JSONObject(); response.put("type", "room-created"); response.put("roomId", roomId); response.put("roomName", roomName); sendMessage(peer.conn, response); statistics.roomCreated(); System.out.println("Room created: " + roomId + " by " + peer.peerId); } private void handleListRooms(Peer peer) { List<Map<String, Object>> roomList = new ArrayList<>(); for (Room room : rooms.values()) { if (room.isPublic) { Map<String, Object> roomInfo = new HashMap<>(); roomInfo.put("roomId", room.roomId); roomInfo.put("roomName", room.roomName); roomInfo.put("peerCount", room.peers.size()); roomInfo.put("maxPeers", room.maxPeers); roomInfo.put("createdAt", room.createdAt); roomList.add(roomInfo); } } JSONObject response = new JSONObject(); response.put("type", "room-list"); response.put("rooms", roomList); sendMessage(peer.conn, response); } private void handleDataMessage(Peer peer, JSONObject message) { if (peer.roomId == null) { sendError(peer.conn, "Not in a room"); return; } Room room = rooms.get(peer.roomId); if (room == null) { sendError(peer.conn, "Room not found"); return; } String targetPeerId = message.optString("targetPeerId"); JSONObject data = message.getJSONObject("data"); JSONObject relayMessage = new JSONObject(); relayMessage.put("type", "data"); relayMessage.put("data", data); relayMessage.put("senderPeerId", peer.peerId); relayMessage.put("timestamp", System.currentTimeMillis()); if (targetPeerId != null && !targetPeerId.isEmpty()) { // Send to specific peer Peer targetPeer = room.peers.get(targetPeerId); if (targetPeer != null) { sendMessage(targetPeer.conn, relayMessage); } } else { // Broadcast to all peers in room (except sender) broadcastToRoom(room, relayMessage, peer.peerId); } statistics.dataMessageRelayed(); } private void handlePing(Peer peer) { JSONObject response = new JSONObject(); response.put("type", "pong"); response.put("timestamp", System.currentTimeMillis()); response.put("serverTime", System.currentTimeMillis()); sendMessage(peer.conn, response); } private void broadcastToRoom(Room room, JSONObject message, String excludePeerId) { for (Peer roomPeer : room.peers.values()) { if (!roomPeer.peerId.equals(excludePeerId)) { sendMessage(roomPeer.conn, message); } } } private void cleanupEmptyRooms() { Iterator<Map.Entry<String, Room>> iterator = rooms.entrySet().iterator(); int cleanedCount = 0; while (iterator.hasNext()) { Map.Entry<String, Room> entry = iterator.next(); Room room = entry.getValue(); synchronized (room) { if (room.peers.isEmpty() && System.currentTimeMillis() - room.joinTime > TimeUnit.HOURS.toMillis(1)) { iterator.remove(); cleanedCount++; } } } if (cleanedCount > 0) { System.out.println("Cleaned up " + cleanedCount + " empty rooms"); } } private String getClientIp(WebSocket conn, ClientHandshake handshake) { String forwardedFor = handshake.getFieldValue("X-Forwarded-For"); if (forwardedFor != null && !forwardedFor.isEmpty()) { return forwardedFor.split(",")[0].trim(); } return conn.getRemoteSocketAddress().getAddress().getHostAddress(); } // Enhanced data classes private static class Peer { WebSocket conn; String peerId; String displayName; String roomId; String clientIp; long connectedAt; Peer(WebSocket conn, String clientIp) { this.conn = conn; this.clientIp = clientIp; this.connectedAt = System.currentTimeMillis(); } } private static class Room { String roomId; String roomName; Map<String, Peer> peers = new ConcurrentHashMap<>(); int maxPeers; boolean isPublic; long createdAt; long joinTime; Room(String roomId, String roomName, int maxPeers, boolean isPublic) { this.roomId = roomId; this.roomName = roomName; this.maxPeers = maxPeers; this.isPublic = isPublic; this.createdAt = System.currentTimeMillis(); this.joinTime = System.currentTimeMillis(); } boolean isFull() { return peers.size() >= maxPeers; } JSONObject getConfig() { JSONObject config = new JSONObject(); config.put("roomId", roomId); config.put("roomName", roomName); config.put("maxPeers", maxPeers); config.put("isPublic", isPublic); return config; } JSONObject getPeerInfo() { JSONObject peerInfo = new JSONObject(); for (Peer peer : peers.values()) { JSONObject info = new JSONObject(); info.put("displayName", peer.displayName); info.put("connectedAt", peer.connectedAt); peerInfo.put(peer.peerId, info); } return peerInfo; } } // Statistics collection private static class StatisticsCollector { private final AtomicLong connectionsOpened = new AtomicLong(); private final AtomicLong connectionsClosed = new AtomicLong(); private final AtomicLong messagesReceived = new AtomicLong(); private final AtomicLong roomsCreated = new AtomicLong(); private final AtomicLong roomsJoined = new AtomicLong(); private final AtomicLong dataMessagesRelayed = new AtomicLong(); private final LongAdder totalProcessingTime = new LongAdder(); private final AtomicLong processingCount = new AtomicLong(); public void connectionOpened() { connectionsOpened.incrementAndGet(); } public void connectionClosed() { connectionsClosed.incrementAndGet(); } public void messageReceived() { messagesReceived.incrementAndGet(); } public void roomCreated() { roomsCreated.incrementAndGet(); } public void roomJoined() { roomsJoined.incrementAndGet(); } public void dataMessageRelayed() { dataMessagesRelayed.incrementAndGet(); } public void recordProcessingTime(long time) { totalProcessingTime.add(time); processingCount.incrementAndGet(); } public void reportStatistics() { long currentConnections = connectionsOpened.get() - connectionsClosed.get(); double avgProcessingTime = processingCount.get() > 0 ? (double) totalProcessingTime.sum() / processingCount.get() : 0; System.out.println("\n=== Server Statistics ==="); System.out.println("Active connections: " + currentConnections); System.out.println("Total messages: " + messagesReceived.get()); System.out.println("Rooms created: " + roomsCreated.get()); System.out.println("Rooms joined: " + roomsJoined.get()); System.out.println("Data messages relayed: " + dataMessagesRelayed.get()); System.out.printf("Average processing time: %.2f ms\n", avgProcessingTime); System.out.println("=========================\n"); } } // Utility methods (same as basic server) private void sendMessage(WebSocket conn, JSONObject message) { if (conn.isOpen()) { conn.send(message.toString()); } } private void sendError(WebSocket conn, String error) { JSONObject message = new JSONObject(); message.put("type", "error"); message.put("message", error); sendMessage(conn, message); } private String generatePeerId() { return "peer_" + System.currentTimeMillis() + "_" + Math.abs(new Random().nextInt(10000)); } @Override public void stop() throws InterruptedException { scheduler.shutdown(); super.stop(); } public static void main(String[] args) { int port = 8887; if (args.length > 0) { port = Integer.parseInt(args[0]); } AdvancedSignalingServer server = new AdvancedSignalingServer(port); server.start(); System.out.println("Advanced WebRTC Signaling Server running on port " + port); // Add shutdown hook for graceful shutdown Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { server.stop(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } })); } } Spring Boot Integration
Example 3: Spring Boot WebRTC Signaling Server
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @SpringBootApplication public class SpringWebRTCSignalingServer { public static void main(String[] args) { SpringApplication.run(SpringWebRTCSignalingServer.class, args); } @Configuration @EnableWebSocket public static class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(signalingHandler(), "/signaling") .setAllowedOrigins("*"); } @Bean public SignalingHandler signalingHandler() { return new SignalingHandler(); } } public static class SignalingHandler extends TextWebSocketHandler { private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); private final Map<String, Room> rooms = new ConcurrentHashMap<>(); private final Map<String, String> sessionToPeer = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String sessionId = session.getId(); sessions.put(sessionId, session); System.out.println("New WebSocket connection: " + sessionId); // Send welcome message sendMessage(session, createMessage("connected", Map.of("sessionId", sessionId))); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); // Use the same message handling logic as previous examples // (Parse JSON and handle different message types) } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String sessionId = session.getId(); sessions.remove(sessionId); String peerId = sessionToPeer.get(sessionId); if (peerId != null) { // Handle peer leaving room sessionToPeer.remove(sessionId); } System.out.println("WebSocket connection closed: " + sessionId); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { System.err.println("WebSocket transport error: " + exception.getMessage()); } private void sendMessage(WebSocketSession session, String message) throws IOException { if (session.isOpen()) { session.sendMessage(new TextMessage(message)); } } private String createMessage(String type, Map<String, Object> data) { // Create JSON message return "{\"type\":\"" + type + "\",\"data\":" + new org.json.JSONObject(data).toString() + "}"; } } } Client-Side JavaScript Example
Example 4: HTML/JavaScript WebRTC Client
<!DOCTYPE html> <html> <head> <title>WebRTC Client</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .container { max-width: 800px; margin: 0 auto; } .control-panel { margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; } .video-container { display: flex; gap: 10px; } video { width: 400px; height: 300px; background: #000; } .status { margin-top: 10px; padding: 10px; background: #f0f0f0; } .messages { height: 200px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; } </style> </head> <body> <div class="container"> <h1>WebRTC Video Chat</h1> <div class="control-panel"> <input type="text" id="roomId" placeholder="Room ID" value="room1"> <input type="text" id="peerId" placeholder="Peer ID (optional)"> <button onclick="joinRoom()">Join Room</button> <button onclick="leaveRoom()">Leave Room</button> <button onclick="createRoom()">Create Room</button> <button onclick="listRooms()">List Rooms</button> </div> <div class="video-container"> <video id="localVideo" autoplay muted></video> <video id="remoteVideo" autoplay></video> </div> <div class="control-panel"> <button onclick="startVideo()">Start Video</button> <button onclick="stopVideo()">Stop Video</button> <button onclick="startAudio()">Start Audio</button> <button onclick="stopAudio()">Stop Audio</button> <button onclick="shareScreen()">Share Screen</button> </div> <div class="status"> <div>Connection Status: <span id="connectionStatus">Disconnected</span></div> <div>Room: <span id="currentRoom">None</span></div> <div>Peers: <span id="peerCount">0</span></div> </div> <div class="messages" id="messageLog"></div> </div> <script> class WebRTCClient { constructor() { this.ws = null; this.peerConnection = null; this.localStream = null; this.remoteStream = new MediaStream(); this.roomId = null; this.peerId = null; this.targetPeerId = null; // STUN servers this.configuration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] }; this.initializeElements(); } initializeElements() { this.remoteVideo = document.getElementById('remoteVideo'); this.localVideo = document.getElementById('localVideo'); this.connectionStatus = document.getElementById('connectionStatus'); this.currentRoom = document.getElementById('currentRoom'); this.peerCount = document.getElementById('peerCount'); this.messageLog = document.getElementById('messageLog'); this.remoteVideo.srcObject = this.remoteStream; } connect(serverUrl) { this.ws = new WebSocket(serverUrl); this.ws.onopen = () => { this.log('Connected to signaling server'); this.updateStatus('Connected'); }; this.ws.onclose = () => { this.log('Disconnected from signaling server'); this.updateStatus('Disconnected'); }; this.ws.onerror = (error) => { this.log('WebSocket error: ' + error); }; this.ws.onmessage = (event) => { this.handleSignalingMessage(JSON.parse(event.data)); }; } handleSignalingMessage(message) { this.log('Received: ' + message.type); switch (message.type) { case 'connected': this.log('Server connection established'); break; case 'joined': this.handleJoined(message); break; case 'peer-joined': this.handlePeerJoined(message); break; case 'peer-left': this.handlePeerLeft(message); break; case 'offer': this.handleOffer(message); break; case 'answer': this.handleAnswer(message); break; case 'ice-candidate': this.handleIceCandidate(message); break; case 'room-list': this.handleRoomList(message); break; case 'error': this.log('Error: ' + message.message); break; } } handleJoined(message) { this.roomId = message.roomId; this.peerId = message.peerId; this.currentRoom.textContent = this.roomId; this.peerCount.textContent = Object.keys(message.peers || {}).length; this.log(`Joined room ${this.roomId} as ${this.peerId}`); // If there are other peers, we'll wait for them to send offers // If we're the first peer, we'll create an offer when someone joins } handlePeerJoined(message) { this.log(`Peer joined: ${message.peerId}`); this.targetPeerId = message.peerId; // If we have local stream, create offer if (this.localStream) { this.createOffer(); } } handlePeerLeft(message) { this.log(`Peer left: ${message.peerId}`); if (this.peerConnection) { this.peerConnection.close(); this.peerConnection = null; } this.remoteStream = new MediaStream(); this.remoteVideo.srcObject = this.remoteStream; } async createOffer() { if (!this.targetPeerId) return; try { this.peerConnection = new RTCPeerConnection(this.configuration); this.setupPeerConnection(); // Add local stream tracks if (this.localStream) { this.localStream.getTracks().forEach(track => { this.peerConnection.addTrack(track, this.localStream); }); } // Create data channel const dataChannel = this.peerConnection.createDataChannel('chat'); this.setupDataChannel(dataChannel); const offer = await this.peerConnection.createOffer(); await this.peerConnection.setLocalDescription(offer); this.sendSignalingMessage({ type: 'offer', targetPeerId: this.targetPeerId, offer: offer }); } catch (error) { this.log('Error creating offer: ' + error); } } async handleOffer(message) { try { this.targetPeerId = message.senderPeerId; this.peerConnection = new RTCPeerConnection(this.configuration); this.setupPeerConnection(); // Add local stream tracks if (this.localStream) { this.localStream.getTracks().forEach(track => { this.peerConnection.addTrack(track, this.localStream); }); } // Setup data channel this.peerConnection.ondatachannel = (event) => { this.setupDataChannel(event.channel); }; await this.peerConnection.setRemoteDescription(message.offer); const answer = await this.peerConnection.createAnswer(); await this.peerConnection.setLocalDescription(answer); this.sendSignalingMessage({ type: 'answer', targetPeerId: this.targetPeerId, answer: answer }); } catch (error) { this.log('Error handling offer: ' + error); } } async handleAnswer(message) { try { await this.peerConnection.setRemoteDescription(message.answer); } catch (error) { this.log('Error handling answer: ' + error); } } handleIceCandidate(message) { if (this.peerConnection && message.candidate) { this.peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate)) .catch(error => this.log('Error adding ICE candidate: ' + error)); } } setupPeerConnection() { this.peerConnection.onicecandidate = (event) => { if (event.candidate) { this.sendSignalingMessage({ type: 'ice-candidate', targetPeerId: this.targetPeerId, candidate: event.candidate }); } }; this.peerConnection.ontrack = (event) => { this.log('Received remote track'); event.streams[0].getTracks().forEach(track => { this.remoteStream.addTrack(track); }); }; this.peerConnection.onconnectionstatechange = () => { this.log('Connection state: ' + this.peerConnection.connectionState); }; } setupDataChannel(dataChannel) { dataChannel.onopen = () => { this.log('Data channel opened'); dataChannel.send('Hello from ' + this.peerId); }; dataChannel.onmessage = (event) => { this.log('Data channel message: ' + event.data); }; } sendSignalingMessage(message) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); this.log('Sent: ' + message.type); } } joinRoom(roomId, peerId) { this.sendSignalingMessage({ type: 'join', roomId: roomId, peerId: peerId }); } leaveRoom() { if (this.roomId) { this.sendSignalingMessage({ type: 'leave' }); this.roomId = null; this.currentRoom.textContent = 'None'; this.peerCount.textContent = '0'; } } createRoom() { const roomId = document.getElementById('roomId').value; this.sendSignalingMessage({ type: 'create-room', roomId: roomId, roomName: roomId, maxPeers: 10, isPublic: true }); } listRooms() { this.sendSignalingMessage({ type: 'list-rooms' }); } handleRoomList(message) { this.log('Available rooms: ' + message.rooms.length); message.rooms.forEach(room => { this.log(`Room: ${room.roomName} (${room.peerCount}/${room.maxPeers} peers)`); }); } async startVideo() { try { this.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); this.localVideo.srcObject = this.localStream; this.log('Local video started'); } catch (error) { this.log('Error starting video: ' + error); } } async startAudio() { try { const audioStream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }); if (this.localStream) { audioStream.getAudioTracks().forEach(track => { this.localStream.addTrack(track); }); } else { this.localStream = audioStream; this.localVideo.srcObject = this.localStream; } this.log('Local audio started'); } catch (error) { this.log('Error starting audio: ' + error); } } stopVideo() { if (this.localStream) { this.localStream.getVideoTracks().forEach(track => track.stop()); this.log('Local video stopped'); } } stopAudio() { if (this.localStream) { this.localStream.getAudioTracks().forEach(track => track.stop()); this.log('Local audio stopped'); } } async shareScreen() { try { const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); // Replace video track in existing connection if (this.peerConnection && this.localStream) { const videoTrack = screenStream.getVideoTracks()[0]; const sender = this.peerConnection.getSenders().find(s => s.track && s.track.kind === 'video' ); if (sender) { await sender.replaceTrack(videoTrack); } // Update local video display this.localStream.getVideoTracks().forEach(track => track.stop()); this.localStream.addTrack(videoTrack); this.log('Screen sharing started'); } } catch (error) { this.log('Error sharing screen: ' + error); } } log(message) { const timestamp = new Date().toLocaleTimeString(); this.messageLog.innerHTML += `[${timestamp}] ${message}\n`; this.messageLog.scrollTop = this.messageLog.scrollHeight; } updateStatus(status) { this.connectionStatus.textContent = status; } } // Initialize client const client = new WebRTCClient(); client.connect('ws://localhost:8887'); // Global functions for HTML buttons function joinRoom() { const roomId = document.getElementById('roomId').value; const peerId = document.getElementById('peerId').value || undefined; client.joinRoom(roomId, peerId); } function leaveRoom() { client.leaveRoom(); } function createRoom() { client.createRoom(); } function listRooms() { client.listRooms(); } function startVideo() { client.startVideo(); } function stopVideo() { client.stopVideo(); } function startAudio() { client.startAudio(); } function stopAudio() { client.stopAudio(); } function shareScreen() { client.shareScreen(); } </script> </body> </html> Production Considerations
Security Enhancements
import java.security.SecureRandom; import java.util.Base64; public class SecurityManager { private final SecureRandom random = new SecureRandom(); private final Map<String, Long> authTokens = new ConcurrentHashMap<>(); public String generateAuthToken(String peerId) { byte[] bytes = new byte[32]; random.nextBytes(bytes); String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); authTokens.put(token, System.currentTimeMillis()); return token; } public boolean validateAuthToken(String token, String peerId) { Long timestamp = authTokens.get(token); if (timestamp == null) return false; // Token expires after 1 hour if (System.currentTimeMillis() - timestamp > 3600000) { authTokens.remove(token); return false; } return true; } public void revokeAuthToken(String token) { authTokens.remove(token); } } Load Balancing and Scaling
public class ClusterAwareSignalingServer extends WebRTCSignalingServer { private final String nodeId; private final ClusterManager clusterManager; public ClusterAwareSignalingServer(int port, String nodeId) { super(port); this.nodeId = nodeId; this.clusterManager = new ClusterManager(nodeId); } @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { super.onOpen(conn, handshake); clusterManager.notifyConnectionOpened(); } @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { super.onClose(conn, code, reason, remote); clusterManager.notifyConnectionClosed(); } public static class ClusterManager { private final String nodeId; private final Map<String, NodeStats> clusterNodes = new ConcurrentHashMap<>(); public ClusterManager(String nodeId) { this.nodeId = nodeId; } public void notifyConnectionOpened() { // Update local node stats and notify other nodes } public void notifyConnectionClosed() { // Update local node stats and notify other nodes } public boolean shouldAcceptNewConnection() { // Implement load balancing logic return getCurrentLoad() < getMaxLoad(); } private double getCurrentLoad() { // Calculate current node load return 0.0; } private double getMaxLoad() { // Get maximum load for this node return 1000.0; } } } Conclusion
Key Features Implemented
- WebSocket Communication: Real-time signaling between peers
- Room Management: Multi-room support with peer limits
- SDP Exchange: Offer/answer model for connection establishment
- ICE Candidate Relay: NAT traversal support
- Data Channels: Bidirectional data communication
- Error Handling: Robust error handling and recovery
- Statistics: Monitoring and performance tracking
Deployment Options
- Standalone: Single server deployment
- Docker Container: Containerized deployment
- Kubernetes: Scalable cluster deployment
- Cloud Services: AWS, GCP, Azure deployment
Use Cases
- Video Conferencing: Multi-party video calls
- Live Streaming: Real-time media broadcasting
- Gaming: Real-time multiplayer games
- Collaboration Tools: Shared whiteboards, document editing
- IoT Applications: Real-time device communication
Performance Optimization Tips
- Use efficient data structures for room and peer management
- Implement connection pooling for database operations
- Use binary protocols for large data transfers
- Implement rate limiting to prevent abuse
- Use CDN for static content delivery
- Monitor resource usage and scale accordingly
This WebRTC signaling server provides a solid foundation for building real-time communication applications with support for scaling, security, and advanced features.
Next Steps: Explore TURN server integration for better NAT traversal, implement SFU/MCU architectures for large-scale conferences, or add recording and streaming capabilities.