193
README.md
193
README.md
@@ -1,9 +1,19 @@
|
||||
# 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/
|
||||
|
||||
@@ -12,92 +22,185 @@ This is a real time video chat application built using WebRTC, Socket.io, Node.j
|
||||
<p><img align="center" src="https://github.com/hirentimbadiya/Video-Peers/assets/86219935/24fa55ab-c08d-40f5-8023-060c06da78f5" alt="hirentimbadiya"/></p>
|
||||
|
||||
## How To Use :
|
||||
First of all join any room lets say 7 for example with your email and room no. as 7 and then notify other person to enter in room no. 7, now you will get call button on screen this will allow to call other person and then call will be initiated and other person will be having a button called Send Streams and as other person clicks Send Streams Button then video call will be initiated.
|
||||
1. Enter your email and a room ID on the homepage
|
||||
2. Share the room ID with someone you want to video chat with
|
||||
3. Once both users are in the room, click the "Start Call" button
|
||||
4. Use the control buttons to mute/unmute audio, turn camera on/off, or end the call
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
- **Rate Limiting**: Prevents abuse with request limits
|
||||
- **Input Validation**: All user inputs are validated and sanitized
|
||||
- **CORS Protection**: Configured for secure cross-origin requests
|
||||
- **Helmet.js**: Adds security headers to protect against common attacks
|
||||
- **Error Handling**: Comprehensive error handling and logging
|
||||
- **Connection Timeouts**: Prevents hanging connections
|
||||
|
||||
## Key Features
|
||||
|
||||
- [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
|
||||
|
||||
|
||||
|
||||
- [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
|
||||
```bash
|
||||
cd server #in Video-Peers/server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Technologies used / Prerequisites Of The Project
|
||||
|
||||
- [Next.js](https://nextjs.org/) - React framework
|
||||
- [WebRTC](https://webrtc.org/) - Real-time communication
|
||||
- [Socket.io](https://socket.io/) - Bidirectional communication
|
||||
- [Node.js](https://nodejs.org/) - Backend runtime
|
||||
- [Express](https://expressjs.com/) - Node.js framework
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - Styling
|
||||
|
||||
- Run the client side:
|
||||
```bash
|
||||
cd client
|
||||
npm run dev
|
||||
```
|
||||
|
||||
6. Open your browser and navigate to `http://localhost:3000`
|
||||
|
||||
## 🛠 Technologies Used
|
||||
|
||||
### Frontend
|
||||
- [Next.js 14](https://nextjs.org/) - React framework with latest features
|
||||
- [React 18](https://reactjs.org/) - UI library
|
||||
- [Tailwind CSS 3](https://tailwindcss.com/) - Utility-first CSS framework
|
||||
- [Framer Motion](https://www.framer.com/motion/) - Animation library
|
||||
- [Lucide React](https://lucide.dev/) - Beautiful, customizable icons
|
||||
- [WebRTC](https://webrtc.org/) - Real-time communication
|
||||
|
||||
### Backend
|
||||
- [Node.js](https://nodejs.org/) - Backend runtime
|
||||
- [Express.js](https://expressjs.com/) - Web framework
|
||||
- [Socket.io](https://socket.io/) - Real-time bidirectional communication
|
||||
- [Helmet.js](https://helmetjs.github.io/) - Security middleware
|
||||
- [Joi](https://joi.dev/) - Input validation
|
||||
- [Express Rate Limit](https://github.com/nfriedly/express-rate-limit) - Rate limiting
|
||||
- [Validator.js](https://github.com/validatorjs/validator.js) - String validation
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
VideoPeersJS/
|
||||
├── client/ # Next.js frontend
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── pages/ # Next.js pages
|
||||
│ │ ├── context/ # React context providers
|
||||
│ │ ├── service/ # WebRTC service
|
||||
│ │ └── styles/ # Global styles
|
||||
│ ├── .env.example # Environment variables template
|
||||
│ └── package.json
|
||||
├── server/ # Node.js backend
|
||||
│ ├── index.js # Main server file
|
||||
│ ├── .env.example # Environment variables template
|
||||
│ └── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### REST Endpoints
|
||||
- `GET /` - Health check and server info
|
||||
- `GET /health` - Health status endpoint
|
||||
|
||||
#### Socket Events
|
||||
- `room:join` - Join a video chat room
|
||||
- `user:call` - Initiate a call to another user
|
||||
- `call:accepted` - Accept an incoming call
|
||||
- `peer:nego:needed` - WebRTC negotiation
|
||||
- `peer:nego:done` - Complete WebRTC negotiation
|
||||
- `call:end` - End the current call
|
||||
- `error` - Error handling
|
||||
|
||||
## Working Demo
|
||||
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
|
||||
8876
client/package-lock.json
generado
8876
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,23 +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",
|
||||
"@vercel/analytics": "^1.1.1",
|
||||
"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,183 @@
|
||||
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;
|
||||
setIsLoading(false);
|
||||
router.push(`/room/${room}`);
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("room:join", handleJoinRoom);
|
||||
socket.on("error", (error) => {
|
||||
console.error('Socket error:', error);
|
||||
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)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200 bg-white/50 backdrop-blur-sm"
|
||||
placeholder="Enter room ID"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Join Button */}
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={isLoading || !email || !room}
|
||||
className="w-full bg-gradient-to-r from-indigo-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
whileHover={{ scale: isLoading ? 1 : 1.02 }}
|
||||
whileTap={{ scale: isLoading ? 1 : 0.98 }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||
<span>Joining...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Users className="h-5 w-5" />
|
||||
<span>Join Room</span>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</form>
|
||||
|
||||
{/* Features */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-100">
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div className="space-y-1">
|
||||
<div className="text-2xl">🔒</div>
|
||||
<p className="text-xs text-gray-600 font-medium">Secure</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-2xl">⚡</div>
|
||||
<p className="text-xs text-gray-600 font-medium">Fast</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Footer */}
|
||||
<motion.div
|
||||
className="text-center mt-6"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.8 }}
|
||||
>
|
||||
<p className="text-sm text-gray-500">
|
||||
No downloads required • Works in your browser
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,51 @@ const RoomPage = () => {
|
||||
const [isVideoOnHold, setIsVideoOnHold] = useState(false);
|
||||
const [callButton, setCallButton] = useState(true);
|
||||
const [isSendButtonVisible, setIsSendButtonVisible] = useState(true);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
|
||||
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 +72,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 +83,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 +107,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 +135,9 @@ const RoomPage = () => {
|
||||
|
||||
setRemoteStream(null);
|
||||
setRemoteSocketId(null);
|
||||
setCallButton(true);
|
||||
setIsSendButtonVisible(true);
|
||||
setIsConnecting(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,7 +146,6 @@ const RoomPage = () => {
|
||||
}
|
||||
}, [remoteSocketId, myStream, socket]);
|
||||
|
||||
//* for disappearing call button
|
||||
useEffect(() => {
|
||||
socket.on("call:initiated", ({ from }) => {
|
||||
if (from === remoteSocketId) {
|
||||
@@ -139,46 +158,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 +213,9 @@ const RoomPage = () => {
|
||||
}
|
||||
|
||||
setRemoteStream(null);
|
||||
setCallButton(true);
|
||||
setIsSendButtonVisible(true);
|
||||
setIsConnecting(false);
|
||||
|
||||
if (remoteSocketId) {
|
||||
socket.emit("call:end", { to: remoteSocketId });
|
||||
@@ -197,59 +223,168 @@ const RoomPage = () => {
|
||||
setRemoteSocketId(null);
|
||||
}, [myStream, remoteSocketId, socket]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { slug } = router.query;
|
||||
const handleGoBack = () => {
|
||||
handleEndCall();
|
||||
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} />
|
||||
}
|
||||
<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 (typeof window !== 'undefined' && !this.peer) {
|
||||
this.peer = new RTCPeerConnection({
|
||||
iceServers: [{
|
||||
// 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,17 +63,58 @@ class PeerService {
|
||||
}
|
||||
|
||||
toggleAudio = () => {
|
||||
const audioTracks = this.peer.getSenders().find(sender => sender.track.kind === 'audio').track;
|
||||
audioTracks.enabled = !audioTracks.enabled;
|
||||
|
||||
// Mute the local audio track
|
||||
const localAudioTrack = this.peer.getLocalStreams()[0].getAudioTracks()[0];
|
||||
localAudioTrack.enabled = !localAudioTrack.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,203 @@
|
||||
@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 btn-primary;
|
||||
}
|
||||
|
||||
.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
|
||||
376
server/index.js
376
server/index.js
@@ -3,63 +3,381 @@ 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().alphanum().min(3).max(50).required()
|
||||
});
|
||||
|
||||
const callSchema = Joi.object({
|
||||
to: Joi.string().required(),
|
||||
offer: Joi.object().required()
|
||||
});
|
||||
|
||||
const callAcceptedSchema = Joi.object({
|
||||
to: Joi.string().required(),
|
||||
ans: Joi.object().required()
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
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) {
|
||||
socket.emit("error", { message: "Invalid room join data" });
|
||||
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