Comparar commits
10 Commits
5b90b18788
...
6d1dd42e6d
| Autor | SHA1 | Fecha | |
|---|---|---|---|
|
6d1dd42e6d
|
|||
|
|
9d013a7c87 | ||
|
|
3a71ab2092 | ||
|
|
926842a12a | ||
|
|
a0411fb0b6 | ||
|
|
457698c4d2 | ||
|
|
502acf3155 | ||
|
|
36c8b35715 | ||
|
|
79d9760980 | ||
|
|
c69de40b55 |
194
README.md
194
README.md
@@ -1,90 +1,206 @@
|
|||||||
# VideoPeers - Real Time P2P Video Chat Application
|
# VideoPeersJS - Real Time P2P Video Chat Application
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
This is a real time video chat application built using WebRTC, Socket.io, Node.js, Express.js, and Next.js. It allows users to create a room and share the room ID with other users to join the room for a peer-to-peer video call.
|
This is a modern, secure real-time video chat application built using WebRTC, Socket.io, Node.js, Express.js, and Next.js. VideoPeersJS allows users to create rooms and share room IDs with other users for peer-to-peer video calls with enhanced security and a beautiful, responsive design.
|
||||||
|
|
||||||
|
## 🚀 Latest Updates (2025)
|
||||||
|
|
||||||
|
- **Enhanced Security**: Implemented comprehensive security measures including Helmet.js, rate limiting, input validation, and CORS protection
|
||||||
|
- **Modern UI/UX**: Complete redesign with beautiful gradients, animations, and responsive design using Tailwind CSS
|
||||||
|
- **Updated Dependencies**: All packages updated to latest stable versions for security and performance
|
||||||
|
- **Improved User Experience**: Better loading states, error handling, and user feedback
|
||||||
|
- **Enhanced Architecture**: Better code organization and error handling
|
||||||
|
- **Environment Configuration**: ICE servers and configuration moved to environment variables
|
||||||
|
- **Enhanced Typography**: Added multiple Google Fonts for better visual hierarchy
|
||||||
|
- **Local Fonts**: Migrated to self-hosted fonts for better performance, privacy, and reliability (156KB total)
|
||||||
|
|
||||||
|
## Deployment Link : https://video-peers.vercel.app/
|
||||||
|
|
||||||
|
## Screen Shots :
|
||||||
|
<p><img align="right" src="https://github.com/hirentimbadiya/Video-Peers/assets/86219935/3cc34115-e4db-460a-9825-d0e97a5f6cae" /></p>
|
||||||
|
<p><img align="center" src="https://github.com/hirentimbadiya/Video-Peers/assets/86219935/24fa55ab-c08d-40f5-8023-060c06da78f5" alt="hirentimbadiya"/></p>
|
||||||
|
|
||||||
|
## How To Use :
|
||||||
|
1. Enter your email and a room ID on the homepage
|
||||||
|
2. Share the room ID with someone you want to video chat with
|
||||||
|
3. Once both users are in the room, click the "Start Call" button
|
||||||
|
4. Use the control buttons to mute/unmute audio, turn camera on/off, or end the call
|
||||||
|
|
||||||
|
## 🔒 Security Features
|
||||||
|
|
||||||
|
- **Rate Limiting**: Prevents abuse with request limits
|
||||||
|
- **Input Validation**: All user inputs are validated and sanitized
|
||||||
|
- **CORS Protection**: Configured for secure cross-origin requests
|
||||||
|
- **Helmet.js**: Adds security headers to protect against common attacks
|
||||||
|
- **Error Handling**: Comprehensive error handling and logging
|
||||||
|
- **Connection Timeouts**: Prevents hanging connections
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- [x] Create and join video chat rooms using room IDs
|
- [x] Create and join video chat rooms using room IDs
|
||||||
- [x] Real-time peer-to-peer video calling using WebRTC
|
- [x] Real-time peer-to-peer video calling using WebRTC
|
||||||
- [x] Audio mute/unmute controls
|
- [x] Audio mute/unmute controls with visual indicators
|
||||||
- [x] Video Hold/Play controls
|
- [x] Video on/off controls
|
||||||
- [x] Call End option
|
- [x] Call end functionality
|
||||||
- [x] Responsive Design
|
- [x] Modern, responsive design with animations
|
||||||
- [x] Built with Next.js, Node.js, Express, Socket.io
|
- [x] Enhanced typography with local fonts for better performance
|
||||||
|
- [x] Self-hosted fonts (no external dependencies)
|
||||||
## Technologies used / Prerequisites Of The Project
|
- [x] Environment-based configuration for ICE servers
|
||||||
|
- [x] Input validation and sanitization
|
||||||
- [Next.js](https://nextjs.org/) - React framework
|
- [x] Connection status indicators
|
||||||
- [WebRTC](https://webrtc.org/) - Real-time communication
|
- [x] Beautiful gradients and glass-morphism effects
|
||||||
- [Socket.io](https://socket.io/) - Bidirectional communication
|
- [x] Accessible design with proper ARIA labels
|
||||||
- [Node.js](https://nodejs.org/) - Backend runtime
|
|
||||||
- [Express](https://expressjs.com/) - Node.js framework
|
|
||||||
- [Tailwind CSS](https://tailwindcss.com/) - Styling
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Clone the repository
|
1. Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/hirentimbadiya/Video-Peers.git
|
git clone https://github.com/hirentimbadiya/VideoPeersJS.git
|
||||||
|
cd VideoPeersJS
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies for Client Side
|
2. Install dependencies for Client Side
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd client #in Video-Peers/client
|
cd client
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Install dependencies for Server Side
|
3. Install dependencies for Server Side
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd server #in Video-Peers/server
|
cd server
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run the application
|
4. Configure environment variables
|
||||||
|
|
||||||
- Run the client side
|
```bash
|
||||||
|
# In server directory, copy .env.example to .env
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit .env with your preferred settings:
|
||||||
|
# PORT=8000
|
||||||
|
# CLIENT_URL=http://localhost:3000
|
||||||
|
# NODE_ENV=development
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run the application
|
||||||
|
|
||||||
|
- Run the server side first:
|
||||||
```bash
|
```bash
|
||||||
cd client #in Video-Peers/client
|
cd server
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
- Run the server side
|
|
||||||
|
- Run the client side:
|
||||||
```bash
|
```bash
|
||||||
cd server #in Video-Peers/server
|
cd client
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
6. Open your browser and navigate to `http://localhost:3000`
|
||||||
|
|
||||||
|
## 🛠 Technologies Used
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [Next.js 14](https://nextjs.org/) - React framework with latest features
|
||||||
|
- [React 18](https://reactjs.org/) - UI library
|
||||||
|
- [Tailwind CSS 3](https://tailwindcss.com/) - Utility-first CSS framework
|
||||||
|
- [Framer Motion](https://www.framer.com/motion/) - Animation library
|
||||||
|
- [Lucide React](https://lucide.dev/) - Beautiful, customizable icons
|
||||||
|
- [WebRTC](https://webrtc.org/) - Real-time communication
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [Node.js](https://nodejs.org/) - Backend runtime
|
||||||
|
- [Express.js](https://expressjs.com/) - Web framework
|
||||||
|
- [Socket.io](https://socket.io/) - Real-time bidirectional communication
|
||||||
|
- [Helmet.js](https://helmetjs.github.io/) - Security middleware
|
||||||
|
- [Joi](https://joi.dev/) - Input validation
|
||||||
|
- [Express Rate Limit](https://github.com/nfriedly/express-rate-limit) - Rate limiting
|
||||||
|
- [Validator.js](https://github.com/validatorjs/validator.js) - String validation
|
||||||
|
|
||||||
|
## 🔧 Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
VideoPeersJS/
|
||||||
|
├── client/ # Next.js frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React components
|
||||||
|
│ │ ├── pages/ # Next.js pages
|
||||||
|
│ │ ├── context/ # React context providers
|
||||||
|
│ │ ├── service/ # WebRTC service
|
||||||
|
│ │ └── styles/ # Global styles
|
||||||
|
│ ├── .env.example # Environment variables template
|
||||||
|
│ └── package.json
|
||||||
|
├── server/ # Node.js backend
|
||||||
|
│ ├── index.js # Main server file
|
||||||
|
│ ├── .env.example # Environment variables template
|
||||||
|
│ └── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
#### REST Endpoints
|
||||||
|
- `GET /` - Health check and server info
|
||||||
|
- `GET /health` - Health status endpoint
|
||||||
|
|
||||||
|
#### Socket Events
|
||||||
|
- `room:join` - Join a video chat room
|
||||||
|
- `user:call` - Initiate a call to another user
|
||||||
|
- `call:accepted` - Accept an incoming call
|
||||||
|
- `peer:nego:needed` - WebRTC negotiation
|
||||||
|
- `peer:nego:done` - Complete WebRTC negotiation
|
||||||
|
- `call:end` - End the current call
|
||||||
|
- `error` - Error handling
|
||||||
|
|
||||||
## Working Demo
|
## Working Demo
|
||||||
https://github.com/hirentimbadiya/Video-Peers/assets/86219935/7ce8caf9-0881-4abe-b633-79d3bb0a87ef
|
https://github.com/hirentimbadiya/Video-Peers/assets/86219935/7ce8caf9-0881-4abe-b633-79d3bb0a87ef
|
||||||
|
|
||||||
## Future Scope
|
## 🚀 Future Scope
|
||||||
|
|
||||||
- Text chat support in rooms
|
- [ ] Text chat support in rooms
|
||||||
- Screen Sharing Feature
|
- [ ] Screen sharing feature
|
||||||
- Multiple Participants in a room
|
- [ ] Multiple participants in a room (group calls)
|
||||||
- Video recording option
|
- [ ] Video recording and playback
|
||||||
|
- [ ] Virtual backgrounds
|
||||||
|
- [ ] File sharing capabilities
|
||||||
|
- [ ] Mobile app development
|
||||||
|
- [ ] Integration with calendar systems
|
||||||
|
|
||||||
|
## 📚 My Learnings
|
||||||
|
|
||||||
## My Learnings
|
Building this project taught me about:
|
||||||
|
|
||||||
By Building this project I learned about the following concepts:
|
- **WebRTC**: Understanding peer-to-peer communication, ICE candidates, and media streams
|
||||||
|
- **Socket.io**: Real-time bidirectional communication patterns
|
||||||
|
- **Security**: Implementing comprehensive security measures for web applications
|
||||||
|
- **Modern React**: Using hooks, context, and latest React patterns
|
||||||
|
- **UI/UX Design**: Creating beautiful, accessible interfaces with Tailwind CSS
|
||||||
|
- **Next.js**: Server-side rendering, file-based routing, and optimization
|
||||||
|
- **Node.js Security**: Rate limiting, input validation, and security headers
|
||||||
|
|
||||||
- WebRTC (Web Real-Time Communication) is a technology that enables real-time peer-to-peer communication between browsers and mobile applications. It is an open-source and free project that used to provide web browsers and mobile applications with real-time communication (RTC) via simple application programming interfaces (APIs).
|
## 🤝 Contributing
|
||||||
|
|
||||||
- Socket.IO is a library that enables real-time, bidirectional and event-based communication between the browser and the server. It consists of: a Node.js server: Source | API. a Javascript client library for the browser (which can be also run from Node.js): Source | API.
|
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|
||||||
- Next.js is a React framework that enables several server-side rendering (SSR) features such as static site generation (SSG), automatic code splitting, server-side rendering, and client-side routing. It is a framework that is built on top of React.js and Node.js.
|
1. Fork the project
|
||||||
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
## License
|
|
||||||
[MIT](https://choosealicense.com/licenses/mit/)
|
[MIT](https://choosealicense.com/licenses/mit/)
|
||||||
|
|
||||||
## Author
|
## 👨💻 Author
|
||||||
- [hirentimbadiya](https://github.com/hirentimbadiya)
|
- [hirentimbadiya](https://github.com/hirentimbadiya)
|
||||||
- Email : hirentimbadiya74@gmail.com
|
- Email: hirentimbadiya74@gmail.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
⭐ Star this repo if you find it helpful!
|
||||||
|
|||||||
3
client/.env.example
Archivo normal
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-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
*.lock
|
||||||
|
*-lock.json
|
||||||
8849
client/package-lock.json
generado
8849
client/package-lock.json
generado
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "videopeersjs-client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -9,22 +9,25 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@mui/icons-material": "^5.14.16",
|
"@mui/icons-material": "^6.1.0",
|
||||||
"@mui/material": "^5.14.17",
|
"@mui/material": "^6.1.0",
|
||||||
"next": "14.0.2",
|
"@vercel/analytics": "^1.3.1",
|
||||||
"react": "^18",
|
"framer-motion": "^11.5.4",
|
||||||
"react-dom": "^18",
|
"lucide-react": "^0.439.0",
|
||||||
"react-player": "^2.13.0",
|
"next": "^14.2.9",
|
||||||
"socket.io-client": "^4.7.2"
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-player": "^2.16.0",
|
||||||
|
"socket.io-client": "^4.7.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.0.1",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "14.0.2",
|
"eslint-config-next": "^14.2.9",
|
||||||
"postcss": "^8",
|
"postcss": "^8.4.45",
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.78.0",
|
||||||
"tailwindcss": "^3.3.0"
|
"tailwindcss": "^3.4.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Archivo binario no mostrado.
|
Antes Anchura: | Altura: | Tamaño: 25 KiB Después Anchura: | Altura: | Tamaño: 3.7 KiB |
58
client/public/fonts/README.md
Archivo normal
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 { motion } from 'framer-motion';
|
||||||
import MicOffIcon from '@mui/icons-material/MicOff';
|
import { Mic, MicOff, Video, VideoOff, PhoneOff } from 'lucide-react';
|
||||||
import KeyboardVoiceIcon from '@mui/icons-material/KeyboardVoice';
|
|
||||||
import VideocamIcon from '@mui/icons-material/Videocam';
|
const CallHandleButtons = ({ isAudioMute, isVideoOnHold, onToggleAudio, onToggleVideo, onEndCall }) => {
|
||||||
import VideocamOffIcon from '@mui/icons-material/VideocamOff';
|
const buttonVariants = {
|
||||||
import CallEndIcon from '@mui/icons-material/CallEnd';
|
hover: { scale: 1.1 },
|
||||||
|
tap: { scale: 0.95 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className='fixed bottom-8 left-1/2 transform -translate-x-1/2 z-30'
|
||||||
|
initial={{ opacity: 0, y: 100 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className='flex items-center space-x-4 bg-black/20 backdrop-blur-lg rounded-2xl p-4 border border-white/10'>
|
||||||
|
{/* Audio Toggle Button */}
|
||||||
|
<motion.button
|
||||||
|
variants={buttonVariants}
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onClick={onToggleAudio}
|
||||||
|
className={`w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200 ${
|
||||||
|
isAudioMute
|
||||||
|
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
: 'bg-white/20 hover:bg-white/30 text-white border border-white/20'
|
||||||
|
}`}
|
||||||
|
title={isAudioMute ? 'Unmute microphone' : 'Mute microphone'}
|
||||||
|
>
|
||||||
|
{isAudioMute ? (
|
||||||
|
<MicOff className="h-6 w-6" />
|
||||||
|
) : (
|
||||||
|
<Mic className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Video Toggle Button */}
|
||||||
|
<motion.button
|
||||||
|
variants={buttonVariants}
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onClick={onToggleVideo}
|
||||||
|
className={`w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200 ${
|
||||||
|
isVideoOnHold
|
||||||
|
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
: 'bg-white/20 hover:bg-white/30 text-white border border-white/20'
|
||||||
|
}`}
|
||||||
|
title={isVideoOnHold ? 'Turn on camera' : 'Turn off camera'}
|
||||||
|
>
|
||||||
|
{isVideoOnHold ? (
|
||||||
|
<VideoOff className="h-6 w-6" />
|
||||||
|
) : (
|
||||||
|
<Video className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* End Call Button */}
|
||||||
|
<motion.button
|
||||||
|
variants={buttonVariants}
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onClick={onEndCall}
|
||||||
|
className='w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 text-white flex items-center justify-center transition-all duration-200 shadow-lg'
|
||||||
|
title='End call'
|
||||||
|
>
|
||||||
|
<PhoneOff className="h-6 w-6" />
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtle glow effect */}
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-indigo-400/20 to-purple-400/20 blur-xl -z-10"></div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CallHandleButtons = ({ isAudioMute, isVideoOnHold, onToggleAudio, onToggleVideo, onEndCall }) => (
|
|
||||||
<div className='absolute bottom-0 flex w-full space-x-4 h-[80px] items-center justify-center rounded-md'>
|
|
||||||
<div className=' bg-[#2c3e508b] rounded-md flex px-4 py-2 justify-center gap-10'>
|
|
||||||
<button className="callButtons text-white bg-blue-700 hover:bg-white hover:text-blue-700
|
|
||||||
focus:ring-4 focus:ring-blue-300" onClick={onToggleAudio}>
|
|
||||||
{isAudioMute ? <MicOffIcon fontSize="large" /> : <KeyboardVoiceIcon fontSize="large" />}
|
|
||||||
</button>
|
|
||||||
<button className="callButtons text-white bg-blue-700 hover:bg-white hover:text-blue-700
|
|
||||||
focus:ring-4 focus:ring-blue-300"
|
|
||||||
onClick={onToggleVideo}
|
|
||||||
>
|
|
||||||
{isVideoOnHold ? <VideocamOffIcon fontSize="large" /> : <VideocamIcon fontSize="large" />}
|
|
||||||
</button>
|
|
||||||
<button className="callButtons text-white bg-red-600 hover:text-red-700 hover:bg-white
|
|
||||||
focus:ring-4 focus:ring-white" onClick={onEndCall}>
|
|
||||||
<CallEndIcon fontSize="large" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
export default CallHandleButtons;
|
export default CallHandleButtons;
|
||||||
|
|||||||
@@ -1,72 +1,183 @@
|
|||||||
import { useSocket } from '@/context/SocketProvider';
|
import { useSocket } from '@/context/SocketProvider';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import VideoCallIcon from '@mui/icons-material/VideoCall';
|
import { motion } from 'framer-motion';
|
||||||
|
import { Video, Users, ArrowRight, Mail, Hash } from 'lucide-react';
|
||||||
|
|
||||||
const LobbyScreen = () => {
|
const LobbyScreen = () => {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [room, setRoom] = useState("");
|
const [room, setRoom] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// console.log(socket);
|
|
||||||
|
|
||||||
const handleSubmitForm = useCallback((e) => {
|
const handleSubmitForm = useCallback(async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
socket.emit('room:join', { email, room });
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.emit('room:join', { email, room });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error joining room:', error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}, [email, room, socket]);
|
}, [email, room, socket]);
|
||||||
|
|
||||||
const handleJoinRoom = useCallback((data) => {
|
const handleJoinRoom = useCallback((data) => {
|
||||||
const { email, room } = data;
|
const { email, room } = data;
|
||||||
|
setIsLoading(false);
|
||||||
router.push(`/room/${room}`);
|
router.push(`/room/${room}`);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on("room:join", handleJoinRoom);
|
socket.on("room:join", handleJoinRoom);
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
console.error('Socket error:', error);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("room:join", handleJoinRoom);
|
socket.off("room:join", handleJoinRoom);
|
||||||
|
socket.off("error");
|
||||||
}
|
}
|
||||||
}, [socket, handleJoinRoom]);
|
}, [socket, handleJoinRoom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center h-screen bg-gray-100'>
|
<div className='min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 flex flex-col items-center justify-center p-4'>
|
||||||
<title>VideoPeers</title>
|
<title>VideoPeersJS - Connect & Collaborate</title>
|
||||||
<link rel="shortcut icon" href="../../public/favicon.ico" type="image/x-icon" />
|
|
||||||
<h1 className='text-5xl font-[15px] mb-5 mt-5 text-center font-josefin tracking-tighter'>Video<VideoCallIcon sx={{ fontSize: 70, color: 'rgb(30,220,30)' }} />Peers</h1>
|
{/* Background decorative elements */}
|
||||||
<p className='text-2xl mt-2 mb-4 text-center md:max-w-[400px] max-w-[300px] text-gray-600'>
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
Peer-to-Peer video calls, powered by <b>WebRTC!</b>
|
<div className="absolute -top-4 -right-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
|
||||||
<br />
|
<div className="absolute -bottom-8 -left-4 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse delay-1000"></div>
|
||||||
Bring People Closer Together.
|
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse delay-500"></div>
|
||||||
</p>
|
|
||||||
<div className='bg-white p-6 rounded shadow-md'>
|
|
||||||
<form className='flex flex-col items-center justify-center'
|
|
||||||
onSubmit={handleSubmitForm}
|
|
||||||
>
|
|
||||||
<label htmlFor="email">Email ID</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id='email'
|
|
||||||
required
|
|
||||||
value={email}
|
|
||||||
autoComplete='off'
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<label htmlFor="room">Room Number</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id='room'
|
|
||||||
required
|
|
||||||
autoComplete='off'
|
|
||||||
value={room}
|
|
||||||
onChange={(e) => setRoom(e.target.value)}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<button className='bg-blue-500 hover:bg-blue-600'>
|
|
||||||
Join
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="relative z-10 w-full max-w-md"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
{/* Logo and Title */}
|
||||||
|
<motion.div
|
||||||
|
className="text-center mb-8"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-4">
|
||||||
|
<Video className="h-12 w-12 text-indigo-600 mr-2" />
|
||||||
|
<h1 className="text-5xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||||
|
{process.env.NEXT_PUBLIC_APP_NAME || 'VideoPeersJS'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
|
Peer-to-Peer Video Calls
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-2 leading-relaxed">
|
||||||
|
Powered by <span className="font-semibold text-indigo-600">WebRTC</span> •
|
||||||
|
Bringing people closer together
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Main Card */}
|
||||||
|
<motion.div
|
||||||
|
className="bg-white/80 backdrop-blur-lg rounded-2xl shadow-xl border border-white/20 p-8"
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmitForm} className="space-y-6">
|
||||||
|
{/* Email Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="email" className="flex items-center text-sm font-medium text-gray-700">
|
||||||
|
<Mail className="h-4 w-4 mr-2 text-indigo-500" />
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
autoComplete="email"
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200 bg-white/50 backdrop-blur-sm"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Room Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="room" className="flex items-center text-sm font-medium text-gray-700">
|
||||||
|
<Hash className="h-4 w-4 mr-2 text-indigo-500" />
|
||||||
|
Room ID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="room"
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
value={room}
|
||||||
|
onChange={(e) => setRoom(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200 bg-white/50 backdrop-blur-sm"
|
||||||
|
placeholder="Enter room ID"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Join Button */}
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !email || !room}
|
||||||
|
className="w-full bg-gradient-to-r from-indigo-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center space-x-2"
|
||||||
|
whileHover={{ scale: isLoading ? 1 : 1.02 }}
|
||||||
|
whileTap={{ scale: isLoading ? 1 : 0.98 }}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
|
||||||
|
<span>Joining...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Users className="h-5 w-5" />
|
||||||
|
<span>Join Room</span>
|
||||||
|
<ArrowRight className="h-5 w-5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="mt-8 pt-6 border-t border-gray-100">
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-center">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-2xl">🔒</div>
|
||||||
|
<p className="text-xs text-gray-600 font-medium">Secure</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-2xl">⚡</div>
|
||||||
|
<p className="text-xs text-gray-600 font-medium">Fast</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<motion.div
|
||||||
|
className="text-center mt-6"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.8 }}
|
||||||
|
>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
No downloads required • Works in your browser
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,89 @@
|
|||||||
import ReactPlayer from 'react-player';
|
import ReactPlayer from 'react-player';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { User, Mic, MicOff } from 'lucide-react';
|
||||||
|
|
||||||
const VideoPlayer = ({ stream, isAudioMute, name }) => {
|
const VideoPlayer = ({ stream, isAudioMute, name }) => {
|
||||||
const myStream = name === "My Stream" ? true : false;
|
const isMyStream = name === "My Stream";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<motion.div
|
||||||
<div className={`${name === "My Stream" ? "flex flex-col items-center justify-center absolute top-2 right-3 z-10" : "px-2"}`}>
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
<h1 className={`text-sm font-poppins font-semibold md:text-xl mb-1 text-center ${myStream ? "mt-1" : "mt-4"}`}>
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
{name}
|
transition={{ duration: 0.5 }}
|
||||||
</h1>
|
className={`relative ${isMyStream
|
||||||
<div className={`relative rounded-[30px] overflow-hidden
|
? "absolute top-4 right-4 z-20 w-32 md:w-48 lg:w-64"
|
||||||
${myStream ? " mxs:w-[80px] mxs:h-[120px] msm:w-[100px] msm:rounded-md msm:h-[140px] mmd:w-[140px] md:w-[200px] lg:w-[280px]"
|
: "w-full max-w-4xl mx-auto"
|
||||||
: "mxs:h-[450px] mss:h-[500px] mmd:h-[600px] md:w-[800px] md:h-[500px]"}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{/* Name Badge */}
|
||||||
|
<div className={`absolute top-2 left-2 z-30 flex items-center space-x-2 px-3 py-1 rounded-full bg-black/50 backdrop-blur-sm text-white text-xs font-medium ${
|
||||||
|
isMyStream ? "scale-75" : ""
|
||||||
|
}`}>
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span>{name}</span>
|
||||||
|
{isAudioMute ? (
|
||||||
|
<MicOff className="h-3 w-3 text-red-400" />
|
||||||
|
) : (
|
||||||
|
<Mic className="h-3 w-3 text-green-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Container */}
|
||||||
|
<div className={`relative overflow-hidden rounded-2xl shadow-2xl border-2 border-white/20 ${
|
||||||
|
isMyStream
|
||||||
|
? "aspect-[3/4] bg-gradient-to-br from-gray-900 to-gray-800"
|
||||||
|
: "aspect-video bg-gradient-to-br from-gray-900 to-gray-800"
|
||||||
|
}`}>
|
||||||
|
{stream ? (
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
url={stream}
|
url={stream}
|
||||||
playing
|
playing
|
||||||
muted={isAudioMute}
|
muted={isMyStream ? true : isAudioMute}
|
||||||
height="100%"
|
height="100%"
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{ transform: 'scaleX(-1)' }}
|
style={{
|
||||||
|
transform: 'scaleX(-1)',
|
||||||
|
}}
|
||||||
|
config={{
|
||||||
|
file: {
|
||||||
|
attributes: {
|
||||||
|
style: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<div className="text-center text-white/60">
|
||||||
|
<User className="h-16 w-16 mx-auto mb-2 opacity-40" />
|
||||||
|
<p className="text-sm">Camera not available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Overlay for connection status */}
|
||||||
|
{!stream && (
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center">
|
||||||
|
<div className="text-center text-white">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<User className="h-12 w-12 mx-auto mb-2" />
|
||||||
|
<p className="text-sm font-medium">Connecting...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
{/* Glow effect for my stream */}
|
||||||
|
{isMyStream && (
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-indigo-400 to-purple-400 opacity-20 blur-xl -z-10"></div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoPlayer;
|
export default VideoPlayer;
|
||||||
@@ -9,7 +9,22 @@ export const useSocket = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SocketProvider = (props) => {
|
const SocketProvider = (props) => {
|
||||||
const socket = useMemo(() => io("video-peers-server.onrender.com/"), []);
|
const socket = useMemo(() => {
|
||||||
|
// Get server URL from environment variables
|
||||||
|
const serverUrl = process.env.NEXT_PUBLIC_SOCKET_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
console.log('Connecting to socket server:', serverUrl);
|
||||||
|
|
||||||
|
return io(serverUrl, {
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
autoConnect: true,
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
|
timeout: 20000,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={socket}>
|
<SocketContext.Provider value={socket}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Html, Head, Main, NextScript } from 'next/document'
|
import { Html, Head, Main, NextScript } from 'next/document';
|
||||||
|
import { Analytics } from '@vercel/analytics/react';
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
@@ -7,6 +8,7 @@ export default function Document() {
|
|||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { useSocket } from '@/context/SocketProvider';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import peer from '@/service/peer';
|
import peer from '@/service/peer';
|
||||||
import CallIcon from '@mui/icons-material/Call';
|
import { motion } from 'framer-motion';
|
||||||
import VideoCallIcon from '@mui/icons-material/VideoCall';
|
import { Video, Phone, Users, Send, ArrowLeft } from 'lucide-react';
|
||||||
import VideoPlayer from '@/components/VideoPlayer';
|
import VideoPlayer from '@/components/VideoPlayer';
|
||||||
import CallHandleButtons from '@/components/CallHandleButtons';
|
import CallHandleButtons from '@/components/CallHandleButtons';
|
||||||
|
|
||||||
const RoomPage = () => {
|
const RoomPage = () => {
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
|
const router = useRouter();
|
||||||
|
const { slug } = router.query;
|
||||||
|
|
||||||
const [remoteSocketId, setRemoteSocketId] = useState(null);
|
const [remoteSocketId, setRemoteSocketId] = useState(null);
|
||||||
const [myStream, setMyStream] = useState(null);
|
const [myStream, setMyStream] = useState(null);
|
||||||
const [remoteStream, setRemoteStream] = useState(null);
|
const [remoteStream, setRemoteStream] = useState(null);
|
||||||
@@ -16,36 +19,51 @@ const RoomPage = () => {
|
|||||||
const [isVideoOnHold, setIsVideoOnHold] = useState(false);
|
const [isVideoOnHold, setIsVideoOnHold] = useState(false);
|
||||||
const [callButton, setCallButton] = useState(true);
|
const [callButton, setCallButton] = useState(true);
|
||||||
const [isSendButtonVisible, setIsSendButtonVisible] = useState(true);
|
const [isSendButtonVisible, setIsSendButtonVisible] = useState(true);
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
|
||||||
const handleUserJoined = useCallback(({ email, id }) => {
|
const handleUserJoined = useCallback(({ email, id }) => {
|
||||||
//! console.log(`Email ${email} joined the room!`);
|
console.log(`User ${email} joined the room!`);
|
||||||
setRemoteSocketId(id);
|
setRemoteSocketId(id);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleUserLeft = useCallback(({ email }) => {
|
||||||
|
console.log(`User ${email} left the room`);
|
||||||
|
setRemoteSocketId(null);
|
||||||
|
setRemoteStream(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleIncomingCall = useCallback(async ({ from, offer }) => {
|
const handleIncomingCall = useCallback(async ({ from, offer }) => {
|
||||||
setRemoteSocketId(from);
|
setRemoteSocketId(from);
|
||||||
//! console.log(`incoming call from ${from} with offer ${offer}`);
|
setIsConnecting(true);
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: true,
|
try {
|
||||||
video: true
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
});
|
audio: true,
|
||||||
setMyStream(stream);
|
video: true
|
||||||
|
});
|
||||||
|
setMyStream(stream);
|
||||||
|
|
||||||
const ans = await peer.getAnswer(offer);
|
const ans = await peer.getAnswer(offer);
|
||||||
socket.emit("call:accepted", { to: from, ans });
|
socket.emit("call:accepted", { to: from, ans });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling incoming call:', error);
|
||||||
|
setIsConnecting(false);
|
||||||
|
}
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
const sendStreams = useCallback(() => {
|
const sendStreams = useCallback(() => {
|
||||||
for (const track of myStream.getTracks()) {
|
if (myStream) {
|
||||||
peer.peer.addTrack(track, myStream);
|
for (const track of myStream.getTracks()) {
|
||||||
|
peer.peer.addTrack(track, myStream);
|
||||||
|
}
|
||||||
|
setIsSendButtonVisible(false);
|
||||||
}
|
}
|
||||||
setIsSendButtonVisible(false);
|
|
||||||
}, [myStream]);
|
}, [myStream]);
|
||||||
|
|
||||||
const handleCallAccepted = useCallback(({ from, ans }) => {
|
const handleCallAccepted = useCallback(({ from, ans }) => {
|
||||||
peer.setLocalDescription(ans);
|
peer.setLocalDescription(ans);
|
||||||
//! console.log("Call Accepted");
|
console.log("Call Accepted");
|
||||||
|
setIsConnecting(false);
|
||||||
sendStreams();
|
sendStreams();
|
||||||
}, [sendStreams]);
|
}, [sendStreams]);
|
||||||
|
|
||||||
@@ -54,7 +72,6 @@ const RoomPage = () => {
|
|||||||
socket.emit("peer:nego:done", { to: from, ans });
|
socket.emit("peer:nego:done", { to: from, ans });
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
|
|
||||||
const handleNegoNeeded = useCallback(async () => {
|
const handleNegoNeeded = useCallback(async () => {
|
||||||
const offer = await peer.getOffer();
|
const offer = await peer.getOffer();
|
||||||
socket.emit("peer:nego:needed", { offer, to: remoteSocketId });
|
socket.emit("peer:nego:needed", { offer, to: remoteSocketId });
|
||||||
@@ -66,23 +83,23 @@ const RoomPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
peer.peer.addEventListener('negotiationneeded', handleNegoNeeded);
|
peer.peer.addEventListener('negotiationneeded', handleNegoNeeded);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
peer.peer.removeEventListener('negotiationneeded', handleNegoNeeded);
|
peer.peer.removeEventListener('negotiationneeded', handleNegoNeeded);
|
||||||
}
|
}
|
||||||
}, [handleNegoNeeded]);
|
}, [handleNegoNeeded]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
peer.peer.addEventListener('track', async ev => {
|
peer.peer.addEventListener('track', async ev => {
|
||||||
const remoteStream = ev.streams;
|
const remoteStream = ev.streams;
|
||||||
console.log("GOT TRACKS!");
|
console.log("GOT TRACKS!");
|
||||||
setRemoteStream(remoteStream[0]);
|
setRemoteStream(remoteStream[0]);
|
||||||
|
setIsConnecting(false);
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on("user:joined", handleUserJoined);
|
socket.on("user:joined", handleUserJoined);
|
||||||
|
socket.on("user:left", handleUserLeft);
|
||||||
socket.on("incoming:call", handleIncomingCall);
|
socket.on("incoming:call", handleIncomingCall);
|
||||||
socket.on("call:accepted", handleCallAccepted);
|
socket.on("call:accepted", handleCallAccepted);
|
||||||
socket.on("peer:nego:needed", handleNegoNeededIncoming);
|
socket.on("peer:nego:needed", handleNegoNeededIncoming);
|
||||||
@@ -90,21 +107,21 @@ const RoomPage = () => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("user:joined", handleUserJoined);
|
socket.off("user:joined", handleUserJoined);
|
||||||
|
socket.off("user:left", handleUserLeft);
|
||||||
socket.off("incoming:call", handleIncomingCall);
|
socket.off("incoming:call", handleIncomingCall);
|
||||||
socket.off("call:accepted", handleCallAccepted);
|
socket.off("call:accepted", handleCallAccepted);
|
||||||
socket.off("peer:nego:needed", handleNegoNeededIncoming);
|
socket.off("peer:nego:needed", handleNegoNeededIncoming);
|
||||||
socket.off("peer:nego:final", handleNegoFinal);
|
socket.off("peer:nego:final", handleNegoFinal);
|
||||||
};
|
};
|
||||||
},
|
}, [
|
||||||
[
|
socket,
|
||||||
socket,
|
handleUserJoined,
|
||||||
handleUserJoined,
|
handleUserLeft,
|
||||||
handleIncomingCall,
|
handleIncomingCall,
|
||||||
handleCallAccepted,
|
handleCallAccepted,
|
||||||
handleNegoNeededIncoming,
|
handleNegoNeededIncoming,
|
||||||
handleNegoFinal
|
handleNegoFinal
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on("call:end", ({ from }) => {
|
socket.on("call:end", ({ from }) => {
|
||||||
@@ -118,6 +135,9 @@ const RoomPage = () => {
|
|||||||
|
|
||||||
setRemoteStream(null);
|
setRemoteStream(null);
|
||||||
setRemoteSocketId(null);
|
setRemoteSocketId(null);
|
||||||
|
setCallButton(true);
|
||||||
|
setIsSendButtonVisible(true);
|
||||||
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,7 +146,6 @@ const RoomPage = () => {
|
|||||||
}
|
}
|
||||||
}, [remoteSocketId, myStream, socket]);
|
}, [remoteSocketId, myStream, socket]);
|
||||||
|
|
||||||
//* for disappearing call button
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on("call:initiated", ({ from }) => {
|
socket.on("call:initiated", ({ from }) => {
|
||||||
if (from === remoteSocketId) {
|
if (from === remoteSocketId) {
|
||||||
@@ -139,46 +158,50 @@ const RoomPage = () => {
|
|||||||
}
|
}
|
||||||
}, [socket, remoteSocketId]);
|
}, [socket, remoteSocketId]);
|
||||||
|
|
||||||
|
|
||||||
const handleCallUser = useCallback(async () => {
|
const handleCallUser = useCallback(async () => {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
setIsConnecting(true);
|
||||||
audio: true,
|
|
||||||
video: true
|
try {
|
||||||
});
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: true
|
||||||
|
});
|
||||||
|
|
||||||
if (isAudioMute) {
|
if (isAudioMute) {
|
||||||
const audioTracks = stream.getAudioTracks();
|
const audioTracks = stream.getAudioTracks();
|
||||||
audioTracks.forEach(track => track.enabled = false);
|
audioTracks.forEach(track => track.enabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideoOnHold) {
|
||||||
|
const videoTracks = stream.getVideoTracks();
|
||||||
|
videoTracks.forEach(track => track.enabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const offer = await peer.getOffer();
|
||||||
|
socket.emit("user:call", { to: remoteSocketId, offer })
|
||||||
|
setMyStream(stream);
|
||||||
|
setCallButton(false);
|
||||||
|
socket.emit("call:initiated", { to: remoteSocketId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting call:', error);
|
||||||
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
|
}, [remoteSocketId, socket, isAudioMute, isVideoOnHold]);
|
||||||
if (isVideoOnHold) {
|
|
||||||
const videoTracks = stream.getVideoTracks();
|
|
||||||
videoTracks.forEach(track => track.enabled = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
//! create offer
|
|
||||||
const offer = await peer.getOffer();
|
|
||||||
//* send offer to remote user
|
|
||||||
socket.emit("user:call", { to: remoteSocketId, offer })
|
|
||||||
// set my stream
|
|
||||||
setMyStream(stream);
|
|
||||||
|
|
||||||
//* hide the call button
|
|
||||||
setCallButton(false);
|
|
||||||
|
|
||||||
//* Inform the remote user to hide their "CALL" button
|
|
||||||
socket.emit("call:initiated", { to: remoteSocketId });
|
|
||||||
}, [remoteSocketId, socket, isAudioMute, isVideoOnHold, callButton]);
|
|
||||||
|
|
||||||
|
|
||||||
const handleToggleAudio = () => {
|
const handleToggleAudio = () => {
|
||||||
peer.toggleAudio();
|
if (myStream) {
|
||||||
setIsAudioMute(!isAudioMute);
|
const audioTracks = myStream.getAudioTracks();
|
||||||
|
audioTracks.forEach(track => track.enabled = !track.enabled);
|
||||||
|
setIsAudioMute(!isAudioMute);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleVideo = () => {
|
const handleToggleVideo = () => {
|
||||||
peer.toggleVideo();
|
if (myStream) {
|
||||||
setIsVideoOnHold(!isVideoOnHold);
|
const videoTracks = myStream.getVideoTracks();
|
||||||
|
videoTracks.forEach(track => track.enabled = !track.enabled);
|
||||||
|
setIsVideoOnHold(!isVideoOnHold);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEndCall = useCallback(() => {
|
const handleEndCall = useCallback(() => {
|
||||||
@@ -190,6 +213,9 @@ const RoomPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRemoteStream(null);
|
setRemoteStream(null);
|
||||||
|
setCallButton(true);
|
||||||
|
setIsSendButtonVisible(true);
|
||||||
|
setIsConnecting(false);
|
||||||
|
|
||||||
if (remoteSocketId) {
|
if (remoteSocketId) {
|
||||||
socket.emit("call:end", { to: remoteSocketId });
|
socket.emit("call:end", { to: remoteSocketId });
|
||||||
@@ -197,59 +223,168 @@ const RoomPage = () => {
|
|||||||
setRemoteSocketId(null);
|
setRemoteSocketId(null);
|
||||||
}, [myStream, remoteSocketId, socket]);
|
}, [myStream, remoteSocketId, socket]);
|
||||||
|
|
||||||
const router = useRouter();
|
const handleGoBack = () => {
|
||||||
|
handleEndCall();
|
||||||
const { slug } = router.query;
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center w-screen h-screen overflow-hidden'>
|
<div className='min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 relative overflow-hidden'>
|
||||||
<title>Room No. {slug}</title>
|
<title>Room {slug} - VideoPeersJS</title>
|
||||||
<h1 className='absolute top-0 left-0 text-5xl
|
|
||||||
text-center font-josefin tracking-tighter mt-5 ml-5 mmd:text-xl mxs:text-sm'>Video
|
{/* Background decorative elements */}
|
||||||
<VideoCallIcon sx={{ fontSize: 50, color: 'rgb(30,220,30)' }} />
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
Peers
|
<div className="absolute top-1/4 -left-4 w-72 h-72 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse"></div>
|
||||||
</h1>
|
<div className="absolute bottom-1/4 -right-4 w-72 h-72 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse delay-1000"></div>
|
||||||
<h4 className='font-bold text-xl md:text-2xl
|
|
||||||
mmd:text-sm mt-5 mb-4 msm:max-w-[100px] text-center'>
|
|
||||||
{remoteSocketId ? "Connected With Remote User!" : "No One In Room"}
|
|
||||||
</h4>
|
|
||||||
{(remoteStream && remoteSocketId && isSendButtonVisible) &&
|
|
||||||
<button className='bg-green-500 hover:bg-green-600' onClick={sendStreams}>
|
|
||||||
Send Stream
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
{(remoteSocketId && callButton) &&
|
|
||||||
(
|
|
||||||
<button className='text-xl bg-green-500 hover:bg-green-600 rounded-3xl'
|
|
||||||
onClick={handleCallUser}
|
|
||||||
style={{ display: !remoteStream ? 'block' : 'none' }}>
|
|
||||||
Call <CallIcon fontSize='medium' className=' animate-pulse scale-125' />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div className="flex flex-col w-full items-center justify-center overflow-hidden">
|
|
||||||
{
|
|
||||||
myStream &&
|
|
||||||
<VideoPlayer stream={myStream} name={"My Stream"} isAudioMute={isAudioMute} />
|
|
||||||
}
|
|
||||||
{
|
|
||||||
remoteStream &&
|
|
||||||
<VideoPlayer stream={remoteStream} name={"Remote Stream"} isAudioMute={isAudioMute} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
{myStream && remoteStream && !isSendButtonVisible &&
|
|
||||||
(
|
|
||||||
<CallHandleButtons
|
|
||||||
isAudioMute={isAudioMute}
|
|
||||||
isVideoOnHold={isVideoOnHold}
|
|
||||||
onToggleAudio={handleToggleAudio}
|
|
||||||
onToggleVideo={handleToggleVideo}
|
|
||||||
onEndCall={handleEndCall}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<motion.header
|
||||||
|
className="absolute top-0 left-0 right-0 z-20 p-6"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<motion.button
|
||||||
|
onClick={handleGoBack}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white hover:bg-white/20 transition-all duration-200"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
<span>Back</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Video className="h-6 w-6 text-indigo-400" />
|
||||||
|
<h1 className="text-xl font-bold text-white">
|
||||||
|
{process.env.NEXT_PUBLIC_APP_NAME || 'VideoPeersJS'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white">
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Room {slug}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.header>
|
||||||
|
|
||||||
|
{/* Connection Status */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-24 left-1/2 transform -translate-x-1/2 z-20"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className={`px-6 py-3 rounded-full backdrop-blur-sm text-white text-center ${
|
||||||
|
remoteSocketId ? 'bg-green-500/20 border border-green-400/30' : 'bg-orange-500/20 border border-orange-400/30'
|
||||||
|
}`}>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{isConnecting ? (
|
||||||
|
<span className="flex items-center space-x-2">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
|
||||||
|
<span>Connecting...</span>
|
||||||
|
</span>
|
||||||
|
) : remoteSocketId ? (
|
||||||
|
"🟢 Connected with remote user"
|
||||||
|
) : (
|
||||||
|
"🟡 Waiting for someone to join..."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Video Container */}
|
||||||
|
<div className="relative h-screen flex items-center justify-center p-6 pt-32">
|
||||||
|
{/* Remote Stream (Main) */}
|
||||||
|
{remoteStream ? (
|
||||||
|
<VideoPlayer
|
||||||
|
stream={remoteStream}
|
||||||
|
name={"Remote Stream"}
|
||||||
|
isAudioMute={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
className="w-full max-w-4xl aspect-video bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl border-2 border-white/10 flex items-center justify-center"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
<div className="text-center text-white/60">
|
||||||
|
<Users className="h-24 w-24 mx-auto mb-4 opacity-40" />
|
||||||
|
<p className="text-xl font-medium mb-2">Waiting for remote video...</p>
|
||||||
|
<p className="text-sm">Share this room ID with someone to start a call</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* My Stream (Picture-in-Picture) */}
|
||||||
|
{myStream && (
|
||||||
|
<VideoPlayer
|
||||||
|
stream={myStream}
|
||||||
|
name={"My Stream"}
|
||||||
|
isAudioMute={isAudioMute}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-30">
|
||||||
|
{remoteStream && isSendButtonVisible && (
|
||||||
|
<motion.button
|
||||||
|
onClick={sendStreams}
|
||||||
|
className="mb-4 flex items-center space-x-2 px-6 py-3 bg-green-500 hover:bg-green-600 text-white rounded-xl font-semibold transition-all duration-200"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
>
|
||||||
|
<Send className="h-5 w-5" />
|
||||||
|
<span>Send Stream</span>
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{remoteSocketId && callButton && !remoteStream && (
|
||||||
|
<motion.button
|
||||||
|
onClick={handleCallUser}
|
||||||
|
disabled={isConnecting}
|
||||||
|
className="flex items-center space-x-2 px-8 py-4 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white rounded-2xl font-semibold text-lg shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
||||||
|
whileHover={{ scale: isConnecting ? 1 : 1.05 }}
|
||||||
|
whileTap={{ scale: isConnecting ? 1 : 0.95 }}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
>
|
||||||
|
{isConnecting ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent"></div>
|
||||||
|
<span>Connecting...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Phone className="h-6 w-6" />
|
||||||
|
<span>Start Call</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Call Control Buttons */}
|
||||||
|
{myStream && remoteStream && !isSendButtonVisible && (
|
||||||
|
<CallHandleButtons
|
||||||
|
isAudioMute={isAudioMute}
|
||||||
|
isVideoOnHold={isVideoOnHold}
|
||||||
|
onToggleAudio={handleToggleAudio}
|
||||||
|
onToggleVideo={handleToggleVideo}
|
||||||
|
onEndCall={handleEndCall}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,41 @@
|
|||||||
class PeerService {
|
class PeerService {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!this.peer) {
|
if (typeof window !== 'undefined' && !this.peer) {
|
||||||
this.peer = new RTCPeerConnection({
|
// Get ICE servers from environment variables
|
||||||
iceServers: [{
|
const getIceServers = () => {
|
||||||
|
try {
|
||||||
|
const iceServersEnv = process.env.NEXT_PUBLIC_ICE_SERVERS;
|
||||||
|
if (iceServersEnv) {
|
||||||
|
const servers = JSON.parse(iceServersEnv);
|
||||||
|
return [{ urls: servers }];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error parsing ICE servers from environment:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default ICE servers
|
||||||
|
return [{
|
||||||
urls: [
|
urls: [
|
||||||
"stun:stun.l.google.com:19302",
|
"stun:stun.l.google.com:19302",
|
||||||
"stun:global.stun.twilio.com:3478",
|
"stun:global.stun.twilio.com:3478",
|
||||||
|
"stun:stun1.l.google.com:19302",
|
||||||
|
"stun:stun2.l.google.com:19302"
|
||||||
]
|
]
|
||||||
}]
|
}];
|
||||||
})
|
};
|
||||||
|
|
||||||
|
this.peer = new RTCPeerConnection({
|
||||||
|
iceServers: getIceServers()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add connection state monitoring
|
||||||
|
this.peer.onconnectionstatechange = () => {
|
||||||
|
console.log('Connection state:', this.peer.connectionState);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.peer.oniceconnectionstatechange = () => {
|
||||||
|
console.log('ICE connection state:', this.peer.iceConnectionState);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,13 +63,58 @@ class PeerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleAudio = () => {
|
toggleAudio = () => {
|
||||||
const audioTracks = this.peer.getSenders().find(sender => sender.track.kind === 'audio').track;
|
try {
|
||||||
audioTracks.enabled = !audioTracks.enabled;
|
const senders = this.peer.getSenders();
|
||||||
|
const audioSender = senders.find(sender =>
|
||||||
|
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 = () => {
|
toggleVideo = () => {
|
||||||
const videoTracks = this.peer.getSenders().find(sender => sender.track.kind === 'video').track;
|
try {
|
||||||
videoTracks.enabled = !videoTracks.enabled;
|
const senders = this.peer.getSenders();
|
||||||
|
const videoSender = senders.find(sender =>
|
||||||
|
sender.track && sender.track.kind === 'video'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (videoSender && videoSender.track) {
|
||||||
|
videoSender.track.enabled = !videoSender.track.enabled;
|
||||||
|
return videoSender.track.enabled;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling video:', error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method to close and cleanup the peer connection
|
||||||
|
close = () => {
|
||||||
|
if (this.peer) {
|
||||||
|
this.peer.close();
|
||||||
|
this.peer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method to get connection statistics
|
||||||
|
getStats = async () => {
|
||||||
|
if (this.peer) {
|
||||||
|
try {
|
||||||
|
return await this.peer.getStats();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting connection stats:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 Local Fonts Configuration */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@500&display=swap');
|
@import './fonts/index.css';
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Global Base Styles */
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
body{
|
body {
|
||||||
overflow: hidden;
|
font-family: var(--font-primary);
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
font-feature-settings: 'kern' 1, 'liga' 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label{
|
/* Component Styles */
|
||||||
color: #333;
|
@layer components {
|
||||||
margin-bottom: 0.5rem;
|
/* Form Elements */
|
||||||
font-size: 1.125rem;
|
.form-label {
|
||||||
line-height: 1.75rem;
|
@apply flex items-center text-sm font-medium text-gray-700 mb-2;
|
||||||
font-weight: 500;
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
@apply w-full px-4 py-3 border border-gray-200 rounded-xl transition-all duration-200 bg-white/50 backdrop-blur-sm;
|
||||||
|
@apply focus:ring-2 focus:ring-indigo-500 focus:border-transparent;
|
||||||
|
@apply placeholder:text-gray-400 disabled:opacity-50 disabled:cursor-not-allowed;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:hover {
|
||||||
|
@apply border-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Variants */
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold py-3 px-6 rounded-xl;
|
||||||
|
@apply hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200;
|
||||||
|
@apply flex items-center justify-center space-x-2;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-white/20 hover:bg-white/30 text-white border border-white/20 font-medium py-2 px-4 rounded-xl;
|
||||||
|
@apply transition-all duration-200 backdrop-blur-sm;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply bg-red-500 hover:bg-red-600 text-white font-medium py-2 px-4 rounded-xl;
|
||||||
|
@apply transition-all duration-200 shadow-lg;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Call Control Buttons */
|
||||||
|
.call-button {
|
||||||
|
@apply w-14 h-14 rounded-full flex items-center justify-center transition-all duration-200;
|
||||||
|
@apply shadow-lg backdrop-blur-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-button-active {
|
||||||
|
@apply bg-white/20 hover:bg-white/30 text-white border border-white/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-button-muted {
|
||||||
|
@apply bg-red-500 hover:bg-red-600 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Effect */
|
||||||
|
.glass {
|
||||||
|
@apply bg-white/10 backdrop-blur-lg border border-white/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
@apply bg-white/80 backdrop-blur-lg rounded-2xl shadow-xl border border-white/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient Text */
|
||||||
|
.gradient-text {
|
||||||
|
@apply bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Indicators */
|
||||||
|
.status-connected {
|
||||||
|
@apply bg-green-500/20 border border-green-400/30 text-green-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-waiting {
|
||||||
|
@apply bg-orange-500/20 border border-orange-400/30 text-orange-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
@apply bg-red-500/20 border border-red-400/30 text-red-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
@layer utilities {
|
||||||
|
/* Typography */
|
||||||
|
.font-display {
|
||||||
|
font-family: 'Space Grotesk', 'Josefin Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-accent {
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: 'Poppins', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
input{
|
/* Animations */
|
||||||
margin-bottom: 1rem;
|
.animate-float {
|
||||||
padding: 0.5rem;
|
animation: float 6s ease-in-out infinite;
|
||||||
border-radius: 0.25rem;
|
}
|
||||||
outline: 2px solid transparent;
|
|
||||||
outline-offset: 2px;
|
.animate-glow {
|
||||||
border-width: 1px;
|
animation: glow 2s ease-in-out infinite alternate;
|
||||||
border-color: rgb(170, 170, 170);
|
}
|
||||||
&:focus{
|
|
||||||
outline: 2px solid transparent;
|
.animate-pulse-slow {
|
||||||
outline-offset: 2px;
|
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
--tw-border-opacity: 1;
|
}
|
||||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
|
||||||
|
/* Custom animations */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
}
|
}
|
||||||
|
50% {
|
||||||
}
|
transform: translateY(-10px);
|
||||||
|
|
||||||
button{
|
|
||||||
color: white;
|
|
||||||
padding: 0.5rem 1rem 0.5rem 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
transition: background-color 250ms ease-in-out;
|
|
||||||
font-family: "Poppins", sans-serif;
|
|
||||||
|
|
||||||
&:focus{
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 20px rgba(99, 102, 241, 0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 30px rgba(99, 102, 241, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video specific styles */
|
||||||
|
.video-container {
|
||||||
|
@apply relative overflow-hidden rounded-2xl shadow-2xl border-2 border-white/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-overlay {
|
||||||
|
@apply absolute inset-0 bg-gradient-to-br from-indigo-500/20 to-purple-500/20;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.callButtons{
|
/* Legacy styles for backward compatibility */
|
||||||
font-weight: 500;
|
label {
|
||||||
border-radius: 9999px;
|
@apply form-label;
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
padding: 0.625rem;
|
|
||||||
text-align: center;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
video{
|
input[type="email"],
|
||||||
width: 100%;
|
input[type="text"],
|
||||||
height: 100%;
|
input[type="number"] {
|
||||||
object-fit: cover;
|
@apply form-input;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(.call-button):not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
|
||||||
|
@apply btn-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callButtons {
|
||||||
|
@apply call-button call-button-active;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
@apply w-full h-full object-cover;
|
||||||
}
|
}
|
||||||
@@ -7,23 +7,46 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
// Primary font stack with local fonts
|
||||||
|
'sans': ['var(--font-primary)', 'Inter', 'system-ui', 'sans-serif'],
|
||||||
|
'body': ['var(--font-primary)', 'Inter', 'system-ui', 'sans-serif'],
|
||||||
|
'accent': ['var(--font-accent)', 'Poppins', 'system-ui', 'sans-serif'],
|
||||||
|
'display': ['var(--font-display)', 'Space Grotesk', 'Josefin Sans', 'system-ui', 'sans-serif'],
|
||||||
|
'mono': ['var(--font-mono)', 'monospace'],
|
||||||
|
|
||||||
|
// Legacy support
|
||||||
|
'poppins': ['Poppins', 'system-ui', 'sans-serif'],
|
||||||
|
'josefin': ['Josefin Sans', 'system-ui', 'sans-serif'],
|
||||||
|
'inter': ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
'space-grotesk': ['Space Grotesk', 'system-ui', 'sans-serif']
|
||||||
|
},
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
'gradient-conic':
|
'gradient-conic':
|
||||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||||
},
|
},
|
||||||
fontFamily: {
|
|
||||||
'poppins': ['Poppins', 'sans-serif'],
|
|
||||||
'josefin': ['Josefin Sans', 'sans-serif']
|
|
||||||
},
|
|
||||||
keyframes: {
|
keyframes: {
|
||||||
pulse: {
|
pulse: {
|
||||||
'0%, 100%': { opacity: '1' },
|
'0%, 100%': { opacity: '1' },
|
||||||
'50%': { opacity: '0.6' },
|
'50%': { opacity: '0.6' },
|
||||||
|
},
|
||||||
|
'fade-in': {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
'scale-in': {
|
||||||
|
'0%': { transform: 'scale(0.8)', opacity: '0' },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: '1' },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'pulse': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
'pulse': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
'fade-in': 'fade-in 0.5s ease-out',
|
||||||
|
'scale-in': 'scale-in 0.3s ease-out',
|
||||||
|
},
|
||||||
|
backdropBlur: {
|
||||||
|
xs: '2px',
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
mxxl: { 'max': '1535px' },
|
mxxl: { 'max': '1535px' },
|
||||||
|
|||||||
@@ -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 cors = require('cors');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const Joi = require('joi');
|
||||||
|
const validator = require('validator');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = new Server(server, {
|
|
||||||
cors: true
|
// Security middlewares
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "wss:", "ws:"],
|
||||||
|
fontSrc: ["'self'"],
|
||||||
|
objectSrc: ["'none'"],
|
||||||
|
mediaSrc: ["'self'"],
|
||||||
|
frameSrc: ["'none'"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
crossOriginEmbedderPolicy: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutos
|
||||||
|
max: 100, // Límite de 100 requests por ventana de tiempo por IP
|
||||||
|
message: 'Too many requests from this IP, please try again later.',
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(cors());
|
app.use(limiter);
|
||||||
|
|
||||||
|
// CORS configuration
|
||||||
|
const corsOptions = {
|
||||||
|
origin: process.env.CLIENT_URL || ["http://localhost:3000", "https://localhost:3000"],
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
credentials: true
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
|
||||||
|
const io = new Server(server, {
|
||||||
|
cors: corsOptions,
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
allowEIO3: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation schemas
|
||||||
|
const roomJoinSchema = Joi.object({
|
||||||
|
email: Joi.string().email().required(),
|
||||||
|
room: Joi.string().alphanum().min(3).max(50).required()
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSchema = Joi.object({
|
||||||
|
to: Joi.string().required(),
|
||||||
|
offer: Joi.object().required()
|
||||||
|
});
|
||||||
|
|
||||||
|
const callAcceptedSchema = Joi.object({
|
||||||
|
to: Joi.string().required(),
|
||||||
|
ans: Joi.object().required()
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
res.send("Hello World");
|
res.json({
|
||||||
|
message: `${process.env.APP_NAME || 'VideoPeersJS'} Server`,
|
||||||
|
status: "running",
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/health", (req, res) => {
|
||||||
|
res.status(200).json({ status: "healthy" });
|
||||||
});
|
});
|
||||||
|
|
||||||
const emailToSocket = new Map();
|
const emailToSocket = new Map();
|
||||||
const socketToEmail = new Map();
|
const socketToEmail = new Map();
|
||||||
|
const rooms = new Map();
|
||||||
|
|
||||||
|
// Helper function to sanitize input
|
||||||
|
function sanitizeInput(input) {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
return validator.escape(input.trim());
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to validate room access
|
||||||
|
function validateRoomAccess(socket, room) {
|
||||||
|
const userEmail = socketToEmail.get(socket.id);
|
||||||
|
if (!userEmail) return false;
|
||||||
|
|
||||||
|
const roomData = rooms.get(room);
|
||||||
|
return roomData && roomData.participants.has(userEmail);
|
||||||
|
}
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
io.on("connection", (socket) => {
|
||||||
console.log(`Socket Connected: ${socket.id}`);
|
console.log(`Socket Connected: ${socket.id} at ${new Date().toISOString()}`);
|
||||||
|
|
||||||
|
// Set a timeout for socket connection
|
||||||
|
const connectionTimeout = setTimeout(() => {
|
||||||
|
socket.disconnect(true);
|
||||||
|
}, 30000); // 30 segundos
|
||||||
|
|
||||||
socket.on("room:join", data => {
|
socket.on("room:join", (data) => {
|
||||||
const { email, room } = data;
|
try {
|
||||||
emailToSocket.set(email, socket.id);
|
// Validate input
|
||||||
socketToEmail.set(socket.id, email);
|
const { error, value } = roomJoinSchema.validate(data);
|
||||||
|
if (error) {
|
||||||
|
socket.emit("error", { message: "Invalid room join data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket.join(room);
|
const { email, room } = value;
|
||||||
io.to(room).emit("user:joined", { email, id: socket.id });
|
|
||||||
|
// Sanitize inputs
|
||||||
|
const sanitizedEmail = sanitizeInput(email);
|
||||||
|
const sanitizedRoom = sanitizeInput(room);
|
||||||
|
|
||||||
// emits a 'room:joined' event back to the client
|
// Additional email validation
|
||||||
// that just joined the room.
|
if (!validator.isEmail(sanitizedEmail)) {
|
||||||
io.to(socket.id).emit("room:join", data);
|
socket.emit("error", { message: "Invalid email format" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear connection timeout
|
||||||
|
clearTimeout(connectionTimeout);
|
||||||
|
|
||||||
|
// Remove old socket mapping if exists
|
||||||
|
const oldSocketId = emailToSocket.get(sanitizedEmail);
|
||||||
|
if (oldSocketId && oldSocketId !== socket.id) {
|
||||||
|
socketToEmail.delete(oldSocketId);
|
||||||
|
io.sockets.sockets.get(oldSocketId)?.leave(sanitizedRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
emailToSocket.set(sanitizedEmail, socket.id);
|
||||||
|
socketToEmail.set(socket.id, sanitizedEmail);
|
||||||
|
|
||||||
|
// Manage room data
|
||||||
|
if (!rooms.has(sanitizedRoom)) {
|
||||||
|
rooms.set(sanitizedRoom, {
|
||||||
|
id: uuidv4(),
|
||||||
|
participants: new Set(),
|
||||||
|
createdAt: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomData = rooms.get(sanitizedRoom);
|
||||||
|
roomData.participants.add(sanitizedEmail);
|
||||||
|
|
||||||
|
socket.join(sanitizedRoom);
|
||||||
|
|
||||||
|
// Notify other users in the room
|
||||||
|
socket.to(sanitizedRoom).emit("user:joined", {
|
||||||
|
email: sanitizedEmail,
|
||||||
|
id: socket.id,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm room join to the user
|
||||||
|
socket.emit("room:join", {
|
||||||
|
email: sanitizedEmail,
|
||||||
|
room: sanitizedRoom,
|
||||||
|
participants: Array.from(roomData.participants),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in room:join:", error);
|
||||||
|
socket.emit("error", { message: "Internal server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("user:call", ({ to, offer }) => {
|
socket.on("user:call", (data) => {
|
||||||
io.to(to).emit("incoming:call", { from: socket.id, offer });
|
try {
|
||||||
|
const { error, value } = callSchema.validate(data);
|
||||||
|
if (error) {
|
||||||
|
socket.emit("error", { message: "Invalid call data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { to, offer } = value;
|
||||||
|
|
||||||
|
// Verify that the target socket exists
|
||||||
|
const targetSocket = io.sockets.sockets.get(to);
|
||||||
|
if (!targetSocket) {
|
||||||
|
socket.emit("error", { message: "Target user not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(to).emit("incoming:call", {
|
||||||
|
from: socket.id,
|
||||||
|
offer,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in user:call:", error);
|
||||||
|
socket.emit("error", { message: "Internal server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("call:accepted", ({ to, ans }) => {
|
socket.on("call:accepted", (data) => {
|
||||||
io.to(to).emit("call:accepted", { from: socket.id, ans });
|
try {
|
||||||
|
const { error, value } = callAcceptedSchema.validate(data);
|
||||||
|
if (error) {
|
||||||
|
socket.emit("error", { message: "Invalid call acceptance data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { to, ans } = value;
|
||||||
|
|
||||||
|
const targetSocket = io.sockets.sockets.get(to);
|
||||||
|
if (!targetSocket) {
|
||||||
|
socket.emit("error", { message: "Target user not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(to).emit("call:accepted", {
|
||||||
|
from: socket.id,
|
||||||
|
ans,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in call:accepted:", error);
|
||||||
|
socket.emit("error", { message: "Internal server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("peer:nego:needed", ({ to, offer }) => {
|
socket.on("peer:nego:needed", (data) => {
|
||||||
io.to(to).emit("peer:nego:needed", { from: socket.id, offer });
|
try {
|
||||||
|
const { to, offer } = data;
|
||||||
|
if (!to || !offer) {
|
||||||
|
socket.emit("error", { message: "Invalid negotiation data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetSocket = io.sockets.sockets.get(to);
|
||||||
|
if (!targetSocket) {
|
||||||
|
socket.emit("error", { message: "Target user not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(to).emit("peer:nego:needed", {
|
||||||
|
from: socket.id,
|
||||||
|
offer,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in peer:nego:needed:", error);
|
||||||
|
socket.emit("error", { message: "Internal server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("peer:nego:done", ({ to, ans }) => {
|
socket.on("peer:nego:done", (data) => {
|
||||||
io.to(to).emit("peer:nego:final", { from: socket.id, ans });
|
try {
|
||||||
|
const { to, ans } = data;
|
||||||
|
if (!to || !ans) {
|
||||||
|
socket.emit("error", { message: "Invalid negotiation data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetSocket = io.sockets.sockets.get(to);
|
||||||
|
if (!targetSocket) {
|
||||||
|
socket.emit("error", { message: "Target user not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(to).emit("peer:nego:final", {
|
||||||
|
from: socket.id,
|
||||||
|
ans,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in peer:nego:done:", error);
|
||||||
|
socket.emit("error", { message: "Internal server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("call:end", ({ to }) => {
|
socket.on("call:end", (data) => {
|
||||||
io.to(to).emit("call:end", { from: socket.id });
|
try {
|
||||||
|
const { to } = data;
|
||||||
|
if (!to) {
|
||||||
|
socket.emit("error", { message: "Invalid call end data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetSocket = io.sockets.sockets.get(to);
|
||||||
|
if (targetSocket) {
|
||||||
|
io.to(to).emit("call:end", {
|
||||||
|
from: socket.id,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in call:end:", error);
|
||||||
|
socket.emit("error", { message: "Internal server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("call:initiated", ({ to }) => {
|
socket.on("call:initiated", (data) => {
|
||||||
io.to(to).emit("call:initiated", { from: socket.id });
|
try {
|
||||||
});
|
const { to } = data;
|
||||||
})
|
if (!to) {
|
||||||
|
socket.emit("error", { message: "Invalid call initiation data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
server.listen(process.env.PORT, () => console.log(`Server has started.`));
|
const targetSocket = io.sockets.sockets.get(to);
|
||||||
|
if (targetSocket) {
|
||||||
|
io.to(to).emit("call:initiated", {
|
||||||
|
from: socket.id,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in call:initiated:", error);
|
||||||
|
socket.emit("error", { message: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", (reason) => {
|
||||||
|
try {
|
||||||
|
console.log(`Socket Disconnected: ${socket.id}, Reason: ${reason}`);
|
||||||
|
|
||||||
|
const email = socketToEmail.get(socket.id);
|
||||||
|
if (email) {
|
||||||
|
emailToSocket.delete(email);
|
||||||
|
socketToEmail.delete(socket.id);
|
||||||
|
|
||||||
|
// Remove from all rooms
|
||||||
|
rooms.forEach((roomData, roomName) => {
|
||||||
|
if (roomData.participants.has(email)) {
|
||||||
|
roomData.participants.delete(email);
|
||||||
|
|
||||||
|
// If room is empty, delete it
|
||||||
|
if (roomData.participants.size === 0) {
|
||||||
|
rooms.delete(roomName);
|
||||||
|
} else {
|
||||||
|
// Notify other participants
|
||||||
|
socket.to(roomName).emit("user:left", {
|
||||||
|
email,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(connectionTimeout);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in disconnect:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error('Error:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'Internal server error',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 8000;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🕒 Started at ${new Date().toISOString()}`);
|
||||||
|
});
|
||||||
2257
server/package-lock.json
generado
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",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "VideoPeersJS - Real-time P2P video chat server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
@@ -12,12 +12,17 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.19.2",
|
||||||
"socket.io": "^4.7.2"
|
"express-rate-limit": "^7.4.0",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"joi": "^17.13.3",
|
||||||
|
"socket.io": "^4.7.5",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"validator": "^13.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 2,
|
|
||||||
"builds": [{
|
|
||||||
"src": "*.js",
|
|
||||||
"use": "@vercel/node"
|
|
||||||
}],
|
|
||||||
"routes": [{
|
|
||||||
"src": "/(.*)",
|
|
||||||
"dest": "/"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
Referencia en una nueva incidencia
Block a user