277
SETUP.md
Archivo normal
277
SETUP.md
Archivo normal
@@ -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.**
|
||||||
@@ -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
35
server/.env.example
Archivo normal
@@ -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
124
server/.gitignore
vendido
Archivo normal
@@ -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
345
server/README.md
Archivo normal
@@ -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
23
server/ecosystem.config.js
Archivo normal
@@ -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
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
58
server/package.json
Archivo normal
@@ -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
541
server/server.js
Archivo normal
@@ -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
222
server/start-server.sh
Archivo ejecutable
@@ -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
195
server/test-client.js
Archivo ejecutable
@@ -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);
|
||||||
|
});
|
||||||
Referencia en una nueva incidencia
Block a user