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.