const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const cors = require('cors'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"], allowedHeaders: ["Content-Type"], credentials: true }, transports: ['websocket', 'polling'] }); // Middleware app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Store active users and rooms const users = new Map(); // socketId -> user info const rooms = new Map(); // roomId -> Set of socketIds const userRooms = new Map(); // socketId -> roomId // Default room for all users const DEFAULT_ROOM = 'main-chat'; // Utility functions function getUserInfo(socketId) { return users.get(socketId); } function addUserToRoom(socketId, roomId = DEFAULT_ROOM) { if (!rooms.has(roomId)) { rooms.set(roomId, new Set()); } rooms.get(roomId).add(socketId); userRooms.set(socketId, roomId); } function removeUserFromRoom(socketId) { const roomId = userRooms.get(socketId); if (roomId && rooms.has(roomId)) { rooms.get(roomId).delete(socketId); if (rooms.get(roomId).size === 0) { rooms.delete(roomId); } } userRooms.delete(socketId); } function getRoomUsers(roomId) { const roomUsers = []; if (rooms.has(roomId)) { for (const socketId of rooms.get(roomId)) { const user = users.get(socketId); if (user) { roomUsers.push({ socketId, nickname: user.nickname, joinedAt: user.joinedAt }); } } } return roomUsers; } function broadcastToRoom(roomId, event, data, exceptSocketId = null) { if (rooms.has(roomId)) { for (const socketId of rooms.get(roomId)) { if (socketId !== exceptSocketId) { io.to(socketId).emit(event, data); } } } } // Socket.IO connection handling io.on('connection', (socket) => { console.log(`[${new Date().toISOString()}] User connected: ${socket.id}`); // Handle user joining socket.on('join-room', (data) => { try { const { nickname, roomId = DEFAULT_ROOM } = typeof data === 'string' ? { nickname: data, roomId: DEFAULT_ROOM } : data; if (!nickname || nickname.trim().length === 0) { socket.emit('error', { message: 'Nickname is required' }); return; } // Check if nickname is already taken in the room const roomUsers = getRoomUsers(roomId); const nicknameExists = roomUsers.some(user => user.nickname.toLowerCase() === nickname.toLowerCase() ); if (nicknameExists) { socket.emit('error', { message: 'Nickname already taken in this room', code: 'NICKNAME_TAKEN' }); return; } // Store user information const userInfo = { nickname: nickname.trim(), roomId, joinedAt: new Date().toISOString(), isVideoEnabled: true, isAudioEnabled: true }; users.set(socket.id, userInfo); addUserToRoom(socket.id, roomId); // Join socket room socket.join(roomId); // Notify user they successfully joined socket.emit('joined-room', { roomId, nickname: userInfo.nickname, users: getRoomUsers(roomId) }); // Notify others in the room broadcastToRoom(roomId, 'user-joined', { socketId: socket.id, nickname: userInfo.nickname, joinedAt: userInfo.joinedAt }, socket.id); console.log(`[${new Date().toISOString()}] ${userInfo.nickname} joined room: ${roomId}`); } catch (error) { console.error('Error in join-room:', error); socket.emit('error', { message: 'Failed to join room' }); } }); // Handle WebRTC signaling - Offer socket.on('offer', (data) => { try { const user = getUserInfo(socket.id); if (!user) { socket.emit('error', { message: 'Not authenticated' }); return; } const { targetSocketId, offer } = data; if (targetSocketId) { // Send to specific user io.to(targetSocketId).emit('offer', { fromSocketId: socket.id, fromNickname: user.nickname, offer }); } else { // Broadcast to room (for group calls) broadcastToRoom(user.roomId, 'offer', { fromSocketId: socket.id, fromNickname: user.nickname, offer }, socket.id); } console.log(`[${new Date().toISOString()}] Offer from ${user.nickname} to ${targetSocketId || 'room'}`); } catch (error) { console.error('Error in offer:', error); socket.emit('error', { message: 'Failed to send offer' }); } }); // Handle WebRTC signaling - Answer socket.on('answer', (data) => { try { const user = getUserInfo(socket.id); if (!user) { socket.emit('error', { message: 'Not authenticated' }); return; } const { targetSocketId, answer } = data; if (targetSocketId) { io.to(targetSocketId).emit('answer', { fromSocketId: socket.id, fromNickname: user.nickname, answer }); } console.log(`[${new Date().toISOString()}] Answer from ${user.nickname} to ${targetSocketId}`); } catch (error) { console.error('Error in answer:', error); socket.emit('error', { message: 'Failed to send answer' }); } }); // Handle WebRTC signaling - ICE Candidate socket.on('ice-candidate', (data) => { try { const user = getUserInfo(socket.id); if (!user) { socket.emit('error', { message: 'Not authenticated' }); return; } const { targetSocketId, candidate } = data; if (targetSocketId) { io.to(targetSocketId).emit('ice-candidate', { fromSocketId: socket.id, fromNickname: user.nickname, candidate }); } else { // Broadcast to room broadcastToRoom(user.roomId, 'ice-candidate', { fromSocketId: socket.id, fromNickname: user.nickname, candidate }, socket.id); } } catch (error) { console.error('Error in ice-candidate:', error); socket.emit('error', { message: 'Failed to send ICE candidate' }); } }); // Handle media state changes socket.on('media-state', (data) => { try { const user = getUserInfo(socket.id); if (!user) { socket.emit('error', { message: 'Not authenticated' }); return; } const { isVideoEnabled, isAudioEnabled } = data; // Update user state user.isVideoEnabled = isVideoEnabled; user.isAudioEnabled = isAudioEnabled; // Broadcast to room broadcastToRoom(user.roomId, 'user-media-state', { socketId: socket.id, nickname: user.nickname, isVideoEnabled, isAudioEnabled }, socket.id); console.log(`[${new Date().toISOString()}] ${user.nickname} media state - Video: ${isVideoEnabled}, Audio: ${isAudioEnabled}`); } catch (error) { console.error('Error in media-state:', error); } }); // Handle chat messages (backup for data channel failures) socket.on('chat-message', (data) => { try { const user = getUserInfo(socket.id); if (!user) { socket.emit('error', { message: 'Not authenticated' }); return; } const { message } = data; if (!message || message.trim().length === 0) { return; } const messageData = { fromSocketId: socket.id, fromNickname: user.nickname, message: message.trim(), timestamp: new Date().toISOString() }; // Broadcast to room broadcastToRoom(user.roomId, 'chat-message', messageData, socket.id); console.log(`[${new Date().toISOString()}] Message from ${user.nickname}: ${message}`); } catch (error) { console.error('Error in chat-message:', error); } }); // Handle room list request socket.on('get-rooms', () => { try { const roomList = []; for (const [roomId, userSet] of rooms.entries()) { roomList.push({ roomId, userCount: userSet.size, users: getRoomUsers(roomId).map(u => ({ nickname: u.nickname, joinedAt: u.joinedAt })) }); } socket.emit('rooms-list', roomList); } catch (error) { console.error('Error getting rooms:', error); socket.emit('error', { message: 'Failed to get rooms' }); } }); // Handle user list request for current room socket.on('get-room-users', () => { try { const user = getUserInfo(socket.id); if (user) { const roomUsers = getRoomUsers(user.roomId); socket.emit('room-users', { roomId: user.roomId, users: roomUsers }); } } catch (error) { console.error('Error getting room users:', error); } }); // Handle ping for connection monitoring socket.on('ping', () => { socket.emit('pong', { timestamp: Date.now() }); }); // Handle disconnection socket.on('disconnect', (reason) => { try { const user = getUserInfo(socket.id); if (user) { // Notify others in the room broadcastToRoom(user.roomId, 'user-left', { socketId: socket.id, nickname: user.nickname, reason }); console.log(`[${new Date().toISOString()}] ${user.nickname} left room: ${user.roomId} (${reason})`); // Clean up removeUserFromRoom(socket.id); users.delete(socket.id); } else { console.log(`[${new Date().toISOString()}] Unknown user disconnected: ${socket.id} (${reason})`); } } catch (error) { console.error('Error handling disconnect:', error); } }); // Handle errors socket.on('error', (error) => { console.error(`[${new Date().toISOString()}] Socket error for ${socket.id}:`, error); }); }); // REST API endpoints app.get('/api/status', (req, res) => { res.json({ status: 'running', timestamp: new Date().toISOString(), connectedUsers: users.size, activeRooms: rooms.size, uptime: process.uptime() }); }); app.get('/api/rooms', (req, res) => { const roomList = []; for (const [roomId, userSet] of rooms.entries()) { roomList.push({ roomId, userCount: userSet.size, users: getRoomUsers(roomId).map(u => ({ nickname: u.nickname, joinedAt: u.joinedAt })) }); } res.json(roomList); }); app.get('/api/rooms/:roomId', (req, res) => { const { roomId } = req.params; if (rooms.has(roomId)) { res.json({ roomId, userCount: rooms.get(roomId).size, users: getRoomUsers(roomId) }); } else { res.status(404).json({ error: 'Room not found' }); } }); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // Serve a simple test page app.get('/', (req, res) => { res.send(`
The server handles the following Socket.IO events for WebRTC signaling:
join-room - Join a chat roomoffer - WebRTC offer signalinganswer - WebRTC answer signalingice-candidate - ICE candidate exchangemedia-state - Video/audio state updateschat-message - Text message fallbackUpdate your Android app's WebRTCManager.java with this server URL:
private static final String SIGNALING_SERVER_URL = "http://YOUR_SERVER_IP:${process.env.PORT || 3000}";
Server started at: ${new Date().toISOString()}