signalling server

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-06-15 16:32:37 +02:00
padre e8a203a71d
commit ce9e977aa8
Se han modificado 11 ficheros con 9922 adiciones y 18 borrados

277
SETUP.md Archivo normal
Ver fichero

@@ -0,0 +1,277 @@
# 🚀 ChatRTC - Complete Setup Guide
## Project Overview
ChatRTC is a complete Android WebRTC chat application with a Node.js signaling server. Users can join with just a nickname and enjoy:
- 💬 **Text messaging with emoji support**
- 📹 **Video calling with WebRTC**
- 🎤 **Audio calling**
- 👥 **Multi-user chat rooms**
- 📱 **Modern Android UI**
## 📁 Project Structure
```
chatrtc/
├── app/ # Android Application
│ ├── src/main/java/com/chatrtc/app/
│ │ ├── MainActivity.java # Nickname entry & permissions
│ │ ├── ChatActivity.java # Main chat interface
│ │ ├── adapter/
│ │ │ └── ChatAdapter.java # Chat message RecyclerView adapter
│ │ ├── model/
│ │ │ └── ChatMessage.java # Message data model
│ │ └── webrtc/
│ │ └── WebRTCManager.java # WebRTC connection management
│ └── src/main/res/ # Android resources (layouts, drawables, etc.)
├── server/ # Node.js Signaling Server
│ ├── server.js # Main server implementation
│ ├── package.json # Dependencies and scripts
│ ├── start-server.sh # Easy startup script
│ ├── test-client.js # Server testing utility
│ └── README.md # Server documentation
└── README.md # This file
```
## 🛠️ Quick Setup
### 1. Server Setup (5 minutes)
```bash
# Navigate to server directory
cd server
# Install dependencies
npm install
# Start the server
npm start
# or use the convenient script:
./start-server.sh start dev
```
**Server will be running at:**
- 🌐 **Local**: http://localhost:3000
- 📱 **Android Emulator**: http://10.0.2.2:3000
- 🔗 **Network**: http://YOUR_IP:3000
### 2. Android App Setup
1. **Open in Android Studio**: Open the `chatrtc` folder
2. **Update Server URL**: In `WebRTCManager.java`, update:
```java
private static final String SIGNALING_SERVER_URL = "http://10.0.2.2:3000"; // For emulator
// or "http://YOUR_SERVER_IP:3000" for real device
```
3. **Build & Run**: Connect device/emulator and click Run
### 3. Test the App
1. **Grant Permissions**: Allow camera and microphone access
2. **Enter Nickname**: Type any nickname and join
3. **Start Chatting**: Send messages, toggle video/audio
4. **Multi-user**: Run on multiple devices to test P2P connections
## 🎯 Key Features Implemented
### Android App Features
- ✅ **Simple nickname entry** - No registration required
- ✅ **Emoji support** - Full emoji keyboard and rendering
- ✅ **WebRTC video/audio** - Peer-to-peer communication
- ✅ **Modern chat UI** - Material Design with chat bubbles
- ✅ **Media controls** - Toggle video/audio, switch cameras
- ✅ **Permission handling** - Smooth camera/mic permission flow
### Server Features
- ✅ **Real-time signaling** - WebRTC offer/answer/ICE exchange
- ✅ **Room management** - Multiple users in chat rooms
- ✅ **User tracking** - Monitor connections and states
- ✅ **REST API** - Server monitoring and room info
- ✅ **Error handling** - Comprehensive error management
- ✅ **Production ready** - PM2 support, logging, CORS
## 📱 Android Integration Details
### WebRTC Connection Flow
1. User enters nickname → joins default room
2. Server notifies other users → triggers WebRTC offer
3. Peer connection established → direct P2P communication
4. Data channels used for text messages
5. Media streams for audio/video
### Key Android Components
- **MainActivity**: Handles nickname input and permissions
- **ChatActivity**: Main UI with video views and chat list
- **WebRTCManager**: Manages all WebRTC connections and signaling
- **ChatAdapter**: Handles different message types (own/other/system)
## 🖥️ Server API Reference
### REST Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Server status dashboard (HTML) |
| `/api/status` | GET | Server statistics (JSON) |
| `/api/rooms` | GET | List all active rooms |
| `/api/rooms/:id` | GET | Specific room information |
| `/health` | GET | Health check endpoint |
### Socket.IO Events
**Client → Server:**
- `join-room` - Join chat room with nickname
- `offer/answer/ice-candidate` - WebRTC signaling
- `media-state` - Video/audio state updates
- `chat-message` - Text message fallback
**Server → Client:**
- `joined-room` - Successful room join confirmation
- `user-joined/user-left` - User connection notifications
- `offer/answer/ice-candidate` - WebRTC signaling relay
- `user-media-state` - Media state broadcasts
## 🔧 Development & Testing
### Server Commands
```bash
# Development with auto-restart
npm run dev
# Production mode
npm start
# Process manager (recommended for production)
./start-server.sh start pm2
# Test server connectivity
./start-server.sh test
# Show server info
./start-server.sh info
```
### Android Development
```bash
# Build debug APK
./gradlew assembleDebug
# Install on connected device
adb install app/build/outputs/apk/debug/app-debug.apk
# View app logs
adb logcat | grep ChatRTC
```
### Testing Multi-User Scenarios
1. **Multiple Devices**: Install on different Android devices
2. **Emulator + Device**: Run on emulator and real device
3. **Multiple Emulators**: Create multiple AVDs for testing
## 🚀 Production Deployment
### Server Deployment
```bash
# Using PM2 (recommended)
npm install -g pm2
npm run pm2:start
# Using Docker
docker build -t chatrtc-server .
docker run -p 3000:3000 chatrtc-server
# Manual deployment
NODE_ENV=production npm start
```
### Android Release
```bash
# Generate release APK
./gradlew assembleRelease
# Generate signed APK (configure keystore first)
./gradlew bundleRelease
```
## 🐛 Troubleshooting
### Common Issues
**Server won't start:**
- Check if port 3000 is available: `lsof -i :3000`
- Verify Node.js installation: `node --version`
**Android connection failed:**
- Verify server URL in WebRTCManager.java
- Check network connectivity
- Ensure server is accessible from device network
**WebRTC not working:**
- Grant camera/microphone permissions
- Check for HTTPS requirement in production
- Verify STUN server connectivity
**No video/audio:**
- Test on real device (emulator has limited media support)
- Check device camera/microphone functionality
- Verify WebRTC peer connection establishment
### Debug Commands
```bash
# Check server status
curl http://localhost:3000/api/status
# Monitor server logs
tail -f server/logs/chatrtc-server.log
# Android logs
adb logcat | grep -E "(WebRTC|ChatRTC|WebRTCManager)"
```
## 📊 Performance & Scalability
### Current Limitations
- **P2P only**: Direct connections between 2 users
- **Single server**: One signaling server instance
- **Memory-based storage**: User data not persisted
### Scaling Options
- **MCU/SFU**: Media server for group calls
- **Redis**: Distributed session storage
- **Load balancer**: Multiple server instances
- **Database**: Persistent user/room data
## 🎨 Customization
### UI Customization
- **Colors**: Edit `app/src/main/res/values/colors.xml`
- **Layouts**: Modify XML layouts in `res/layout/`
- **Icons**: Replace drawable resources
- **Themes**: Update `res/values/themes.xml`
### Server Customization
- **Room limits**: Modify server configuration
- **STUN/TURN servers**: Update WebRTC configuration
- **Message limits**: Configure rate limiting
- **Logging**: Adjust log levels and destinations
## 📄 License
MIT License - Feel free to use and modify for your projects!
## 🤝 Contributing
1. Fork the repository
2. Create feature branch: `git checkout -b feature/amazing-feature`
3. Commit changes: `git commit -m 'Add amazing feature'`
4. Push to branch: `git push origin feature/amazing-feature`
5. Open Pull Request
## 🆘 Support
- 📝 **Issues**: Create GitHub issues for bugs/features
- 📚 **Documentation**: Check server and app README files
- 🔍 **Debugging**: Enable debug logs for detailed information
---
**🎉 You're all set! Your ChatRTC application is ready for development and deployment.**

