WebRTC Signaling Server in Java: Complete Implementation Guide

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

  1. Join Room → Peer connects to a room
  2. Offer → Peer A sends SDP offer
  3. Answer → Peer B sends SDP answer
  4. ICE Candidates → Exchange network information
  5. 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

  1. WebSocket Communication: Real-time signaling between peers
  2. Room Management: Multi-room support with peer limits
  3. SDP Exchange: Offer/answer model for connection establishment
  4. ICE Candidate Relay: NAT traversal support
  5. Data Channels: Bidirectional data communication
  6. Error Handling: Robust error handling and recovery
  7. Statistics: Monitoring and performance tracking

Deployment Options

  1. Standalone: Single server deployment
  2. Docker Container: Containerized deployment
  3. Kubernetes: Scalable cluster deployment
  4. 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

  1. Use efficient data structures for room and peer management
  2. Implement connection pooling for database operations
  3. Use binary protocols for large data transfers
  4. Implement rate limiting to prevent abuse
  5. Use CDN for static content delivery
  6. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper