Comparar commits
12 Commits
5b90b18788
...
master
| Autor | SHA1 | Fecha | |
|---|---|---|---|
|
34750bcdb1
|
|||
|
c62dde327d
|
|||
|
6d1dd42e6d
|
|||
|
|
9d013a7c87 | ||
|
|
3a71ab2092 | ||
|
|
926842a12a | ||
|
|
a0411fb0b6 | ||
|
|
457698c4d2 | ||
|
|
502acf3155 | ||
|
|
36c8b35715 | ||
|
|
79d9760980 | ||
|
|
c69de40b55 |
194
README.md
194
README.md
@@ -1,90 +1,206 @@
|
||||
# VideoPeers - Real Time P2P Video Chat Application
|
||||
# VideoPeersJS - Real Time P2P Video Chat Application
|
||||
|
||||
## 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/
|
||||
|
||||
## Screen Shots :
|
||||
<p><img align="right" src="https://github.com/hirentimbadiya/Video-Peers/assets/86219935/3cc34115-e4db-460a-9825-d0e97a5f6cae" /></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 :
|
||||
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
|
||||
|
||||
- [x] Create and join video chat rooms using room IDs
|
||||
- [x] Real-time peer-to-peer video calling using WebRTC
|
||||
- [x] Audio mute/unmute controls
|
||||
- [x] Video Hold/Play controls
|
||||
- [x] Call End option
|
||||
- [x] Responsive Design
|
||||
- [x] Built with Next.js, Node.js, Express, Socket.io
|
||||
|
||||
## 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
|
||||
|
||||
- [x] Audio mute/unmute controls with visual indicators
|
||||
- [x] Video on/off controls
|
||||
- [x] Call end functionality
|
||||
- [x] Modern, responsive design with animations
|
||||
- [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
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```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
|
||||
|
||||
```bash
|
||||
cd client #in Video-Peers/client
|
||||
cd client
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Install dependencies for Server Side
|
||||
|
||||
```bash
|
||||
cd server #in Video-Peers/server
|
||||
cd server
|
||||
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
|
||||
cd client #in Video-Peers/client
|
||||
cd server
|
||||
npm run dev
|
||||
```
|
||||
- Run the server side
|
||||
|
||||
- Run the client side:
|
||||
```bash
|
||||
cd server #in Video-Peers/server
|
||||
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
|
||||
https://github.com/hirentimbadiya/Video-Peers/assets/86219935/7ce8caf9-0881-4abe-b633-79d3bb0a87ef
|
||||
|
||||
## Future Scope
|
||||
## 🚀 Future Scope
|
||||
|
||||
- Text chat support in rooms
|
||||
- Screen Sharing Feature
|
||||
- Multiple Participants in a room
|
||||
- Video recording option
|
||||
- [ ] Text chat support in rooms
|
||||
- [ ] Screen sharing feature
|
||||
- [ ] Multiple participants in a room (group calls)
|
||||
- [ ] 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/)
|
||||
|
||||
## Author
|
||||
## 👨💻 Author
|
||||
- [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
3
client/.env.example
Archivo normal
@@ -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
3
client/.env.local
Archivo normal
@@ -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
6
client/.gitignore
vendido
@@ -25,12 +25,12 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
*.lock
|
||||
*-lock.json
|
||||
8849
client/package-lock.json
generado
8849
client/package-lock.json
generado
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"name": "videopeersjs-client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -9,22 +9,25 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.16",
|
||||
"@mui/material": "^5.14.17",
|
||||
"next": "14.0.2",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-player": "^2.13.0",
|
||||
"socket.io-client": "^4.7.2"
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/icons-material": "^6.1.0",
|
||||
"@mui/material": "^6.1.0",
|
||||
"@vercel/analytics": "^1.3.1",
|
||||
"framer-motion": "^11.5.4",
|
||||
"lucide-react": "^0.439.0",
|
||||
"next": "^14.2.9",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-player": "^2.16.0",
|
||||
"socket.io-client": "^4.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.2",
|
||||
"postcss": "^8",
|
||||
"sass": "^1.69.5",
|
||||
"tailwindcss": "^3.3.0"
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.2.9",
|
||||
"postcss": "^8.4.45",
|
||||
"sass": "^1.78.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
58
client/public/fonts/README.md
Archivo normal
@@ -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.
|
||||
BIN
client/public/fonts/inter/inter-400.woff2
Archivo normal
BIN
client/public/fonts/inter/inter-400.woff2
Archivo normal
Archivo binario no mostrado.
BIN
client/public/fonts/inter/inter-500.woff2
Archivo normal
BIN
client/public/fonts/inter/inter-500.woff2
Archivo normal
Archivo binario no mostrado.
11
client/public/fonts/inter/inter-600.woff2
Archivo normal
11
client/public/fonts/inter/inter-600.woff2
Archivo normal
@@ -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>
|
||||
BIN
client/public/fonts/inter/inter-700.woff2
Archivo normal
BIN
client/public/fonts/inter/inter-700.woff2
Archivo normal
Archivo binario no mostrado.
11
client/public/fonts/josefin-sans/josefin-sans-400.woff2
Archivo normal
11
client/public/fonts/josefin-sans/josefin-sans-400.woff2
Archivo normal
@@ -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>
|
||||
11
client/public/fonts/josefin-sans/josefin-sans-500.woff2
Archivo normal
11
client/public/fonts/josefin-sans/josefin-sans-500.woff2
Archivo normal
@@ -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>
|
||||
11
client/public/fonts/josefin-sans/josefin-sans-600.woff2
Archivo normal
11
client/public/fonts/josefin-sans/josefin-sans-600.woff2
Archivo normal
@@ -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>
|
||||
BIN
client/public/fonts/poppins/poppins-400.woff2
Archivo normal
BIN
client/public/fonts/poppins/poppins-400.woff2
Archivo normal
Archivo binario no mostrado.
BIN
client/public/fonts/poppins/poppins-500.woff2
Archivo normal
BIN
client/public/fonts/poppins/poppins-500.woff2
Archivo normal
Archivo binario no mostrado.
BIN
client/public/fonts/poppins/poppins-600.woff2
Archivo normal
BIN
client/public/fonts/poppins/poppins-600.woff2
Archivo normal
Archivo binario no mostrado.
BIN
client/public/fonts/poppins/poppins-700.woff2
Archivo normal
BIN
client/public/fonts/poppins/poppins-700.woff2
Archivo normal
Archivo binario no mostrado.
11
client/public/fonts/space-grotesk/space-grotesk-400.woff2
Archivo normal
11
client/public/fonts/space-grotesk/space-grotesk-400.woff2
Archivo normal
@@ -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>
|
||||
11
client/public/fonts/space-grotesk/space-grotesk-500.woff2
Archivo normal
11
client/public/fonts/space-grotesk/space-grotesk-500.woff2
Archivo normal
@@ -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>
|
||||
11
client/public/fonts/space-grotesk/space-grotesk-600.woff2
Archivo normal
11
client/public/fonts/space-grotesk/space-grotesk-600.woff2
Archivo normal
@@ -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>
|
||||
@@ -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 |
@@ -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 |
@@ -1,28 +1,77 @@
|
||||
// components/CallButtons.jsx
|
||||
import MicOffIcon from '@mui/icons-material/MicOff';
|
||||
import KeyboardVoiceIcon from '@mui/icons-material/KeyboardVoice';
|
||||
import VideocamIcon from '@mui/icons-material/Videocam';
|
||||
import VideocamOffIcon from '@mui/icons-material/VideocamOff';
|
||||
import CallEndIcon from '@mui/icons-material/CallEnd';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Mic, MicOff, Video, VideoOff, PhoneOff } from 'lucide-react';
|
||||
|
||||
const CallHandleButtons = ({ isAudioMute, isVideoOnHold, onToggleAudio, onToggleVideo, onEndCall }) => {
|
||||
const buttonVariants = {
|
||||
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;
|
||||
|
||||
@@ -1,72 +1,191 @@
|
||||
import { useSocket } from '@/context/SocketProvider';
|
||||
import { useRouter } from 'next/router';
|
||||
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 [email, setEmail] = useState("");
|
||||
const [room, setRoom] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const socket = useSocket();
|
||||
const router = useRouter();
|
||||
// console.log(socket);
|
||||
|
||||
const handleSubmitForm = useCallback((e) => {
|
||||
const handleSubmitForm = useCallback(async (e) => {
|
||||
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]);
|
||||
|
||||
const handleJoinRoom = useCallback((data) => {
|
||||
const { email, room } = data;
|
||||
|
||||
// Store user session data
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('userEmail', email);
|
||||
localStorage.setItem('currentRoom', room);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
router.push(`/room/${room}`);
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("room:join", handleJoinRoom);
|
||||
socket.on("error", (error) => {
|
||||
console.error('Socket error:', error);
|
||||
alert(`Error: ${error.message || 'Failed to join room. Please check your room ID format (letters, numbers, hyphens, underscores only).'}`);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("room:join", handleJoinRoom);
|
||||
socket.off("error");
|
||||
}
|
||||
}, [socket, handleJoinRoom]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center h-screen bg-gray-100'>
|
||||
<title>VideoPeers</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>
|
||||
<p className='text-2xl mt-2 mb-4 text-center md:max-w-[400px] max-w-[300px] text-gray-600'>
|
||||
Peer-to-Peer video calls, powered by <b>WebRTC!</b>
|
||||
<br />
|
||||
Bring People Closer Together.
|
||||
</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 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>VideoPeersJS - Connect & Collaborate</title>
|
||||
|
||||
{/* Background decorative elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</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.replace(/[^a-zA-Z0-9-_]/g, ''))}
|
||||
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 (letters, numbers, -, _)"
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,89 @@
|
||||
import ReactPlayer from 'react-player';
|
||||
import { motion } from 'framer-motion';
|
||||
import { User, Mic, MicOff } from 'lucide-react';
|
||||
|
||||
const VideoPlayer = ({ stream, isAudioMute, name }) => {
|
||||
const myStream = name === "My Stream" ? true : false;
|
||||
const isMyStream = name === "My Stream";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${name === "My Stream" ? "flex flex-col items-center justify-center absolute top-2 right-3 z-10" : "px-2"}`}>
|
||||
<h1 className={`text-sm font-poppins font-semibold md:text-xl mb-1 text-center ${myStream ? "mt-1" : "mt-4"}`}>
|
||||
{name}
|
||||
</h1>
|
||||
<div className={`relative rounded-[30px] overflow-hidden
|
||||
${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]"
|
||||
: "mxs:h-[450px] mss:h-[500px] mmd:h-[600px] md:w-[800px] md:h-[500px]"}`}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className={`relative ${isMyStream
|
||||
? "absolute top-4 right-4 z-20 w-32 md:w-48 lg:w-64"
|
||||
: "w-full max-w-4xl mx-auto"
|
||||
}`}
|
||||
>
|
||||
{/* 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
|
||||
url={stream}
|
||||
playing
|
||||
muted={isAudioMute}
|
||||
muted={isMyStream ? true : isAudioMute}
|
||||
height="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>
|
||||
)
|
||||
|
||||
{/* 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;
|
||||
@@ -9,7 +9,22 @@ export const useSocket = () => {
|
||||
}
|
||||
|
||||
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 (
|
||||
<SocketContext.Provider value={socket}>
|
||||
{props.children}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
@@ -7,6 +8,7 @@ export default function Document() {
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
<Analytics />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
|
||||
@@ -2,13 +2,16 @@ import { useSocket } from '@/context/SocketProvider';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import peer from '@/service/peer';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
import VideoCallIcon from '@mui/icons-material/VideoCall';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Video, Phone, Users, Send, ArrowLeft } from 'lucide-react';
|
||||
import VideoPlayer from '@/components/VideoPlayer';
|
||||
import CallHandleButtons from '@/components/CallHandleButtons';
|
||||
|
||||
const RoomPage = () => {
|
||||
const socket = useSocket();
|
||||
const router = useRouter();
|
||||
const { slug } = router.query;
|
||||
|
||||
const [remoteSocketId, setRemoteSocketId] = useState(null);
|
||||
const [myStream, setMyStream] = useState(null);
|
||||
const [remoteStream, setRemoteStream] = useState(null);
|
||||
@@ -16,36 +19,91 @@ const RoomPage = () => {
|
||||
const [isVideoOnHold, setIsVideoOnHold] = useState(false);
|
||||
const [callButton, setCallButton] = useState(true);
|
||||
const [isSendButtonVisible, setIsSendButtonVisible] = useState(true);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [hasJoinedRoom, setHasJoinedRoom] = useState(false);
|
||||
|
||||
// Check if user came from lobby (has proper session) or direct navigation
|
||||
useEffect(() => {
|
||||
// Check if user has a valid session or email stored (you might want to implement proper session management)
|
||||
const hasValidSession = localStorage.getItem('userEmail') || document.referrer.includes('/');
|
||||
|
||||
if (!hasValidSession && typeof window !== 'undefined') {
|
||||
// User navigated directly to room page without going through lobby
|
||||
console.warn('Direct navigation to room detected, redirecting to lobby');
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// If user has a session, mark as joined
|
||||
setHasJoinedRoom(true);
|
||||
}, [router]);
|
||||
|
||||
// Store user email when they join from lobby
|
||||
useEffect(() => {
|
||||
socket.on("room:join", (data) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('userEmail', data.email);
|
||||
localStorage.setItem('currentRoom', data.room);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for socket errors and provide user feedback
|
||||
socket.on("error", (error) => {
|
||||
console.error('Socket error:', error);
|
||||
alert(`Connection Error: ${error.message || 'Something went wrong. Please try again.'}`);
|
||||
// Redirect to lobby on error
|
||||
router.push('/');
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("room:join");
|
||||
socket.off("error");
|
||||
};
|
||||
}, [socket, router]);
|
||||
|
||||
const handleUserJoined = useCallback(({ email, id }) => {
|
||||
//! console.log(`Email ${email} joined the room!`);
|
||||
console.log(`User ${email} joined the room!`);
|
||||
setRemoteSocketId(id);
|
||||
}, []);
|
||||
|
||||
const handleUserLeft = useCallback(({ email }) => {
|
||||
console.log(`User ${email} left the room`);
|
||||
setRemoteSocketId(null);
|
||||
setRemoteStream(null);
|
||||
}, []);
|
||||
|
||||
const handleIncomingCall = useCallback(async ({ from, offer }) => {
|
||||
setRemoteSocketId(from);
|
||||
//! console.log(`incoming call from ${from} with offer ${offer}`);
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: true
|
||||
});
|
||||
setMyStream(stream);
|
||||
setIsConnecting(true);
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: true
|
||||
});
|
||||
setMyStream(stream);
|
||||
|
||||
const ans = await peer.getAnswer(offer);
|
||||
socket.emit("call:accepted", { to: from, ans });
|
||||
const ans = await peer.getAnswer(offer);
|
||||
socket.emit("call:accepted", { to: from, ans });
|
||||
} catch (error) {
|
||||
console.error('Error handling incoming call:', error);
|
||||
setIsConnecting(false);
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
const sendStreams = useCallback(() => {
|
||||
for (const track of myStream.getTracks()) {
|
||||
peer.peer.addTrack(track, myStream);
|
||||
if (myStream) {
|
||||
for (const track of myStream.getTracks()) {
|
||||
peer.peer.addTrack(track, myStream);
|
||||
}
|
||||
setIsSendButtonVisible(false);
|
||||
}
|
||||
setIsSendButtonVisible(false);
|
||||
}, [myStream]);
|
||||
|
||||
const handleCallAccepted = useCallback(({ from, ans }) => {
|
||||
peer.setLocalDescription(ans);
|
||||
//! console.log("Call Accepted");
|
||||
|
||||
console.log("Call Accepted");
|
||||
setIsConnecting(false);
|
||||
sendStreams();
|
||||
}, [sendStreams]);
|
||||
|
||||
@@ -54,7 +112,6 @@ const RoomPage = () => {
|
||||
socket.emit("peer:nego:done", { to: from, ans });
|
||||
}, [socket]);
|
||||
|
||||
|
||||
const handleNegoNeeded = useCallback(async () => {
|
||||
const offer = await peer.getOffer();
|
||||
socket.emit("peer:nego:needed", { offer, to: remoteSocketId });
|
||||
@@ -66,23 +123,23 @@ const RoomPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
peer.peer.addEventListener('negotiationneeded', handleNegoNeeded);
|
||||
|
||||
return () => {
|
||||
peer.peer.removeEventListener('negotiationneeded', handleNegoNeeded);
|
||||
}
|
||||
}, [handleNegoNeeded]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
peer.peer.addEventListener('track', async ev => {
|
||||
const remoteStream = ev.streams;
|
||||
console.log("GOT TRACKS!");
|
||||
setRemoteStream(remoteStream[0]);
|
||||
setIsConnecting(false);
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("user:joined", handleUserJoined);
|
||||
socket.on("user:left", handleUserLeft);
|
||||
socket.on("incoming:call", handleIncomingCall);
|
||||
socket.on("call:accepted", handleCallAccepted);
|
||||
socket.on("peer:nego:needed", handleNegoNeededIncoming);
|
||||
@@ -90,21 +147,21 @@ const RoomPage = () => {
|
||||
|
||||
return () => {
|
||||
socket.off("user:joined", handleUserJoined);
|
||||
socket.off("user:left", handleUserLeft);
|
||||
socket.off("incoming:call", handleIncomingCall);
|
||||
socket.off("call:accepted", handleCallAccepted);
|
||||
socket.off("peer:nego:needed", handleNegoNeededIncoming);
|
||||
socket.off("peer:nego:final", handleNegoFinal);
|
||||
};
|
||||
},
|
||||
[
|
||||
socket,
|
||||
handleUserJoined,
|
||||
handleIncomingCall,
|
||||
handleCallAccepted,
|
||||
handleNegoNeededIncoming,
|
||||
handleNegoFinal
|
||||
]);
|
||||
|
||||
}, [
|
||||
socket,
|
||||
handleUserJoined,
|
||||
handleUserLeft,
|
||||
handleIncomingCall,
|
||||
handleCallAccepted,
|
||||
handleNegoNeededIncoming,
|
||||
handleNegoFinal
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("call:end", ({ from }) => {
|
||||
@@ -118,6 +175,9 @@ const RoomPage = () => {
|
||||
|
||||
setRemoteStream(null);
|
||||
setRemoteSocketId(null);
|
||||
setCallButton(true);
|
||||
setIsSendButtonVisible(true);
|
||||
setIsConnecting(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,7 +186,6 @@ const RoomPage = () => {
|
||||
}
|
||||
}, [remoteSocketId, myStream, socket]);
|
||||
|
||||
//* for disappearing call button
|
||||
useEffect(() => {
|
||||
socket.on("call:initiated", ({ from }) => {
|
||||
if (from === remoteSocketId) {
|
||||
@@ -139,46 +198,50 @@ const RoomPage = () => {
|
||||
}
|
||||
}, [socket, remoteSocketId]);
|
||||
|
||||
|
||||
const handleCallUser = useCallback(async () => {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: true
|
||||
});
|
||||
setIsConnecting(true);
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: true
|
||||
});
|
||||
|
||||
if (isAudioMute) {
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
audioTracks.forEach(track => track.enabled = false);
|
||||
if (isAudioMute) {
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
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);
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
}, [remoteSocketId, socket, isAudioMute, isVideoOnHold]);
|
||||
|
||||
const handleToggleAudio = () => {
|
||||
peer.toggleAudio();
|
||||
setIsAudioMute(!isAudioMute);
|
||||
if (myStream) {
|
||||
const audioTracks = myStream.getAudioTracks();
|
||||
audioTracks.forEach(track => track.enabled = !track.enabled);
|
||||
setIsAudioMute(!isAudioMute);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleVideo = () => {
|
||||
peer.toggleVideo();
|
||||
setIsVideoOnHold(!isVideoOnHold);
|
||||
if (myStream) {
|
||||
const videoTracks = myStream.getVideoTracks();
|
||||
videoTracks.forEach(track => track.enabled = !track.enabled);
|
||||
setIsVideoOnHold(!isVideoOnHold);
|
||||
}
|
||||
}
|
||||
|
||||
const handleEndCall = useCallback(() => {
|
||||
@@ -190,6 +253,9 @@ const RoomPage = () => {
|
||||
}
|
||||
|
||||
setRemoteStream(null);
|
||||
setCallButton(true);
|
||||
setIsSendButtonVisible(true);
|
||||
setIsConnecting(false);
|
||||
|
||||
if (remoteSocketId) {
|
||||
socket.emit("call:end", { to: remoteSocketId });
|
||||
@@ -197,59 +263,186 @@ const RoomPage = () => {
|
||||
setRemoteSocketId(null);
|
||||
}, [myStream, remoteSocketId, socket]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { slug } = router.query;
|
||||
const handleGoBack = () => {
|
||||
handleEndCall();
|
||||
|
||||
// Clear session data when leaving room
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('userEmail');
|
||||
localStorage.removeItem('currentRoom');
|
||||
}
|
||||
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center w-screen h-screen overflow-hidden'>
|
||||
<title>Room No. {slug}</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
|
||||
<VideoCallIcon sx={{ fontSize: 50, color: 'rgb(30,220,30)' }} />
|
||||
Peers
|
||||
</h1>
|
||||
<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} />
|
||||
}
|
||||
<>
|
||||
{!hasJoinedRoom ? (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 flex items-center justify-center">
|
||||
<div className="text-center text-white">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent mx-auto mb-4"></div>
|
||||
<p>Verifying access...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 relative overflow-hidden'>
|
||||
<title>Room {slug} - VideoPeersJS</title>
|
||||
|
||||
{/* Background decorative elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
class PeerService {
|
||||
constructor() {
|
||||
if (!this.peer) {
|
||||
this.peer = new RTCPeerConnection({
|
||||
iceServers: [{
|
||||
if (typeof window !== 'undefined' && !this.peer) {
|
||||
// Get ICE servers from environment variables
|
||||
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: [
|
||||
"stun:stun.l.google.com:19302",
|
||||
"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,13 +63,58 @@ class PeerService {
|
||||
}
|
||||
|
||||
toggleAudio = () => {
|
||||
const audioTracks = this.peer.getSenders().find(sender => sender.track.kind === 'audio').track;
|
||||
audioTracks.enabled = !audioTracks.enabled;
|
||||
try {
|
||||
const senders = this.peer.getSenders();
|
||||
const audioSender = senders.find(sender =>
|
||||
sender.track && sender.track.kind === 'audio'
|
||||
);
|
||||
|
||||
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 = () => {
|
||||
const videoTracks = this.peer.getSenders().find(sender => sender.track.kind === 'video').track;
|
||||
videoTracks.enabled = !videoTracks.enabled;
|
||||
try {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
54
client/src/styles/fonts/index.css
Archivo normal
54
client/src/styles/fonts/index.css
Archivo normal
@@ -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;
|
||||
}
|
||||
36
client/src/styles/fonts/inter.css
Archivo normal
36
client/src/styles/fonts/inter.css
Archivo normal
@@ -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;
|
||||
}
|
||||
27
client/src/styles/fonts/josefin-sans.css
Archivo normal
27
client/src/styles/fonts/josefin-sans.css
Archivo normal
@@ -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;
|
||||
}
|
||||
36
client/src/styles/fonts/poppins.css
Archivo normal
36
client/src/styles/fonts/poppins.css
Archivo normal
@@ -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;
|
||||
}
|
||||
27
client/src/styles/fonts/space-grotesk.css
Archivo normal
27
client/src/styles/fonts/space-grotesk.css
Archivo normal
@@ -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;
|
||||
}
|
||||
@@ -1,66 +1,206 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@500&display=swap');
|
||||
/* Import Local Fonts Configuration */
|
||||
@import './fonts/index.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Global Base Styles */
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body{
|
||||
overflow: hidden;
|
||||
body {
|
||||
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{
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 500;
|
||||
/* Component Styles */
|
||||
@layer components {
|
||||
/* Form Elements */
|
||||
.form-label {
|
||||
@apply flex items-center text-sm font-medium text-gray-700 mb-2;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
input{
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
border-width: 1px;
|
||||
border-color: rgb(170, 170, 170);
|
||||
&:focus{
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
/* Animations */
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-glow {
|
||||
animation: glow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Custom animations */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@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{
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
padding: 0.625rem;
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
/* Legacy styles for backward compatibility */
|
||||
label {
|
||||
@apply form-label;
|
||||
}
|
||||
|
||||
video{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
input[type="email"],
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
@apply form-input;
|
||||
}
|
||||
|
||||
button:not(.call-button):not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
|
||||
@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;
|
||||
}
|
||||
|
||||
.callButtons {
|
||||
@apply call-button call-button-active;
|
||||
}
|
||||
|
||||
video {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
@@ -7,23 +7,46 @@ module.exports = {
|
||||
],
|
||||
theme: {
|
||||
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: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
fontFamily: {
|
||||
'poppins': ['Poppins', 'sans-serif'],
|
||||
'josefin': ['Josefin Sans', 'sans-serif']
|
||||
},
|
||||
keyframes: {
|
||||
pulse: {
|
||||
'0%, 100%': { opacity: '1' },
|
||||
'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: {
|
||||
'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: {
|
||||
mxxl: { 'max': '1535px' },
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
PORT=8080
|
||||
PORT=8000
|
||||
CLIENT_URL=http://localhost:3000
|
||||
NODE_ENV=development
|
||||
APP_NAME=VideoPeersJS
|
||||
4
server/.env.example
Archivo normal
4
server/.env.example
Archivo normal
@@ -0,0 +1,4 @@
|
||||
PORT=8000
|
||||
CLIENT_URL=http://localhost:3000
|
||||
NODE_ENV=development
|
||||
APP_NAME=VideoPeersJS
|
||||
4
server/.gitignore
vendido
4
server/.gitignore
vendido
@@ -1 +1,3 @@
|
||||
node_modules
|
||||
node_modules
|
||||
*.lock
|
||||
*-lock.json
|
||||
382
server/index.js
382
server/index.js
@@ -3,63 +3,387 @@ const express = require('express');
|
||||
const cors = require('cors');
|
||||
const http = require('http');
|
||||
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();
|
||||
|
||||
const app = express();
|
||||
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().pattern(/^[a-zA-Z0-9-_]+$/).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) => {
|
||||
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 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) => {
|
||||
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 => {
|
||||
const { email, room } = data;
|
||||
emailToSocket.set(email, socket.id);
|
||||
socketToEmail.set(socket.id, email);
|
||||
socket.on("room:join", (data) => {
|
||||
try {
|
||||
// Validate input
|
||||
const { error, value } = roomJoinSchema.validate(data);
|
||||
if (error) {
|
||||
let errorMessage = "Invalid room join data";
|
||||
if (error.details[0]?.context?.key === 'room') {
|
||||
errorMessage = "Room ID must contain only letters, numbers, hyphens, and underscores (3-50 characters)";
|
||||
} else if (error.details[0]?.context?.key === 'email') {
|
||||
errorMessage = "Please provide a valid email address";
|
||||
}
|
||||
socket.emit("error", { message: errorMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
socket.join(room);
|
||||
io.to(room).emit("user:joined", { email, id: socket.id });
|
||||
const { email, room } = value;
|
||||
|
||||
// Sanitize inputs
|
||||
const sanitizedEmail = sanitizeInput(email);
|
||||
const sanitizedRoom = sanitizeInput(room);
|
||||
|
||||
// emits a 'room:joined' event back to the client
|
||||
// that just joined the room.
|
||||
io.to(socket.id).emit("room:join", data);
|
||||
// Additional email validation
|
||||
if (!validator.isEmail(sanitizedEmail)) {
|
||||
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 }) => {
|
||||
io.to(to).emit("incoming:call", { from: socket.id, offer });
|
||||
socket.on("user:call", (data) => {
|
||||
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 }) => {
|
||||
io.to(to).emit("call:accepted", { from: socket.id, ans });
|
||||
socket.on("call:accepted", (data) => {
|
||||
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 }) => {
|
||||
io.to(to).emit("peer:nego:needed", { from: socket.id, offer });
|
||||
socket.on("peer:nego:needed", (data) => {
|
||||
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 }) => {
|
||||
io.to(to).emit("peer:nego:final", { from: socket.id, ans });
|
||||
socket.on("peer:nego:done", (data) => {
|
||||
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 }) => {
|
||||
io.to(to).emit("call:end", { from: socket.id });
|
||||
socket.on("call:end", (data) => {
|
||||
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 }) => {
|
||||
io.to(to).emit("call:initiated", { from: socket.id });
|
||||
});
|
||||
})
|
||||
socket.on("call:initiated", (data) => {
|
||||
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
2257
server/package-lock.json
generado
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "server",
|
||||
"name": "videopeersjs-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"description": "VideoPeersJS - Real-time P2P video chat server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
@@ -12,12 +12,17 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
"nodemon": "^3.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.2"
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{
|
||||
"src": "*.js",
|
||||
"use": "@vercel/node"
|
||||
}],
|
||||
"routes": [{
|
||||
"src": "/(.*)",
|
||||
"dest": "/"
|
||||
}]
|
||||
}
|
||||
Referencia en una nueva incidencia
Block a user