Ver fichero

@@ -40,7 +40,9 @@ import io.socket.client.Socket;
public class WebRTCManager { public class WebRTCManager {
private static final String TAG = "WebRTCManager"; private static final String TAG = "WebRTCManager";
private static final String SIGNALING_SERVER_URL = "https://your-signaling-server.com"; // Replace with your server private static final String SIGNALING_SERVER_URL = "http://10.0.2.2:3000"; // For Android emulator
// For real device on same network, use: "http://192.168.1.XXX:3000"
// For production: "https://your-server.com"
public interface WebRTCListener { public interface WebRTCListener {
void onMessageReceived(String senderNickname, String message); void onMessageReceived(String senderNickname, String message);
@@ -208,13 +210,25 @@ public class WebRTCManager {
listener.onDisconnected(); listener.onDisconnected();
}); });
socket.on("joined-room", args -> {
Log.d(TAG, "Successfully joined room");
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
String roomId = data.get("roomId").getAsString();
// Room joined successfully, ready for WebRTC
});
socket.on("user-joined", args -> { socket.on("user-joined", args -> {
String userNickname = (String) args[0]; JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
String userNickname = data.get("nickname").getAsString();
listener.onUserJoined(userNickname); listener.onUserJoined(userNickname);
// Initiate WebRTC connection for new user
createOffer();
}); });
socket.on("user-left", args -> { socket.on("user-left", args -> {
String userNickname = (String) args[0]; JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
String userNickname = data.get("nickname").getAsString();
listener.onUserLeft(userNickname); listener.onUserLeft(userNickname);
}); });
@@ -233,6 +247,13 @@ public class WebRTCManager {
handleIceCandidate(data); handleIceCandidate(data);
}); });
socket.on("error", args -> {
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
String errorMessage = data.get("message").getAsString();
Log.e(TAG, "Server error: " + errorMessage);
listener.onError(errorMessage);
});
socket.connect(); socket.connect();
} catch (Exception e) { } catch (Exception e) {
@@ -244,7 +265,10 @@ public class WebRTCManager {
public void joinRoom(String nickname) { public void joinRoom(String nickname) {
this.nickname = nickname; this.nickname = nickname;
if (socket != null && socket.connected()) { if (socket != null && socket.connected()) {
socket.emit("join-room", nickname); JsonObject joinData = new JsonObject();
joinData.addProperty("nickname", nickname);
joinData.addProperty("roomId", "main-chat"); // Default room
socket.emit("join-room", joinData);
} }
} }
@@ -285,48 +309,101 @@ public class WebRTCManager {
} }
private void handleOffer(JsonObject data) { private void handleOffer(JsonObject data) {
SessionDescription offer = new SessionDescription( JsonObject offer = data.getAsJsonObject("offer");
SessionDescription offerSdp = new SessionDescription(
SessionDescription.Type.OFFER, SessionDescription.Type.OFFER,
data.get("sdp").getAsString() offer.get("sdp").getAsString()
); );
peerConnection.setRemoteDescription(new SdpObserver(), offer); peerConnection.setRemoteDescription(new SdpObserver(), offerSdp);
// Create answer // Create answer
MediaConstraints constraints = new MediaConstraints(); MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
peerConnection.createAnswer(new SdpObserver() { peerConnection.createAnswer(new SdpObserver() {
@Override @Override
public void onCreateSuccess(SessionDescription sessionDescription) { public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(TAG, "Answer created successfully");
peerConnection.setLocalDescription(new SdpObserver(), sessionDescription); peerConnection.setLocalDescription(new SdpObserver(), sessionDescription);
JsonObject answerData = new JsonObject(); JsonObject answerData = new JsonObject();
answerData.addProperty("type", sessionDescription.type.canonicalForm()); JsonObject answer = new JsonObject();
answerData.addProperty("sdp", sessionDescription.description); answer.addProperty("type", sessionDescription.type.canonicalForm());
answer.addProperty("sdp", sessionDescription.description);
answerData.add("answer", answer);
// Send to the specific user who sent the offer
if (data.has("fromSocketId")) {
answerData.addProperty("targetSocketId", data.get("fromSocketId").getAsString());
}
socket.emit("answer", answerData); socket.emit("answer", answerData);
} }
@Override
public void onCreateFailure(String error) {
Log.e(TAG, "Failed to create answer: " + error);
listener.onError("Failed to create answer: " + error);
}
}, constraints); }, constraints);
} }
private void handleAnswer(JsonObject data) { private void handleAnswer(JsonObject data) {
SessionDescription answer = new SessionDescription( JsonObject answer = data.getAsJsonObject("answer");
SessionDescription answerSdp = new SessionDescription(
SessionDescription.Type.ANSWER, SessionDescription.Type.ANSWER,
data.get("sdp").getAsString() answer.get("sdp").getAsString()
); );
peerConnection.setRemoteDescription(new SdpObserver(), answer); peerConnection.setRemoteDescription(new SdpObserver(), answerSdp);
} }
private void handleIceCandidate(JsonObject data) { private void handleIceCandidate(JsonObject data) {
JsonObject candidateObj = data.getAsJsonObject("candidate");
IceCandidate iceCandidate = new IceCandidate( IceCandidate iceCandidate = new IceCandidate(
data.get("sdpMid").getAsString(), candidateObj.get("sdpMid").getAsString(),
data.get("sdpMLineIndex").getAsInt(), candidateObj.get("sdpMLineIndex").getAsInt(),
data.get("candidate").getAsString() candidateObj.get("candidate").getAsString()
); );
peerConnection.addIceCandidate(iceCandidate); peerConnection.addIceCandidate(iceCandidate);
} }
private void createOffer() {
if (peerConnection == null) {
Log.w(TAG, "PeerConnection not initialized when creating offer");
return;
}
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
peerConnection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(TAG, "Offer created successfully");
peerConnection.setLocalDescription(new SdpObserver(), sessionDescription);
JsonObject offerData = new JsonObject();
JsonObject offer = new JsonObject();
offer.addProperty("type", sessionDescription.type.canonicalForm());
offer.addProperty("sdp", sessionDescription.description);
offerData.add("offer", offer);
socket.emit("offer", offerData);
}
@Override
public void onCreateFailure(String error) {
Log.e(TAG, "Failed to create offer: " + error);
listener.onError("Failed to create offer: " + error);
}
}, constraints);
}
public void cleanup() { public void cleanup() {
if (localVideoTrack != null) { if (localVideoTrack != null) {
localVideoTrack.dispose(); localVideoTrack.dispose();
@@ -389,9 +466,11 @@ public class WebRTCManager {
Log.d(TAG, "onIceCandidate: " + iceCandidate); Log.d(TAG, "onIceCandidate: " + iceCandidate);
JsonObject candidateData = new JsonObject(); JsonObject candidateData = new JsonObject();
candidateData.addProperty("candidate", iceCandidate.sdp); JsonObject candidate = new JsonObject();
candidateData.addProperty("sdpMid", iceCandidate.sdpMid); candidate.addProperty("candidate", iceCandidate.sdp);
candidateData.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex); candidate.addProperty("sdpMid", iceCandidate.sdpMid);
candidate.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
candidateData.add("candidate", candidate);
socket.emit("ice-candidate", candidateData); socket.emit("ice-candidate", candidateData);
} }

35
server/.env.example Archivo normal
Ver fichero

@@ -0,0 +1,35 @@
# Environment Configuration
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
# CORS Configuration (comma-separated origins)
ALLOWED_ORIGINS=*
# Socket.IO Configuration
SOCKET_IO_PING_TIMEOUT=60000
SOCKET_IO_PING_INTERVAL=25000
# Room Configuration
DEFAULT_ROOM_NAME=main-chat
MAX_USERS_PER_ROOM=10
MAX_MESSAGE_LENGTH=1000
# Rate Limiting
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=100
# Logging
LOG_LEVEL=info
LOG_FILE=logs/chatrtc-server.log
# Security
ENABLE_HELMET=true
TRUST_PROXY=false
# Production Configuration (uncomment for production)
# NODE_ENV=production
# PORT=443
# SSL_KEY_PATH=/path/to/private-key.pem
# SSL_CERT_PATH=/path/to/certificate.pem
# SSL_CA_PATH=/path/to/ca-bundle.pem

124
server/.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1,124 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.production
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# PM2 logs and pids
.pm2/
# SSL certificates
*.pem
*.key
*.crt
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

345
server/README.md Archivo normal
Ver fichero

@@ -0,0 +1,345 @@
# ChatRTC Signaling Server
Advanced WebRTC signaling server built with Node.js and Socket.IO for the ChatRTC Android application.
## Features
- 🚀 **Real-time WebRTC Signaling** - Handle offer/answer/ICE candidate exchange
- 🏠 **Room Management** - Support for multiple chat rooms
- 👥 **User Management** - Track users, nicknames, and states
- 💬 **Message Fallback** - Backup text messaging via Socket.IO
- 📊 **REST API** - Monitor server status and room information
- 🔒 **Error Handling** - Comprehensive error handling and logging
- 📱 **Mobile Optimized** - Optimized for Android WebRTC connections
- 🌐 **CORS Support** - Flexible cross-origin configuration
-**Performance** - Built for high-performance real-time communication
## Quick Start
### 1. Installation
```bash
cd server
npm install
```
### 2. Configuration
Copy the environment configuration:
```bash
cp .env.example .env
```
Edit `.env` file with your settings:
```env
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
ALLOWED_ORIGINS=*
```
### 3. Start the Server
**Development mode:**
```bash
npm run dev
```
**Production mode:**
```bash
npm start
```
**With PM2 (recommended for production):**
```bash
npm run pm2:start
```
### 4. Verify Installation
Open your browser and navigate to `http://localhost:3000` to see the server status page.
## API Endpoints
### REST API
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Server status page (HTML) |
| `/api/status` | GET | Server statistics (JSON) |
| `/api/rooms` | GET | List all active rooms |
| `/api/rooms/:roomId` | GET | Get specific room information |
| `/health` | GET | Health check endpoint |
### Example API Responses
**GET /api/status**
```json
{
"status": "running",
"timestamp": "2025-06-15T10:30:00.000Z",
"connectedUsers": 5,
"activeRooms": 2,
"uptime": 3600
}
```
**GET /api/rooms**
```json
[
{
"roomId": "main-chat",
"userCount": 3,
"users": [
{
"nickname": "Alice",
"joinedAt": "2025-06-15T10:25:00.000Z"
},
{
"nickname": "Bob",
"joinedAt": "2025-06-15T10:28:00.000Z"
}
]
}
]
```
## Socket.IO Events
### Client → Server
| Event | Data | Description |
|-------|------|-------------|
| `join-room` | `{nickname, roomId?}` | Join a chat room |
| `offer` | `{targetSocketId?, offer}` | Send WebRTC offer |
| `answer` | `{targetSocketId, answer}` | Send WebRTC answer |
| `ice-candidate` | `{targetSocketId?, candidate}` | Send ICE candidate |
| `media-state` | `{isVideoEnabled, isAudioEnabled}` | Update media state |
| `chat-message` | `{message}` | Send text message (fallback) |
| `get-rooms` | - | Request room list |
| `get-room-users` | - | Request current room users |
| `ping` | - | Connection health check |
### Server → Client
| Event | Data | Description |
|-------|------|-------------|
| `joined-room` | `{roomId, nickname, users}` | Successful room join |
| `user-joined` | `{socketId, nickname, joinedAt}` | New user joined |
| `user-left` | `{socketId, nickname, reason}` | User disconnected |
| `offer` | `{fromSocketId, fromNickname, offer}` | Received WebRTC offer |
| `answer` | `{fromSocketId, fromNickname, answer}` | Received WebRTC answer |
| `ice-candidate` | `{fromSocketId, fromNickname, candidate}` | Received ICE candidate |
| `user-media-state` | `{socketId, nickname, isVideoEnabled, isAudioEnabled}` | User media state changed |
| `chat-message` | `{fromSocketId, fromNickname, message, timestamp}` | Received text message |
| `rooms-list` | `[{roomId, userCount, users}]` | Available rooms |
| `room-users` | `{roomId, users}` | Current room users |
| `error` | `{message, code?}` | Error occurred |
| `pong` | `{timestamp}` | Ping response |
## Android Integration
### Update WebRTCManager.java
Replace the signaling server URL in your Android app:
```java
// In WebRTCManager.java
private static final String SIGNALING_SERVER_URL = "http://YOUR_SERVER_IP:3000";
// For local development with Android emulator:
private static final String SIGNALING_SERVER_URL = "http://10.0.2.2:3000";
// For real device on same network:
private static final String SIGNALING_SERVER_URL = "http://192.168.1.XXX:3000";
```
### Socket.IO Connection Example
```java
// Connect to server
socket = IO.socket(SIGNALING_SERVER_URL);
// Join room
JsonObject joinData = new JsonObject();
joinData.addProperty("nickname", nickname);
joinData.addProperty("roomId", "main-chat"); // optional
socket.emit("join-room", joinData);
// Handle room joined
socket.on("joined-room", args -> {
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
String roomId = data.get("roomId").getAsString();
// Handle successful join
});
```
## Production Deployment
### 1. Environment Setup
```bash
# Create production environment file
cp .env.example .env
# Edit for production
nano .env
```
Set production values:
```env
NODE_ENV=production
PORT=3000
HOST=0.0.0.0
ALLOWED_ORIGINS=https://yourdomain.com
```
### 2. SSL/HTTPS (Recommended)
For production, use HTTPS. You can:
- Use a reverse proxy (nginx, Apache)
- Configure SSL directly in the server
- Use a load balancer with SSL termination
### 3. Process Management
**Using PM2:**
```bash
npm install -g pm2
npm run pm2:start
# Monitor
pm2 status
pm2 logs chatrtc-server
pm2 monit
```
**Using Docker:**
```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
```
### 4. Performance Optimization
- Enable gzip compression (included)
- Use clustering for multiple CPU cores
- Configure proper logging levels
- Set up monitoring and alerting
- Use Redis for scaling (future enhancement)
## Monitoring
### Health Checks
```bash
# Basic health check
curl http://localhost:3000/health
# Detailed status
curl http://localhost:3000/api/status
```
### Logs
```bash
# View logs (PM2)
pm2 logs chatrtc-server
# View logs (direct)
tail -f logs/chatrtc-server.log
```
## Troubleshooting
### Common Issues
1. **Port already in use**
```bash
# Check what's using the port
lsof -i :3000
# Kill the process or change PORT in .env
```
2. **CORS errors from Android**
- Ensure `ALLOWED_ORIGINS=*` in .env
- Check network security config in Android app
3. **Connection timeouts**
- Verify firewall settings
- Check if port is accessible from client network
4. **Socket.IO connection fails**
- Enable polling transport as fallback
- Check network connectivity
- Verify server URL in Android app
### Debug Mode
Enable detailed logging:
```env
LOG_LEVEL=debug
```
### Testing Connection
Use a Socket.IO client to test:
```javascript
const io = require('socket.io-client');
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('Connected to server');
socket.emit('join-room', { nickname: 'TestUser' });
});
```
## Development
### Project Structure
```
server/
├── server.js # Main server file
├── package.json # Dependencies and scripts
├── ecosystem.config.js # PM2 configuration
├── .env.example # Environment template
├── .gitignore # Git ignore rules
├── README.md # This file
└── logs/ # Log files (created automatically)
```
### Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
### Running Tests
```bash
npm test
```
### Code Style
```bash
npm run lint
```
## License
MIT License - see LICENSE file for details.
## Support
- Create an issue on GitHub
- Check the troubleshooting section
- Review server logs for errors

23
server/ecosystem.config.js Archivo normal
Ver fichero

@@ -0,0 +1,23 @@
module.exports = {
apps: [{
name: 'chatrtc-server',
script: 'server.js',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
log_file: 'logs/combined.log',
out_file: 'logs/out.log',
error_file: 'logs/error.log',
log_date_format: 'YYYY-MM-DD HH:mm Z',
merge_logs: true
}]
};

8005
server/package-lock.json generado Archivo normal

La diferencia del archivo ha sido suprimido porque es demasiado grande Cargar Diff

58
server/package.json Archivo normal
Ver fichero

@@ -0,0 +1,58 @@
{
"name": "chatrtc-signaling-server",
"version": "1.0.0",
"description": "Advanced WebRTC signaling server for ChatRTC Android app with room management and real-time communication",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest",
"lint": "eslint .",
"pm2:start": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop chatrtc-server",
"pm2:restart": "pm2 restart chatrtc-server",
"pm2:logs": "pm2 logs chatrtc-server"
},
"keywords": [
"webrtc",
"signaling",
"socket.io",
"chat",
"android",
"nodejs",
"real-time",
"video-chat",
"audio-chat"
],
"author": "ChatRTC Team",
"license": "MIT",
"dependencies": {
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"socket.io": "^4.7.4",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"eslint": "^8.56.0",
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"pm2": "^5.3.0",
"supertest": "^6.3.4"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/your-username/chatrtc-signaling-server.git"
},
"bugs": {
"url": "https://github.com/your-username/chatrtc-signaling-server/issues"
},
"homepage": "https://github.com/your-username/chatrtc-signaling-server#readme"
}

541
server/server.js Archivo normal
Ver fichero

@@ -0,0 +1,541 @@
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 };

