Files
chatrtc/server/server.js
2025-06-15 16:32:37 +02:00

542 líneas
18 KiB
JavaScript

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(`
<!DOCTYPE html>
<html>
<head>
<title>ChatRTC Signaling Server</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #2196F3; }
.status { background: #e8f5e8; padding: 15px; border-radius: 5px; margin: 20px 0; }
.endpoint { background: #f8f9fa; padding: 10px; margin: 10px 0; border-left: 4px solid #2196F3; }
code { background: #f0f0f0; padding: 2px 5px; border-radius: 3px; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 ChatRTC Signaling Server</h1>
<div class="status">
<strong>✅ Server is running successfully!</strong><br>
Connected Users: <span id="users">${users.size}</span><br>
Active Rooms: <span id="rooms">${rooms.size}</span><br>
Uptime: <span id="uptime">${Math.floor(process.uptime())}s</span>
</div>
<h2>📡 API Endpoints</h2>
<div class="endpoint">
<strong>GET /api/status</strong> - Server status and statistics
</div>
<div class="endpoint">
<strong>GET /api/rooms</strong> - List all active rooms
</div>
<div class="endpoint">
<strong>GET /api/rooms/:roomId</strong> - Get specific room info
</div>
<div class="endpoint">
<strong>GET /health</strong> - Health check endpoint
</div>
<h2>🔧 Socket.IO Events</h2>
<p>The server handles the following Socket.IO events for WebRTC signaling:</p>
<ul>
<li><code>join-room</code> - Join a chat room</li>
<li><code>offer</code> - WebRTC offer signaling</li>
<li><code>answer</code> - WebRTC answer signaling</li>
<li><code>ice-candidate</code> - ICE candidate exchange</li>
<li><code>media-state</code> - Video/audio state updates</li>
<li><code>chat-message</code> - Text message fallback</li>
</ul>
<h2>📱 Android App Connection</h2>
<p>Update your Android app's WebRTCManager.java with this server URL:</p>
<div class="endpoint">
<code>private static final String SIGNALING_SERVER_URL = "http://YOUR_SERVER_IP:${process.env.PORT || 3000}";</code>
</div>
<p><em>Server started at: ${new Date().toISOString()}</em></p>
</div>
<script>
// Auto-refresh stats every 5 seconds
setInterval(async () => {
try {
const response = await fetch('/api/status');
const data = await response.json();
document.getElementById('users').textContent = data.connectedUsers;
document.getElementById('rooms').textContent = data.activeRooms;
document.getElementById('uptime').textContent = Math.floor(data.uptime) + 's';
} catch (error) {
console.error('Failed to update stats:', error);
}
}, 5000);
</script>
</body>
</html>
`);
});
// Error handling
app.use((err, req, res, next) => {
console.error('Express error:', err);
res.status(500).json({ error: 'Internal server error' });
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
// Start server
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0';
server.listen(PORT, HOST, () => {
console.log('🚀 ChatRTC Signaling Server Started');
console.log(`📡 Server running on http://${HOST}:${PORT}`);
console.log(`🌐 Socket.IO enabled with CORS`);
console.log(`📱 Ready for Android app connections`);
console.log(`⏰ Started at: ${new Date().toISOString()}`);
});
module.exports = { app, server, io };