initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-09-16 01:54:29 +02:00
padre 9d013a7c87
commit 6d1dd42e6d
Se han modificado 44 ficheros con 1719 adiciones y 11509 borrados

193
README.md
Ver fichero

@@ -1,9 +1,19 @@
# VideoPeers - Real Time P2P Video Chat Application # VideoPeersJS - Real Time P2P Video Chat Application
## Description ## Description
This is a real time video chat application built using WebRTC, Socket.io, Node.js, Express.js, and Next.js. It allows users to create a room and share the room ID with other users to join the room for a peer-to-peer video call. This is a modern, secure real-time video chat application built using WebRTC, Socket.io, Node.js, Express.js, and Next.js. VideoPeersJS allows users to create rooms and share room IDs with other users for peer-to-peer video calls with enhanced security and a beautiful, responsive design.
## 🚀 Latest Updates (2025)
- **Enhanced Security**: Implemented comprehensive security measures including Helmet.js, rate limiting, input validation, and CORS protection
- **Modern UI/UX**: Complete redesign with beautiful gradients, animations, and responsive design using Tailwind CSS
- **Updated Dependencies**: All packages updated to latest stable versions for security and performance
- **Improved User Experience**: Better loading states, error handling, and user feedback
- **Enhanced Architecture**: Better code organization and error handling
- **Environment Configuration**: ICE servers and configuration moved to environment variables
- **Enhanced Typography**: Added multiple Google Fonts for better visual hierarchy
- **Local Fonts**: Migrated to self-hosted fonts for better performance, privacy, and reliability (156KB total)
## Deployment Link : https://video-peers.vercel.app/ ## Deployment Link : https://video-peers.vercel.app/
@@ -12,92 +22,185 @@ This is a real time video chat application built using WebRTC, Socket.io, Node.j
<p><img align="center" src="https://github.com/hirentimbadiya/Video-Peers/assets/86219935/24fa55ab-c08d-40f5-8023-060c06da78f5" alt="hirentimbadiya"/></p> <p><img align="center" src="https://github.com/hirentimbadiya/Video-Peers/assets/86219935/24fa55ab-c08d-40f5-8023-060c06da78f5" alt="hirentimbadiya"/></p>
## How To Use : ## How To Use :
First of all join any room lets say 7 for example with your email and room no. as 7 and then notify other person to enter in room no. 7, now you will get call button on screen this will allow to call other person and then call will be initiated and other person will be having a button called Send Streams and as other person clicks Send Streams Button then video call will be initiated. 1. Enter your email and a room ID on the homepage
2. Share the room ID with someone you want to video chat with
3. Once both users are in the room, click the "Start Call" button
4. Use the control buttons to mute/unmute audio, turn camera on/off, or end the call
## 🔒 Security Features
- **Rate Limiting**: Prevents abuse with request limits
- **Input Validation**: All user inputs are validated and sanitized
- **CORS Protection**: Configured for secure cross-origin requests
- **Helmet.js**: Adds security headers to protect against common attacks
- **Error Handling**: Comprehensive error handling and logging
- **Connection Timeouts**: Prevents hanging connections
## Key Features ## Key Features
- [x] Create and join video chat rooms using room IDs - [x] Create and join video chat rooms using room IDs
- [x] Real-time peer-to-peer video calling using WebRTC - [x] Real-time peer-to-peer video calling using WebRTC
- [x] Audio mute/unmute controls - [x] Audio mute/unmute controls with visual indicators
- [x] Video Hold/Play controls - [x] Video on/off controls
- [x] Call End option - [x] Call end functionality
- [x] Responsive Design - [x] Modern, responsive design with animations
- [x] Built with Next.js, Node.js, Express, Socket.io - [x] Enhanced typography with local fonts for better performance
- [x] Self-hosted fonts (no external dependencies)
- [x] Environment-based configuration for ICE servers
- [x] Input validation and sanitization
- [x] Connection status indicators
- [x] Beautiful gradients and glass-morphism effects
- [x] Accessible design with proper ARIA labels
## Installation ## Installation
1. Clone the repository 1. Clone the repository
```bash ```bash
git clone https://github.com/hirentimbadiya/Video-Peers.git git clone https://github.com/hirentimbadiya/VideoPeersJS.git
cd VideoPeersJS
``` ```
2. Install dependencies for Client Side 2. Install dependencies for Client Side
```bash ```bash
cd client #in Video-Peers/client cd client
npm install npm install
``` ```
3. Install dependencies for Server Side 3. Install dependencies for Server Side
```bash ```bash
cd server #in Video-Peers/server cd server
npm install npm install
``` ```
4. Run the application 4. Configure environment variables
- Run the client side ```bash
# In server directory, copy .env.example to .env
cp .env.example .env
# Edit .env with your preferred settings:
# PORT=8000
# CLIENT_URL=http://localhost:3000
# NODE_ENV=development
```
5. Run the application
- Run the server side first:
```bash ```bash
cd client #in Video-Peers/client cd server
npm run dev npm run dev
``` ```
- Run the server side
```bash
cd server #in Video-Peers/server
npm run dev
```
## Technologies used / Prerequisites Of The Project
- [Next.js](https://nextjs.org/) - React framework
- [WebRTC](https://webrtc.org/) - Real-time communication
- [Socket.io](https://socket.io/) - Bidirectional communication
- [Node.js](https://nodejs.org/) - Backend runtime
- [Express](https://expressjs.com/) - Node.js framework
- [Tailwind CSS](https://tailwindcss.com/) - Styling
- Run the client side:
```bash
cd client
npm run dev
```
6. Open your browser and navigate to `http://localhost:3000`
## 🛠 Technologies Used
### Frontend
- [Next.js 14](https://nextjs.org/) - React framework with latest features
- [React 18](https://reactjs.org/) - UI library
- [Tailwind CSS 3](https://tailwindcss.com/) - Utility-first CSS framework
- [Framer Motion](https://www.framer.com/motion/) - Animation library
- [Lucide React](https://lucide.dev/) - Beautiful, customizable icons
- [WebRTC](https://webrtc.org/) - Real-time communication
### Backend
- [Node.js](https://nodejs.org/) - Backend runtime
- [Express.js](https://expressjs.com/) - Web framework
- [Socket.io](https://socket.io/) - Real-time bidirectional communication
- [Helmet.js](https://helmetjs.github.io/) - Security middleware
- [Joi](https://joi.dev/) - Input validation
- [Express Rate Limit](https://github.com/nfriedly/express-rate-limit) - Rate limiting
- [Validator.js](https://github.com/validatorjs/validator.js) - String validation
## 🔧 Development
### Project Structure
```
VideoPeersJS/
├── client/ # Next.js frontend
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── pages/ # Next.js pages
│ │ ├── context/ # React context providers
│ │ ├── service/ # WebRTC service
│ │ └── styles/ # Global styles
│ ├── .env.example # Environment variables template
│ └── package.json
├── server/ # Node.js backend
│ ├── index.js # Main server file
│ ├── .env.example # Environment variables template
│ └── package.json
└── README.md
```
### API Endpoints
#### REST Endpoints
- `GET /` - Health check and server info
- `GET /health` - Health status endpoint
#### Socket Events
- `room:join` - Join a video chat room
- `user:call` - Initiate a call to another user
- `call:accepted` - Accept an incoming call
- `peer:nego:needed` - WebRTC negotiation
- `peer:nego:done` - Complete WebRTC negotiation
- `call:end` - End the current call
- `error` - Error handling
## Working Demo ## Working Demo
https://github.com/hirentimbadiya/Video-Peers/assets/86219935/7ce8caf9-0881-4abe-b633-79d3bb0a87ef https://github.com/hirentimbadiya/Video-Peers/assets/86219935/7ce8caf9-0881-4abe-b633-79d3bb0a87ef
## Future Scope ## 🚀 Future Scope
- Text chat support in rooms - [ ] Text chat support in rooms
- Screen Sharing Feature - [ ] Screen sharing feature
- Multiple Participants in a room - [ ] Multiple participants in a room (group calls)
- Video recording option - [ ] Video recording and playback
- [ ] Virtual backgrounds
- [ ] File sharing capabilities
- [ ] Mobile app development
- [ ] Integration with calendar systems
## 📚 My Learnings
## My Learnings Building this project taught me about:
By Building this project I learned about the following concepts: - **WebRTC**: Understanding peer-to-peer communication, ICE candidates, and media streams
- **Socket.io**: Real-time bidirectional communication patterns
- **Security**: Implementing comprehensive security measures for web applications
- **Modern React**: Using hooks, context, and latest React patterns
- **UI/UX Design**: Creating beautiful, accessible interfaces with Tailwind CSS
- **Next.js**: Server-side rendering, file-based routing, and optimization
- **Node.js Security**: Rate limiting, input validation, and security headers
- WebRTC (Web Real-Time Communication) is a technology that enables real-time peer-to-peer communication between browsers and mobile applications. It is an open-source and free project that used to provide web browsers and mobile applications with real-time communication (RTC) via simple application programming interfaces (APIs). ## 🤝 Contributing
- Socket.IO is a library that enables real-time, bidirectional and event-based communication between the browser and the server. It consists of: a Node.js server: Source | API. a Javascript client library for the browser (which can be also run from Node.js): Source | API. Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Next.js is a React framework that enables several server-side rendering (SSR) features such as static site generation (SSG), automatic code splitting, server-side rendering, and client-side routing. It is a framework that is built on top of React.js and Node.js. 1. Fork the project
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## 📄 License
## License
[MIT](https://choosealicense.com/licenses/mit/) [MIT](https://choosealicense.com/licenses/mit/)
## Author ## 👨‍💻 Author
- [hirentimbadiya](https://github.com/hirentimbadiya) - [hirentimbadiya](https://github.com/hirentimbadiya)
- Email : hirentimbadiya74@gmail.com - Email: hirentimbadiya74@gmail.com
---
⭐ Star this repo if you find it helpful!

3
client/.env.example Archivo normal
Ver fichero

@@ -0,0 +1,3 @@
NEXT_PUBLIC_SOCKET_SERVER_URL=http://localhost:8000
NEXT_PUBLIC_ICE_SERVERS=["stun:stun.l.google.com:19302","stun:global.stun.twilio.com:3478","stun:stun1.l.google.com:19302","stun:stun2.l.google.com:19302"]
NEXT_PUBLIC_APP_NAME=VideoPeersJS

3
client/.env.local Archivo normal
Ver fichero

@@ -0,0 +1,3 @@
NEXT_PUBLIC_SOCKET_SERVER_URL=http://localhost:8000
NEXT_PUBLIC_ICE_SERVERS=["stun:stun.l.google.com:19302","stun:global.stun.twilio.com:3478","stun:stun1.l.google.com:19302","stun:stun2.l.google.com:19302"]
NEXT_PUBLIC_APP_NAME=VideoPeersJS

6
client/.gitignore vendido
Ver fichero

@@ -25,12 +25,12 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files
.env*.local
# vercel # vercel
.vercel .vercel
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
*.lock
*-lock.json

8876
client/package-lock.json generado

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

Ver fichero

@@ -1,5 +1,5 @@
{ {
"name": "frontend", "name": "videopeersjs-client",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -9,23 +9,25 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.13.0",
"@mui/icons-material": "^5.14.16", "@mui/icons-material": "^6.1.0",
"@mui/material": "^5.14.17", "@mui/material": "^6.1.0",
"@vercel/analytics": "^1.1.1", "@vercel/analytics": "^1.3.1",
"next": "14.0.2", "framer-motion": "^11.5.4",
"react": "^18", "lucide-react": "^0.439.0",
"react-dom": "^18", "next": "^14.2.9",
"react-player": "^2.13.0", "react": "^18.3.1",
"socket.io-client": "^4.7.2" "react-dom": "^18.3.1",
"react-player": "^2.16.0",
"socket.io-client": "^4.7.5"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.0.1", "autoprefixer": "^10.4.20",
"eslint": "^8", "eslint": "^8.57.0",
"eslint-config-next": "14.0.2", "eslint-config-next": "^14.2.9",
"postcss": "^8", "postcss": "^8.4.45",
"sass": "^1.69.5", "sass": "^1.78.0",
"tailwindcss": "^3.3.0" "tailwindcss": "^3.4.10"
} }
} }

Archivo binario no mostrado.

Antes

Anchura:  |  Altura:  |  Tamaño: 25 KiB

Después

Anchura:  |  Altura:  |  Tamaño: 3.7 KiB

58
client/public/fonts/README.md Archivo normal
Ver fichero

@@ -0,0 +1,58 @@
# Fuentes Locales - VideoPeersJS
Este directorio contiene todas las fuentes descargadas localmente para el proyecto VideoPeersJS, eliminando la dependencia de Google Fonts y mejorando el rendimiento.
## Estructura de Fuentes
### Inter
- **Archivo**: `inter/`
- **Pesos**: 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold)
- **Uso**: Fuente principal para interfaces y textos de cuerpo
- **Formato**: WOFF2 (optimizado para web)
### Poppins
- **Archivo**: `poppins/`
- **Pesos**: 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold)
- **Uso**: Fuente secundaria para elementos especiales
- **Formato**: WOFF2 (optimizado para web)
### Josefin Sans
- **Archivo**: `josefin-sans/`
- **Pesos**: 400 (Regular), 500 (Medium), 600 (SemiBold)
- **Uso**: Fuente para títulos y displays
- **Formato**: WOFF2 (optimizado para web)
### Space Grotesk
- **Archivo**: `space-grotesk/`
- **Pesos**: 400 (Regular), 500 (Medium), 600 (SemiBold)
- **Uso**: Fuente decorativa y elementos especiales
- **Formato**: WOFF2 (optimizado para web)
## Beneficios de Fuentes Locales
1. **Rendimiento Mejorado**: No hay dependencias externas, carga más rápida
2. **Confiabilidad**: No depende de la disponibilidad de Google Fonts
3. **Privacidad**: No se envían datos a terceros
4. **Control Total**: Optimización y personalización completa
5. **Offline**: Funciona sin conexión a internet
## Uso en CSS
Las fuentes se importan automáticamente en `src/styles/globals.css`:
```css
@import './fonts/inter.css';
@import './fonts/poppins.css';
@import './fonts/josefin-sans.css';
@import './fonts/space-grotesk.css';
```
## Clases de Tailwind Disponibles
- `.font-body` - Inter (fuente principal)
- `.font-accent` - Poppins (elementos especiales)
- `.font-display` - Space Grotesk y Josefin Sans (títulos)
## Tamaño Total
Aproximadamente ~80KB total para todas las fuentes en formato WOFF2 comprimido.

Archivo binario no mostrado.

Archivo binario no mostrado.

Ver fichero

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuGKfAZ9hiA.woff2</code> was not found on this server. <ins>That’s all we know.</ins>

Archivo binario no mostrado.

Ver fichero

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/josefinsans/v25/Qw3FZQNVED7rKGKxtqIqX5E-AVSJrOCfjY46_DjQXME.woff2</code> was not found on this server. <ins>That’s all we know.</ins>

Ver fichero

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/josefinsans/v25/Qw3FZQNVED7rKGKxtqIqX5E-AVSJrOCfjY46_LjQXME.woff2</code> was not found on this server. <ins>That’s all we know.</ins>

Ver fichero

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/josefinsans/v25/Qw3FZQNVED7rKGKxtqIqX5E-AVSJrOCfjY46_GbQXME.woff2</code> was not found on this server. <ins>That’s all we know.</ins>

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Ver fichero

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/spacegrotesk/v13/V8mQoQDjQSkFtoMM3T6r8E7mPbF4C0i-3qLWA-XEqhZQjOTm.woff2</code> was not found on this server. <ins>That’s all we know.</ins>

Ver fichero

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/spacegrotesk/v13/V8mQoQDjQSkFtoMM3T6r8E7mPbF4C0i-3qLWEuXEqhZQjOTm.woff2</code> was not found on this server. <ins>That’s all we know.</ins>

Ver fichero

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/spacegrotesk/v13/V8mQoQDjQSkFtoMM3T6r8E7mPbF4C0i-3qLWZOXEqhZQjOTm.woff2</code> was not found on this server. <ins>That’s all we know.</ins>

Ver fichero

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Antes

Anchura:  |  Altura:  |  Tamaño: 1.3 KiB

Ver fichero

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Antes

Anchura:  |  Altura:  |  Tamaño: 629 B

Ver fichero

@@ -1,28 +1,77 @@
// components/CallButtons.jsx import { motion } from 'framer-motion';
import MicOffIcon from '@mui/icons-material/MicOff'; import { Mic, MicOff, Video, VideoOff, PhoneOff } from 'lucide-react';
import KeyboardVoiceIcon from '@mui/icons-material/KeyboardVoice';
import VideocamIcon from '@mui/icons-material/Videocam'; const CallHandleButtons = ({ isAudioMute, isVideoOnHold, onToggleAudio, onToggleVideo, onEndCall }) => {
import VideocamOffIcon from '@mui/icons-material/VideocamOff'; const buttonVariants = {
import CallEndIcon from '@mui/icons-material/CallEnd'; hover: { scale: 1.1 },
tap: { scale: 0.95 }
};
return (
<motion.div
className='fixed bottom-8 left-1/2 transform -translate-x-1/2 z-30'
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className='flex items-center space-x-4 bg-black/20 backdrop-blur-lg rounded-2xl p-4 border border-white/10'>
{/* Audio Toggle Button */}
<motion.button
variants={buttonVariants}
whileHover="hover"
whileTap="tap"
onClick={onToggleAudio}
className={`w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200 ${
isAudioMute
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-white/20 hover:bg-white/30 text-white border border-white/20'
}`}
title={isAudioMute ? 'Unmute microphone' : 'Mute microphone'}
>
{isAudioMute ? (
<MicOff className="h-6 w-6" />
) : (
<Mic className="h-6 w-6" />
)}
</motion.button>
{/* Video Toggle Button */}
<motion.button
variants={buttonVariants}
whileHover="hover"
whileTap="tap"
onClick={onToggleVideo}
className={`w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200 ${
isVideoOnHold
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-white/20 hover:bg-white/30 text-white border border-white/20'
}`}
title={isVideoOnHold ? 'Turn on camera' : 'Turn off camera'}
>
{isVideoOnHold ? (
<VideoOff className="h-6 w-6" />
) : (
<Video className="h-6 w-6" />
)}
</motion.button>
{/* End Call Button */}
<motion.button
variants={buttonVariants}
whileHover="hover"
whileTap="tap"
onClick={onEndCall}
className='w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 text-white flex items-center justify-center transition-all duration-200 shadow-lg'
title='End call'
>
<PhoneOff className="h-6 w-6" />
</motion.button>
</div>
{/* Subtle glow effect */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-indigo-400/20 to-purple-400/20 blur-xl -z-10"></div>
</motion.div>
);
};
const CallHandleButtons = ({ isAudioMute, isVideoOnHold, onToggleAudio, onToggleVideo, onEndCall }) => (
<div className='absolute bottom-0 flex w-full space-x-4 h-[80px] items-center justify-center rounded-md'>
<div className=' bg-[#2c3e508b] rounded-md flex px-4 py-2 justify-center gap-10'>
<button className="callButtons text-white bg-blue-700 hover:bg-white hover:text-blue-700
focus:ring-4 focus:ring-blue-300" onClick={onToggleAudio}>
{isAudioMute ? <MicOffIcon fontSize="large" /> : <KeyboardVoiceIcon fontSize="large" />}
</button>
<button className="callButtons text-white bg-blue-700 hover:bg-white hover:text-blue-700
focus:ring-4 focus:ring-blue-300"
onClick={onToggleVideo}
>
{isVideoOnHold ? <VideocamOffIcon fontSize="large" /> : <VideocamIcon fontSize="large" />}
</button>
<button className="callButtons text-white bg-red-600 hover:text-red-700 hover:bg-white
focus:ring-4 focus:ring-white" onClick={onEndCall}>
<CallEndIcon fontSize="large" />
</button>
</div>
</div>
);
export default CallHandleButtons; export default CallHandleButtons;

Ver fichero

@@ -1,72 +1,183 @@
import { useSocket } from '@/context/SocketProvider'; import { useSocket } from '@/context/SocketProvider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import VideoCallIcon from '@mui/icons-material/VideoCall'; import { motion } from 'framer-motion';
import { Video, Users, ArrowRight, Mail, Hash } from 'lucide-react';
const LobbyScreen = () => { const LobbyScreen = () => {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [room, setRoom] = useState(""); const [room, setRoom] = useState("");
const [isLoading, setIsLoading] = useState(false);
const socket = useSocket(); const socket = useSocket();
const router = useRouter(); const router = useRouter();
// console.log(socket);
const handleSubmitForm = useCallback((e) => { const handleSubmitForm = useCallback(async (e) => {
e.preventDefault(); e.preventDefault();
socket.emit('room:join', { email, room }); setIsLoading(true);
try {
socket.emit('room:join', { email, room });
} catch (error) {
console.error('Error joining room:', error);
setIsLoading(false);
}
}, [email, room, socket]); }, [email, room, socket]);
const handleJoinRoom = useCallback((data) => { const handleJoinRoom = useCallback((data) => {
const { email, room } = data; const { email, room } = data;
setIsLoading(false);
router.push(`/room/${room}`); router.push(`/room/${room}`);
}, [router]); }, [router]);
useEffect(() => { useEffect(() => {
socket.on("room:join", handleJoinRoom); socket.on("room:join", handleJoinRoom);
socket.on("error", (error) => {
console.error('Socket error:', error);
setIsLoading(false);
});
return () => { return () => {
socket.off("room:join", handleJoinRoom); socket.off("room:join", handleJoinRoom);
socket.off("error");
} }
}, [socket, handleJoinRoom]); }, [socket, handleJoinRoom]);
return ( return (
<div className='flex flex-col items-center justify-center h-screen bg-gray-100'> <div className='min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 flex flex-col items-center justify-center p-4'>
<title>VideoPeers</title> <title>VideoPeersJS - Connect & Collaborate</title>
<link rel="shortcut icon" href="../../public/favicon.ico" type="image/x-icon" />
<h1 className='text-5xl font-[15px] mb-5 mt-5 text-center font-josefin tracking-tighter'>Video<VideoCallIcon sx={{ fontSize: 70, color: 'rgb(30,220,30)' }} />Peers</h1> {/* Background decorative elements */}
<p className='text-2xl mt-2 mb-4 text-center md:max-w-[400px] max-w-[300px] text-gray-600'> <div className="absolute inset-0 overflow-hidden pointer-events-none">
Peer-to-Peer video calls, powered by <b>WebRTC!</b> <div className="absolute -top-4 -right-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
<br /> <div className="absolute -bottom-8 -left-4 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse delay-1000"></div>
Bring People Closer Together. <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse delay-500"></div>
</p>
<div className='bg-white p-6 rounded shadow-md'>
<form className='flex flex-col items-center justify-center'
onSubmit={handleSubmitForm}
>
<label htmlFor="email">Email ID</label>
<input
type="email"
id='email'
required
value={email}
autoComplete='off'
onChange={(e) => setEmail(e.target.value)}
/>
<br />
<label htmlFor="room">Room Number</label>
<input
type="number"
id='room'
required
autoComplete='off'
value={room}
onChange={(e) => setRoom(e.target.value)}
/>
<br />
<button className='bg-blue-500 hover:bg-blue-600'>
Join
</button>
</form>
</div> </div>
<motion.div
className="relative z-10 w-full max-w-md"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{/* Logo and Title */}
<motion.div
className="text-center mb-8"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
<div className="flex items-center justify-center mb-4">
<Video className="h-12 w-12 text-indigo-600 mr-2" />
<h1 className="text-5xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
{process.env.NEXT_PUBLIC_APP_NAME || 'VideoPeersJS'}
</h1>
</div>
<p className="text-xl text-gray-600 font-medium">
Peer-to-Peer Video Calls
</p>
<p className="text-sm text-gray-500 mt-2 leading-relaxed">
Powered by <span className="font-semibold text-indigo-600">WebRTC</span>
Bringing people closer together
</p>
</motion.div>
{/* Main Card */}
<motion.div
className="bg-white/80 backdrop-blur-lg rounded-2xl shadow-xl border border-white/20 p-8"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
>
<form onSubmit={handleSubmitForm} className="space-y-6">
{/* Email Input */}
<div className="space-y-2">
<label htmlFor="email" className="flex items-center text-sm font-medium text-gray-700">
<Mail className="h-4 w-4 mr-2 text-indigo-500" />
Email Address
</label>
<input
type="email"
id="email"
required
value={email}
autoComplete="email"
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200 bg-white/50 backdrop-blur-sm"
placeholder="Enter your email"
disabled={isLoading}
/>
</div>
{/* Room Input */}
<div className="space-y-2">
<label htmlFor="room" className="flex items-center text-sm font-medium text-gray-700">
<Hash className="h-4 w-4 mr-2 text-indigo-500" />
Room ID
</label>
<input
type="text"
id="room"
required
autoComplete="off"
value={room}
onChange={(e) => setRoom(e.target.value)}
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200 bg-white/50 backdrop-blur-sm"
placeholder="Enter room ID"
disabled={isLoading}
/>
</div>
{/* Join Button */}
<motion.button
type="submit"
disabled={isLoading || !email || !room}
className="w-full bg-gradient-to-r from-indigo-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center space-x-2"
whileHover={{ scale: isLoading ? 1 : 1.02 }}
whileTap={{ scale: isLoading ? 1 : 0.98 }}
>
{isLoading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
<span>Joining...</span>
</>
) : (
<>
<Users className="h-5 w-5" />
<span>Join Room</span>
<ArrowRight className="h-5 w-5" />
</>
)}
</motion.button>
</form>
{/* Features */}
<div className="mt-8 pt-6 border-t border-gray-100">
<div className="grid grid-cols-2 gap-4 text-center">
<div className="space-y-1">
<div className="text-2xl">🔒</div>
<p className="text-xs text-gray-600 font-medium">Secure</p>
</div>
<div className="space-y-1">
<div className="text-2xl"></div>
<p className="text-xs text-gray-600 font-medium">Fast</p>
</div>
</div>
</div>
</motion.div>
{/* Footer */}
<motion.div
className="text-center mt-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.8 }}
>
<p className="text-sm text-gray-500">
No downloads required Works in your browser
</p>
</motion.div>
</motion.div>
</div> </div>
) )
} }

Ver fichero

@@ -1,29 +1,89 @@
import ReactPlayer from 'react-player'; import ReactPlayer from 'react-player';
import { motion } from 'framer-motion';
import { User, Mic, MicOff } from 'lucide-react';
const VideoPlayer = ({ stream, isAudioMute, name }) => { const VideoPlayer = ({ stream, isAudioMute, name }) => {
const myStream = name === "My Stream" ? true : false; const isMyStream = name === "My Stream";
return ( return (
<div> <motion.div
<div className={`${name === "My Stream" ? "flex flex-col items-center justify-center absolute top-2 right-3 z-10" : "px-2"}`}> initial={{ opacity: 0, scale: 0.8 }}
<h1 className={`text-sm font-poppins font-semibold md:text-xl mb-1 text-center ${myStream ? "mt-1" : "mt-4"}`}> animate={{ opacity: 1, scale: 1 }}
{name} transition={{ duration: 0.5 }}
</h1> className={`relative ${isMyStream
<div className={`relative rounded-[30px] overflow-hidden ? "absolute top-4 right-4 z-20 w-32 md:w-48 lg:w-64"
${myStream ? " mxs:w-[80px] mxs:h-[120px] msm:w-[100px] msm:rounded-md msm:h-[140px] mmd:w-[140px] md:w-[200px] lg:w-[280px]" : "w-full max-w-4xl mx-auto"
: "mxs:h-[450px] mss:h-[500px] mmd:h-[600px] md:w-[800px] md:h-[500px]"}`} }`}
> >
{/* Name Badge */}
<div className={`absolute top-2 left-2 z-30 flex items-center space-x-2 px-3 py-1 rounded-full bg-black/50 backdrop-blur-sm text-white text-xs font-medium ${
isMyStream ? "scale-75" : ""
}`}>
<User className="h-3 w-3" />
<span>{name}</span>
{isAudioMute ? (
<MicOff className="h-3 w-3 text-red-400" />
) : (
<Mic className="h-3 w-3 text-green-400" />
)}
</div>
{/* Video Container */}
<div className={`relative overflow-hidden rounded-2xl shadow-2xl border-2 border-white/20 ${
isMyStream
? "aspect-[3/4] bg-gradient-to-br from-gray-900 to-gray-800"
: "aspect-video bg-gradient-to-br from-gray-900 to-gray-800"
}`}>
{stream ? (
<ReactPlayer <ReactPlayer
url={stream} url={stream}
playing playing
muted={isAudioMute} muted={isMyStream ? true : isAudioMute}
height="100%" height="100%"
width="100%" width="100%"
style={{ transform: 'scaleX(-1)' }} style={{
transform: 'scaleX(-1)',
}}
config={{
file: {
attributes: {
style: {
width: '100%',
height: '100%',
objectFit: 'cover'
}
}
}
}}
/> />
</div> ) : (
<div className="w-full h-full flex items-center justify-center">
<div className="text-center text-white/60">
<User className="h-16 w-16 mx-auto mb-2 opacity-40" />
<p className="text-sm">Camera not available</p>
</div>
</div>
)}
{/* Overlay for connection status */}
{!stream && (
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center">
<div className="text-center text-white">
<div className="animate-pulse">
<User className="h-12 w-12 mx-auto mb-2" />
<p className="text-sm font-medium">Connecting...</p>
</div>
</div>
</div>
)}
</div> </div>
</div>
) {/* Glow effect for my stream */}
{isMyStream && (
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-indigo-400 to-purple-400 opacity-20 blur-xl -z-10"></div>
)}
</motion.div>
);
}; };
export default VideoPlayer; export default VideoPlayer;

Ver fichero

@@ -9,7 +9,22 @@ export const useSocket = () => {
} }
const SocketProvider = (props) => { const SocketProvider = (props) => {
const socket = useMemo(() => io("video-peers-server.onrender.com/"), []); const socket = useMemo(() => {
// Get server URL from environment variables
const serverUrl = process.env.NEXT_PUBLIC_SOCKET_SERVER_URL || 'http://localhost:8000';
console.log('Connecting to socket server:', serverUrl);
return io(serverUrl, {
transports: ['websocket', 'polling'],
autoConnect: true,
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
timeout: 20000,
});
}, []);
return ( return (
<SocketContext.Provider value={socket}> <SocketContext.Provider value={socket}>
{props.children} {props.children}

Ver fichero

@@ -2,13 +2,16 @@ import { useSocket } from '@/context/SocketProvider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import peer from '@/service/peer'; import peer from '@/service/peer';
import CallIcon from '@mui/icons-material/Call'; import { motion } from 'framer-motion';
import VideoCallIcon from '@mui/icons-material/VideoCall'; import { Video, Phone, Users, Send, ArrowLeft } from 'lucide-react';
import VideoPlayer from '@/components/VideoPlayer'; import VideoPlayer from '@/components/VideoPlayer';
import CallHandleButtons from '@/components/CallHandleButtons'; import CallHandleButtons from '@/components/CallHandleButtons';
const RoomPage = () => { const RoomPage = () => {
const socket = useSocket(); const socket = useSocket();
const router = useRouter();
const { slug } = router.query;
const [remoteSocketId, setRemoteSocketId] = useState(null); const [remoteSocketId, setRemoteSocketId] = useState(null);
const [myStream, setMyStream] = useState(null); const [myStream, setMyStream] = useState(null);
const [remoteStream, setRemoteStream] = useState(null); const [remoteStream, setRemoteStream] = useState(null);
@@ -16,36 +19,51 @@ const RoomPage = () => {
const [isVideoOnHold, setIsVideoOnHold] = useState(false); const [isVideoOnHold, setIsVideoOnHold] = useState(false);
const [callButton, setCallButton] = useState(true); const [callButton, setCallButton] = useState(true);
const [isSendButtonVisible, setIsSendButtonVisible] = useState(true); const [isSendButtonVisible, setIsSendButtonVisible] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
const handleUserJoined = useCallback(({ email, id }) => { const handleUserJoined = useCallback(({ email, id }) => {
//! console.log(`Email ${email} joined the room!`); console.log(`User ${email} joined the room!`);
setRemoteSocketId(id); setRemoteSocketId(id);
}, []); }, []);
const handleUserLeft = useCallback(({ email }) => {
console.log(`User ${email} left the room`);
setRemoteSocketId(null);
setRemoteStream(null);
}, []);
const handleIncomingCall = useCallback(async ({ from, offer }) => { const handleIncomingCall = useCallback(async ({ from, offer }) => {
setRemoteSocketId(from); setRemoteSocketId(from);
//! console.log(`incoming call from ${from} with offer ${offer}`); setIsConnecting(true);
const stream = await navigator.mediaDevices.getUserMedia({
audio: true, try {
video: true const stream = await navigator.mediaDevices.getUserMedia({
}); audio: true,
setMyStream(stream); video: true
});
setMyStream(stream);
const ans = await peer.getAnswer(offer); const ans = await peer.getAnswer(offer);
socket.emit("call:accepted", { to: from, ans }); socket.emit("call:accepted", { to: from, ans });
} catch (error) {
console.error('Error handling incoming call:', error);
setIsConnecting(false);
}
}, [socket]); }, [socket]);
const sendStreams = useCallback(() => { const sendStreams = useCallback(() => {
for (const track of myStream.getTracks()) { if (myStream) {
peer.peer.addTrack(track, myStream); for (const track of myStream.getTracks()) {
peer.peer.addTrack(track, myStream);
}
setIsSendButtonVisible(false);
} }
setIsSendButtonVisible(false);
}, [myStream]); }, [myStream]);
const handleCallAccepted = useCallback(({ from, ans }) => { const handleCallAccepted = useCallback(({ from, ans }) => {
peer.setLocalDescription(ans); peer.setLocalDescription(ans);
//! console.log("Call Accepted"); console.log("Call Accepted");
setIsConnecting(false);
sendStreams(); sendStreams();
}, [sendStreams]); }, [sendStreams]);
@@ -54,7 +72,6 @@ const RoomPage = () => {
socket.emit("peer:nego:done", { to: from, ans }); socket.emit("peer:nego:done", { to: from, ans });
}, [socket]); }, [socket]);
const handleNegoNeeded = useCallback(async () => { const handleNegoNeeded = useCallback(async () => {
const offer = await peer.getOffer(); const offer = await peer.getOffer();
socket.emit("peer:nego:needed", { offer, to: remoteSocketId }); socket.emit("peer:nego:needed", { offer, to: remoteSocketId });
@@ -66,23 +83,23 @@ const RoomPage = () => {
useEffect(() => { useEffect(() => {
peer.peer.addEventListener('negotiationneeded', handleNegoNeeded); peer.peer.addEventListener('negotiationneeded', handleNegoNeeded);
return () => { return () => {
peer.peer.removeEventListener('negotiationneeded', handleNegoNeeded); peer.peer.removeEventListener('negotiationneeded', handleNegoNeeded);
} }
}, [handleNegoNeeded]); }, [handleNegoNeeded]);
useEffect(() => { useEffect(() => {
peer.peer.addEventListener('track', async ev => { peer.peer.addEventListener('track', async ev => {
const remoteStream = ev.streams; const remoteStream = ev.streams;
console.log("GOT TRACKS!"); console.log("GOT TRACKS!");
setRemoteStream(remoteStream[0]); setRemoteStream(remoteStream[0]);
setIsConnecting(false);
}) })
}, []) }, [])
useEffect(() => { useEffect(() => {
socket.on("user:joined", handleUserJoined); socket.on("user:joined", handleUserJoined);
socket.on("user:left", handleUserLeft);
socket.on("incoming:call", handleIncomingCall); socket.on("incoming:call", handleIncomingCall);
socket.on("call:accepted", handleCallAccepted); socket.on("call:accepted", handleCallAccepted);
socket.on("peer:nego:needed", handleNegoNeededIncoming); socket.on("peer:nego:needed", handleNegoNeededIncoming);
@@ -90,21 +107,21 @@ const RoomPage = () => {
return () => { return () => {
socket.off("user:joined", handleUserJoined); socket.off("user:joined", handleUserJoined);
socket.off("user:left", handleUserLeft);
socket.off("incoming:call", handleIncomingCall); socket.off("incoming:call", handleIncomingCall);
socket.off("call:accepted", handleCallAccepted); socket.off("call:accepted", handleCallAccepted);
socket.off("peer:nego:needed", handleNegoNeededIncoming); socket.off("peer:nego:needed", handleNegoNeededIncoming);
socket.off("peer:nego:final", handleNegoFinal); socket.off("peer:nego:final", handleNegoFinal);
}; };
}, }, [
[ socket,
socket, handleUserJoined,
handleUserJoined, handleUserLeft,
handleIncomingCall, handleIncomingCall,
handleCallAccepted, handleCallAccepted,
handleNegoNeededIncoming, handleNegoNeededIncoming,
handleNegoFinal handleNegoFinal
]); ]);
useEffect(() => { useEffect(() => {
socket.on("call:end", ({ from }) => { socket.on("call:end", ({ from }) => {
@@ -118,6 +135,9 @@ const RoomPage = () => {
setRemoteStream(null); setRemoteStream(null);
setRemoteSocketId(null); setRemoteSocketId(null);
setCallButton(true);
setIsSendButtonVisible(true);
setIsConnecting(false);
} }
}); });
@@ -126,7 +146,6 @@ const RoomPage = () => {
} }
}, [remoteSocketId, myStream, socket]); }, [remoteSocketId, myStream, socket]);
//* for disappearing call button
useEffect(() => { useEffect(() => {
socket.on("call:initiated", ({ from }) => { socket.on("call:initiated", ({ from }) => {
if (from === remoteSocketId) { if (from === remoteSocketId) {
@@ -139,46 +158,50 @@ const RoomPage = () => {
} }
}, [socket, remoteSocketId]); }, [socket, remoteSocketId]);
const handleCallUser = useCallback(async () => { const handleCallUser = useCallback(async () => {
const stream = await navigator.mediaDevices.getUserMedia({ setIsConnecting(true);
audio: true,
video: true try {
}); const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
if (isAudioMute) { if (isAudioMute) {
const audioTracks = stream.getAudioTracks(); const audioTracks = stream.getAudioTracks();
audioTracks.forEach(track => track.enabled = false); audioTracks.forEach(track => track.enabled = false);
}
if (isVideoOnHold) {
const videoTracks = stream.getVideoTracks();
videoTracks.forEach(track => track.enabled = false);
}
const offer = await peer.getOffer();
socket.emit("user:call", { to: remoteSocketId, offer })
setMyStream(stream);
setCallButton(false);
socket.emit("call:initiated", { to: remoteSocketId });
} catch (error) {
console.error('Error starting call:', error);
setIsConnecting(false);
} }
}, [remoteSocketId, socket, isAudioMute, isVideoOnHold]);
if (isVideoOnHold) {
const videoTracks = stream.getVideoTracks();
videoTracks.forEach(track => track.enabled = false);
}
//! create offer
const offer = await peer.getOffer();
//* send offer to remote user
socket.emit("user:call", { to: remoteSocketId, offer })
// set my stream
setMyStream(stream);
//* hide the call button
setCallButton(false);
//* Inform the remote user to hide their "CALL" button
socket.emit("call:initiated", { to: remoteSocketId });
}, [remoteSocketId, socket, isAudioMute, isVideoOnHold, callButton]);
const handleToggleAudio = () => { const handleToggleAudio = () => {
peer.toggleAudio(); if (myStream) {
setIsAudioMute(!isAudioMute); const audioTracks = myStream.getAudioTracks();
audioTracks.forEach(track => track.enabled = !track.enabled);
setIsAudioMute(!isAudioMute);
}
}; };
const handleToggleVideo = () => { const handleToggleVideo = () => {
peer.toggleVideo(); if (myStream) {
setIsVideoOnHold(!isVideoOnHold); const videoTracks = myStream.getVideoTracks();
videoTracks.forEach(track => track.enabled = !track.enabled);
setIsVideoOnHold(!isVideoOnHold);
}
} }
const handleEndCall = useCallback(() => { const handleEndCall = useCallback(() => {
@@ -190,6 +213,9 @@ const RoomPage = () => {
} }
setRemoteStream(null); setRemoteStream(null);
setCallButton(true);
setIsSendButtonVisible(true);
setIsConnecting(false);
if (remoteSocketId) { if (remoteSocketId) {
socket.emit("call:end", { to: remoteSocketId }); socket.emit("call:end", { to: remoteSocketId });
@@ -197,59 +223,168 @@ const RoomPage = () => {
setRemoteSocketId(null); setRemoteSocketId(null);
}, [myStream, remoteSocketId, socket]); }, [myStream, remoteSocketId, socket]);
const router = useRouter(); const handleGoBack = () => {
handleEndCall();
const { slug } = router.query; router.push('/');
};
return ( return (
<div className='flex flex-col items-center justify-center w-screen h-screen overflow-hidden'> <div className='min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 relative overflow-hidden'>
<title>Room No. {slug}</title> <title>Room {slug} - VideoPeersJS</title>
<h1 className='absolute top-0 left-0 text-5xl
text-center font-josefin tracking-tighter mt-5 ml-5 mmd:text-xl mxs:text-sm'>Video {/* Background decorative elements */}
<VideoCallIcon sx={{ fontSize: 50, color: 'rgb(30,220,30)' }} /> <div className="absolute inset-0 overflow-hidden pointer-events-none">
Peers <div className="absolute top-1/4 -left-4 w-72 h-72 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse"></div>
</h1> <div className="absolute bottom-1/4 -right-4 w-72 h-72 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse delay-1000"></div>
<h4 className='font-bold text-xl md:text-2xl
mmd:text-sm mt-5 mb-4 msm:max-w-[100px] text-center'>
{remoteSocketId ? "Connected With Remote User!" : "No One In Room"}
</h4>
{(remoteStream && remoteSocketId && isSendButtonVisible) &&
<button className='bg-green-500 hover:bg-green-600' onClick={sendStreams}>
Send Stream
</button>
}
{(remoteSocketId && callButton) &&
(
<button className='text-xl bg-green-500 hover:bg-green-600 rounded-3xl'
onClick={handleCallUser}
style={{ display: !remoteStream ? 'block' : 'none' }}>
Call <CallIcon fontSize='medium' className=' animate-pulse scale-125' />
</button>
)
}
<div className="flex flex-col w-full items-center justify-center overflow-hidden">
{
myStream &&
<VideoPlayer stream={myStream} name={"My Stream"} isAudioMute={isAudioMute} />
}
{
remoteStream &&
<VideoPlayer stream={remoteStream} name={"Remote Stream"} isAudioMute={isAudioMute} />
}
</div> </div>
{myStream && remoteStream && !isSendButtonVisible &&
(
<CallHandleButtons
isAudioMute={isAudioMute}
isVideoOnHold={isVideoOnHold}
onToggleAudio={handleToggleAudio}
onToggleVideo={handleToggleVideo}
onEndCall={handleEndCall}
/>
)
}
</div>
{/* Header */}
<motion.header
className="absolute top-0 left-0 right-0 z-20 p-6"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<motion.button
onClick={handleGoBack}
className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white hover:bg-white/20 transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<ArrowLeft className="h-4 w-4" />
<span>Back</span>
</motion.button>
<div className="flex items-center space-x-2">
<Video className="h-6 w-6 text-indigo-400" />
<h1 className="text-xl font-bold text-white">
{process.env.NEXT_PUBLIC_APP_NAME || 'VideoPeersJS'}
</h1>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white">
<Users className="h-4 w-4" />
<span className="text-sm">Room {slug}</span>
</div>
</div>
</div>
</motion.header>
{/* Connection Status */}
<motion.div
className="absolute top-24 left-1/2 transform -translate-x-1/2 z-20"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<div className={`px-6 py-3 rounded-full backdrop-blur-sm text-white text-center ${
remoteSocketId ? 'bg-green-500/20 border border-green-400/30' : 'bg-orange-500/20 border border-orange-400/30'
}`}>
<p className="text-sm font-medium">
{isConnecting ? (
<span className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
<span>Connecting...</span>
</span>
) : remoteSocketId ? (
"🟢 Connected with remote user"
) : (
"🟡 Waiting for someone to join..."
)}
</p>
</div>
</motion.div>
{/* Video Container */}
<div className="relative h-screen flex items-center justify-center p-6 pt-32">
{/* Remote Stream (Main) */}
{remoteStream ? (
<VideoPlayer
stream={remoteStream}
name={"Remote Stream"}
isAudioMute={false}
/>
) : (
<motion.div
className="w-full max-w-4xl aspect-video bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl border-2 border-white/10 flex items-center justify-center"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6 }}
>
<div className="text-center text-white/60">
<Users className="h-24 w-24 mx-auto mb-4 opacity-40" />
<p className="text-xl font-medium mb-2">Waiting for remote video...</p>
<p className="text-sm">Share this room ID with someone to start a call</p>
</div>
</motion.div>
)}
{/* My Stream (Picture-in-Picture) */}
{myStream && (
<VideoPlayer
stream={myStream}
name={"My Stream"}
isAudioMute={isAudioMute}
/>
)}
</div>
{/* Action Buttons */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-30">
{remoteStream && isSendButtonVisible && (
<motion.button
onClick={sendStreams}
className="mb-4 flex items-center space-x-2 px-6 py-3 bg-green-500 hover:bg-green-600 text-white rounded-xl font-semibold transition-all duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Send className="h-5 w-5" />
<span>Send Stream</span>
</motion.button>
)}
{remoteSocketId && callButton && !remoteStream && (
<motion.button
onClick={handleCallUser}
disabled={isConnecting}
className="flex items-center space-x-2 px-8 py-4 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white rounded-2xl font-semibold text-lg shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
whileHover={{ scale: isConnecting ? 1 : 1.05 }}
whileTap={{ scale: isConnecting ? 1 : 0.95 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{isConnecting ? (
<>
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent"></div>
<span>Connecting...</span>
</>
) : (
<>
<Phone className="h-6 w-6" />
<span>Start Call</span>
</>
)}
</motion.button>
)}
</div>
{/* Call Control Buttons */}
{myStream && remoteStream && !isSendButtonVisible && (
<CallHandleButtons
isAudioMute={isAudioMute}
isVideoOnHold={isVideoOnHold}
onToggleAudio={handleToggleAudio}
onToggleVideo={handleToggleVideo}
onEndCall={handleEndCall}
/>
)}
</div>
) )
} }

Ver fichero

@@ -1,14 +1,41 @@
class PeerService { class PeerService {
constructor() { constructor() {
if (typeof window !== 'undefined' && !this.peer) { if (typeof window !== 'undefined' && !this.peer) {
this.peer = new RTCPeerConnection({ // Get ICE servers from environment variables
iceServers: [{ const getIceServers = () => {
try {
const iceServersEnv = process.env.NEXT_PUBLIC_ICE_SERVERS;
if (iceServersEnv) {
const servers = JSON.parse(iceServersEnv);
return [{ urls: servers }];
}
} catch (error) {
console.warn('Error parsing ICE servers from environment:', error);
}
// Fallback to default ICE servers
return [{
urls: [ urls: [
"stun:stun.l.google.com:19302", "stun:stun.l.google.com:19302",
"stun:global.stun.twilio.com:3478", "stun:global.stun.twilio.com:3478",
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302"
] ]
}] }];
}) };
this.peer = new RTCPeerConnection({
iceServers: getIceServers()
});
// Add connection state monitoring
this.peer.onconnectionstatechange = () => {
console.log('Connection state:', this.peer.connectionState);
};
this.peer.oniceconnectionstatechange = () => {
console.log('ICE connection state:', this.peer.iceConnectionState);
};
} }
} }
@@ -36,17 +63,58 @@ class PeerService {
} }
toggleAudio = () => { toggleAudio = () => {
const audioTracks = this.peer.getSenders().find(sender => sender.track.kind === 'audio').track; try {
audioTracks.enabled = !audioTracks.enabled; const senders = this.peer.getSenders();
const audioSender = senders.find(sender =>
// Mute the local audio track sender.track && sender.track.kind === 'audio'
const localAudioTrack = this.peer.getLocalStreams()[0].getAudioTracks()[0]; );
localAudioTrack.enabled = !localAudioTrack.enabled;
if (audioSender && audioSender.track) {
audioSender.track.enabled = !audioSender.track.enabled;
return audioSender.track.enabled;
}
} catch (error) {
console.error('Error toggling audio:', error);
}
return false;
}; };
toggleVideo = () => { toggleVideo = () => {
const videoTracks = this.peer.getSenders().find(sender => sender.track.kind === 'video').track; try {
videoTracks.enabled = !videoTracks.enabled; const senders = this.peer.getSenders();
const videoSender = senders.find(sender =>
sender.track && sender.track.kind === 'video'
);
if (videoSender && videoSender.track) {
videoSender.track.enabled = !videoSender.track.enabled;
return videoSender.track.enabled;
}
} catch (error) {
console.error('Error toggling video:', error);
}
return false;
};
// Method to close and cleanup the peer connection
close = () => {
if (this.peer) {
this.peer.close();
this.peer = null;
}
};
// Method to get connection statistics
getStats = async () => {
if (this.peer) {
try {
return await this.peer.getStats();
} catch (error) {
console.error('Error getting connection stats:', error);
return null;
}
}
return null;
}; };
} }

Ver fichero

@@ -0,0 +1,54 @@
/*
* VideoPeersJS - Font Configuration
* Local fonts configuration and fallbacks
*/
/* Import all local font families */
@import './inter.css';
@import './poppins.css';
@import './josefin-sans.css';
@import './space-grotesk.css';
/* Font Stack Definitions */
:root {
/* Primary font stack with fallbacks */
--font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
/* Accent font stack */
--font-accent: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
/* Display font stack */
--font-display: 'Space Grotesk', 'Josefin Sans', 'Inter', system-ui, sans-serif;
/* Monospace stack (for code/technical elements) */
--font-mono: 'SF Mono', Monaco, 'Inconsolata', 'Roboto Mono', 'Source Code Pro', monospace;
}
/* CSS Custom Properties for font weights */
:root {
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
}
/* Apply font optimization to all elements */
* {
font-feature-settings: 'kern' 1, 'liga' 1;
text-rendering: optimizeLegibility;
}
/* Utility classes for font optimization */
.font-optimized {
font-feature-settings: 'kern' 1, 'liga' 1, 'ss01' 1;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Font preload hints (for performance) */
.font-preload-hint {
font-display: swap;
}

Ver fichero

@@ -0,0 +1,36 @@
/* Inter Font Family - Local */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/inter/inter-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/inter/inter-500.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/inter/inter-600.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/inter/inter-700.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Ver fichero

@@ -0,0 +1,27 @@
/* Josefin Sans Font Family - Local */
@font-face {
font-family: 'Josefin Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/josefin-sans/josefin-sans-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Josefin Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/josefin-sans/josefin-sans-500.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Josefin Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/josefin-sans/josefin-sans-600.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Ver fichero

@@ -0,0 +1,36 @@
/* Poppins Font Family - Local */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/poppins/poppins-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/poppins/poppins-500.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/poppins/poppins-600.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/poppins/poppins-700.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Ver fichero

@@ -0,0 +1,27 @@
/* Space Grotesk Font Family - Local */
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/space-grotesk/space-grotesk-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/space-grotesk/space-grotesk-500.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/space-grotesk/space-grotesk-600.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Ver fichero

@@ -1,66 +1,203 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); /* Import Local Fonts Configuration */
@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@500&display=swap'); @import './fonts/index.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Global Base Styles */
@layer base {
html {
scroll-behavior: smooth;
}
body{ body {
overflow: hidden; font-family: var(--font-primary);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: 'kern' 1, 'liga' 1;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
}
} }
label{ /* Component Styles */
color: #333; @layer components {
margin-bottom: 0.5rem; /* Form Elements */
font-size: 1.125rem; .form-label {
line-height: 1.75rem; @apply flex items-center text-sm font-medium text-gray-700 mb-2;
font-weight: 500; font-family: 'Inter', sans-serif;
}
.form-input {
@apply w-full px-4 py-3 border border-gray-200 rounded-xl transition-all duration-200 bg-white/50 backdrop-blur-sm;
@apply focus:ring-2 focus:ring-indigo-500 focus:border-transparent;
@apply placeholder:text-gray-400 disabled:opacity-50 disabled:cursor-not-allowed;
font-family: 'Inter', sans-serif;
}
.form-input:hover {
@apply border-gray-300;
}
/* Button Variants */
.btn-primary {
@apply bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold py-3 px-6 rounded-xl;
@apply hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200;
@apply flex items-center justify-center space-x-2;
font-family: 'Inter', sans-serif;
}
.btn-secondary {
@apply bg-white/20 hover:bg-white/30 text-white border border-white/20 font-medium py-2 px-4 rounded-xl;
@apply transition-all duration-200 backdrop-blur-sm;
font-family: 'Inter', sans-serif;
}
.btn-danger {
@apply bg-red-500 hover:bg-red-600 text-white font-medium py-2 px-4 rounded-xl;
@apply transition-all duration-200 shadow-lg;
font-family: 'Inter', sans-serif;
}
/* Call Control Buttons */
.call-button {
@apply w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200;
@apply shadow-lg backdrop-blur-sm;
}
.call-button-active {
@apply bg-white/20 hover:bg-white/30 text-white border border-white/20;
}
.call-button-muted {
@apply bg-red-500 hover:bg-red-600 text-white;
}
/* Glass Effect */
.glass {
@apply bg-white/10 backdrop-blur-lg border border-white/20;
}
.glass-card {
@apply bg-white/80 backdrop-blur-lg rounded-2xl shadow-xl border border-white/20;
}
/* Gradient Text */
.gradient-text {
@apply bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent;
}
/* Status Indicators */
.status-connected {
@apply bg-green-500/20 border border-green-400/30 text-green-100;
}
.status-waiting {
@apply bg-orange-500/20 border border-orange-400/30 text-orange-100;
}
.status-error {
@apply bg-red-500/20 border border-red-400/30 text-red-100;
}
}
/* Utility Classes */
@layer utilities {
/* Typography */
.font-display {
font-family: 'Space Grotesk', 'Josefin Sans', sans-serif;
}
.font-body {
font-family: 'Inter', system-ui, sans-serif;
}
.font-accent {
font-family: 'Poppins', sans-serif; font-family: 'Poppins', sans-serif;
} }
input{ /* Animations */
margin-bottom: 1rem; .animate-float {
padding: 0.5rem; animation: float 6s ease-in-out infinite;
border-radius: 0.25rem; }
outline: 2px solid transparent;
outline-offset: 2px; .animate-glow {
border-width: 1px; animation: glow 2s ease-in-out infinite alternate;
border-color: rgb(170, 170, 170); }
&:focus{
outline: 2px solid transparent; .animate-pulse-slow {
outline-offset: 2px; animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--tw-border-opacity: 1; }
border-color: rgb(59 130 246 / var(--tw-border-opacity));
/* Custom animations */
@keyframes float {
0%, 100% {
transform: translateY(0px);
} }
50% {
} transform: translateY(-10px);
button{
color: white;
padding: 0.5rem 1rem 0.5rem 1rem;
border-radius: 0.25rem;
transition: background-color 250ms ease-in-out;
font-family: "Poppins", sans-serif;
&:focus{
outline: 2px solid transparent;
outline-offset: 2px;
} }
}
@keyframes glow {
0% {
box-shadow: 0 0 20px rgba(99, 102, 241, 0.4);
}
100% {
box-shadow: 0 0 30px rgba(99, 102, 241, 0.8);
}
}
/* Video specific styles */
.video-container {
@apply relative overflow-hidden rounded-2xl shadow-2xl border-2 border-white/20;
}
.video-overlay {
@apply absolute inset-0 bg-gradient-to-br from-indigo-500/20 to-purple-500/20;
}
} }
.callButtons{ /* Legacy styles for backward compatibility */
font-weight: 500; label {
border-radius: 9999px; @apply form-label;
font-size: 0.875rem;
line-height: 1.25rem;
padding: 0.625rem;
text-align: center;
display: inline-flex;
align-items: center;
} }
video{ input[type="email"],
width: 100%; input[type="text"],
height: 100%; input[type="number"] {
object-fit: cover; @apply form-input;
}
button:not(.call-button):not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
@apply btn-primary;
}
.callButtons {
@apply call-button call-button-active;
}
video {
@apply w-full h-full object-cover;
} }

Ver fichero

@@ -7,23 +7,46 @@ module.exports = {
], ],
theme: { theme: {
extend: { extend: {
fontFamily: {
// Primary font stack with local fonts
'sans': ['var(--font-primary)', 'Inter', 'system-ui', 'sans-serif'],
'body': ['var(--font-primary)', 'Inter', 'system-ui', 'sans-serif'],
'accent': ['var(--font-accent)', 'Poppins', 'system-ui', 'sans-serif'],
'display': ['var(--font-display)', 'Space Grotesk', 'Josefin Sans', 'system-ui', 'sans-serif'],
'mono': ['var(--font-mono)', 'monospace'],
// Legacy support
'poppins': ['Poppins', 'system-ui', 'sans-serif'],
'josefin': ['Josefin Sans', 'system-ui', 'sans-serif'],
'inter': ['Inter', 'system-ui', 'sans-serif'],
'space-grotesk': ['Space Grotesk', 'system-ui', 'sans-serif']
},
backgroundImage: { backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
}, },
fontFamily: {
'poppins': ['Poppins', 'sans-serif'],
'josefin': ['Josefin Sans', 'sans-serif']
},
keyframes: { keyframes: {
pulse: { pulse: {
'0%, 100%': { opacity: '1' }, '0%, 100%': { opacity: '1' },
'50%': { opacity: '0.6' }, '50%': { opacity: '0.6' },
},
'fade-in': {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
'scale-in': {
'0%': { transform: 'scale(0.8)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' },
} }
}, },
animation: { animation: {
'pulse': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite', 'pulse': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'fade-in': 'fade-in 0.5s ease-out',
'scale-in': 'scale-in 0.3s ease-out',
},
backdropBlur: {
xs: '2px',
}, },
screens: { screens: {
mxxl: { 'max': '1535px' }, mxxl: { 'max': '1535px' },

Ver fichero

@@ -1 +1,4 @@
PORT=8080 PORT=8000
CLIENT_URL=http://localhost:3000
NODE_ENV=development
APP_NAME=VideoPeersJS

4
server/.env.example Archivo normal
Ver fichero

@@ -0,0 +1,4 @@
PORT=8000
CLIENT_URL=http://localhost:3000
NODE_ENV=development
APP_NAME=VideoPeersJS

4
server/.gitignore vendido
Ver fichero

@@ -1 +1,3 @@
node_modules node_modules
*.lock
*-lock.json

Ver fichero

@@ -3,63 +3,381 @@ const express = require('express');
const cors = require('cors'); const cors = require('cors');
const http = require('http'); const http = require('http');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const Joi = require('joi');
const validator = require('validator');
const { v4: uuidv4 } = require('uuid');
dotenv.config(); dotenv.config();
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const io = new Server(server, {
cors: true // Security middlewares
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "wss:", "ws:"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // Límite de 100 requests por ventana de tiempo por IP
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
}); });
app.use(cors()); app.use(limiter);
// CORS configuration
const corsOptions = {
origin: process.env.CLIENT_URL || ["http://localhost:3000", "https://localhost:3000"],
methods: ["GET", "POST"],
credentials: true
};
app.use(cors(corsOptions));
app.use(express.json({ limit: '10mb' }));
const io = new Server(server, {
cors: corsOptions,
transports: ['websocket', 'polling'],
allowEIO3: true
});
// Validation schemas
const roomJoinSchema = Joi.object({
email: Joi.string().email().required(),
room: Joi.string().alphanum().min(3).max(50).required()
});
const callSchema = Joi.object({
to: Joi.string().required(),
offer: Joi.object().required()
});
const callAcceptedSchema = Joi.object({
to: Joi.string().required(),
ans: Joi.object().required()
});
app.get("/", (req, res) => { app.get("/", (req, res) => {
res.send("Hello World"); res.json({
message: `${process.env.APP_NAME || 'VideoPeersJS'} Server`,
status: "running",
timestamp: new Date().toISOString()
});
});
app.get("/health", (req, res) => {
res.status(200).json({ status: "healthy" });
}); });
const emailToSocket = new Map(); const emailToSocket = new Map();
const socketToEmail = new Map(); const socketToEmail = new Map();
const rooms = new Map();
// Helper function to sanitize input
function sanitizeInput(input) {
if (typeof input === 'string') {
return validator.escape(input.trim());
}
return input;
}
// Helper function to validate room access
function validateRoomAccess(socket, room) {
const userEmail = socketToEmail.get(socket.id);
if (!userEmail) return false;
const roomData = rooms.get(room);
return roomData && roomData.participants.has(userEmail);
}
io.on("connection", (socket) => { io.on("connection", (socket) => {
console.log(`Socket Connected: ${socket.id}`); console.log(`Socket Connected: ${socket.id} at ${new Date().toISOString()}`);
// Set a timeout for socket connection
const connectionTimeout = setTimeout(() => {
socket.disconnect(true);
}, 30000); // 30 segundos
socket.on("room:join", data => { socket.on("room:join", (data) => {
const { email, room } = data; try {
emailToSocket.set(email, socket.id); // Validate input
socketToEmail.set(socket.id, email); const { error, value } = roomJoinSchema.validate(data);
if (error) {
socket.emit("error", { message: "Invalid room join data" });
return;
}
socket.join(room); const { email, room } = value;
io.to(room).emit("user:joined", { email, id: socket.id });
// Sanitize inputs
const sanitizedEmail = sanitizeInput(email);
const sanitizedRoom = sanitizeInput(room);
// emits a 'room:joined' event back to the client // Additional email validation
// that just joined the room. if (!validator.isEmail(sanitizedEmail)) {
io.to(socket.id).emit("room:join", data); socket.emit("error", { message: "Invalid email format" });
return;
}
// Clear connection timeout
clearTimeout(connectionTimeout);
// Remove old socket mapping if exists
const oldSocketId = emailToSocket.get(sanitizedEmail);
if (oldSocketId && oldSocketId !== socket.id) {
socketToEmail.delete(oldSocketId);
io.sockets.sockets.get(oldSocketId)?.leave(sanitizedRoom);
}
emailToSocket.set(sanitizedEmail, socket.id);
socketToEmail.set(socket.id, sanitizedEmail);
// Manage room data
if (!rooms.has(sanitizedRoom)) {
rooms.set(sanitizedRoom, {
id: uuidv4(),
participants: new Set(),
createdAt: new Date()
});
}
const roomData = rooms.get(sanitizedRoom);
roomData.participants.add(sanitizedEmail);
socket.join(sanitizedRoom);
// Notify other users in the room
socket.to(sanitizedRoom).emit("user:joined", {
email: sanitizedEmail,
id: socket.id,
timestamp: new Date().toISOString()
});
// Confirm room join to the user
socket.emit("room:join", {
email: sanitizedEmail,
room: sanitizedRoom,
participants: Array.from(roomData.participants),
timestamp: new Date().toISOString()
});
} catch (error) {
console.error("Error in room:join:", error);
socket.emit("error", { message: "Internal server error" });
}
}); });
socket.on("user:call", ({ to, offer }) => { socket.on("user:call", (data) => {
io.to(to).emit("incoming:call", { from: socket.id, offer }); try {
const { error, value } = callSchema.validate(data);
if (error) {
socket.emit("error", { message: "Invalid call data" });
return;
}
const { to, offer } = value;
// Verify that the target socket exists
const targetSocket = io.sockets.sockets.get(to);
if (!targetSocket) {
socket.emit("error", { message: "Target user not found" });
return;
}
io.to(to).emit("incoming:call", {
from: socket.id,
offer,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error("Error in user:call:", error);
socket.emit("error", { message: "Internal server error" });
}
}); });
socket.on("call:accepted", ({ to, ans }) => { socket.on("call:accepted", (data) => {
io.to(to).emit("call:accepted", { from: socket.id, ans }); try {
const { error, value } = callAcceptedSchema.validate(data);
if (error) {
socket.emit("error", { message: "Invalid call acceptance data" });
return;
}
const { to, ans } = value;
const targetSocket = io.sockets.sockets.get(to);
if (!targetSocket) {
socket.emit("error", { message: "Target user not found" });
return;
}
io.to(to).emit("call:accepted", {
from: socket.id,
ans,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error("Error in call:accepted:", error);
socket.emit("error", { message: "Internal server error" });
}
}); });
socket.on("peer:nego:needed", ({ to, offer }) => { socket.on("peer:nego:needed", (data) => {
io.to(to).emit("peer:nego:needed", { from: socket.id, offer }); try {
const { to, offer } = data;
if (!to || !offer) {
socket.emit("error", { message: "Invalid negotiation data" });
return;
}
const targetSocket = io.sockets.sockets.get(to);
if (!targetSocket) {
socket.emit("error", { message: "Target user not found" });
return;
}
io.to(to).emit("peer:nego:needed", {
from: socket.id,
offer,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error("Error in peer:nego:needed:", error);
socket.emit("error", { message: "Internal server error" });
}
}); });
socket.on("peer:nego:done", ({ to, ans }) => { socket.on("peer:nego:done", (data) => {
io.to(to).emit("peer:nego:final", { from: socket.id, ans }); try {
const { to, ans } = data;
if (!to || !ans) {
socket.emit("error", { message: "Invalid negotiation data" });
return;
}
const targetSocket = io.sockets.sockets.get(to);
if (!targetSocket) {
socket.emit("error", { message: "Target user not found" });
return;
}
io.to(to).emit("peer:nego:final", {
from: socket.id,
ans,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error("Error in peer:nego:done:", error);
socket.emit("error", { message: "Internal server error" });
}
}); });
socket.on("call:end", ({ to }) => { socket.on("call:end", (data) => {
io.to(to).emit("call:end", { from: socket.id }); try {
const { to } = data;
if (!to) {
socket.emit("error", { message: "Invalid call end data" });
return;
}
const targetSocket = io.sockets.sockets.get(to);
if (targetSocket) {
io.to(to).emit("call:end", {
from: socket.id,
timestamp: new Date().toISOString()
});
}
} catch (error) {
console.error("Error in call:end:", error);
socket.emit("error", { message: "Internal server error" });
}
}); });
socket.on("call:initiated", ({ to }) => { socket.on("call:initiated", (data) => {
io.to(to).emit("call:initiated", { from: socket.id }); try {
}); const { to } = data;
}) if (!to) {
socket.emit("error", { message: "Invalid call initiation data" });
return;
}
server.listen(process.env.PORT, () => console.log(`Server has started.`)); const targetSocket = io.sockets.sockets.get(to);
if (targetSocket) {
io.to(to).emit("call:initiated", {
from: socket.id,
timestamp: new Date().toISOString()
});
}
} catch (error) {
console.error("Error in call:initiated:", error);
socket.emit("error", { message: "Internal server error" });
}
});
socket.on("disconnect", (reason) => {
try {
console.log(`Socket Disconnected: ${socket.id}, Reason: ${reason}`);
const email = socketToEmail.get(socket.id);
if (email) {
emailToSocket.delete(email);
socketToEmail.delete(socket.id);
// Remove from all rooms
rooms.forEach((roomData, roomName) => {
if (roomData.participants.has(email)) {
roomData.participants.delete(email);
// If room is empty, delete it
if (roomData.participants.size === 0) {
rooms.delete(roomName);
} else {
// Notify other participants
socket.to(roomName).emit("user:left", {
email,
timestamp: new Date().toISOString()
});
}
}
});
}
clearTimeout(connectionTimeout);
} catch (error) {
console.error("Error in disconnect:", error);
}
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({
message: 'Internal server error',
timestamp: new Date().toISOString()
});
});
const PORT = process.env.PORT || 8000;
server.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
console.log(`🕒 Started at ${new Date().toISOString()}`);
});

2257
server/package-lock.json generado

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

Ver fichero

@@ -1,7 +1,7 @@
{ {
"name": "server", "name": "videopeersjs-server",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "VideoPeersJS - Real-time P2P video chat server",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
@@ -12,12 +12,17 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.1" "nodemon": "^3.1.4"
}, },
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.3.1", "dotenv": "^16.4.5",
"express": "^4.18.2", "express": "^4.19.2",
"socket.io": "^4.7.2" "express-rate-limit": "^7.4.0",
"helmet": "^7.1.0",
"joi": "^17.13.3",
"socket.io": "^4.7.5",
"uuid": "^10.0.0",
"validator": "^13.12.0"
} }
} }

Ver fichero

@@ -1,11 +0,0 @@
{
"version": 2,
"builds": [{
"src": "*.js",
"use": "@vercel/node"
}],
"routes": [{
"src": "/(.*)",
"dest": "/"
}]
}