222
server/start-server.sh Archivo ejecutable
Ver fichero

@@ -0,0 +1,222 @@
#!/bin/bash
# ChatRTC Signaling Server Startup Script
set -e
SERVER_DIR="/home/ale/projects/android/chatrtc/server"
LOG_FILE="$SERVER_DIR/logs/startup.log"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Create logs directory if it doesn't exist
mkdir -p "$SERVER_DIR/logs"
echo -e "${BLUE}🚀 ChatRTC Signaling Server${NC}"
echo -e "${BLUE}================================${NC}"
# Function to print colored messages
print_status() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
# Check if Node.js is installed
if ! command -v node &> /dev/null; then
print_error "Node.js is not installed. Please install Node.js 16 or later."
exit 1
fi
# Check Node.js version
NODE_VERSION=$(node --version | cut -d'.' -f1 | cut -d'v' -f2)
if [ "$NODE_VERSION" -lt 16 ]; then
print_warning "Node.js version is $NODE_VERSION. Recommended version is 16 or later."
fi
# Check if npm is installed
if ! command -v npm &> /dev/null; then
print_error "npm is not installed. Please install npm."
exit 1
fi
# Change to server directory
cd "$SERVER_DIR"
print_info "Working directory: $SERVER_DIR"
# Check if package.json exists
if [ ! -f "package.json" ]; then
print_error "package.json not found. Make sure you're in the correct directory."
exit 1
fi
# Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then
print_info "Installing dependencies..."
npm install
print_status "Dependencies installed"
else
print_status "Dependencies already installed"
fi
# Create .env file if it doesn't exist
if [ ! -f ".env" ]; then
print_info "Creating .env file from template..."
cp .env.example .env
print_status ".env file created"
fi
# Get the local IP address
if command -v hostname &> /dev/null; then
LOCAL_IP=$(hostname -I | awk '{print $1}')
else
LOCAL_IP="localhost"
fi
# Function to start server
start_server() {
local MODE=$1
print_info "Starting server in $MODE mode..."
case $MODE in
"dev")
npm run dev
;;
"prod")
npm start
;;
"pm2")
if ! command -v pm2 &> /dev/null; then
print_warning "PM2 not installed. Installing PM2..."
npm install -g pm2
fi
npm run pm2:start
print_status "Server started with PM2"
echo
print_info "Useful PM2 commands:"
echo " pm2 status - Check server status"
echo " pm2 logs chatrtc-server - View logs"
echo " pm2 restart chatrtc-server - Restart server"
echo " pm2 stop chatrtc-server - Stop server"
;;
*)
print_error "Invalid mode. Use: dev, prod, or pm2"
exit 1
;;
esac
}
# Function to test server
test_server() {
local SERVER_URL=${1:-"http://localhost:3000"}
print_info "Testing server at $SERVER_URL..."
if [ -f "test-client.js" ]; then
node test-client.js "$SERVER_URL"
else
print_warning "test-client.js not found. Skipping server test."
fi
}
# Function to show server info
show_info() {
local PORT=${PORT:-3000}
echo
print_status "Server Information:"
echo " 📍 Local URL: http://localhost:$PORT"
echo " 🌐 Network URL: http://$LOCAL_IP:$PORT"
echo " 📱 Android Emulator URL: http://10.0.2.2:$PORT"
echo
print_info "Update your Android app's WebRTCManager.java:"
echo " private static final String SIGNALING_SERVER_URL = \"http://$LOCAL_IP:$PORT\";"
echo
print_info "Available endpoints:"
echo " GET / - Server status page"
echo " GET /api/status - Server statistics"
echo " GET /api/rooms - Active rooms"
echo " GET /health - Health check"
echo
}
# Function to show usage
show_usage() {
echo "Usage: $0 [COMMAND] [OPTIONS]"
echo
echo "Commands:"
echo " start [dev|prod|pm2] - Start the server (default: dev)"
echo " test [URL] - Test server connection"
echo " info - Show server information"
echo " stop - Stop PM2 server"
echo " logs - Show PM2 logs"
echo " status - Show PM2 status"
echo
echo "Examples:"
echo " $0 start dev - Start in development mode"
echo " $0 start pm2 - Start with PM2 process manager"
echo " $0 test - Test local server"
echo " $0 info - Show connection information"
}
# Parse command line arguments
case ${1:-start} in
"start")
MODE=${2:-dev}
show_info
start_server "$MODE"
;;
"test")
test_server "$2"
;;
"info")
show_info
;;
"stop")
if command -v pm2 &> /dev/null; then
pm2 stop chatrtc-server
print_status "Server stopped"
else
print_error "PM2 not installed"
fi
;;
"logs")
if command -v pm2 &> /dev/null; then
pm2 logs chatrtc-server
else
print_error "PM2 not installed"
fi
;;
"status")
if command -v pm2 &> /dev/null; then
pm2 status chatrtc-server
else
print_error "PM2 not installed"
fi
;;
"help"|"-h"|"--help")
show_usage
;;
*)
print_error "Unknown command: $1"
show_usage
exit 1
;;
esac

195
server/test-client.js Archivo ejecutable
Ver fichero

@@ -0,0 +1,195 @@
#!/usr/bin/env node
// Simple test client for ChatRTC signaling server
const io = require('socket.io-client');
const SERVER_URL = process.argv[2] || 'http://localhost:3000';
const NICKNAME = process.argv[3] || `TestUser_${Math.floor(Math.random() * 1000)}`;
console.log(`🧪 Testing ChatRTC Signaling Server`);
console.log(`📡 Connecting to: ${SERVER_URL}`);
console.log(`👤 Nickname: ${NICKNAME}`);
console.log('─'.repeat(50));
const socket = io(SERVER_URL, {
transports: ['websocket', 'polling']
});
// Connection events
socket.on('connect', () => {
console.log('✅ Connected to server');
console.log(`🔗 Socket ID: ${socket.id}`);
// Join room after connection
setTimeout(() => {
socket.emit('join-room', {
nickname: NICKNAME,
roomId: 'test-room'
});
}, 100);
});
socket.on('disconnect', (reason) => {
console.log(`❌ Disconnected: ${reason}`);
});
socket.on('connect_error', (error) => {
console.error('❌ Connection error:', error.message);
process.exit(1);
});
// Room events
socket.on('joined-room', (data) => {
console.log('🏠 Successfully joined room:');
console.log(` Room ID: ${data.roomId}`);
console.log(` Nickname: ${data.nickname}`);
console.log(` Users in room: ${data.users.length}`);
// Send a test message after joining
setTimeout(() => {
socket.emit('chat-message', {
message: `Hello from ${NICKNAME}! 👋`
});
}, 500);
});
socket.on('user-joined', (data) => {
console.log(`👋 User joined: ${data.nickname} (${data.socketId})`);
});
socket.on('user-left', (data) => {
console.log(`👋 User left: ${data.nickname} (${data.socketId})`);
});
// Message events
socket.on('chat-message', (data) => {
console.log(`💬 Message from ${data.fromNickname}: ${data.message}`);
});
// WebRTC signaling events
socket.on('offer', (data) => {
console.log(`📞 Received offer from ${data.fromNickname}`);
// Send back a mock answer
setTimeout(() => {
socket.emit('answer', {
targetSocketId: data.fromSocketId,
answer: {
type: 'answer',
sdp: 'mock-answer-sdp-for-testing'
}
});
}, 100);
});
socket.on('answer', (data) => {
console.log(`📞 Received answer from ${data.fromNickname}`);
});
socket.on('ice-candidate', (data) => {
console.log(`🧊 Received ICE candidate from ${data.fromNickname}`);
});
// Error handling
socket.on('error', (data) => {
console.error(`❌ Server error: ${data.message}`);
if (data.code) {
console.error(` Error code: ${data.code}`);
}
});
// Test sequence
let testStep = 0;
const runTests = () => {
testStep++;
switch (testStep) {
case 1:
console.log('🧪 Test 1: Sending ping...');
socket.emit('ping');
break;
case 2:
console.log('🧪 Test 2: Requesting room list...');
socket.emit('get-rooms');
break;
case 3:
console.log('🧪 Test 3: Requesting room users...');
socket.emit('get-room-users');
break;
case 4:
console.log('🧪 Test 4: Sending mock WebRTC offer...');
socket.emit('offer', {
offer: {
type: 'offer',
sdp: 'mock-offer-sdp-for-testing'
}
});
break;
case 5:
console.log('🧪 Test 5: Updating media state...');
socket.emit('media-state', {
isVideoEnabled: false,
isAudioEnabled: true
});
break;
default:
console.log('✅ All tests completed!');
setTimeout(() => {
console.log('👋 Disconnecting...');
socket.disconnect();
process.exit(0);
}, 1000);
return;
}
setTimeout(runTests, 2000);
};
// Additional event handlers for tests
socket.on('pong', (data) => {
console.log(`🏓 Pong received (${data.timestamp})`);
});
socket.on('rooms-list', (rooms) => {
console.log(`🏠 Rooms list received (${rooms.length} rooms):`);
rooms.forEach(room => {
console.log(` - ${room.roomId}: ${room.userCount} users`);
});
});
socket.on('room-users', (data) => {
console.log(`👥 Room users for ${data.roomId} (${data.users.length} users):`);
data.users.forEach(user => {
console.log(` - ${user.nickname} (joined: ${user.joinedAt})`);
});
});
socket.on('user-media-state', (data) => {
console.log(`📹 Media state update from ${data.nickname}: Video=${data.isVideoEnabled}, Audio=${data.isAudioEnabled}`);
});
// Start tests after successful room join
socket.on('joined-room', () => {
setTimeout(() => {
console.log('🧪 Starting test sequence...');
runTests();
}, 1000);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n👋 Shutting down test client...');
socket.disconnect();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n👋 Shutting down test client...');
socket.disconnect();
process.exit(0);
});