Compare commits
No commits in common. "64924f99999a31e1c290f0a010ae6d6ce838f31b" and "63dfc43a46afa46645e135420bdd5d623ccc3f30" have entirely different histories.
64924f9999
...
63dfc43a46
@ -1,49 +0,0 @@
|
|||||||
# CONFIGURACIÓN PARA EASYPANEL + LIVEKIT SELF-HOSTED
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# CONFIGURACIÓN BÁSICA
|
|
||||||
# ===========================================
|
|
||||||
NODE_ENV=production
|
|
||||||
MEET_LOG_LEVEL=info
|
|
||||||
MEET_BLOB_STORAGE_MODE=memory
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# USUARIO ADMINISTRADOR
|
|
||||||
# ===========================================
|
|
||||||
ADMIN_USER=admin
|
|
||||||
ADMIN_PASSWORD=admin123
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# LIVEKIT SELF-HOSTED (RED LOCAL + PÚBLICO)
|
|
||||||
# ===========================================
|
|
||||||
# ✅ LiveKit corriendo en servidor dedicado
|
|
||||||
# ✅ Puertos UDP expuestos en router
|
|
||||||
# ✅ SSL/WSS configurado
|
|
||||||
|
|
||||||
# URL de tu LiveKit self-hosted (cambiar por tu dominio/IP)
|
|
||||||
LIVEKIT_URL=wss://mi-livekit.duckdns.org
|
|
||||||
# O con IP fija: wss://TU_IP_PUBLICA:443
|
|
||||||
|
|
||||||
LIVEKIT_API_KEY=production-key
|
|
||||||
LIVEKIT_API_SECRET=tu-super-secret-de-32-caracteres-o-mas
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# REDIS (OPCIONAL para EasyPanel)
|
|
||||||
# ===========================================
|
|
||||||
# Si quieres Redis también en EasyPanel
|
|
||||||
# REDIS_HOST=redis
|
|
||||||
# REDIS_PORT=6379
|
|
||||||
# REDIS_PASSWORD=admin-redis
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# ARQUITECTURA FINAL:
|
|
||||||
# ===========================================
|
|
||||||
# Internet → Router (Port Forward) → LiveKit Server (192.168.1.19)
|
|
||||||
# ↘ EasyPanel → OpenVidu Meet Backend
|
|
||||||
#
|
|
||||||
# Ventajas:
|
|
||||||
# ✅ Control total sobre LiveKit
|
|
||||||
# ✅ Sin costos mensuales externos
|
|
||||||
# ✅ Rendimiento en red local
|
|
||||||
# ✅ Acceso desde internet
|
|
||||||
# ✅ Escalable según hardware
|
|
||||||
397
.github/workflows/.copilot-instructions.md
vendored
397
.github/workflows/.copilot-instructions.md
vendored
@ -1,397 +0,0 @@
|
|||||||
# OpenVidu Meet - Copilot Instructions
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
**OpenVidu Meet** is a production-ready videoconferencing application built on top of OpenVidu and LiveKit. It provides a complete, customizable solution for video conferencing with both Community Edition (CE) and Pro versions.
|
|
||||||
|
|
||||||
### Key Technologies
|
|
||||||
- **Frontend**: Angular 20.x, TypeScript, Angular Material
|
|
||||||
- **Backend**: Node.js, Express framework
|
|
||||||
- **Package Manager**: pnpm (v10.18.3+) with workspaces
|
|
||||||
- **Build System**: Nx-like monorepo structure
|
|
||||||
- **Testing**: Playwright (E2E), Jest (Unit)
|
|
||||||
- **Containerization**: Docker multi-stage builds
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
openvidu-meet/
|
|
||||||
├── meet-ce/ # Community Edition
|
|
||||||
│ ├── frontend/ # Angular application
|
|
||||||
│ │ ├── src/ # Main app source
|
|
||||||
│ │ ├── webcomponent/ # Web Component build
|
|
||||||
│ │ └── projects/
|
|
||||||
│ │ └── shared-meet-components/ # Shared Angular library
|
|
||||||
│ ├── backend/ # Express backend
|
|
||||||
│ └── docker/ # Docker configurations
|
|
||||||
├── meet-pro/ # Pro Edition (extends CE)
|
|
||||||
├── testapp/ # Testing application for E2E tests
|
|
||||||
├── meet-demo/ # Demo application for showcasing features
|
|
||||||
├── scripts/ # Build and automation scripts
|
|
||||||
└── meet.sh # Main CLI tool
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Principles
|
|
||||||
|
|
||||||
### 1. **Monorepo with External Dependencies**
|
|
||||||
- Uses pnpm workspaces for internal packages
|
|
||||||
- **External dependency**: `openvidu-components-angular` (located outside this repo)
|
|
||||||
- **Development**: Uses `workspace:*` protocol for local linking
|
|
||||||
- **CI/Docker**: Uses npm registry or tarball installations
|
|
||||||
|
|
||||||
### 2. **Dual Workspace Configuration**
|
|
||||||
```yaml
|
|
||||||
# Development (pnpm-workspace.yaml)
|
|
||||||
packages:
|
|
||||||
- 'meet-ce/**'
|
|
||||||
- '../openvidu/openvidu-components-angular/projects/openvidu-components-angular'
|
|
||||||
|
|
||||||
# CI/Docker (pnpm-workspace.docker.yaml)
|
|
||||||
packages:
|
|
||||||
- 'meet-ce/**' # No external packages
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Library Structure**
|
|
||||||
- `@openvidu-meet/frontend`: Main Angular application
|
|
||||||
- `@openvidu-meet/shared-components`: Reusable Angular library
|
|
||||||
- Has `openvidu-components-angular` as **peerDependency**
|
|
||||||
- Built as Angular library (ng-packagr)
|
|
||||||
- Does NOT bundle dependencies
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coding Standards
|
|
||||||
|
|
||||||
### TypeScript/Angular
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Use interfaces for data models
|
|
||||||
export interface Conference {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
participants: Participant[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Good: Use Angular dependency injection
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class ConferenceService {
|
|
||||||
constructor(private http: HttpClient) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Good: Use RxJS operators properly
|
|
||||||
this.participants$.pipe(
|
|
||||||
map(participants => participants.filter(p => p.isActive)),
|
|
||||||
takeUntil(this.destroy$)
|
|
||||||
).subscribe();
|
|
||||||
|
|
||||||
// ❌ Bad: Don't use 'any' type
|
|
||||||
// ❌ Bad: Don't forget to unsubscribe from observables
|
|
||||||
```
|
|
||||||
|
|
||||||
### Express Backend with TypeScript and InversifyJS
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Use decorators and dependency injection
|
|
||||||
@injectable()
|
|
||||||
export class LoggerService {
|
|
||||||
log(message: string): void {
|
|
||||||
console.log(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build & Development
|
|
||||||
|
|
||||||
### Main CLI Tool: `meet.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
./meet.sh dev # Start development servers
|
|
||||||
./meet.sh dev --skip-install # Skip dependency installation
|
|
||||||
|
|
||||||
# Building
|
|
||||||
./meet.sh build # Build all packages
|
|
||||||
./meet.sh build --skip-install # Build without installing deps
|
|
||||||
./meet.sh build --base-href=/custom/ # Custom base href
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
./meet.sh build-docker ov-meet # Build Docker image (CE)
|
|
||||||
./meet.sh build-docker ov-meet --components-angular-version 3.5.0-beta1
|
|
||||||
|
|
||||||
# CI Preparation (Important!)
|
|
||||||
./meet.sh prepare-ci-build --components-angular-version 3.5.0-beta1
|
|
||||||
./meet.sh prepare-ci-build --components-angular-tarball ./components.tgz
|
|
||||||
./meet.sh restore-dev-config # Restore development config
|
|
||||||
```
|
|
||||||
|
|
||||||
### CI/Docker Dependency Strategy
|
|
||||||
|
|
||||||
**Problem**: External package `openvidu-components-angular` is not in this repo.
|
|
||||||
|
|
||||||
**Solution**: Dual workspace configuration
|
|
||||||
1. **Development**: Uses `workspace:*` (local linking)
|
|
||||||
2. **CI/Docker**: Replaces `workspace:*` with registry/tarball versions
|
|
||||||
|
|
||||||
**Scripts**:
|
|
||||||
- `scripts/prepare-ci-build.sh`: Prepares workspace for CI/Docker builds
|
|
||||||
- `scripts/restore-dev-config.sh`: Restores development configuration
|
|
||||||
|
|
||||||
**Workflow**:
|
|
||||||
```bash
|
|
||||||
# Before CI/Docker build
|
|
||||||
prepare-ci-build.sh --components-angular-version 3.5.0-beta1
|
|
||||||
# Changes: pnpm-workspace.yaml → pnpm-workspace.docker.yaml
|
|
||||||
# .npmrc → .npmrc.docker
|
|
||||||
# package.json: workspace:* → ^3.5.0-beta1
|
|
||||||
|
|
||||||
# After build
|
|
||||||
restore-dev-config.sh
|
|
||||||
# Restores all original files
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Important Conventions
|
|
||||||
|
|
||||||
### 1. **Never Manually Edit package.json in CI**
|
|
||||||
```bash
|
|
||||||
# ❌ Bad: Manual sed in CI
|
|
||||||
sed -i 's/workspace:\*/3.5.0/g' package.json
|
|
||||||
|
|
||||||
# ✅ Good: Use prepare-ci-build script
|
|
||||||
./scripts/prepare-ci-build.sh --components-angular-version 3.5.0-beta1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **peerDependencies Guidelines**
|
|
||||||
- `shared-meet-components` declares `openvidu-components-angular` as **peerDependency**
|
|
||||||
- ✅ Use semver ranges: `"^3.0.0"`
|
|
||||||
- ✅ Use workspace protocol in dev: `"workspace:*"`
|
|
||||||
- ❌ NEVER use `file:` in peerDependencies (invalid)
|
|
||||||
|
|
||||||
### 3. **Angular Version Compatibility**
|
|
||||||
- Current: Angular 20.x
|
|
||||||
- `openvidu-components-angular` must support same Angular version
|
|
||||||
- Check peer dependency warnings carefully
|
|
||||||
|
|
||||||
### 4. **Docker Best Practices**
|
|
||||||
```dockerfile
|
|
||||||
# ✅ Good: Multi-stage build
|
|
||||||
FROM node:22.19.0 AS builder
|
|
||||||
# ... build stage ...
|
|
||||||
FROM node:22.19.0-alpine AS runner
|
|
||||||
# ... runtime stage ...
|
|
||||||
|
|
||||||
# ✅ Good: Use build args
|
|
||||||
ARG COMPONENTS_VERSION=3.5.0-beta1
|
|
||||||
ARG BASE_HREF=/
|
|
||||||
|
|
||||||
# ✅ Good: Use .dockerignore
|
|
||||||
# Avoid copying unnecessary files
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
### Adding a New Feature
|
|
||||||
1. Create feature in `meet-ce/frontend/src/app/features/`
|
|
||||||
2. If reusable, consider moving to `shared-meet-components`
|
|
||||||
3. Add tests in `*.spec.ts` files
|
|
||||||
4. Update documentation
|
|
||||||
|
|
||||||
### Updating openvidu-components-angular
|
|
||||||
```bash
|
|
||||||
# Development (local changes)
|
|
||||||
cd ../openvidu/openvidu-components-angular
|
|
||||||
npm run lib:build
|
|
||||||
cd -
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# CI/Docker (version update)
|
|
||||||
./meet.sh prepare-ci-build --components-angular-version 3.5.0-beta1
|
|
||||||
./meet.sh build
|
|
||||||
./meet.sh restore-dev-config
|
|
||||||
```
|
|
||||||
|
|
||||||
### Working with Shared Components
|
|
||||||
```bash
|
|
||||||
# Build shared library
|
|
||||||
cd meet-ce/frontend/projects/shared-meet-components
|
|
||||||
ng build
|
|
||||||
|
|
||||||
# Use in main app
|
|
||||||
import { SomeComponent } from '@openvidu-meet/shared-components';
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### E2E Tests (Playwright)
|
|
||||||
```bash
|
|
||||||
npm run test:e2e # Run all E2E tests
|
|
||||||
npm run test:e2e:ui # Run with UI
|
|
||||||
npm run test:e2e:debug # Debug mode
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Tests (Jest)
|
|
||||||
```bash
|
|
||||||
npm run test # Run all unit tests
|
|
||||||
npm run test:watch # Watch mode
|
|
||||||
npm run test:coverage # With coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "workspace package not found" Error
|
|
||||||
**Cause**: Using `workspace:*` in CI/Docker mode
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
./meet.sh prepare-ci-build --components-angular-version 3.5.0-beta1
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Invalid peer dependency" Error
|
|
||||||
**Cause**: Using `file:` in peerDependencies
|
|
||||||
|
|
||||||
**Solution**: Use semver range (`^3.0.0`) in peerDependencies, not `file:` paths
|
|
||||||
|
|
||||||
### Angular Version Mismatch Warnings
|
|
||||||
**Cause**: `openvidu-components-angular` version doesn't support your Angular version
|
|
||||||
|
|
||||||
**Solution**: Update to compatible version
|
|
||||||
```bash
|
|
||||||
./meet.sh prepare-ci-build --components-angular-version 3.5.0-beta1 # Supports Angular 20
|
|
||||||
```
|
|
||||||
|
|
||||||
### pnpm-lock.yaml Conflicts
|
|
||||||
**Solution**: Restore development config first
|
|
||||||
```bash
|
|
||||||
./meet.sh restore-dev-config
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Naming Conventions
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ Good:
|
|
||||||
- conference.service.ts (Services)
|
|
||||||
- conference.component.ts (Components)
|
|
||||||
- conference.component.spec.ts (Tests)
|
|
||||||
- conference.model.ts (Models/Interfaces)
|
|
||||||
- conference.module.ts (Modules)
|
|
||||||
|
|
||||||
❌ Bad:
|
|
||||||
- ConferenceService.ts (PascalCase in filename)
|
|
||||||
- conference_service.ts (snake_case)
|
|
||||||
- conferenceService.ts (camelCase)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
# Frontend
|
|
||||||
OPENVIDU_SERVER_URL=ws://localhost:4443
|
|
||||||
OPENVIDU_SECRET=MY_SECRET
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
PORT=3000
|
|
||||||
OPENVIDU_URL=https://localhost:4443
|
|
||||||
OPENVIDU_SECRET=MY_SECRET
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
Set via `docker-compose.yml` or Dockerfile ARG/ENV
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Never commit secrets** to version control
|
|
||||||
2. Use environment variables for sensitive data
|
|
||||||
3. Validate all user inputs (DTOs in backend)
|
|
||||||
4. Sanitize HTML content in frontend
|
|
||||||
5. Use Angular's built-in XSS protection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Guidelines
|
|
||||||
|
|
||||||
1. **Lazy load modules** when possible
|
|
||||||
2. **Use OnPush change detection** for performance-critical components
|
|
||||||
3. **Unsubscribe from observables** (use `takeUntil` or async pipe)
|
|
||||||
4. **Optimize Docker images** (multi-stage builds, alpine images)
|
|
||||||
5. **Use pnpm** for faster installs and efficient disk usage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation References
|
|
||||||
|
|
||||||
- Full CI/Docker strategy: `docs/ci-docker-dependencies-strategy.md`
|
|
||||||
- Dependency diagrams: `docs/ci-docker-dependencies-diagrams.md`
|
|
||||||
- Implementation summary: `docs/IMPLEMENTATION_SUMMARY.md`
|
|
||||||
- Validation checklist: `docs/VALIDATION_CHECKLIST.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## When Suggesting Code Changes
|
|
||||||
|
|
||||||
1. **Understand the context**: Is this for development or CI/Docker?
|
|
||||||
2. **Check package.json**: Are dependencies using `workspace:*` or versions?
|
|
||||||
3. **Consider peerDependencies**: Never use `file:` paths in peerDependencies
|
|
||||||
4. **Follow conventions**: Use existing patterns in the codebase
|
|
||||||
5. **Test implications**: Consider impact on both local dev and CI/Docker builds
|
|
||||||
6. **Use prepare-ci-build**: For any CI/Docker related changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Reference Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
./meet.sh dev # Start dev servers
|
|
||||||
./meet.sh build # Build all
|
|
||||||
|
|
||||||
# CI/Docker
|
|
||||||
./meet.sh prepare-ci-build --components-angular-version 3.5.0-beta1
|
|
||||||
./meet.sh build-docker ov-meet
|
|
||||||
./meet.sh restore-dev-config
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
npm run test # Unit tests
|
|
||||||
npm run test:e2e # E2E tests
|
|
||||||
|
|
||||||
# Package management
|
|
||||||
pnpm install # Install deps
|
|
||||||
pnpm add <package> --filter @openvidu-meet/frontend # Add to specific package
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Remember
|
|
||||||
|
|
||||||
- **Always use scripts** for CI/Docker preparation, never manual edits
|
|
||||||
- **peerDependencies** are for library compatibility declarations, not installation
|
|
||||||
- **workspace:*** works only when package is in workspace
|
|
||||||
- **Test locally** before pushing CI/Docker changes
|
|
||||||
- **Document breaking changes** and update this file accordingly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2025-10-27
|
|
||||||
**Project Version**: OpenVidu Meet CE/Pro
|
|
||||||
**Maintained By**: OpenVidu Team
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -48,4 +48,3 @@ pnpm-debug.log*
|
|||||||
**/**/docs/webcomponent-events.md
|
**/**/docs/webcomponent-events.md
|
||||||
|
|
||||||
**/**/meet-pro
|
**/**/meet-pro
|
||||||
**/**/test_localstorage_state.json
|
|
||||||
|
|||||||
13
.npmrc
13
.npmrc
@ -1,18 +1,10 @@
|
|||||||
# Docker/CI specific npm configuration
|
|
||||||
# This configuration is used during Docker builds and CI workflows
|
|
||||||
# to prevent linking workspace packages and use published versions instead
|
|
||||||
|
|
||||||
# Disable workspace package linking
|
|
||||||
# This forces pnpm to install packages from registry or local tarballs
|
|
||||||
link-workspace-packages=false
|
|
||||||
|
|
||||||
# Strict peer dependencies
|
# Strict peer dependencies
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
|
|
||||||
# Auto install peers
|
# Auto install peers
|
||||||
auto-install-peers=true
|
auto-install-peers=true
|
||||||
|
|
||||||
# Shamefully hoist - necessary for some packages
|
# Shamefully hoist - neccessary for some packages
|
||||||
shamefully-hoist=true
|
shamefully-hoist=true
|
||||||
|
|
||||||
# Node linker - use hoisted for full compatibility
|
# Node linker - use hoisted for full compatibility
|
||||||
@ -20,3 +12,6 @@ node-linker=hoisted
|
|||||||
|
|
||||||
# Lockfile settings
|
# Lockfile settings
|
||||||
lockfile=true
|
lockfile=true
|
||||||
|
|
||||||
|
# Optional: Store location (uncomment if you want to customize)
|
||||||
|
# store-dir=.pnpm-store
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
# CLOUDFLARE TUNNEL - SIN PORT FORWARDING
|
|
||||||
|
|
||||||
## ☁️ Cloudflare Tunnel para LiveKit (Avanzado)
|
|
||||||
|
|
||||||
### Ventajas:
|
|
||||||
- ✅ **Sin port forwarding** en router
|
|
||||||
- ✅ **SSL automático**
|
|
||||||
- ✅ **Protección DDoS**
|
|
||||||
- ✅ **IP oculta**
|
|
||||||
|
|
||||||
### ⚠️ Limitaciones para WebRTC:
|
|
||||||
- ❌ **UDP no soportado** directamente
|
|
||||||
- ⚠️ **Requiere TURN server** para WebRTC
|
|
||||||
- 🔧 **Solo TCP/HTTP** a través del tunnel
|
|
||||||
|
|
||||||
### Configuración (solo si tienes TURN server):
|
|
||||||
|
|
||||||
#### Paso 1: Instalar cloudflared
|
|
||||||
```bash
|
|
||||||
# Descargar cloudflared
|
|
||||||
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
|
|
||||||
sudo dpkg -i cloudflared.deb
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 2: Crear tunnel
|
|
||||||
```bash
|
|
||||||
# Login a Cloudflare
|
|
||||||
cloudflared tunnel login
|
|
||||||
|
|
||||||
# Crear tunnel
|
|
||||||
cloudflared tunnel create livekit-tunnel
|
|
||||||
|
|
||||||
# Configurar tunnel
|
|
||||||
cat > ~/.cloudflared/config.yml << 'EOF'
|
|
||||||
tunnel: livekit-tunnel
|
|
||||||
credentials-file: /home/usuario/.cloudflared/livekit-tunnel.json
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
- hostname: livekit.midominio.com
|
|
||||||
service: http://localhost:7880
|
|
||||||
- service: http_status:404
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Crear DNS record
|
|
||||||
cloudflared tunnel route dns livekit-tunnel livekit.midominio.com
|
|
||||||
|
|
||||||
# Ejecutar tunnel
|
|
||||||
cloudflared tunnel run livekit-tunnel
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Configuración LiveKit (necesita TURN):
|
|
||||||
```yaml
|
|
||||||
# livekit-production.yaml
|
|
||||||
rtc:
|
|
||||||
# SIN puertos UDP directos - usar TURN
|
|
||||||
use_external_ip: false
|
|
||||||
|
|
||||||
ice_servers:
|
|
||||||
- urls: ["stun:stun.l.google.com:19302"]
|
|
||||||
- urls: ["turn:turn.midominio.com:3478"]
|
|
||||||
username: "usuario"
|
|
||||||
credential: "password"
|
|
||||||
```
|
|
||||||
|
|
||||||
### ⚠️ **NO RECOMENDADO** para LiveKit porque WebRTC necesita UDP
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
# CONFIGURACIÓN AVANZADA: ACCESO DESDE INTERNET
|
|
||||||
|
|
||||||
## 🌍 Para usuarios externos (más complejo y riesgoso)
|
|
||||||
|
|
||||||
### ⚠️ PROBLEMAS de exponer UDP públicamente:
|
|
||||||
|
|
||||||
1. **Seguridad**: 10,000 puertos UDP expuestos
|
|
||||||
2. **Complejidad**: Port forwarding masivo
|
|
||||||
3. **NAT Traversal**: Problemas con diferentes ISPs
|
|
||||||
4. **Mantenimiento**: Configuración de router compleja
|
|
||||||
|
|
||||||
### Si NECESITAS acceso externo, opciones:
|
|
||||||
|
|
||||||
#### Opción A: TURN Server (RECOMENDADO)
|
|
||||||
```yaml
|
|
||||||
# livekit-public.yaml
|
|
||||||
rtc:
|
|
||||||
use_external_ip: true
|
|
||||||
external_ip: "TU_IP_PUBLICA"
|
|
||||||
|
|
||||||
# Usar TURN para atravesar NAT
|
|
||||||
ice_servers:
|
|
||||||
- urls: ["stun:stun.l.google.com:19302"]
|
|
||||||
- urls: ["turn:tu-turn-server.com:3478"]
|
|
||||||
username: "usuario"
|
|
||||||
credential: "password"
|
|
||||||
|
|
||||||
# Rango reducido de puertos
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 50100 # Solo 100 puertos
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Opción B: LiveKit Cloud (MÁS RECOMENDADO)
|
|
||||||
```env
|
|
||||||
# .env.production
|
|
||||||
LIVEKIT_URL=wss://tu-proyecto.livekit.cloud
|
|
||||||
LIVEKIT_API_KEY=cloud-api-key
|
|
||||||
LIVEKIT_API_SECRET=cloud-secret
|
|
||||||
|
|
||||||
# ✅ SIN configuración UDP local
|
|
||||||
# ✅ Sin port forwarding
|
|
||||||
# ✅ Infraestructura optimizada
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Opción C: VPN (Para usuarios conocidos)
|
|
||||||
```bash
|
|
||||||
# Configurar WireGuard/OpenVPN
|
|
||||||
# Usuarios se conectan por VPN a tu red local
|
|
||||||
# Acceso como si fueran locales
|
|
||||||
|
|
||||||
# No requiere exponer UDP públicamente
|
|
||||||
# Acceso seguro y controlado
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port Forwarding (Solo si es absolutamente necesario):
|
|
||||||
```
|
|
||||||
⚠️ EN ROUTER:
|
|
||||||
UDP 50000-60000 → 192.168.1.19:50000-60000
|
|
||||||
|
|
||||||
❌ RIESGOS:
|
|
||||||
- 10,000 puertos UDP expuestos
|
|
||||||
- Posibles ataques DDoS
|
|
||||||
- Configuración compleja
|
|
||||||
- Problemas con CGN/CGNAT de ISPs
|
|
||||||
```
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
# CONFIGURACIÓN RECOMENDADA: SOLO RED LOCAL
|
|
||||||
|
|
||||||
## 🔒 Para uso en red local ÚNICAMENTE
|
|
||||||
|
|
||||||
### Firewall UFW (SOLO red local):
|
|
||||||
```bash
|
|
||||||
# Permitir desde red local solamente
|
|
||||||
sudo ufw allow from 192.168.1.0/24 to any port 50000:60000 proto udp
|
|
||||||
sudo ufw allow from 192.168.1.0/24 to any port 7880 proto tcp
|
|
||||||
sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp
|
|
||||||
|
|
||||||
# BLOQUEAR acceso externo a UDP
|
|
||||||
sudo ufw deny 50000:60000/udp
|
|
||||||
|
|
||||||
# Verificar que solo red local tenga acceso
|
|
||||||
sudo ufw status numbered
|
|
||||||
```
|
|
||||||
|
|
||||||
### Router/Modem (NO configurar port forwarding):
|
|
||||||
```
|
|
||||||
❌ NO hacer port forwarding de UDP 50000-60000
|
|
||||||
✅ Solo HTTP (puerto 80) si necesitas acceso web externo
|
|
||||||
✅ Usar VPN si necesitas acceso remoto
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ventajas:
|
|
||||||
- ✅ **Seguridad**: UDP no expuesto a internet
|
|
||||||
- ✅ **Simplicidad**: Sin configuración de router
|
|
||||||
- ✅ **Rendimiento**: Conexión directa en LAN
|
|
||||||
- ✅ **Sin NAT**: Sin problemas de traversal
|
|
||||||
|
|
||||||
### Casos de uso:
|
|
||||||
- Oficina local
|
|
||||||
- Casa/familia
|
|
||||||
- Reuniones internas
|
|
||||||
- Desarrollo y testing
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
# CONFIGURACIÓN DOMINIO PROPIO PARA LIVEKIT
|
|
||||||
|
|
||||||
## 🏠 Dominio propio (ej: livekit.midominio.com)
|
|
||||||
|
|
||||||
### Opción A: Subdominio de tu dominio existente
|
|
||||||
|
|
||||||
#### Paso 1: Configurar DNS
|
|
||||||
```
|
|
||||||
Tipo: A
|
|
||||||
Nombre: livekit
|
|
||||||
Valor: TU_IP_PUBLICA
|
|
||||||
TTL: 300
|
|
||||||
|
|
||||||
Resultado: livekit.midominio.com → TU_IP_PUBLICA
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 2: Port forwarding en router
|
|
||||||
```
|
|
||||||
Puerto 80 → 192.168.1.19:80 # HTTP para Let's Encrypt
|
|
||||||
Puerto 443 → 192.168.1.19:443 # HTTPS/WSS
|
|
||||||
Puerto 7880 → 192.168.1.19:7880 # LiveKit API directo
|
|
||||||
Puerto 50000-50100 (UDP) → 192.168.1.19:50000-50100 # WebRTC
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 3: SSL con Let's Encrypt
|
|
||||||
```bash
|
|
||||||
# Instalar certbot
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install certbot nginx
|
|
||||||
|
|
||||||
# Configurar Nginx básico
|
|
||||||
sudo tee /etc/nginx/sites-available/livekit << 'EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name livekit.midominio.com;
|
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo ln -s /etc/nginx/sites-available/livekit /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t && sudo systemctl restart nginx
|
|
||||||
|
|
||||||
# Generar certificado SSL
|
|
||||||
sudo certbot --nginx -d livekit.midominio.com
|
|
||||||
|
|
||||||
# Resultado: certificados en /etc/letsencrypt/live/livekit.midominio.com/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 4: Configurar Nginx para LiveKit
|
|
||||||
```nginx
|
|
||||||
# /etc/nginx/sites-available/livekit
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name livekit.midominio.com;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/livekit.midominio.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/livekit.midominio.com/privkey.pem;
|
|
||||||
|
|
||||||
# WebSocket proxy para LiveKit
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:7880;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# Timeouts para WebRTC
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name livekit.midominio.com;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Paso 5: Auto-renovación SSL
|
|
||||||
```bash
|
|
||||||
# Agregar a crontab
|
|
||||||
sudo crontab -e
|
|
||||||
|
|
||||||
# Renovar certificados automáticamente
|
|
||||||
0 12 * * * /usr/bin/certbot renew --quiet && systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### URLs finales:
|
|
||||||
- **LiveKit WSS**: `wss://livekit.midominio.com`
|
|
||||||
- **API HTTPS**: `https://livekit.midominio.com`
|
|
||||||
|
|
||||||
### Configurar en OpenVidu Meet:
|
|
||||||
```env
|
|
||||||
LIVEKIT_URL=wss://livekit.midominio.com
|
|
||||||
```
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
# CONFIGURACIÓN DUCKDNS PARA LIVEKIT
|
|
||||||
|
|
||||||
## 🦆 DuckDNS - Dominio gratuito para IP dinámica
|
|
||||||
|
|
||||||
### Paso 1: Registrarse en DuckDNS
|
|
||||||
1. Ir a [duckdns.org](https://www.duckdns.org)
|
|
||||||
2. Login con Google/GitHub
|
|
||||||
3. Crear subdominio: `mi-livekit.duckdns.org`
|
|
||||||
4. Copiar el token
|
|
||||||
|
|
||||||
### Paso 2: Script de actualización automática
|
|
||||||
```bash
|
|
||||||
# Crear script de actualización
|
|
||||||
cat > /home/xesar/update-duckdns.sh << 'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
# Actualizar DuckDNS con IP actual
|
|
||||||
|
|
||||||
DOMAIN="mi-livekit" # Tu subdominio sin .duckdns.org
|
|
||||||
TOKEN="tu-token-aqui" # Token de DuckDNS
|
|
||||||
|
|
||||||
# Obtener IP pública actual
|
|
||||||
CURRENT_IP=$(curl -s https://checkip.amazonaws.com)
|
|
||||||
|
|
||||||
# Actualizar DuckDNS
|
|
||||||
RESPONSE=$(curl -s "https://www.duckdns.org/update?domains=$DOMAIN&token=$TOKEN&ip=$CURRENT_IP")
|
|
||||||
|
|
||||||
if [ "$RESPONSE" = "OK" ]; then
|
|
||||||
echo "$(date): DuckDNS actualizado - $DOMAIN.duckdns.org → $CURRENT_IP"
|
|
||||||
else
|
|
||||||
echo "$(date): ERROR actualizando DuckDNS: $RESPONSE"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x /home/xesar/update-duckdns.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 3: Automatizar con cron
|
|
||||||
```bash
|
|
||||||
# Editar crontab
|
|
||||||
crontab -e
|
|
||||||
|
|
||||||
# Agregar línea para actualizar cada 5 minutos:
|
|
||||||
*/5 * * * * /home/xesar/update-duckdns.sh >> /home/xesar/duckdns.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 4: Configurar LiveKit con dominio
|
|
||||||
```yaml
|
|
||||||
# livekit-production.yaml
|
|
||||||
rtc:
|
|
||||||
external_ip: "mi-livekit.duckdns.org" # Tu dominio DuckDNS
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 50100
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 5: Port forwarding en router
|
|
||||||
```
|
|
||||||
Regla: LiveKit-API
|
|
||||||
- Puerto externo: 7880
|
|
||||||
- Puerto interno: 7880
|
|
||||||
- IP: 192.168.1.19
|
|
||||||
- Protocolo: TCP
|
|
||||||
|
|
||||||
Regla: LiveKit-WebRTC
|
|
||||||
- Puerto externo: 50000-50100
|
|
||||||
- Puerto interno: 50000-50100
|
|
||||||
- IP: 192.168.1.19
|
|
||||||
- Protocolo: UDP
|
|
||||||
```
|
|
||||||
|
|
||||||
### URLs finales:
|
|
||||||
- **LiveKit API**: `ws://mi-livekit.duckdns.org:7880`
|
|
||||||
- **Con SSL**: `wss://mi-livekit.duckdns.org:443` (después de configurar SSL)
|
|
||||||
67
Dockerfile
67
Dockerfile
@ -1,67 +0,0 @@
|
|||||||
# Multi-stage build para OpenVidu Meet
|
|
||||||
FROM node:20-alpine AS builder
|
|
||||||
|
|
||||||
# Instalar pnpm
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
|
|
||||||
# Copiar archivos del workspace
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
|
||||||
COPY meet-ce/backend/package.json ./meet-ce/backend/
|
|
||||||
COPY meet-ce/frontend/package.json ./meet-ce/frontend/
|
|
||||||
COPY meet-ce/frontend/projects/shared-meet-components/package.json ./meet-ce/frontend/projects/shared-meet-components/
|
|
||||||
|
|
||||||
# Instalar dependencias
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# Copiar código fuente
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build backend
|
|
||||||
WORKDIR /app/meet-ce/backend
|
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
# Build frontend
|
|
||||||
WORKDIR /app/meet-ce/frontend
|
|
||||||
RUN pnpm run build:prod
|
|
||||||
|
|
||||||
# Imagen de producción
|
|
||||||
FROM node:20-alpine AS production
|
|
||||||
|
|
||||||
# Instalar pnpm
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
|
|
||||||
# Crear usuario no-root
|
|
||||||
RUN addgroup -g 1001 -S nodejs && \
|
|
||||||
adduser -S openvidu -u 1001
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copiar package.json y dependencias
|
|
||||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
|
||||||
COPY meet-ce/backend/package.json ./meet-ce/backend/
|
|
||||||
|
|
||||||
# Instalar solo dependencias de producción
|
|
||||||
RUN pnpm install --prod --frozen-lockfile
|
|
||||||
|
|
||||||
# Copiar archivos compilados
|
|
||||||
COPY --from=builder /app/meet-ce/backend/dist ./meet-ce/backend/dist
|
|
||||||
COPY --from=builder /app/meet-ce/backend/public ./meet-ce/backend/public
|
|
||||||
|
|
||||||
# Cambiar propietario
|
|
||||||
RUN chown -R openvidu:nodejs /app
|
|
||||||
|
|
||||||
USER openvidu
|
|
||||||
|
|
||||||
# Variables de entorno por defecto
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV PORT=6080
|
|
||||||
ENV MEET_BLOB_STORAGE_MODE=memory
|
|
||||||
ENV MEET_LOG_LEVEL=info
|
|
||||||
|
|
||||||
# Exponer puerto
|
|
||||||
EXPOSE 6080
|
|
||||||
|
|
||||||
# Comando de inicio
|
|
||||||
WORKDIR /app/meet-ce/backend
|
|
||||||
CMD ["node", "dist/src/server.js"]
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
# COMPARACIÓN: OPCIONES PARA EASYPANEL
|
|
||||||
|
|
||||||
## 📊 Tabla comparativa
|
|
||||||
|
|
||||||
| Opción | Configuración UDP | Costo | Complejidad | Rendimiento | Recomendado |
|
|
||||||
|--------|------------------|-------|-------------|-------------|-------------|
|
|
||||||
| **LiveKit Cloud** | ❌ No necesario | $20-100/mes | ⭐ Muy fácil | ⭐⭐⭐ Excelente | ✅ **SÍ** |
|
|
||||||
| **VPS Híbrido** | ✅ En VPS separado | $10-30/mes | ⭐⭐ Medio | ⭐⭐⭐ Excelente | ⚠️ Si budget limitado |
|
|
||||||
| **TURN Server** | ✅ 3 puertos en VPS | $5-15/mes | ⭐⭐⭐ Complejo | ⭐⭐ Bueno | ⚠️ Para expertos |
|
|
||||||
| **EasyPanel Solo** | ❌ Imposible | $0 extra | ❌ No funciona | ❌ No WebRTC | ❌ NO |
|
|
||||||
|
|
||||||
## 🏆 RECOMENDACIÓN FINAL
|
|
||||||
|
|
||||||
### Para la mayoría de casos: **LiveKit Cloud**
|
|
||||||
|
|
||||||
```env
|
|
||||||
# .env.production para EasyPanel
|
|
||||||
LIVEKIT_URL=wss://tu-proyecto.livekit.cloud
|
|
||||||
LIVEKIT_API_KEY=api-key-de-cloud
|
|
||||||
LIVEKIT_API_SECRET=secret-de-cloud
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ventajas de LiveKit Cloud:
|
|
||||||
- ✅ **Cero configuración UDP**
|
|
||||||
- ✅ **Sin VPS adicionales**
|
|
||||||
- ✅ **Infraestructura global**
|
|
||||||
- ✅ **Escalabilidad automática**
|
|
||||||
- ✅ **Soporte oficial**
|
|
||||||
- ✅ **TURN servers incluidos**
|
|
||||||
- ✅ **Monitoreo y analytics**
|
|
||||||
|
|
||||||
### Costos LiveKit Cloud:
|
|
||||||
- **Free tier**: 50GB/mes gratis
|
|
||||||
- **Starter**: $20/mes - 500GB
|
|
||||||
- **Pro**: $99/mes - 2TB + features
|
|
||||||
|
|
||||||
### Setup LiveKit Cloud:
|
|
||||||
1. **Registro**: https://cloud.livekit.io
|
|
||||||
2. **Crear proyecto**
|
|
||||||
3. **Copiar credenciales**
|
|
||||||
4. **Configurar en EasyPanel**
|
|
||||||
5. **Deploy** ✅
|
|
||||||
|
|
||||||
## 🎯 PASOS SIGUIENTE PARA TI
|
|
||||||
|
|
||||||
¿Qué opción prefieres?
|
|
||||||
|
|
||||||
1. **LiveKit Cloud** (fácil, costo medio)
|
|
||||||
2. **VPS Híbrido** (control total, setup complejo)
|
|
||||||
3. **TURN Server** (experto, costo bajo)
|
|
||||||
|
|
||||||
Te ayudo a configurar la que elijas.
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
# CONFIGURACIÓN EASYPANEL - OpenVidu Meet
|
|
||||||
# ========================================
|
|
||||||
|
|
||||||
## 📋 CONFIGURACIÓN DEL PROYECTO
|
|
||||||
|
|
||||||
### 1. Crear Proyecto en EasyPanel
|
|
||||||
- Tipo: Docker Compose o Docker Build
|
|
||||||
- Repositorio: Tu repo con estos archivos
|
|
||||||
- Branch: main
|
|
||||||
|
|
||||||
### 2. Variables de Entorno Requeridas
|
|
||||||
```env
|
|
||||||
# Básicas
|
|
||||||
NODE_ENV=production
|
|
||||||
MEET_LOG_LEVEL=info
|
|
||||||
MEET_BLOB_STORAGE_MODE=memory
|
|
||||||
|
|
||||||
# Admin (CAMBIAR)
|
|
||||||
ADMIN_PASSWORD=tu-password-seguro-aqui
|
|
||||||
|
|
||||||
# LiveKit (ajustar según tu setup)
|
|
||||||
LIVEKIT_URL=wss://tu-livekit-domain.com
|
|
||||||
LIVEKIT_API_KEY=tu-api-key
|
|
||||||
LIVEKIT_API_SECRET=tu-secret-32-caracteres-minimo
|
|
||||||
|
|
||||||
# Proxy
|
|
||||||
TRUST_PROXY=true
|
|
||||||
SERVER_CORS_ORIGIN=*
|
|
||||||
USE_HTTPS=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configuración de Puertos
|
|
||||||
- **Opción A (con Nginx)**: Puerto 80
|
|
||||||
- **Opción B (directo)**: Puerto 6080
|
|
||||||
|
|
||||||
### 4. Configuración de Dominio
|
|
||||||
- Agregar tu dominio en EasyPanel
|
|
||||||
- Habilitar SSL automático
|
|
||||||
- Configurar redirects HTTP → HTTPS
|
|
||||||
|
|
||||||
## 🐳 OPCIONES DE DEPLOY
|
|
||||||
|
|
||||||
### Opción A: Con Nginx Proxy (Recomendado)
|
|
||||||
```yaml
|
|
||||||
# Usar docker-compose.yml completo
|
|
||||||
# Puerto expuesto: 80/443
|
|
||||||
# Incluye rate limiting y optimizaciones
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opción B: Solo Backend
|
|
||||||
```yaml
|
|
||||||
# Solo el servicio openvidu-meet del compose
|
|
||||||
# Puerto expuesto: 6080
|
|
||||||
# EasyPanel maneja el proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 CONFIGURACIONES ADICIONALES
|
|
||||||
|
|
||||||
### LiveKit Setup
|
|
||||||
1. Desplegar LiveKit en servidor separado
|
|
||||||
2. Configurar LIVEKIT_URL con dominio público
|
|
||||||
3. Generar API keys seguros
|
|
||||||
|
|
||||||
### Redis (Opcional)
|
|
||||||
- Usar servicio Redis de EasyPanel
|
|
||||||
- O mantener storage en memoria para simplicidad
|
|
||||||
|
|
||||||
### SSL/TLS
|
|
||||||
- EasyPanel maneja certificados automáticamente
|
|
||||||
- Configurar HTTPS en variables de entorno
|
|
||||||
|
|
||||||
## 🚨 CONSIDERACIONES DE SEGURIDAD
|
|
||||||
|
|
||||||
1. **Cambiar credenciales por defecto**
|
|
||||||
2. **Usar secrets seguros para LiveKit**
|
|
||||||
3. **Configurar CORS apropiadamente**
|
|
||||||
4. **Habilitar rate limiting**
|
|
||||||
5. **Usar HTTPS únicamente**
|
|
||||||
|
|
||||||
## 📊 MONITOREO
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
- `/nginx-health` - Estado del proxy
|
|
||||||
- `/api/health` - Estado del backend
|
|
||||||
- Logs en EasyPanel dashboard
|
|
||||||
|
|
||||||
### Métricas
|
|
||||||
- CPU/Memory usage
|
|
||||||
- Response times
|
|
||||||
- Error rates
|
|
||||||
|
|
||||||
## 🔄 ACTUALIZACIONES
|
|
||||||
|
|
||||||
1. Push cambios al repo
|
|
||||||
2. EasyPanel rebuilds automáticamente
|
|
||||||
3. Zero-downtime deployment
|
|
||||||
|
|
||||||
## 🐛 TROUBLESHOOTING
|
|
||||||
|
|
||||||
### Backend no arranca
|
|
||||||
- Verificar variables de entorno
|
|
||||||
- Revisar logs en EasyPanel
|
|
||||||
- Verificar puertos
|
|
||||||
|
|
||||||
### Error de proxy
|
|
||||||
- Verificar nginx.conf
|
|
||||||
- Revisar headers
|
|
||||||
- Verificar upstreams
|
|
||||||
|
|
||||||
### LiveKit no conecta
|
|
||||||
- Verificar LIVEKIT_URL
|
|
||||||
- Verificar API keys
|
|
||||||
- Verificar connectivity
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
# 🚀 **DEPLOYMENT EN EASYPANEL - SIMPLIFICADO**
|
|
||||||
|
|
||||||
## 📋 **RESUMEN DE CONFIGURACIÓN**
|
|
||||||
|
|
||||||
✅ **EasyPanel maneja automáticamente:**
|
|
||||||
- SSL/TLS con certificados gratuitos
|
|
||||||
- Subdominios (ej: `tu-app.easypanel.host`)
|
|
||||||
- Proxy reverso con Traefik
|
|
||||||
- HTTPS redirect
|
|
||||||
|
|
||||||
✅ **Tu aplicación expone:**
|
|
||||||
- Solo puerto 80 (HTTP)
|
|
||||||
- Nginx como proxy interno
|
|
||||||
- OpenVidu Meet backend
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 **PASOS DE DEPLOYMENT**
|
|
||||||
|
|
||||||
### **1. Preparar archivos**
|
|
||||||
```bash
|
|
||||||
# Los archivos ya están listos:
|
|
||||||
# ✅ Dockerfile (optimizado)
|
|
||||||
# ✅ docker-compose.yml (puerto 80 solamente)
|
|
||||||
# ✅ nginx.conf (sin SSL - solo HTTP)
|
|
||||||
# ✅ .env.production (variables de entorno)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Subir a repositorio Git**
|
|
||||||
```bash
|
|
||||||
git add Dockerfile docker-compose.yml nginx.conf .env.production
|
|
||||||
git commit -m "Add EasyPanel deployment config"
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Crear proyecto en EasyPanel**
|
|
||||||
|
|
||||||
#### **Opción A: Docker Compose (Recomendado)**
|
|
||||||
1. **Nuevo Proyecto** → **Deploy from Git**
|
|
||||||
2. **Conectar repositorio**
|
|
||||||
3. **Tipo:** Docker Compose
|
|
||||||
4. **Archivo:** `docker-compose.yml`
|
|
||||||
5. **Puerto expuesto:** `80`
|
|
||||||
|
|
||||||
#### **Opción B: Dockerfile simple**
|
|
||||||
1. **Nuevo Proyecto** → **Deploy from Git**
|
|
||||||
2. **Tipo:** Dockerfile
|
|
||||||
3. **Puerto:** `6080`
|
|
||||||
4. **Health Check:** `/health`
|
|
||||||
|
|
||||||
### **4. Variables de entorno en EasyPanel**
|
|
||||||
|
|
||||||
En el dashboard de EasyPanel, configurar:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# ADMIN (¡CAMBIAR!)
|
|
||||||
ADMIN_PASSWORD=mi-password-super-seguro
|
|
||||||
|
|
||||||
# LIVEKIT (configurar según tu setup)
|
|
||||||
LIVEKIT_URL=wss://tu-livekit-domain.com
|
|
||||||
LIVEKIT_API_KEY=tu-api-key
|
|
||||||
LIVEKIT_API_SECRET=tu-secret-de-32-caracteres
|
|
||||||
|
|
||||||
# REDIS (opcional)
|
|
||||||
REDIS_HOST=tu-redis-host
|
|
||||||
REDIS_PASSWORD=tu-redis-password
|
|
||||||
```
|
|
||||||
|
|
||||||
### **5. Deploy**
|
|
||||||
- Hacer clic en **Deploy**
|
|
||||||
- EasyPanel construirá automáticamente
|
|
||||||
- Generará subdominio (ej: `openvidu-meet.easypanel.host`)
|
|
||||||
- Aplicará SSL automáticamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 **RESULTADO FINAL**
|
|
||||||
|
|
||||||
```
|
|
||||||
https://tu-app.easypanel.host
|
|
||||||
├── EasyPanel Traefik Proxy (SSL/HTTPS)
|
|
||||||
└── Tu Container (puerto 80)
|
|
||||||
├── Nginx (proxy interno)
|
|
||||||
└── OpenVidu Meet Backend (:6080)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **URLs disponibles:**
|
|
||||||
- **Aplicación:** `https://tu-app.easypanel.host`
|
|
||||||
- **Admin Login:** `https://tu-app.easypanel.host/admin`
|
|
||||||
- **API:** `https://tu-app.easypanel.host/api/`
|
|
||||||
- **Health Check:** `https://tu-app.easypanel.host/health`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 **CONFIGURACIÓN LIVEKIT**
|
|
||||||
|
|
||||||
Para que funcione completamente, necesitas **LiveKit server** separado:
|
|
||||||
|
|
||||||
### **Opción 1: LiveKit en EasyPanel (otro proyecto)**
|
|
||||||
```yaml
|
|
||||||
# livekit.yaml para EasyPanel
|
|
||||||
port: 7880
|
|
||||||
redis:
|
|
||||||
address: tu-redis:6379
|
|
||||||
password: tu-password
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Opción 2: LiveKit Cloud**
|
|
||||||
- Registrarse en [LiveKit Cloud](https://cloud.livekit.io)
|
|
||||||
- Copiar `LIVEKIT_URL`, `API_KEY`, `API_SECRET`
|
|
||||||
- Configurar en variables de entorno
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 **SEGURIDAD**
|
|
||||||
|
|
||||||
### **Cambiar credenciales por defecto:**
|
|
||||||
```env
|
|
||||||
ADMIN_PASSWORD=un-password-muy-seguro
|
|
||||||
LIVEKIT_API_SECRET=secret-de-al-menos-32-caracteres
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Headers de seguridad incluidos:**
|
|
||||||
- Rate limiting (API: 10req/s, Login: 1req/s)
|
|
||||||
- X-Frame-Options
|
|
||||||
- X-Content-Type-Options
|
|
||||||
- X-XSS-Protection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 **TROUBLESHOOTING**
|
|
||||||
|
|
||||||
### **El container no inicia:**
|
|
||||||
```bash
|
|
||||||
# Ver logs en EasyPanel dashboard
|
|
||||||
# O conectar por SSH:
|
|
||||||
docker logs container-name
|
|
||||||
```
|
|
||||||
|
|
||||||
### **502 Bad Gateway:**
|
|
||||||
- Verificar que el backend responde en puerto 6080
|
|
||||||
- Health check: `curl localhost:6080/health`
|
|
||||||
|
|
||||||
### **WebSocket no funciona:**
|
|
||||||
- Verificar configuración de LiveKit
|
|
||||||
- Headers de WebSocket están configurados en nginx
|
|
||||||
|
|
||||||
### **Admin login no funciona:**
|
|
||||||
- Verificar variable `ADMIN_PASSWORD`
|
|
||||||
- Limpiar datos Redis si está configurado
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ **CHECKLIST FINAL**
|
|
||||||
|
|
||||||
- [ ] Repository con archivos de deployment subido
|
|
||||||
- [ ] Proyecto creado en EasyPanel
|
|
||||||
- [ ] Variables de entorno configuradas
|
|
||||||
- [ ] Password admin cambiado
|
|
||||||
- [ ] LiveKit configurado (separado)
|
|
||||||
- [ ] SSL funcionando automáticamente
|
|
||||||
- [ ] Admin login funcional en `/admin`
|
|
||||||
|
|
||||||
**¡Ya tienes OpenVidu Meet funcionando en producción con EasyPanel!** 🎉
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
# SOLUCIÓN: TURN SERVER PARA EASYPANEL
|
|
||||||
|
|
||||||
## 🔄 TURN Server como alternativa para NAT traversal
|
|
||||||
|
|
||||||
### ¿Qué es TURN?
|
|
||||||
TURN (Traversal Using Relays around NAT) permite que WebRTC funcione sin exponer miles de puertos UDP.
|
|
||||||
|
|
||||||
## 🏗️ ARQUITECTURA CON TURN
|
|
||||||
|
|
||||||
```
|
|
||||||
Cliente → Internet → TURN Server → EasyPanel
|
|
||||||
(3 puertos) (sin UDP)
|
|
||||||
|
|
||||||
Vs. directo:
|
|
||||||
Cliente → Internet → EasyPanel
|
|
||||||
(10,000 UDP) ❌ No posible
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 IMPLEMENTACIÓN
|
|
||||||
|
|
||||||
### 1. TURN Server en VPS separado
|
|
||||||
```bash
|
|
||||||
# Instalar Coturn en VPS
|
|
||||||
apt-get update
|
|
||||||
apt-get install coturn
|
|
||||||
|
|
||||||
# /etc/turnserver.conf
|
|
||||||
listening-port=3478
|
|
||||||
tls-listening-port=5349
|
|
||||||
external-ip=IP_PUBLICA_VPS
|
|
||||||
realm=turn.tu-dominio.com
|
|
||||||
lt-cred-mech
|
|
||||||
user=usuario:password123
|
|
||||||
verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Firewall VPS (Solo 3 puertos)
|
|
||||||
```bash
|
|
||||||
# Solo estos 3 puertos para TURN
|
|
||||||
ufw allow 3478/tcp # TURN TCP
|
|
||||||
ufw allow 3478/udp # TURN UDP
|
|
||||||
ufw allow 5349/tcp # TURN over TLS
|
|
||||||
ufw enable
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. LiveKit en EasyPanel con TURN
|
|
||||||
```yaml
|
|
||||||
# livekit.yaml para EasyPanel
|
|
||||||
port: 7880
|
|
||||||
|
|
||||||
keys:
|
|
||||||
devkey: tu-secret-32-chars
|
|
||||||
|
|
||||||
# SIN puertos UDP locales - usar TURN
|
|
||||||
rtc:
|
|
||||||
# NO port_range - usa TURN
|
|
||||||
use_external_ip: false
|
|
||||||
|
|
||||||
# Configurar TURN servers
|
|
||||||
ice_servers:
|
|
||||||
- urls: ["stun:stun.l.google.com:19302"]
|
|
||||||
- urls: ["turn:turn.tu-dominio.com:3478"]
|
|
||||||
username: "usuario"
|
|
||||||
credential: "password123"
|
|
||||||
- urls: ["turns:turn.tu-dominio.com:5349"]
|
|
||||||
username: "usuario"
|
|
||||||
credential: "password123"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Variables EasyPanel
|
|
||||||
```env
|
|
||||||
# Solo TCP - SIN UDP
|
|
||||||
LIVEKIT_URL=wss://tu-app.easypanel.host/livekit
|
|
||||||
LIVEKIT_API_KEY=devkey
|
|
||||||
LIVEKIT_API_SECRET=tu-secret-32-chars
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Nginx en EasyPanel para proxy LiveKit
|
|
||||||
```nginx
|
|
||||||
# nginx.conf - agregar ruta para LiveKit
|
|
||||||
location /livekit {
|
|
||||||
proxy_pass http://openvidu-meet:7880;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💰 COSTOS
|
|
||||||
- **TURN VPS**: $5-10/mes (pequeño VPS)
|
|
||||||
- **EasyPanel**: Tu plan actual
|
|
||||||
- **Total**: +$5-10/mes vs LiveKit Cloud
|
|
||||||
|
|
||||||
## ✅ VENTAJAS
|
|
||||||
- ✅ Solo 3 puertos UDP en VPS externo
|
|
||||||
- ✅ EasyPanel sin UDP
|
|
||||||
- ✅ NAT traversal garantizado
|
|
||||||
- ✅ Menor costo que LiveKit Cloud
|
|
||||||
|
|
||||||
## ❌ DESVENTAJAS
|
|
||||||
- ❌ Configuración más compleja
|
|
||||||
- ❌ VPS adicional para TURN
|
|
||||||
- ❌ Latencia adicional (relay)
|
|
||||||
- ❌ Ancho de banda TURN server
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
# LIMITACIONES DE EASYPANEL PARA UDP
|
|
||||||
|
|
||||||
## ❌ Por qué EasyPanel NO puede exponer UDP:
|
|
||||||
|
|
||||||
### Arquitectura de EasyPanel:
|
|
||||||
```
|
|
||||||
Internet → Traefik (HTTP/HTTPS Proxy) → Tu Container
|
|
||||||
↑
|
|
||||||
Solo maneja TCP/HTTP/HTTPS
|
|
||||||
NO puede proxy UDP
|
|
||||||
```
|
|
||||||
|
|
||||||
### Limitaciones técnicas:
|
|
||||||
1. **Traefik**: Solo HTTP/HTTPS reverse proxy
|
|
||||||
2. **Docker networking**: Limitado a puertos TCP expuestos
|
|
||||||
3. **UI de EasyPanel**: Solo configuración HTTP
|
|
||||||
4. **Load balancing**: Diseñado para web apps, no media streaming
|
|
||||||
|
|
||||||
### Puertos disponibles en EasyPanel:
|
|
||||||
- ✅ 80 (HTTP)
|
|
||||||
- ✅ 443 (HTTPS)
|
|
||||||
- ✅ Puertos TCP custom
|
|
||||||
- ❌ Puertos UDP (NO DISPONIBLE)
|
|
||||||
|
|
||||||
## ⚠️ Problemas si intentas exponer UDP:
|
|
||||||
- EasyPanel UI no tiene opción para UDP
|
|
||||||
- Traefik no puede hacer proxy de UDP
|
|
||||||
- Docker compose limitado a TCP en EasyPanel
|
|
||||||
- No hay configuración de port ranges UDP
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
# SOLUCIÓN: LIVEKIT EN VPS SEPARADO + EASYPANEL
|
|
||||||
|
|
||||||
## 🏗️ ARQUITECTURA HÍBRIDA
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─ EasyPanel ────────────────┐ ┌─ VPS Separado ─────────┐
|
|
||||||
│ │ │ │
|
|
||||||
│ OpenVidu Meet Backend ──────────→ LiveKit Server │
|
|
||||||
│ (HTTP/HTTPS only) │ │ (UDP 50000-60000) │
|
|
||||||
│ │ │ │
|
|
||||||
└────────────────────────────┘ └────────────────────────┘
|
|
||||||
↑ ↑
|
|
||||||
Traefik/SSL Firewall/UDP abierto
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 CONFIGURACIÓN PASO A PASO
|
|
||||||
|
|
||||||
### 1. EasyPanel (Solo OpenVidu Meet Backend)
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml para EasyPanel
|
|
||||||
version: '3.8'
|
|
||||||
services:
|
|
||||||
openvidu-meet:
|
|
||||||
build: .
|
|
||||||
environment:
|
|
||||||
# LiveKit en VPS externo
|
|
||||||
LIVEKIT_URL: wss://livekit.tu-vps.com:7880
|
|
||||||
LIVEKIT_API_KEY: devkey
|
|
||||||
LIVEKIT_API_SECRET: tu-secret-32-chars
|
|
||||||
ports:
|
|
||||||
- "80:6080" # Solo HTTP - EasyPanel maneja SSL
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. VPS Separado (Solo LiveKit + Redis)
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml en VPS
|
|
||||||
version: '3.8'
|
|
||||||
services:
|
|
||||||
livekit:
|
|
||||||
image: livekit/livekit-server:latest
|
|
||||||
ports:
|
|
||||||
- "7880:7880" # API/WebSocket
|
|
||||||
- "50000-60000:50000-60000/udp" # WebRTC
|
|
||||||
volumes:
|
|
||||||
- ./livekit.yaml:/livekit.yaml
|
|
||||||
command: --config /livekit.yaml
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
command: redis-server --requirepass redispassword
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configuración LiveKit en VPS
|
|
||||||
```yaml
|
|
||||||
# livekit.yaml en VPS
|
|
||||||
port: 7880
|
|
||||||
bind_addresses: ["0.0.0.0"]
|
|
||||||
|
|
||||||
keys:
|
|
||||||
devkey: tu-secret-de-32-caracteres-minimo
|
|
||||||
|
|
||||||
redis:
|
|
||||||
address: "localhost:6379"
|
|
||||||
password: "redispassword"
|
|
||||||
|
|
||||||
rtc:
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 60000
|
|
||||||
use_external_ip: true
|
|
||||||
external_ip: "IP_PUBLICA_DEL_VPS"
|
|
||||||
|
|
||||||
ice_servers:
|
|
||||||
- urls: ["stun:stun.l.google.com:19302"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Firewall en VPS
|
|
||||||
```bash
|
|
||||||
# Configurar firewall en VPS
|
|
||||||
ufw allow 7880/tcp # LiveKit API
|
|
||||||
ufw allow 50000:60000/udp # WebRTC UDP
|
|
||||||
ufw allow 6379/tcp # Redis (si acceso externo)
|
|
||||||
ufw enable
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. SSL para LiveKit (Nginx en VPS)
|
|
||||||
```nginx
|
|
||||||
# /etc/nginx/sites-available/livekit
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name livekit.tu-vps.com;
|
|
||||||
|
|
||||||
ssl_certificate /path/to/cert.pem;
|
|
||||||
ssl_certificate_key /path/to/key.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:7880;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💰 COSTOS ESTIMADOS
|
|
||||||
|
|
||||||
### VPS para LiveKit:
|
|
||||||
- **Básico**: $5-10/mes (2GB RAM, 1 CPU)
|
|
||||||
- **Medio**: $15-25/mes (4GB RAM, 2 CPU)
|
|
||||||
- **Alto**: $30-50/mes (8GB RAM, 4 CPU)
|
|
||||||
|
|
||||||
### Proveedores recomendados:
|
|
||||||
- DigitalOcean
|
|
||||||
- Linode
|
|
||||||
- Hetzner
|
|
||||||
- Vultr
|
|
||||||
|
|
||||||
## ✅ VENTAJAS
|
|
||||||
- ✅ EasyPanel para web app (fácil)
|
|
||||||
- ✅ VPS dedicado para WebRTC (potencia)
|
|
||||||
- ✅ Escalabilidad independiente
|
|
||||||
- ✅ Control total sobre LiveKit
|
|
||||||
|
|
||||||
## ❌ DESVENTAJAS
|
|
||||||
- ❌ Costo adicional VPS
|
|
||||||
- ❌ Más complejidad de setup
|
|
||||||
- ❌ Mantenimiento de dos servicios
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
# SERVIDOR LIVEKIT SELF-HOSTING DEDICADO
|
|
||||||
|
|
||||||
## 🖥️ Setup en servidor dedicado (192.168.1.19)
|
|
||||||
|
|
||||||
### Docker Compose para LiveKit Server:
|
|
||||||
```yaml
|
|
||||||
# docker-compose-livekit-server.yml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# LiveKit Server Principal
|
|
||||||
livekit-server:
|
|
||||||
image: livekit/livekit-server:latest
|
|
||||||
container_name: livekit-production
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
# API/WebSocket (EXPONER PÚBLICAMENTE)
|
|
||||||
- "7880:7880"
|
|
||||||
|
|
||||||
# Rango UDP para WebRTC (EXPONER PÚBLICAMENTE)
|
|
||||||
- "50000-50100:50000-50100/udp" # 100 puertos para ~10 usuarios concurrentes
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- ./livekit-production.yaml:/livekit.yaml:ro
|
|
||||||
- ./logs:/app/logs
|
|
||||||
command: --config /livekit.yaml
|
|
||||||
environment:
|
|
||||||
- LIVEKIT_CONFIG=/livekit.yaml
|
|
||||||
networks:
|
|
||||||
- livekit-network
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
# Redis para LiveKit
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: livekit-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD:-livekitredis123}
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- livekit-network
|
|
||||||
|
|
||||||
# Nginx SSL Termination (para HTTPS/WSS)
|
|
||||||
nginx-livekit:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: livekit-nginx
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "443:443" # HTTPS/WSS (EXPONER PÚBLICAMENTE)
|
|
||||||
- "80:80" # HTTP redirect
|
|
||||||
volumes:
|
|
||||||
- ./nginx-livekit.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- ./ssl:/etc/nginx/ssl:ro # Certificados SSL
|
|
||||||
depends_on:
|
|
||||||
- livekit-server
|
|
||||||
networks:
|
|
||||||
- livekit-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
livekit-network:
|
|
||||||
driver: bridge
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuración LiveKit Production:
|
|
||||||
```yaml
|
|
||||||
# livekit-production.yaml
|
|
||||||
port: 7880
|
|
||||||
bind_addresses: ["0.0.0.0"]
|
|
||||||
|
|
||||||
# API Keys seguros
|
|
||||||
keys:
|
|
||||||
production-key: tu-super-secret-de-32-caracteres-o-mas
|
|
||||||
|
|
||||||
# Redis para scaling y persistence
|
|
||||||
redis:
|
|
||||||
address: "redis:6379"
|
|
||||||
password: "livekitredis123"
|
|
||||||
db: 0
|
|
||||||
|
|
||||||
# RTC Configuration para acceso público
|
|
||||||
rtc:
|
|
||||||
# Puertos UDP (coincidir con docker-compose)
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 50100
|
|
||||||
|
|
||||||
# IP pública/externa (tu IP pública o dominio)
|
|
||||||
use_external_ip: true
|
|
||||||
external_ip: "TU_IP_PUBLICA_O_DOMINIO" # ej: "mi-casa.duckdns.org"
|
|
||||||
|
|
||||||
# STUN servers para NAT traversal
|
|
||||||
ice_servers:
|
|
||||||
- urls: ["stun:stun.l.google.com:19302"]
|
|
||||||
- urls: ["stun:stun1.l.google.com:19302"]
|
|
||||||
|
|
||||||
# Room settings para producción
|
|
||||||
room:
|
|
||||||
auto_create: true
|
|
||||||
max_participants: 50
|
|
||||||
empty_timeout: 600 # 10 minutos
|
|
||||||
|
|
||||||
# Security
|
|
||||||
webhook:
|
|
||||||
# Opcional: webhook para eventos
|
|
||||||
api_key: "tu-webhook-key"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
log_level: info
|
|
||||||
log_format: json
|
|
||||||
|
|
||||||
# Enable egress (grabaciones)
|
|
||||||
# Automático con Redis
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nginx SSL para LiveKit:
|
|
||||||
```nginx
|
|
||||||
# nginx-livekit.conf
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
# Redirect HTTP to HTTPS
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS/WSS Server
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# SSL Configuration
|
|
||||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
# WebSocket support para LiveKit
|
|
||||||
location / {
|
|
||||||
proxy_pass http://livekit-server:7880;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# Timeouts para WebRTC
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔥 Firewall en servidor LiveKit:
|
|
||||||
```bash
|
|
||||||
# UFW rules para exposición pública segura
|
|
||||||
sudo ufw allow 80/tcp comment "HTTP redirect"
|
|
||||||
sudo ufw allow 443/tcp comment "HTTPS/WSS LiveKit"
|
|
||||||
sudo ufw allow 7880/tcp comment "LiveKit API directo"
|
|
||||||
sudo ufw allow 50000:50100/udp comment "WebRTC UDP range"
|
|
||||||
|
|
||||||
# Opcional: limitar SSH a red local solamente
|
|
||||||
sudo ufw allow from 192.168.1.0/24 to any port 22
|
|
||||||
|
|
||||||
sudo ufw enable
|
|
||||||
sudo ufw status numbered
|
|
||||||
```
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
# CONFIGURACIÓN TURN PARA LIVEKIT
|
|
||||||
|
|
||||||
## 🔧 Si necesitas TURN server para LiveKit detrás de firewall:
|
|
||||||
|
|
||||||
### 1. Configurar Coturn (TURN server)
|
|
||||||
```bash
|
|
||||||
# Instalar coturn
|
|
||||||
apt-get install coturn
|
|
||||||
|
|
||||||
# /etc/turnserver.conf
|
|
||||||
listening-port=3478
|
|
||||||
tls-listening-port=5349
|
|
||||||
external-ip=TU_IP_PUBLICA
|
|
||||||
realm=tu-dominio.com
|
|
||||||
lt-cred-mech
|
|
||||||
user=usuario:password
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configurar LiveKit con TURN
|
|
||||||
```yaml
|
|
||||||
# livekit.yaml
|
|
||||||
rtc:
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 60000
|
|
||||||
ice_servers:
|
|
||||||
- urls:
|
|
||||||
- "stun:stun.l.google.com:19302"
|
|
||||||
- "turn:tu-turn-server.com:3478"
|
|
||||||
username: "usuario"
|
|
||||||
credential: "password"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Firewall para TURN
|
|
||||||
```bash
|
|
||||||
# Puertos necesarios
|
|
||||||
ufw allow 3478/tcp # TURN TCP
|
|
||||||
ufw allow 3478/udp # TURN UDP
|
|
||||||
ufw allow 5349/tcp # TURN TLS
|
|
||||||
ufw allow 50000:60000/udp # Media streams
|
|
||||||
```
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
# CONFIGURACIÓN ROUTER - PORT FORWARDING PARA LIVEKIT
|
|
||||||
|
|
||||||
## 🌐 Port Forwarding necesario en tu Router
|
|
||||||
|
|
||||||
### Puertos a exponer públicamente:
|
|
||||||
|
|
||||||
| Servicio | Puerto | Protocolo | IP Interna | Descripción |
|
|
||||||
|----------|---------|-----------|------------|-------------|
|
|
||||||
| **HTTP** | 80 | TCP | 192.168.1.19 | Redirect a HTTPS |
|
|
||||||
| **HTTPS/WSS** | 443 | TCP | 192.168.1.19 | LiveKit WebSocket Secure |
|
|
||||||
| **LiveKit API** | 7880 | TCP | 192.168.1.19 | API directa (opcional) |
|
|
||||||
| **WebRTC Media** | 50000-50100 | UDP | 192.168.1.19 | Streams de audio/video |
|
|
||||||
|
|
||||||
### Configuración típica router:
|
|
||||||
|
|
||||||
```
|
|
||||||
Regla 1: LiveKit-HTTPS
|
|
||||||
- Servicio: HTTPS/Custom
|
|
||||||
- Puerto externo: 443
|
|
||||||
- Puerto interno: 443
|
|
||||||
- IP interna: 192.168.1.19
|
|
||||||
- Protocolo: TCP
|
|
||||||
- Estado: Habilitado
|
|
||||||
|
|
||||||
Regla 2: LiveKit-HTTP
|
|
||||||
- Servicio: HTTP
|
|
||||||
- Puerto externo: 80
|
|
||||||
- Puerto interno: 80
|
|
||||||
- IP interna: 192.168.1.19
|
|
||||||
- Protocolo: TCP
|
|
||||||
- Estado: Habilitado
|
|
||||||
|
|
||||||
Regla 3: LiveKit-WebRTC
|
|
||||||
- Servicio: Custom
|
|
||||||
- Puerto externo: 50000-50100
|
|
||||||
- Puerto interno: 50000-50100
|
|
||||||
- IP interna: 192.168.1.19
|
|
||||||
- Protocolo: UDP
|
|
||||||
- Estado: Habilitado
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏠 IP Dinámica - Solución con DuckDNS
|
|
||||||
|
|
||||||
### Si tu IP pública cambia (típico en casa):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Crear cuenta en DuckDNS.org
|
|
||||||
# 2. Crear subdominio: mi-livekit.duckdns.org
|
|
||||||
# 3. Script de actualización automática
|
|
||||||
|
|
||||||
# /home/usuario/update-duckdns.sh
|
|
||||||
#!/bin/bash
|
|
||||||
echo url="https://www.duckdns.org/update?domains=mi-livekit&token=TU_TOKEN&ip=" | curl -k -o ~/duckdns.log -K -
|
|
||||||
|
|
||||||
# Crontab para actualizar cada 5 minutos
|
|
||||||
# crontab -e
|
|
||||||
*/5 * * * * /home/usuario/update-duckdns.sh >/dev/null 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurar dominio en LiveKit:
|
|
||||||
```yaml
|
|
||||||
# livekit-production.yaml
|
|
||||||
rtc:
|
|
||||||
external_ip: "mi-livekit.duckdns.org" # En lugar de IP
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Certificado SSL automático con Let's Encrypt
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Instalar certbot
|
|
||||||
sudo apt install certbot
|
|
||||||
|
|
||||||
# Generar certificado para tu dominio
|
|
||||||
sudo certbot certonly --standalone -d mi-livekit.duckdns.org
|
|
||||||
|
|
||||||
# Copiar certificados para Docker
|
|
||||||
sudo cp /etc/letsencrypt/live/mi-livekit.duckdns.org/fullchain.pem ./ssl/cert.pem
|
|
||||||
sudo cp /etc/letsencrypt/live/mi-livekit.duckdns.org/privkey.pem ./ssl/key.pem
|
|
||||||
sudo chown $USER:$USER ./ssl/*.pem
|
|
||||||
|
|
||||||
# Auto-renovación (crontab)
|
|
||||||
0 12 * * * /usr/bin/certbot renew --quiet && docker-compose restart nginx-livekit
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Verificación de conectividad
|
|
||||||
|
|
||||||
### Tests externos:
|
|
||||||
```bash
|
|
||||||
# Test puertos desde internet
|
|
||||||
nmap -p 80,443,7880 mi-livekit.duckdns.org
|
|
||||||
nmap -sU -p 50000-50010 mi-livekit.duckdns.org
|
|
||||||
|
|
||||||
# Test WebSocket
|
|
||||||
wscat -c wss://mi-livekit.duckdns.org
|
|
||||||
|
|
||||||
# Test HTTPS
|
|
||||||
curl -I https://mi-livekit.duckdns.org
|
|
||||||
```
|
|
||||||
|
|
||||||
### URLs finales:
|
|
||||||
- **LiveKit WSS**: `wss://mi-livekit.duckdns.org`
|
|
||||||
- **API HTTP**: `https://mi-livekit.duckdns.org`
|
|
||||||
- **Monitoreo**: `https://mi-livekit.duckdns.org/debug`
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
# CONFIGURACIÓN MANUAL DE PUERTOS UDP PARA LIVEKIT
|
|
||||||
|
|
||||||
## 🔥 FIREWALL UBUNTU/DEBIAN (UFW)
|
|
||||||
```bash
|
|
||||||
# Puertos TCP
|
|
||||||
sudo ufw allow 80/tcp comment "HTTP"
|
|
||||||
sudo ufw allow 6080/tcp comment "OpenVidu Meet"
|
|
||||||
sudo ufw allow 6379/tcp comment "Redis"
|
|
||||||
sudo ufw allow 7880/tcp comment "LiveKit API"
|
|
||||||
|
|
||||||
# Puertos UDP para WebRTC
|
|
||||||
sudo ufw allow 50000:60000/udp comment "LiveKit WebRTC"
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
sudo ufw status numbered
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔥 FIREWALL CENTOS/RHEL (firewalld)
|
|
||||||
```bash
|
|
||||||
# Puertos TCP
|
|
||||||
sudo firewall-cmd --permanent --add-port=80/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=6080/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=6379/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=7880/tcp
|
|
||||||
|
|
||||||
# Puertos UDP
|
|
||||||
sudo firewall-cmd --permanent --add-port=50000-60000/udp
|
|
||||||
|
|
||||||
# Aplicar
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
sudo firewall-cmd --list-ports
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🖥️ ROUTER/MODEM (Para acceso externo)
|
|
||||||
Si quieres acceso desde internet:
|
|
||||||
|
|
||||||
### Port Forwarding necesario:
|
|
||||||
- **TCP 80** → Tu servidor (OpenVidu Meet)
|
|
||||||
- **TCP 7880** → Tu servidor (LiveKit API)
|
|
||||||
- **UDP 50000-60000** → Tu servidor (WebRTC media)
|
|
||||||
|
|
||||||
### Configuración típica router:
|
|
||||||
```
|
|
||||||
Servicio: OpenVidu-HTTP
|
|
||||||
Puerto externo: 80
|
|
||||||
Puerto interno: 80
|
|
||||||
IP interna: 192.168.1.19
|
|
||||||
Protocolo: TCP
|
|
||||||
|
|
||||||
Servicio: LiveKit-API
|
|
||||||
Puerto externo: 7880
|
|
||||||
Puerto interno: 7880
|
|
||||||
IP interna: 192.168.1.19
|
|
||||||
Protocolo: TCP
|
|
||||||
|
|
||||||
Servicio: WebRTC-Media
|
|
||||||
Puerto externo: 50000-60000
|
|
||||||
Puerto interno: 50000-60000
|
|
||||||
IP interna: 192.168.1.19
|
|
||||||
Protocolo: UDP
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 VERIFICACIÓN DE PUERTOS
|
|
||||||
|
|
||||||
### Verificar puertos abiertos:
|
|
||||||
```bash
|
|
||||||
# Ver todos los puertos TCP/UDP en uso
|
|
||||||
sudo ss -tulnp
|
|
||||||
|
|
||||||
# Específicos de LiveKit
|
|
||||||
sudo ss -tulnp | grep -E "(7880|50000|60000)"
|
|
||||||
|
|
||||||
# Verificar desde otro dispositivo
|
|
||||||
nmap -p 7880,50000-50010 192.168.1.19
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test de conectividad:
|
|
||||||
```bash
|
|
||||||
# Test TCP (LiveKit API)
|
|
||||||
curl http://192.168.1.19:7880
|
|
||||||
|
|
||||||
# Test WebSocket
|
|
||||||
wscat -c ws://192.168.1.19:7880
|
|
||||||
|
|
||||||
# Test UDP (requiere herramientas específicas)
|
|
||||||
nc -u 192.168.1.19 50000
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ CONSIDERACIONES IMPORTANTES
|
|
||||||
|
|
||||||
### Para red local:
|
|
||||||
- ✅ Solo configurar firewall del servidor
|
|
||||||
- ✅ Usar IP local (192.168.x.x)
|
|
||||||
- ✅ No necesita port forwarding
|
|
||||||
|
|
||||||
### Para acceso externo:
|
|
||||||
- ⚠️ Configurar port forwarding en router
|
|
||||||
- ⚠️ Usar IP pública o dominio
|
|
||||||
- ⚠️ Configurar HTTPS para LiveKit
|
|
||||||
- ⚠️ Considerar seguridad (VPN, etc.)
|
|
||||||
|
|
||||||
### Rango de puertos UDP:
|
|
||||||
- **Mínimo:** 100 puertos (ej: 50000-50100)
|
|
||||||
- **Recomendado:** 1000 puertos (50000-51000)
|
|
||||||
- **Máximo configurado:** 10000 puertos (50000-60000)
|
|
||||||
- **Cálculo:** ~10 puertos por participante simultáneo
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script para configurar dominio automáticamente para LiveKit
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
echo -e "${BLUE}🌐 Configurador de Dominio para LiveKit${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Detectar IP pública
|
|
||||||
echo "🔍 Detectando IP pública..."
|
|
||||||
PUBLIC_IP=$(curl -s https://checkip.amazonaws.com || curl -s https://ipinfo.io/ip || echo "No detectada")
|
|
||||||
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
|
||||||
|
|
||||||
echo -e "${BLUE}📊 Información de red:${NC}"
|
|
||||||
echo " IP Local: $LOCAL_IP"
|
|
||||||
echo " IP Pública: $PUBLIC_IP"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Opciones de dominio
|
|
||||||
echo "¿Qué tipo de dominio quieres configurar?"
|
|
||||||
echo "1) DuckDNS (gratuito, IP dinámica)"
|
|
||||||
echo "2) Dominio propio + Let's Encrypt"
|
|
||||||
echo "3) Solo IP pública (sin dominio)"
|
|
||||||
echo ""
|
|
||||||
read -p "Selecciona opción (1-3): " DOMAIN_OPTION
|
|
||||||
|
|
||||||
case $DOMAIN_OPTION in
|
|
||||||
1)
|
|
||||||
echo -e "${GREEN}🦆 Configurando DuckDNS${NC}"
|
|
||||||
read -p "Subdominio DuckDNS (sin .duckdns.org): " DUCKDNS_SUBDOMAIN
|
|
||||||
read -p "Token DuckDNS: " DUCKDNS_TOKEN
|
|
||||||
|
|
||||||
DOMAIN="$DUCKDNS_SUBDOMAIN.duckdns.org"
|
|
||||||
|
|
||||||
# Crear script de actualización
|
|
||||||
cat > update-duckdns.sh << EOF
|
|
||||||
#!/bin/bash
|
|
||||||
CURRENT_IP=\$(curl -s https://checkip.amazonaws.com)
|
|
||||||
RESPONSE=\$(curl -s "https://www.duckdns.org/update?domains=$DUCKDNS_SUBDOMAIN&token=$DUCKDNS_TOKEN&ip=\$CURRENT_IP")
|
|
||||||
if [ "\$RESPONSE" = "OK" ]; then
|
|
||||||
echo "\$(date): DuckDNS actualizado - $DOMAIN → \$CURRENT_IP"
|
|
||||||
else
|
|
||||||
echo "\$(date): ERROR: \$RESPONSE"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
chmod +x update-duckdns.sh
|
|
||||||
|
|
||||||
# Actualizar inmediatamente
|
|
||||||
./update-duckdns.sh
|
|
||||||
|
|
||||||
# Configurar cron
|
|
||||||
(crontab -l 2>/dev/null; echo "*/5 * * * * $(pwd)/update-duckdns.sh >> $(pwd)/duckdns.log 2>&1") | crontab -
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ DuckDNS configurado: $DOMAIN${NC}"
|
|
||||||
LIVEKIT_URL="ws://$DOMAIN:7880"
|
|
||||||
;;
|
|
||||||
|
|
||||||
2)
|
|
||||||
echo -e "${GREEN}🏠 Configurando dominio propio${NC}"
|
|
||||||
read -p "Dominio completo (ej: livekit.midominio.com): " CUSTOM_DOMAIN
|
|
||||||
|
|
||||||
DOMAIN="$CUSTOM_DOMAIN"
|
|
||||||
|
|
||||||
echo -e "${YELLOW}📋 Pasos manuales necesarios:${NC}"
|
|
||||||
echo "1. Configurar DNS A record:"
|
|
||||||
echo " $DOMAIN → $PUBLIC_IP"
|
|
||||||
echo ""
|
|
||||||
echo "2. Port forwarding en router:"
|
|
||||||
echo " TCP 80,443,7880 → $LOCAL_IP"
|
|
||||||
echo " UDP 50000-50100 → $LOCAL_IP"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
read -p "¿Continuar con configuración SSL automática? (y/N): " SSL_SETUP
|
|
||||||
|
|
||||||
if [[ $SSL_SETUP =~ ^[Yy]$ ]]; then
|
|
||||||
echo -e "${YELLOW}🔧 Configurando Nginx + SSL...${NC}"
|
|
||||||
|
|
||||||
# Instalar dependencias
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y nginx certbot python3-certbot-nginx
|
|
||||||
|
|
||||||
# Configurar Nginx básico
|
|
||||||
sudo tee /etc/nginx/sites-available/livekit << EOF
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name $DOMAIN;
|
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 301 https://\$server_name\$request_uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo ln -sf /etc/nginx/sites-available/livekit /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t && sudo systemctl restart nginx
|
|
||||||
|
|
||||||
# Generar certificado
|
|
||||||
sudo certbot --nginx -d $DOMAIN --non-interactive --agree-tos --email admin@$DOMAIN
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ SSL configurado para $DOMAIN${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
LIVEKIT_URL="wss://$DOMAIN"
|
|
||||||
;;
|
|
||||||
|
|
||||||
3)
|
|
||||||
echo -e "${YELLOW}📡 Usando IP pública directa${NC}"
|
|
||||||
DOMAIN="$PUBLIC_IP"
|
|
||||||
LIVEKIT_URL="ws://$PUBLIC_IP:7880"
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo -e "${RED}❌ Opción inválida${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Actualizar configuración LiveKit
|
|
||||||
echo -e "${YELLOW}🔧 Actualizando configuración LiveKit...${NC}"
|
|
||||||
|
|
||||||
# Actualizar livekit.yaml existente o crear nuevo
|
|
||||||
if [ -f "livekit-production.yaml" ]; then
|
|
||||||
sed -i "s/external_ip: .*/external_ip: \"$DOMAIN\"/" livekit-production.yaml
|
|
||||||
echo -e "${GREEN}✅ livekit-production.yaml actualizado${NC}"
|
|
||||||
elif [ -f "livekit.yaml" ]; then
|
|
||||||
sed -i "s/external_ip: .*/external_ip: \"$DOMAIN\"/" livekit.yaml
|
|
||||||
echo -e "${GREEN}✅ livekit.yaml actualizado${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ No se encontró archivo de configuración LiveKit${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Actualizar variables para OpenVidu Meet
|
|
||||||
cat > .env.livekit-domain << EOF
|
|
||||||
# Configuración de dominio para LiveKit
|
|
||||||
DOMAIN=$DOMAIN
|
|
||||||
LIVEKIT_URL=$LIVEKIT_URL
|
|
||||||
PUBLIC_IP=$PUBLIC_IP
|
|
||||||
LOCAL_IP=$LOCAL_IP
|
|
||||||
|
|
||||||
# Variables para EasyPanel/OpenVidu Meet:
|
|
||||||
LIVEKIT_URL=$LIVEKIT_URL
|
|
||||||
LIVEKIT_API_KEY=production-key
|
|
||||||
LIVEKIT_API_SECRET=tu-secret-de-32-caracteres
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo -e "${GREEN}"
|
|
||||||
echo "============================================="
|
|
||||||
echo "🎉 DOMINIO CONFIGURADO EXITOSAMENTE"
|
|
||||||
echo "============================================="
|
|
||||||
echo "🌐 Dominio: $DOMAIN"
|
|
||||||
echo "🔗 LiveKit URL: $LIVEKIT_URL"
|
|
||||||
echo "📍 IP Pública: $PUBLIC_IP"
|
|
||||||
echo "🏠 IP Local: $LOCAL_IP"
|
|
||||||
echo ""
|
|
||||||
echo "📋 CONFIGURACIÓN PARA OPENVIDU MEET:"
|
|
||||||
echo " LIVEKIT_URL=$LIVEKIT_URL"
|
|
||||||
echo ""
|
|
||||||
echo "🔧 PUERTOS NECESARIOS EN ROUTER:"
|
|
||||||
echo " TCP 7880 → $LOCAL_IP:7880"
|
|
||||||
echo " UDP 50000-50100 → $LOCAL_IP:50000-50100"
|
|
||||||
if [[ $DOMAIN_OPTION == 2 ]]; then
|
|
||||||
echo " TCP 80,443 → $LOCAL_IP:80,443"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
echo "📁 Archivos generados:"
|
|
||||||
echo " - .env.livekit-domain (variables)"
|
|
||||||
if [[ $DOMAIN_OPTION == 1 ]]; then
|
|
||||||
echo " - update-duckdns.sh (actualización automática)"
|
|
||||||
fi
|
|
||||||
echo "============================================="
|
|
||||||
echo -e "${NC}"
|
|
||||||
|
|
||||||
# Test conectividad
|
|
||||||
echo -e "${BLUE}🔍 Probando conectividad...${NC}"
|
|
||||||
if timeout 5 bash -c "echo >/dev/tcp/$DOMAIN/7880" 2>/dev/null; then
|
|
||||||
echo -e "${GREEN}✅ Puerto 7880 accesible${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ Puerto 7880 no accesible (verificar port forwarding)${NC}"
|
|
||||||
fi
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script para configurar puertos UDP para LiveKit local
|
|
||||||
|
|
||||||
echo "🔧 Configurando puertos UDP para LiveKit..."
|
|
||||||
|
|
||||||
# Verificar si ufw está disponible
|
|
||||||
if command -v ufw &> /dev/null; then
|
|
||||||
echo "Configurando con UFW..."
|
|
||||||
|
|
||||||
# Puertos TCP para LiveKit API
|
|
||||||
sudo ufw allow 7880/tcp comment "LiveKit API"
|
|
||||||
|
|
||||||
# Rango de puertos UDP para WebRTC (según livekit.yaml)
|
|
||||||
sudo ufw allow 50000:60000/udp comment "LiveKit WebRTC UDP"
|
|
||||||
|
|
||||||
# Verificar reglas
|
|
||||||
echo "Reglas UFW configuradas:"
|
|
||||||
sudo ufw status numbered
|
|
||||||
|
|
||||||
elif command -v firewall-cmd &> /dev/null; then
|
|
||||||
echo "Configurando con firewalld..."
|
|
||||||
|
|
||||||
# Puerto TCP para LiveKit
|
|
||||||
sudo firewall-cmd --permanent --add-port=7880/tcp
|
|
||||||
|
|
||||||
# Rango UDP para WebRTC
|
|
||||||
sudo firewall-cmd --permanent --add-port=50000-60000/udp
|
|
||||||
|
|
||||||
# Recargar firewall
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
|
|
||||||
echo "Reglas firewalld configuradas:"
|
|
||||||
sudo firewall-cmd --list-ports
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "⚠️ No se detectó UFW ni firewalld"
|
|
||||||
echo "Configurar manualmente:"
|
|
||||||
echo "- TCP 7880 (LiveKit API)"
|
|
||||||
echo "- UDP 50000-60000 (WebRTC media)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Configuración de firewall completada"
|
|
||||||
|
|
||||||
# Verificar que LiveKit esté corriendo
|
|
||||||
echo "🔍 Verificando LiveKit..."
|
|
||||||
if curl -s http://localhost:7880 > /dev/null 2>&1; then
|
|
||||||
echo "✅ LiveKit responde en puerto 7880"
|
|
||||||
else
|
|
||||||
echo "❌ LiveKit no responde - verificar que esté corriendo"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mostrar puertos abiertos
|
|
||||||
echo "📊 Puertos actualmente en uso:"
|
|
||||||
if command -v ss &> /dev/null; then
|
|
||||||
ss -tulnp | grep -E "(7880|50000|60000)"
|
|
||||||
elif command -v netstat &> /dev/null; then
|
|
||||||
netstat -tulnp | grep -E "(7880|50000|60000)"
|
|
||||||
fi
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script para configurar UDP de forma segura según el caso de uso
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
echo -e "${BLUE}🔧 Configuración segura de puertos UDP para LiveKit${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Preguntar caso de uso
|
|
||||||
echo "¿Cuál es tu caso de uso?"
|
|
||||||
echo "1) Solo red local (recomendado y seguro)"
|
|
||||||
echo "2) Acceso desde internet (complejo y riesgoso)"
|
|
||||||
echo "3) Mostrar configuración actual"
|
|
||||||
echo ""
|
|
||||||
read -p "Selecciona opción (1-3): " OPTION
|
|
||||||
|
|
||||||
case $OPTION in
|
|
||||||
1)
|
|
||||||
echo -e "${GREEN}✅ Configurando para RED LOCAL únicamente${NC}"
|
|
||||||
|
|
||||||
# Configurar firewall para solo red local
|
|
||||||
if command -v ufw &> /dev/null; then
|
|
||||||
echo "Configurando UFW para red local..."
|
|
||||||
|
|
||||||
# Permitir desde red local
|
|
||||||
sudo ufw allow from 192.168.0.0/16 to any port 50000:60000 proto udp comment "LiveKit UDP (red local)"
|
|
||||||
sudo ufw allow from 192.168.0.0/16 to any port 7880 proto tcp comment "LiveKit API (red local)"
|
|
||||||
sudo ufw allow from 192.168.0.0/16 to any port 80 proto tcp comment "HTTP (red local)"
|
|
||||||
|
|
||||||
# DENEGAR acceso externo a UDP
|
|
||||||
sudo ufw deny 50000:60000/udp comment "BLOQUEAR UDP externo"
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Firewall configurado para red local${NC}"
|
|
||||||
sudo ufw status numbered
|
|
||||||
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ UFW no disponible. Configurar manualmente:${NC}"
|
|
||||||
echo "- Permitir UDP 50000-60000 desde 192.168.x.x"
|
|
||||||
echo "- BLOQUEAR UDP desde internet"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}🔒 CONFIGURACIÓN SEGURA APLICADA:${NC}"
|
|
||||||
echo "- UDP 50000-60000: Solo red local"
|
|
||||||
echo "- Acceso web: http://192.168.1.19"
|
|
||||||
echo "- Sin port forwarding necesario"
|
|
||||||
echo "- Máxima seguridad"
|
|
||||||
;;
|
|
||||||
|
|
||||||
2)
|
|
||||||
echo -e "${RED}⚠️ CONFIGURACIÓN PARA ACCESO PÚBLICO${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}RIESGOS:${NC}"
|
|
||||||
echo "- 10,000 puertos UDP expuestos"
|
|
||||||
echo "- Posibles ataques de red"
|
|
||||||
echo "- Configuración compleja"
|
|
||||||
echo "- Problemas con NAT/CGNAT"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}ALTERNATIVAS RECOMENDADAS:${NC}"
|
|
||||||
echo "1. LiveKit Cloud (sin UDP local)"
|
|
||||||
echo "2. VPN para usuarios remotos"
|
|
||||||
echo "3. TURN server para NAT traversal"
|
|
||||||
echo ""
|
|
||||||
read -p "¿Continuar con configuración pública? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
echo -e "${RED}Configurando acceso público...${NC}"
|
|
||||||
|
|
||||||
if command -v ufw &> /dev/null; then
|
|
||||||
# Abrir UDP para todo el mundo (PELIGROSO)
|
|
||||||
sudo ufw allow 50000:60000/udp comment "LiveKit UDP PUBLICO"
|
|
||||||
sudo ufw allow 7880/tcp comment "LiveKit API PUBLICO"
|
|
||||||
sudo ufw allow 80/tcp comment "HTTP PUBLICO"
|
|
||||||
|
|
||||||
echo -e "${RED}❌ UDP EXPUESTO PÚBLICAMENTE${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${RED}⚠️ CONFIGURACIÓN APLICADA (RIESGOSA):${NC}"
|
|
||||||
echo "- UDP 50000-60000: PÚBLICO"
|
|
||||||
echo "- Configurar port forwarding en router"
|
|
||||||
echo "- Usar IP pública en livekit.yaml"
|
|
||||||
echo "- Considerar VPN o LiveKit Cloud"
|
|
||||||
|
|
||||||
else
|
|
||||||
echo -e "${GREEN}✅ Configuración pública cancelada${NC}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
3)
|
|
||||||
echo -e "${BLUE}📊 Configuración actual:${NC}"
|
|
||||||
|
|
||||||
# Verificar puertos UDP
|
|
||||||
echo ""
|
|
||||||
echo "Puertos UDP en uso:"
|
|
||||||
if command -v ss &> /dev/null; then
|
|
||||||
ss -ulnp | grep -E ":(5[0-9]{4})" | head -10
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar firewall
|
|
||||||
echo ""
|
|
||||||
echo "Reglas de firewall:"
|
|
||||||
if command -v ufw &> /dev/null; then
|
|
||||||
sudo ufw status numbered | grep -E "(50000|7880|80)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar IP externa
|
|
||||||
echo ""
|
|
||||||
echo "IP externa detectada:"
|
|
||||||
curl -s ifconfig.me || echo "No disponible"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "IP local:"
|
|
||||||
hostname -I | awk '{print $1}'
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo -e "${RED}❌ Opción inválida${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}💡 RECOMENDACIÓN FINAL:${NC}"
|
|
||||||
echo "Para máxima seguridad y simplicidad:"
|
|
||||||
echo "- Usar solo en red local"
|
|
||||||
echo "- Para acceso remoto: VPN o LiveKit Cloud"
|
|
||||||
echo "- NO exponer 10,000 puertos UDP públicamente"
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 PREPARANDO DEPLOY PARA EASYPANEL"
|
|
||||||
echo "==================================="
|
|
||||||
|
|
||||||
# 1. Crear directorio ssl
|
|
||||||
echo "1. Creando estructura de directorios..."
|
|
||||||
mkdir -p ssl logs
|
|
||||||
|
|
||||||
# 2. Crear certificados dummy (EasyPanel los reemplazará)
|
|
||||||
echo "2. Creando certificados dummy..."
|
|
||||||
if [ ! -f ssl/cert.pem ]; then
|
|
||||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
|
||||||
-keyout ssl/key.pem \
|
|
||||||
-out ssl/cert.pem \
|
|
||||||
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" 2>/dev/null || \
|
|
||||||
echo "⚠️ OpenSSL no disponible - EasyPanel manejará SSL"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Crear .dockerignore
|
|
||||||
echo "3. Optimizando build..."
|
|
||||||
cat > .dockerignore << 'EOF'
|
|
||||||
node_modules
|
|
||||||
.git
|
|
||||||
.env*
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
ssl/
|
|
||||||
*.md
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
dist
|
|
||||||
coverage
|
|
||||||
.nyc_output
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 4. Verificar archivos necesarios
|
|
||||||
echo "4. Verificando archivos..."
|
|
||||||
REQUIRED_FILES=(
|
|
||||||
"Dockerfile"
|
|
||||||
"docker-compose.yml"
|
|
||||||
"nginx.conf"
|
|
||||||
".env.production"
|
|
||||||
)
|
|
||||||
|
|
||||||
for file in "${REQUIRED_FILES[@]}"; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
echo " ✅ $file"
|
|
||||||
else
|
|
||||||
echo " ❌ $file - FALTANTE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# 5. Test de build local (opcional)
|
|
||||||
echo "5. ¿Quieres probar el build localmente? (y/n)"
|
|
||||||
read -r TEST_BUILD
|
|
||||||
|
|
||||||
if [ "$TEST_BUILD" = "y" ] || [ "$TEST_BUILD" = "Y" ]; then
|
|
||||||
echo "Construyendo imagen de prueba..."
|
|
||||||
docker build -t openvidu-meet-test . || {
|
|
||||||
echo "❌ Error en el build - revisar Dockerfile"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "✅ Build exitoso"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🎉 PREPARACIÓN COMPLETADA"
|
|
||||||
echo "========================="
|
|
||||||
echo ""
|
|
||||||
echo "📋 PASOS PARA EASYPANEL:"
|
|
||||||
echo ""
|
|
||||||
echo "1. Crear nuevo proyecto en EasyPanel"
|
|
||||||
echo "2. Conectar repositorio Git"
|
|
||||||
echo "3. Configurar variables de entorno:"
|
|
||||||
echo " - Copiar contenido de .env.production"
|
|
||||||
echo " - Ajustar LIVEKIT_URL y secrets"
|
|
||||||
echo ""
|
|
||||||
echo "4. Configurar build:"
|
|
||||||
echo " - Dockerfile: ./Dockerfile"
|
|
||||||
echo " - Puerto: 80 (nginx) o 6080 (directo)"
|
|
||||||
echo ""
|
|
||||||
echo "5. Configurar dominio y SSL en EasyPanel"
|
|
||||||
echo ""
|
|
||||||
echo "📁 ARCHIVOS LISTOS:"
|
|
||||||
echo " ✅ Dockerfile (multi-stage optimizado)"
|
|
||||||
echo " ✅ docker-compose.yml (con nginx proxy)"
|
|
||||||
echo " ✅ nginx.conf (configuración completa)"
|
|
||||||
echo " ✅ .env.production (variables de ejemplo)"
|
|
||||||
echo ""
|
|
||||||
echo "🔗 URLs después del deploy:"
|
|
||||||
echo " • Admin: https://tu-dominio.com"
|
|
||||||
echo " • API: https://tu-dominio.com/api/"
|
|
||||||
echo " • Health: https://tu-dominio.com/nginx-health"
|
|
||||||
echo ""
|
|
||||||
echo "👤 Login por defecto:"
|
|
||||||
echo " • Usuario: admin"
|
|
||||||
echo " • Contraseña: [configurar en ADMIN_PASSWORD]"
|
|
||||||
@ -1,221 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script para desplegar LiveKit self-hosted con exposición pública
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
echo -e "${BLUE}🏠 Configurando LiveKit Self-Hosted con exposición pública${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Detectar IP local
|
|
||||||
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
|
||||||
echo -e "${BLUE}🌐 IP Local detectada: $LOCAL_IP${NC}"
|
|
||||||
|
|
||||||
# Preguntar dominio/IP pública
|
|
||||||
echo "¿Cuál es tu configuración de acceso público?"
|
|
||||||
echo "1) Tengo IP pública fija"
|
|
||||||
echo "2) IP dinámica - usar DuckDNS"
|
|
||||||
echo "3) Solo testing local"
|
|
||||||
echo ""
|
|
||||||
read -p "Selecciona opción (1-3): " IP_OPTION
|
|
||||||
|
|
||||||
case $IP_OPTION in
|
|
||||||
1)
|
|
||||||
read -p "Ingresa tu IP pública: " PUBLIC_IP
|
|
||||||
EXTERNAL_HOST="$PUBLIC_IP"
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
read -p "Ingresa tu subdominio DuckDNS (ej: mi-livekit): " DUCKDNS_SUBDOMAIN
|
|
||||||
EXTERNAL_HOST="$DUCKDNS_SUBDOMAIN.duckdns.org"
|
|
||||||
echo -e "${YELLOW}📝 Recuerda configurar DuckDNS token después${NC}"
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
EXTERNAL_HOST="$LOCAL_IP"
|
|
||||||
echo -e "${YELLOW}⚠️ Solo funcionará en red local${NC}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${RED}❌ Opción inválida${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo -e "${GREEN}🌐 Host externo configurado: $EXTERNAL_HOST${NC}"
|
|
||||||
|
|
||||||
# Generar secretos seguros
|
|
||||||
API_SECRET=$(openssl rand -hex 32)
|
|
||||||
REDIS_PASSWORD=$(openssl rand -hex 16)
|
|
||||||
|
|
||||||
echo -e "${YELLOW}🔧 Generando configuración...${NC}"
|
|
||||||
|
|
||||||
# Crear directorio SSL
|
|
||||||
mkdir -p ssl logs
|
|
||||||
|
|
||||||
# Generar livekit-production.yaml
|
|
||||||
cat > livekit-production.yaml << EOF
|
|
||||||
port: 7880
|
|
||||||
bind_addresses: ["0.0.0.0"]
|
|
||||||
|
|
||||||
# API Keys seguros (generados automáticamente)
|
|
||||||
keys:
|
|
||||||
production-key: $API_SECRET
|
|
||||||
|
|
||||||
# Redis para persistence y scaling
|
|
||||||
redis:
|
|
||||||
address: "redis:6379"
|
|
||||||
password: "$REDIS_PASSWORD"
|
|
||||||
db: 0
|
|
||||||
|
|
||||||
# RTC Configuration para acceso público
|
|
||||||
rtc:
|
|
||||||
# Rango de puertos UDP reducido pero suficiente
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 50100
|
|
||||||
|
|
||||||
# Host/IP externa para acceso público
|
|
||||||
use_external_ip: true
|
|
||||||
external_ip: "$EXTERNAL_HOST"
|
|
||||||
|
|
||||||
# STUN servers para NAT traversal
|
|
||||||
ice_servers:
|
|
||||||
- urls: ["stun:stun.l.google.com:19302"]
|
|
||||||
- urls: ["stun:stun1.l.google.com:19302"]
|
|
||||||
|
|
||||||
# Room settings para producción
|
|
||||||
room:
|
|
||||||
auto_create: true
|
|
||||||
max_participants: 25
|
|
||||||
empty_timeout: 600
|
|
||||||
|
|
||||||
# Logging para producción
|
|
||||||
log_level: info
|
|
||||||
log_format: json
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Crear docker-compose-livekit-server.yml
|
|
||||||
cat > docker-compose-livekit-server.yml << EOF
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
livekit-server:
|
|
||||||
image: livekit/livekit-server:latest
|
|
||||||
container_name: livekit-production
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "7880:7880"
|
|
||||||
- "50000-50100:50000-50100/udp"
|
|
||||||
volumes:
|
|
||||||
- ./livekit-production.yaml:/livekit.yaml:ro
|
|
||||||
- ./logs:/app/logs
|
|
||||||
command: --config /livekit.yaml
|
|
||||||
networks:
|
|
||||||
- livekit-network
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: livekit-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
command: redis-server --requirepass $REDIS_PASSWORD
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- livekit-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
livekit-network:
|
|
||||||
driver: bridge
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Crear variables para OpenVidu Meet
|
|
||||||
cat > .env.livekit-client << EOF
|
|
||||||
# Variables para EasyPanel/OpenVidu Meet
|
|
||||||
LIVEKIT_URL=ws://$EXTERNAL_HOST:7880
|
|
||||||
LIVEKIT_API_KEY=production-key
|
|
||||||
LIVEKIT_API_SECRET=$API_SECRET
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Configuración generada${NC}"
|
|
||||||
|
|
||||||
# Configurar firewall
|
|
||||||
echo -e "${YELLOW}🔥 Configurando firewall...${NC}"
|
|
||||||
if command -v ufw &> /dev/null; then
|
|
||||||
sudo ufw allow 7880/tcp comment "LiveKit API"
|
|
||||||
sudo ufw allow 50000:50100/udp comment "LiveKit WebRTC"
|
|
||||||
echo -e "${GREEN}✅ Firewall configurado${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parar servicios existentes
|
|
||||||
echo -e "${YELLOW}🛑 Parando servicios existentes...${NC}"
|
|
||||||
docker-compose -f docker-compose-livekit-server.yml down 2>/dev/null || true
|
|
||||||
|
|
||||||
# Iniciar LiveKit Server
|
|
||||||
echo -e "${YELLOW}🚀 Iniciando LiveKit Server...${NC}"
|
|
||||||
docker-compose -f docker-compose-livekit-server.yml up -d
|
|
||||||
|
|
||||||
# Esperar inicio
|
|
||||||
echo -e "${YELLOW}⏳ Esperando que LiveKit inicie...${NC}"
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
# Verificar servicios
|
|
||||||
echo -e "${BLUE}🔍 Verificando servicios...${NC}"
|
|
||||||
|
|
||||||
if curl -s http://localhost:7880 > /dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}✅ LiveKit API funcionando${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ LiveKit no responde${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if docker exec livekit-redis redis-cli -a $REDIS_PASSWORD ping > /dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}✅ Redis funcionando${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ Redis no responde${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mostrar configuración final
|
|
||||||
echo -e "${GREEN}"
|
|
||||||
echo "============================================="
|
|
||||||
echo "🎉 LIVEKIT SELF-HOSTED CONFIGURADO"
|
|
||||||
echo "============================================="
|
|
||||||
echo "🌐 Host externo: $EXTERNAL_HOST"
|
|
||||||
echo "🔌 Puerto API: 7880"
|
|
||||||
echo "📡 Puertos UDP: 50000-50100"
|
|
||||||
echo ""
|
|
||||||
echo "📋 CONFIGURACIÓN PARA OPENVIDU MEET:"
|
|
||||||
echo " LIVEKIT_URL=ws://$EXTERNAL_HOST:7880"
|
|
||||||
echo " LIVEKIT_API_KEY=production-key"
|
|
||||||
echo " LIVEKIT_API_SECRET=$API_SECRET"
|
|
||||||
echo ""
|
|
||||||
echo "🔧 PASOS SIGUIENTES:"
|
|
||||||
echo "1. Configurar port forwarding en router:"
|
|
||||||
echo " - TCP 7880 → $LOCAL_IP:7880"
|
|
||||||
echo " - UDP 50000-50100 → $LOCAL_IP:50000-50100"
|
|
||||||
echo ""
|
|
||||||
if [[ $IP_OPTION == 2 ]]; then
|
|
||||||
echo "2. Configurar DuckDNS:"
|
|
||||||
echo " - Token en duckdns.org"
|
|
||||||
echo " - Script de actualización automática"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
echo "3. Configurar OpenVidu Meet con variables generadas"
|
|
||||||
echo "4. (Opcional) Configurar SSL/HTTPS con Let's Encrypt"
|
|
||||||
echo "============================================="
|
|
||||||
echo -e "${NC}"
|
|
||||||
|
|
||||||
# Mostrar logs
|
|
||||||
read -p "¿Ver logs de LiveKit en tiempo real? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
docker-compose -f docker-compose-livekit-server.yml logs -f livekit-server
|
|
||||||
fi
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script completo para desplegar OpenVidu Meet con LiveKit local y UDP
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 Desplegando OpenVidu Meet con LiveKit local (UDP)..."
|
|
||||||
|
|
||||||
# Colores para output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Verificar Docker
|
|
||||||
if ! command -v docker &> /dev/null; then
|
|
||||||
echo -e "${RED}❌ Docker no está instalado${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v docker-compose &> /dev/null; then
|
|
||||||
echo -e "${RED}❌ Docker Compose no está instalado${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Obtener IP local
|
|
||||||
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
|
||||||
echo -e "${BLUE}🌐 IP Local detectada: $LOCAL_IP${NC}"
|
|
||||||
|
|
||||||
# Crear .env para local
|
|
||||||
echo -e "${YELLOW}📝 Creando configuración local...${NC}"
|
|
||||||
cat > .env.local << EOF
|
|
||||||
# Configuración LOCAL con LiveKit y UDP
|
|
||||||
ADMIN_PASSWORD=admin123
|
|
||||||
REDIS_PASSWORD=redispassword
|
|
||||||
|
|
||||||
# LiveKit Local
|
|
||||||
LIVEKIT_URL=ws://$LOCAL_IP:7880
|
|
||||||
LIVEKIT_API_KEY=devkey
|
|
||||||
LIVEKIT_API_SECRET=secretsecretsecretsecretsecretsecret
|
|
||||||
|
|
||||||
# Redis Local
|
|
||||||
REDIS_HOST=$LOCAL_IP
|
|
||||||
REDIS_PORT=6379
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Actualizar IP en livekit-local.yaml
|
|
||||||
echo -e "${YELLOW}🔧 Configurando LiveKit para IP $LOCAL_IP...${NC}"
|
|
||||||
sed -i "s/external_ip: \".*\"/external_ip: \"$LOCAL_IP\"/" livekit-local.yaml
|
|
||||||
|
|
||||||
# Configurar firewall
|
|
||||||
echo -e "${YELLOW}🔥 Configurando firewall...${NC}"
|
|
||||||
./configure-udp-ports.sh
|
|
||||||
|
|
||||||
# Parar servicios existentes
|
|
||||||
echo -e "${YELLOW}🛑 Parando servicios existentes...${NC}"
|
|
||||||
docker-compose -f docker-compose-with-livekit.yml down 2>/dev/null || true
|
|
||||||
|
|
||||||
# Construir imágenes
|
|
||||||
echo -e "${YELLOW}🔨 Construyendo imágenes...${NC}"
|
|
||||||
docker-compose -f docker-compose-with-livekit.yml build
|
|
||||||
|
|
||||||
# Iniciar servicios
|
|
||||||
echo -e "${YELLOW}🚀 Iniciando servicios completos...${NC}"
|
|
||||||
docker-compose -f docker-compose-with-livekit.yml --env-file .env.local up -d
|
|
||||||
|
|
||||||
# Esperar a que los servicios estén listos
|
|
||||||
echo -e "${YELLOW}⏳ Esperando servicios...${NC}"
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
# Verificar servicios
|
|
||||||
echo -e "${BLUE}🔍 Verificando servicios...${NC}"
|
|
||||||
|
|
||||||
services=(
|
|
||||||
"redis:6379"
|
|
||||||
"livekit:7880"
|
|
||||||
"openvidu-meet:6080"
|
|
||||||
"nginx:80"
|
|
||||||
)
|
|
||||||
|
|
||||||
for service in "${services[@]}"; do
|
|
||||||
name=$(echo $service | cut -d: -f1)
|
|
||||||
port=$(echo $service | cut -d: -f2)
|
|
||||||
|
|
||||||
if curl -s http://localhost:$port > /dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}✅ $name funcionando en puerto $port${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ $name no responde en puerto $port${NC}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Verificar puertos UDP
|
|
||||||
echo -e "${BLUE}📊 Verificando puertos UDP...${NC}"
|
|
||||||
if ss -tulnp | grep -q ":50000-60000"; then
|
|
||||||
echo -e "${GREEN}✅ Puertos UDP 50000-60000 abiertos${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ No se detectan puertos UDP - verificar manualmente${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mostrar URLs finales
|
|
||||||
echo -e "${GREEN}"
|
|
||||||
echo "============================================="
|
|
||||||
echo "🎉 DESPLIEGUE COMPLETADO"
|
|
||||||
echo "============================================="
|
|
||||||
echo "📱 OpenVidu Meet: http://$LOCAL_IP"
|
|
||||||
echo "👨💼 Admin Panel: http://$LOCAL_IP/admin"
|
|
||||||
echo "🔧 LiveKit API: http://$LOCAL_IP:7880"
|
|
||||||
echo "📊 Redis: $LOCAL_IP:6379"
|
|
||||||
echo ""
|
|
||||||
echo "🔐 Credenciales Admin:"
|
|
||||||
echo " Usuario: admin"
|
|
||||||
echo " Password: admin123"
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ PUERTOS NECESARIOS:"
|
|
||||||
echo " TCP: 80, 6080, 6379, 7880"
|
|
||||||
echo " UDP: 50000-60000 (WebRTC)"
|
|
||||||
echo "============================================="
|
|
||||||
echo -e "${NC}"
|
|
||||||
|
|
||||||
# Mostrar logs en tiempo real (opcional)
|
|
||||||
read -p "¿Ver logs en tiempo real? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
docker-compose -f docker-compose-with-livekit.yml --env-file .env.local logs -f
|
|
||||||
fi
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# OpenVidu Meet Backend
|
|
||||||
openvidu-meet:
|
|
||||||
build: .
|
|
||||||
container_name: openvidu-meet
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
MEET_LOG_LEVEL: info
|
|
||||||
MEET_BLOB_STORAGE_MODE: memory
|
|
||||||
PORT: 6080
|
|
||||||
|
|
||||||
# Admin user
|
|
||||||
MEET_INITIAL_ADMIN_USER: admin
|
|
||||||
MEET_INITIAL_ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}
|
|
||||||
|
|
||||||
# CORS y proxy
|
|
||||||
SERVER_CORS_ORIGIN: "*"
|
|
||||||
TRUST_PROXY: "true"
|
|
||||||
|
|
||||||
# LiveKit LOCAL con UDP
|
|
||||||
LIVEKIT_URL: ${LIVEKIT_URL:-ws://192.168.1.19:7880}
|
|
||||||
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
|
|
||||||
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-secretsecretsecretsecretsecretsecret}
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "6080:6080"
|
|
||||||
volumes:
|
|
||||||
- ./logs:/app/logs
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:6080/"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 60s
|
|
||||||
networks:
|
|
||||||
- openvidu-network
|
|
||||||
|
|
||||||
# LiveKit Server LOCAL con puertos UDP
|
|
||||||
livekit:
|
|
||||||
image: livekit/livekit-server:latest
|
|
||||||
container_name: openvidu-livekit
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
# Puerto API/WebSocket
|
|
||||||
- "7880:7880"
|
|
||||||
# Rango UDP para WebRTC (IMPORTANTE!)
|
|
||||||
- "50000-60000:50000-60000/udp"
|
|
||||||
volumes:
|
|
||||||
- ./livekit.yaml:/livekit.yaml:ro
|
|
||||||
command: --config /livekit.yaml
|
|
||||||
networks:
|
|
||||||
- openvidu-network
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
# Redis para LiveKit
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: openvidu-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD:-redispassword}
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- openvidu-network
|
|
||||||
|
|
||||||
# Nginx Proxy
|
|
||||||
nginx-proxy:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: openvidu-nginx
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- nginx-cache:/var/cache/nginx
|
|
||||||
depends_on:
|
|
||||||
- openvidu-meet
|
|
||||||
- livekit
|
|
||||||
networks:
|
|
||||||
- openvidu-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
nginx-cache:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
openvidu-network:
|
|
||||||
driver: bridge
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# OpenVidu Meet Backend
|
|
||||||
openvidu-meet:
|
|
||||||
build: .
|
|
||||||
container_name: openvidu-meet
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# Configuración básica
|
|
||||||
NODE_ENV: production
|
|
||||||
MEET_LOG_LEVEL: info
|
|
||||||
MEET_BLOB_STORAGE_MODE: memory
|
|
||||||
PORT: 6080
|
|
||||||
|
|
||||||
# Admin user (cambiar en producción)
|
|
||||||
MEET_INITIAL_ADMIN_USER: admin
|
|
||||||
MEET_INITIAL_ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}
|
|
||||||
|
|
||||||
# CORS para proxy
|
|
||||||
SERVER_CORS_ORIGIN: "*"
|
|
||||||
|
|
||||||
# Configuración para proxy
|
|
||||||
TRUST_PROXY: "true"
|
|
||||||
|
|
||||||
# LiveKit (ajustar según tu setup)
|
|
||||||
LIVEKIT_URL: ${LIVEKIT_URL:-ws://localhost:7880}
|
|
||||||
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
|
|
||||||
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-your-secret-key-32-chars-long}
|
|
||||||
|
|
||||||
# Redis (opcional - si no se proporciona, usa memoria)
|
|
||||||
MEET_REDIS_HOST: ${REDIS_HOST:-}
|
|
||||||
MEET_REDIS_PORT: ${REDIS_PORT:-6379}
|
|
||||||
MEET_REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "6080:6080"
|
|
||||||
volumes:
|
|
||||||
# Logs persistentes
|
|
||||||
- ./logs:/app/logs
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:6080/"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 60s
|
|
||||||
networks:
|
|
||||||
- openvidu-network
|
|
||||||
|
|
||||||
# Nginx Proxy - Solo puerto 80 para EasyPanel
|
|
||||||
nginx-proxy:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: openvidu-nginx
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80" # Solo HTTP - EasyPanel maneja SSL con Traefik
|
|
||||||
volumes:
|
|
||||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- nginx-cache:/var/cache/nginx
|
|
||||||
depends_on:
|
|
||||||
- openvidu-meet
|
|
||||||
networks:
|
|
||||||
- openvidu-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
nginx-cache:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
openvidu-network:
|
|
||||||
driver: bridge
|
|
||||||
@ -1,178 +0,0 @@
|
|||||||
|
|
||||||
# Javascript snippets
|
|
||||||
|
|
||||||
- `np` - nextPage
|
|
||||||
- `npssp` - nextPageServerSideProps
|
|
||||||
- `npsp` - nextPageStaticProps
|
|
||||||
- `npspth` - nextPageStaticPaths
|
|
||||||
- `nssp` - nextServerSideProps
|
|
||||||
- `nsp` - nextStaticProps
|
|
||||||
- `nspth` - nextStaticPaths
|
|
||||||
- `nip` - nextInitialProps
|
|
||||||
- `nimg` - nextImage
|
|
||||||
- `napp` - nextApp
|
|
||||||
- `ndoc` - nextDocument
|
|
||||||
- `napi` - nextApi
|
|
||||||
- `nmid` - nextMiddleware
|
|
||||||
|
|
||||||
## `np` - nextPage
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const FileName = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `npssp` - nextPageServerSideProps
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const FileName = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `npsp` - nextPageStaticProps
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const FileName = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStaticProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `npspth` - nextPageStaticPaths
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const FileName = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
|
||||||
return {
|
|
||||||
paths: [],
|
|
||||||
fallback: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nssp` - nextServerSideProps
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const getServerSideProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nsp` - nextStaticProps
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const getStaticProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nspth` - nextStaticPaths
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const getStaticPaths = async () => {
|
|
||||||
return {
|
|
||||||
paths: [],
|
|
||||||
fallback: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nip` - nextInitialProps
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
FileName.getInitialProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nimg` - nextImage
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
<Image src="" alt="" />
|
|
||||||
```
|
|
||||||
|
|
||||||
## `napp` - nextApp
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export default function MyApp({ Component, pageProps }) {
|
|
||||||
return <Component {...pageProps} />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `ndoc` - nextDocument
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
|
||||||
|
|
||||||
class MyDocument extends Document {
|
|
||||||
static async getInitialProps(ctx) {
|
|
||||||
const initialProps = await Document.getInitialProps(ctx)
|
|
||||||
return { ...initialProps }
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MyDocument
|
|
||||||
```
|
|
||||||
|
|
||||||
## `napi` - nextApi
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export default async function handler(req, res) {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nmid` - nextMiddleware
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
export async function middleware(request) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: '/about/:path*',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
# Typescript snippets
|
|
||||||
|
|
||||||
- `np` - nextPage
|
|
||||||
- `npssp` - nextPageServerSideProps
|
|
||||||
- `npsp` - nextPageStaticProps
|
|
||||||
- `npspth` - nextPageStaticPaths
|
|
||||||
- `nssp` - nextServerSideProps
|
|
||||||
- `nsp` - nextStaticProps
|
|
||||||
- `nspth` - nextStaticPaths
|
|
||||||
- `nip` - nextInitialProps
|
|
||||||
- `nimg` - nextImage
|
|
||||||
- `napp` - nextApp
|
|
||||||
- `ndoc` - nextDocument
|
|
||||||
- `napi` - nextApi
|
|
||||||
- `nmid` - nextMiddleware
|
|
||||||
|
|
||||||
## `np` - nextPage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NextPage } from 'next'
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
const FileName: NextPage<Props> = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `npssp` - nextPageServerSideProps
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NextPage, GetServerSideProps } from 'next'
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
const FileName: NextPage<Props> = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `npsp` - nextPageStaticProps
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NextPage, GetStaticProps } from 'next'
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
const FileName: NextPage<Props> = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `npspth` - nextPageStaticPaths
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NextPage, GetStaticPaths } from 'next'
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
const FileName: NextPage<Props> = ({}) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
|
||||||
return {
|
|
||||||
paths: [],
|
|
||||||
fallback: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileName
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nssp` - nextServerSideProps
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nsp` - nextStaticProps
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nspth` - nextStaticPaths
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
|
||||||
return {
|
|
||||||
paths: [],
|
|
||||||
fallback: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nip` - nextInitialProps
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
FileName.getInitialProps = async (ctx) => {
|
|
||||||
return {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nimg` - nextImage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<Image src="" alt="" />
|
|
||||||
```
|
|
||||||
|
|
||||||
## `napp` - nextApp
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type { AppProps } from 'next/app'
|
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
|
||||||
return <Component {...pageProps} />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `ndoc` - nextDocument
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
|
|
||||||
|
|
||||||
class MyDocument extends Document {
|
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
|
||||||
const initialProps = await Document.getInitialProps(ctx)
|
|
||||||
return { ...initialProps }
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MyDocument
|
|
||||||
```
|
|
||||||
|
|
||||||
## `napi` - nextApi
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
interface Data {}
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `nmid` - nextMiddleware
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NextResponse } from 'next/server'
|
|
||||||
import type { NextRequest } from 'next/server'
|
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: '/about/:path*',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🔧 SOLUCIONANDO PROBLEMA DE LOGIN ADMIN"
|
|
||||||
echo "======================================"
|
|
||||||
|
|
||||||
echo "1. Parando backend actual..."
|
|
||||||
pkill -f "node.*dist/src/server.js" 2>/dev/null || true
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "2. Configurando backend con storage en memoria..."
|
|
||||||
cd /home/xesar/Documentos/openvidu-meet/meet-ce/backend
|
|
||||||
|
|
||||||
# Crear backup del log anterior
|
|
||||||
[ -f /tmp/ovm-logs/backend.log ] && mv /tmp/ovm-logs/backend.log /tmp/ovm-logs/backend.log.backup
|
|
||||||
|
|
||||||
echo "3. Arrancando backend con configuración correcta..."
|
|
||||||
nohup env \
|
|
||||||
NODE_ENV=development \
|
|
||||||
MEET_LOG_LEVEL=debug \
|
|
||||||
MEET_BLOB_STORAGE_MODE=memory \
|
|
||||||
MEET_INITIAL_ADMIN_USER=admin \
|
|
||||||
MEET_INITIAL_ADMIN_PASSWORD=admin \
|
|
||||||
LIVEKIT_URL=ws://192.168.1.19:7880 \
|
|
||||||
LIVEKIT_URL_PRIVATE=ws://192.168.1.19:7880 \
|
|
||||||
LIVEKIT_API_KEY=devkey \
|
|
||||||
LIVEKIT_API_SECRET=secretsecretsecretsecretsecretsecret \
|
|
||||||
MEET_REDIS_HOST=192.168.1.19 \
|
|
||||||
MEET_REDIS_PORT=6379 \
|
|
||||||
MEET_REDIS_PASSWORD=redispassword \
|
|
||||||
node dist/src/server.js > /tmp/ovm-logs/backend.log 2>&1 &
|
|
||||||
|
|
||||||
BACKEND_PID=$!
|
|
||||||
echo "✅ Backend iniciado con PID: $BACKEND_PID"
|
|
||||||
|
|
||||||
echo "4. Esperando arranque (10s)..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
echo "5. Verificando estado:"
|
|
||||||
if ps -p $BACKEND_PID >/dev/null 2>&1; then
|
|
||||||
echo "✅ Proceso backend activo"
|
|
||||||
else
|
|
||||||
echo "❌ Proceso backend inactivo"
|
|
||||||
echo "Logs de error:"
|
|
||||||
tail -n 10 /tmp/ovm-logs/backend.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ss -ltn | grep -q :6080; then
|
|
||||||
echo "✅ Puerto 6080 activo"
|
|
||||||
else
|
|
||||||
echo "❌ Puerto 6080 inactivo"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "6. Verificando logs de admin:"
|
|
||||||
grep -i "admin\|storage.*mode\|memory" /tmp/ovm-logs/backend.log | tail -5
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🎉 SOLUCION COMPLETADA"
|
|
||||||
echo "======================"
|
|
||||||
echo "✅ Backend corriendo con storage en memoria"
|
|
||||||
echo "✅ Usuario admin configurado: admin/admin"
|
|
||||||
echo "🌐 Accede a: http://192.168.1.19:6080"
|
|
||||||
echo "📄 Logs en: /tmp/ovm-logs/backend.log"
|
|
||||||
echo ""
|
|
||||||
echo "👤 CREDENCIALES DE LOGIN:"
|
|
||||||
echo " Usuario: admin"
|
|
||||||
echo " Contraseña: admin"
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
# LiveKit Server Configuration for LOCAL deployment with UDP
|
|
||||||
# Para usar con Docker Compose completo incluyendo LiveKit
|
|
||||||
|
|
||||||
port: 7880
|
|
||||||
bind_addresses: [""]
|
|
||||||
|
|
||||||
# API Keys (mismo secret que en backend)
|
|
||||||
keys:
|
|
||||||
devkey: secretsecretsecretsecretsecretsecret
|
|
||||||
|
|
||||||
# Redis para coordinación
|
|
||||||
redis:
|
|
||||||
address: redis:6379
|
|
||||||
password: redispassword
|
|
||||||
db: 0
|
|
||||||
|
|
||||||
# Configuración RTC con UDP para red local
|
|
||||||
rtc:
|
|
||||||
# Rango de puertos UDP (DEBE coincidir con docker-compose)
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 60000
|
|
||||||
|
|
||||||
# IP externa para acceso desde otros dispositivos
|
|
||||||
# Cambiar por tu IP local real
|
|
||||||
use_external_ip: true
|
|
||||||
external_ip: "192.168.1.19"
|
|
||||||
|
|
||||||
# Configuración ICE/STUN
|
|
||||||
ice_servers:
|
|
||||||
- urls: ["stun:stun.l.google.com:19302"]
|
|
||||||
|
|
||||||
# Configuración de rooms
|
|
||||||
room:
|
|
||||||
auto_create: true
|
|
||||||
max_participants: 0
|
|
||||||
empty_timeout: 300
|
|
||||||
|
|
||||||
# Egress para grabaciones (requiere Redis)
|
|
||||||
# Habilitado automáticamente con Redis
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
log_level: info
|
|
||||||
log_format: json
|
|
||||||
|
|
||||||
# Configuración de desarrollo
|
|
||||||
development: true
|
|
||||||
41
livekit.yaml
41
livekit.yaml
@ -1,41 +0,0 @@
|
|||||||
# LiveKit Server Configuration for Development
|
|
||||||
# https://docs.livekit.io/deploy/configuration/
|
|
||||||
|
|
||||||
port: 7880
|
|
||||||
# Admin/API port (HTTP for rooms/egress APIs)
|
|
||||||
# The default admin port is 7880 for WebSocket and HTTP admin API
|
|
||||||
# Some builds use separate ports; adjust if needed
|
|
||||||
|
|
||||||
# API Keys for authentication (secret must be 32+ characters)
|
|
||||||
keys:
|
|
||||||
devkey: secretsecretsecretsecretsecretsecret
|
|
||||||
|
|
||||||
# Redis configuration (required for egress, ingress, and multi-node deployments)
|
|
||||||
redis:
|
|
||||||
address: 192.168.1.19:6379
|
|
||||||
password: redispassword
|
|
||||||
db: 0
|
|
||||||
|
|
||||||
# Enable egress service (recording/streaming)
|
|
||||||
# Egress requires Redis to coordinate recording jobs
|
|
||||||
# If you see "egress not connected (redis required)", ensure Redis config is correct above
|
|
||||||
|
|
||||||
# Development mode settings
|
|
||||||
log_level: debug
|
|
||||||
|
|
||||||
# RTC configuration
|
|
||||||
rtc:
|
|
||||||
# Use ephemeral ports for UDP
|
|
||||||
port_range_start: 50000
|
|
||||||
port_range_end: 60000
|
|
||||||
# Allow connection from network (not just localhost)
|
|
||||||
use_external_ip: true
|
|
||||||
|
|
||||||
# Room settings
|
|
||||||
room:
|
|
||||||
# Auto-create rooms when participants join
|
|
||||||
auto_create: true
|
|
||||||
# Max participants per room (0 = unlimited)
|
|
||||||
max_participants: 0
|
|
||||||
# Empty room timeout (in seconds, 0 = no timeout)
|
|
||||||
empty_timeout: 300
|
|
||||||
8
meet-ce/.vscode/settings.json
vendored
8
meet-ce/.vscode/settings.json
vendored
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"jest.jestCommandLine": "node --experimental-vm-modules ../../node_modules/.bin/jest",
|
|
||||||
"jest.rootPath": "backend",
|
|
||||||
"jest.nodeEnv": {
|
|
||||||
"NODE_OPTIONS": "--experimental-vm-modules"
|
|
||||||
},
|
|
||||||
"jest.runMode": "on-demand"
|
|
||||||
}
|
|
||||||
@ -1,39 +1,4 @@
|
|||||||
USE_HTTPS=true
|
USE_HTTPS=false
|
||||||
MEET_LOG_LEVEL=debug
|
MEET_LOG_LEVEL=debug
|
||||||
SERVER_CORS_ORIGIN=*
|
SERVER_CORS_ORIGIN=*
|
||||||
MEET_INITIAL_API_KEY=meet-api-key
|
MEET_INITIAL_API_KEY=meet-api-key
|
||||||
|
|
||||||
# Admin user configuration (initial admin user created on first startup)
|
|
||||||
MEET_INITIAL_ADMIN_USER=admin
|
|
||||||
MEET_INITIAL_ADMIN_PASSWORD=admin
|
|
||||||
|
|
||||||
# Redis configuration (used by the backend). Defaults in code point to localhost:6379.
|
|
||||||
# If you don't have a Redis server running locally, you can start one with Docker:
|
|
||||||
# docker run --name openvidu-redis -p 6379:6379 -d redis:7
|
|
||||||
# Or with podman:
|
|
||||||
# podman run --name openvidu-redis -p 6379:6379 -d docker.io/library/redis:7
|
|
||||||
# Environment variables read by the server (optional - only needed if you want to change defaults):
|
|
||||||
MEET_REDIS_HOST=192.168.1.19
|
|
||||||
MEET_REDIS_PORT=6379
|
|
||||||
MEET_REDIS_PASSWORD=redispassword
|
|
||||||
MEET_REDIS_DB=0
|
|
||||||
|
|
||||||
# If using Redis Sentinel, set the host list as comma separated host:port pairs and the sentinel password:
|
|
||||||
# MEET_REDIS_SENTINEL_HOST_LIST=sentinel1:26379,sentinel2:26379
|
|
||||||
# MEET_REDIS_SENTINEL_PASSWORD=your-sentinel-password
|
|
||||||
# LiveKit URL — use the websocket URL that corresponds to the admin HTTP port.
|
|
||||||
# The livekit-server process here is listening on 7880 (client) and 7881 (admin).
|
|
||||||
# Point LIVEKIT_URL/LIVEKIT_URL_PRIVATE to the admin-enabled port so server-side
|
|
||||||
# clients (egress/room service) use the correct HTTP admin endpoint.
|
|
||||||
LIVEKIT_URL=ws://nextream.sytes.net:7880
|
|
||||||
LIVEKIT_URL_PRIVATE=ws://nextream.sytes.net:7880
|
|
||||||
LIVEKIT_API_KEY=devkey
|
|
||||||
LIVEKIT_API_SECRET=secretsecretsecretsecretsecretsecret
|
|
||||||
|
|
||||||
# MinIO / S3 configuration for local development (temporarily using memory)
|
|
||||||
MEET_BLOB_STORAGE_MODE=memory
|
|
||||||
# MEET_S3_SERVICE_ENDPOINT=http://192.168.1.19:9000
|
|
||||||
# MEET_S3_ACCESS_KEY=minioadmin
|
|
||||||
# MEET_S3_SECRET_KEY=minioadmin
|
|
||||||
# MEET_S3_BUCKET=openvidu-appdata
|
|
||||||
# MEET_S3_WITH_PATH_STYLE_ACCESS=true
|
|
||||||
@ -2,5 +2,3 @@ USE_HTTPS=false
|
|||||||
MEET_LOG_LEVEL=verbose
|
MEET_LOG_LEVEL=verbose
|
||||||
SERVER_CORS_ORIGIN=*
|
SERVER_CORS_ORIGIN=*
|
||||||
MEET_INITIAL_API_KEY=meet-api-key
|
MEET_INITIAL_API_KEY=meet-api-key
|
||||||
MEET_INITIAL_WEBHOOK_ENABLED=true
|
|
||||||
MEET_INITIAL_WEBHOOK_URL=http://localhost:5080/webhook
|
|
||||||
62
meet-ce/backend/.vscode/launch.json
vendored
Normal file
62
meet-ce/backend/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Jest Test",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": [
|
||||||
|
"--config",
|
||||||
|
"jest.config.mjs",
|
||||||
|
"--runInBand",
|
||||||
|
"--no-cache",
|
||||||
|
"${file}"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"disableOptimisticBPs": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest.cmd"
|
||||||
|
},
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--experimental-vm-modules"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "test"
|
||||||
|
},
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Current Jest Test",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": [
|
||||||
|
"--config",
|
||||||
|
"jest.config.mjs",
|
||||||
|
"--runInBand",
|
||||||
|
"--testNamePattern",
|
||||||
|
"${input:testName}",
|
||||||
|
"${relativeFile}"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--experimental-vm-modules"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "test"
|
||||||
|
},
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "testName",
|
||||||
|
"description": "Test name pattern (optional)",
|
||||||
|
"default": "",
|
||||||
|
"type": "promptString"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
meet-ce/backend/.vscode/settings.json
vendored
Normal file
5
meet-ce/backend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"jest.jestCommandLine": "node --experimental-vm-modules ./node_modules/.bin/jest --config jest.config.mjs",
|
||||||
|
"jest.rootPath": "./",
|
||||||
|
"jest.runMode": "on-demand"
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ const jestConfig = {
|
|||||||
extensionsToTreatAsEsm: ['.ts'],
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@openvidu-meet/typings$': '<rootDir>/../typings/src/index.ts',
|
'^@openvidu-meet/typings$': '<rootDir>/../typings/src/index.ts',
|
||||||
'^(\\.{1,2}/.*)\\.js$': '$1' // Allow importing js files and resolving to ts files
|
'^(\\.{1,2}/.*)\\.js$': '$1' // Permite importar .js que resuelven a .ts
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.tsx?$': ['ts-jest', {
|
'^.+\\.tsx?$': ['ts-jest', {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# ====================================================
|
# ====================================================
|
||||||
# Stage 1: builder
|
# Stage 1: builder
|
||||||
# ====================================================
|
# ====================================================
|
||||||
FROM node:22.21.0 AS builder
|
FROM node:22.19.0 AS builder
|
||||||
|
|
||||||
# Define pnpm version as build argument with default value
|
# Define pnpm version as build argument with default value
|
||||||
ARG PNPM_VERSION=10.18.3
|
ARG PNPM_VERSION=10.18.3
|
||||||
@ -48,6 +48,14 @@ COPY --chown=node:node meet-ce/frontend/package.json ./meet-ce/frontend/
|
|||||||
COPY --chown=node:node meet-ce/frontend/webcomponent/package.json ./meet-ce/frontend/webcomponent/
|
COPY --chown=node:node meet-ce/frontend/webcomponent/package.json ./meet-ce/frontend/webcomponent/
|
||||||
COPY --chown=node:node meet-ce/frontend/projects/shared-meet-components/package.json ./meet-ce/frontend/projects/shared-meet-components/
|
COPY --chown=node:node meet-ce/frontend/projects/shared-meet-components/package.json ./meet-ce/frontend/projects/shared-meet-components/
|
||||||
COPY --chown=node:node meet-ce/backend/package.json ./meet-ce/backend/
|
COPY --chown=node:node meet-ce/backend/package.json ./meet-ce/backend/
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
# Note: openvidu-components-angular will be installed from:
|
||||||
|
# 1. Local tarball (if exists in meet-ce/frontend/openvidu-components-angular*.tgz)
|
||||||
|
# 2. npm registry (if specified in package.json)
|
||||||
|
# The tarball should be placed in meet-ce/frontend/ by CI before docker build
|
||||||
|
RUN pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
# Copy the source code for all packages
|
# Copy the source code for all packages
|
||||||
COPY --chown=node:node meet-ce/typings/ ./meet-ce/typings/
|
COPY --chown=node:node meet-ce/typings/ ./meet-ce/typings/
|
||||||
COPY --chown=node:node meet-ce/frontend/ ./meet-ce/frontend/
|
COPY --chown=node:node meet-ce/frontend/ ./meet-ce/frontend/
|
||||||
@ -59,7 +67,7 @@ COPY --chown=node:node meet.sh .
|
|||||||
ARG BASE_HREF=/
|
ARG BASE_HREF=/
|
||||||
|
|
||||||
# Build OpenVidu Meet project
|
# Build OpenVidu Meet project
|
||||||
RUN pnpm install --frozen-lockfile && \
|
RUN pnpm install --no-frozen-lockfile && \
|
||||||
./meet.sh build --skip-install --base-href=${BASE_HREF}
|
./meet.sh build --skip-install --base-href=${BASE_HREF}
|
||||||
|
|
||||||
# Clean up development dependencies and unnecessary files
|
# Clean up development dependencies and unnecessary files
|
||||||
@ -75,7 +83,7 @@ RUN rm -rf node_modules \
|
|||||||
# ====================================================
|
# ====================================================
|
||||||
# Stage 2: production
|
# Stage 2: production
|
||||||
# ====================================================
|
# ====================================================
|
||||||
FROM node:22.21.0-alpine3.21 AS production
|
FROM node:22.19.0-alpine3.21 AS production
|
||||||
|
|
||||||
# Define pnpm version as build argument with default value
|
# Define pnpm version as build argument with default value
|
||||||
ARG PNPM_VERSION=10.18.3
|
ARG PNPM_VERSION=10.18.3
|
||||||
|
|||||||
8
meet-ce/frontend/.vscode/settings.json
vendored
Normal file
8
meet-ce/frontend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"prettier.jsxSingleQuote": true,
|
||||||
|
"prettier.singleQuote": true,
|
||||||
|
"javascript.preferences.quoteStyle": "single",
|
||||||
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"prettier.useTabs": true
|
||||||
|
}
|
||||||
@ -100,9 +100,7 @@
|
|||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "openvidu-meet:build",
|
"buildTarget": "openvidu-meet:build",
|
||||||
"proxyConfig": "src/proxy.conf.json",
|
"proxyConfig": "src/proxy.conf.json"
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 4200
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
"core-js": "3.45.1",
|
"core-js": "3.45.1",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"livekit-client": "2.15.11",
|
"livekit-client": "2.15.11",
|
||||||
"openvidu-components-angular": "3.4.0",
|
"openvidu-components-angular": "workspace:*",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"unique-names-generator": "4.7.1",
|
"unique-names-generator": "4.7.1",
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"module": "dist/fesm2022/openvidu-meet-shared-components.mjs",
|
"module": "dist/fesm2022/openvidu-meet-shared-components.mjs",
|
||||||
"typings": "dist/index.d.ts",
|
"typings": "dist/index.d.ts",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"openvidu-components-angular": "3.4.0"
|
"openvidu-components-angular": "workspace:*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
|
|||||||
@ -21,14 +21,10 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
@if (shouldShowActions()) {
|
|
||||||
<mat-dialog-actions class="dialog-action">
|
<mat-dialog-actions class="dialog-action">
|
||||||
@if (shouldShowCancelButton()) {
|
|
||||||
<button mat-button mat-dialog-close (click)="close('cancel')" class="cancel-button">
|
<button mat-button mat-dialog-close (click)="close('cancel')" class="cancel-button">
|
||||||
{{ data.cancelText ?? 'Cancel' }}
|
{{ data.cancelText ?? 'Cancel' }}
|
||||||
</button>
|
</button>
|
||||||
}
|
|
||||||
@if (shouldShowConfirmButton()) {
|
|
||||||
<button
|
<button
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
mat-dialog-close
|
mat-dialog-close
|
||||||
@ -39,7 +35,5 @@
|
|||||||
>
|
>
|
||||||
{{ data.confirmText ?? 'Confirm' }}
|
{{ data.confirmText ?? 'Confirm' }}
|
||||||
</button>
|
</button>
|
||||||
}
|
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -46,16 +46,4 @@ export class DialogComponent {
|
|||||||
this.data.cancelCallback();
|
this.data.cancelCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldShowActions(): boolean {
|
|
||||||
return this.data.showActions !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldShowConfirmButton(): boolean {
|
|
||||||
return this.data.showConfirmButton !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldShowCancelButton(): boolean {
|
|
||||||
return this.data.showCancelButton !== false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
export * from './console-nav/console-nav.component';
|
export * from './console-nav/console-nav.component';
|
||||||
export * from './dialogs/basic-dialog/dialog.component';
|
export * from './dialogs/basic-dialog/dialog.component';
|
||||||
export * from './dialogs/share-recording-dialog/share-recording-dialog.component';
|
export * from './dialogs/share-recording-dialog/share-recording-dialog.component';
|
||||||
export * from './dialogs/delete-room-dialog/delete-room-dialog.component';
|
|
||||||
export * from './logo-selector/logo-selector.component';
|
export * from './logo-selector/logo-selector.component';
|
||||||
export * from './pro-feature-badge/pro-feature-badge.component';
|
export * from './pro-feature-badge/pro-feature-badge.component';
|
||||||
export * from './recording-lists/recording-lists.component';
|
export * from './recording-lists/recording-lists.component';
|
||||||
@ -13,18 +12,6 @@ export * from './step-indicator/step-indicator.component';
|
|||||||
export * from './wizard-nav/wizard-nav.component';
|
export * from './wizard-nav/wizard-nav.component';
|
||||||
export * from './share-meeting-link/share-meeting-link.component';
|
export * from './share-meeting-link/share-meeting-link.component';
|
||||||
|
|
||||||
// Meeting modular components
|
export * from './dialogs/basic-dialog/dialog.component';
|
||||||
export * from './meeting-toolbar-buttons/meeting-toolbar-buttons.component';
|
export * from './dialogs/share-recording-dialog/share-recording-dialog.component';
|
||||||
export * from './meeting-participant-panel/meeting-participant-panel.component';
|
export * from './dialogs/delete-room-dialog/delete-room-dialog.component';
|
||||||
export * from './meeting-share-link-panel/meeting-share-link-panel.component';
|
|
||||||
export * from './meeting-share-link-overlay/meeting-share-link-overlay.component';
|
|
||||||
export * from './meeting-lobby/meeting-lobby.component';
|
|
||||||
|
|
||||||
|
|
||||||
// Meeting components
|
|
||||||
export * from './meeting-toolbar-buttons/meeting-toolbar-buttons.component';
|
|
||||||
export * from './meeting-participant-panel/meeting-participant-panel.component';
|
|
||||||
export * from './meeting-share-link-panel/meeting-share-link-panel.component';
|
|
||||||
export * from './meeting-share-link-overlay/meeting-share-link-overlay.component';
|
|
||||||
export * from './meeting-lobby/meeting-lobby.component';
|
|
||||||
|
|
||||||
|
|||||||
@ -1,116 +0,0 @@
|
|||||||
<div class="ov-page-container">
|
|
||||||
<div class="room-access-container fade-in">
|
|
||||||
<!-- Header Section -->
|
|
||||||
<div class="room-header">
|
|
||||||
<mat-icon class="ov-room-icon room-icon">video_chat</mat-icon>
|
|
||||||
<div class="room-info">
|
|
||||||
<h1 class="room-title">{{ roomName }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Cards Grid -->
|
|
||||||
<div class="action-cards-grid">
|
|
||||||
<!-- Join Room Card -->
|
|
||||||
<mat-card class="action-card primary-card fade-in" [ngClass]="{ 'room-closed-card': roomClosed }">
|
|
||||||
<mat-card-header class="card-header">
|
|
||||||
<mat-icon class="ov-room-icon card-icon">{{ roomClosed ? 'lock' : 'meeting_room' }}</mat-icon>
|
|
||||||
<div class="card-title-group">
|
|
||||||
<mat-card-title>{{ roomClosed ? 'Room Closed' : 'Join Meeting' }}</mat-card-title>
|
|
||||||
<mat-card-subtitle>{{
|
|
||||||
roomClosed
|
|
||||||
? 'This room is not available for meetings'
|
|
||||||
: 'Enter the room and start connecting'
|
|
||||||
}}</mat-card-subtitle>
|
|
||||||
</div>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content class="card-content">
|
|
||||||
@if (!roomClosed) {
|
|
||||||
<form [formGroup]="participantForm" (ngSubmit)="onFormSubmit()" class="join-form">
|
|
||||||
<mat-form-field appearance="outline" class="name-field">
|
|
||||||
<mat-label>Your display name</mat-label>
|
|
||||||
<input
|
|
||||||
id="participant-name-input"
|
|
||||||
matInput
|
|
||||||
placeholder="Enter your name"
|
|
||||||
formControlName="name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<mat-icon matSuffix class="ov-action-icon">person</mat-icon>
|
|
||||||
@if (participantForm.get('name')?.hasError('required')) {
|
|
||||||
<mat-error> The name is <strong>required</strong> </mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<button
|
|
||||||
mat-raised-button
|
|
||||||
color="primary"
|
|
||||||
id="participant-name-submit"
|
|
||||||
type="submit"
|
|
||||||
class="join-button"
|
|
||||||
[disabled]="participantForm.invalid"
|
|
||||||
>
|
|
||||||
<span>Join Meeting</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
} @else {
|
|
||||||
<div class="room-closed-message">
|
|
||||||
<mat-icon class="warning-icon">warning</mat-icon>
|
|
||||||
<p>
|
|
||||||
Sorry, this room is closed. You cannot join at this time. Please contact the meeting
|
|
||||||
organizer for more information.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
|
|
||||||
<!-- View Recordings Card -->
|
|
||||||
@if (showRecordingsCard) {
|
|
||||||
<mat-card class="action-card secondary-card fade-in-delayed">
|
|
||||||
<mat-card-header class="card-header">
|
|
||||||
<mat-icon class="ov-recording-icon card-icon">video_library</mat-icon>
|
|
||||||
<div class="card-title-group">
|
|
||||||
<mat-card-title>View Recordings</mat-card-title>
|
|
||||||
<mat-card-subtitle>Browse and manage past recordings</mat-card-subtitle>
|
|
||||||
</div>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content class="card-content">
|
|
||||||
<div class="recordings-info">
|
|
||||||
<p class="recordings-description">
|
|
||||||
Access previously recorded meetings from this room. You can watch, download, or manage
|
|
||||||
existing recordings.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="view-recordings-btn"
|
|
||||||
mat-stroked-button
|
|
||||||
color="accent"
|
|
||||||
(click)="onViewRecordingsClick()"
|
|
||||||
class="recordings-button"
|
|
||||||
>
|
|
||||||
<span>Browse Recordings</span>
|
|
||||||
</button>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Room URL Badge -->
|
|
||||||
@if (!roomClosed && showShareLink) {
|
|
||||||
<ov-share-meeting-link [meetingUrl]="meetingUrl" (copyClicked)="onCopyLinkClick()"></ov-share-meeting-link>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
|
||||||
@if (showBackButton) {
|
|
||||||
<div class="quick-actions fade-in-delayed-more">
|
|
||||||
<button mat-button class="quick-action-button" (click)="onBackClick()">
|
|
||||||
<mat-icon>arrow_back</mat-icon>
|
|
||||||
<span>{{ backButtonText }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,276 +0,0 @@
|
|||||||
@use '../../../../../../src/assets/styles/design-tokens';
|
|
||||||
|
|
||||||
// Room Access Container - Main layout using design tokens
|
|
||||||
.room-access-container {
|
|
||||||
@include design-tokens.ov-container;
|
|
||||||
@include design-tokens.ov-page-content;
|
|
||||||
padding-top: var(--ov-meet-spacing-xxl);
|
|
||||||
background: var(--ov-meet-background-color);
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Room Header - Clean title section
|
|
||||||
.room-header {
|
|
||||||
@include design-tokens.ov-flex-center;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--ov-meet-spacing-md);
|
|
||||||
margin-bottom: var(--ov-meet-spacing-xxl);
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.room-icon {
|
|
||||||
@include design-tokens.ov-icon(xl);
|
|
||||||
color: var(--ov-meet-icon-rooms);
|
|
||||||
margin-bottom: var(--ov-meet-spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-info {
|
|
||||||
.room-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--ov-meet-font-size-hero);
|
|
||||||
font-weight: var(--ov-meet-font-weight-light);
|
|
||||||
color: var(--ov-meet-text-primary);
|
|
||||||
line-height: var(--ov-meet-line-height-tight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action Cards Grid - Responsive layout
|
|
||||||
.action-cards-grid {
|
|
||||||
@include design-tokens.ov-grid-responsive(320px);
|
|
||||||
gap: var(--ov-meet-spacing-xl);
|
|
||||||
margin-bottom: var(--ov-meet-spacing-xxl);
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
// When there's only one card, limit its width to maintain visual consistency
|
|
||||||
&:has(.action-card:only-child) {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.action-card {
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include design-tokens.ov-tablet-down {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: var(--ov-meet-spacing-lg);
|
|
||||||
|
|
||||||
// On tablets and mobile, single cards should use full width
|
|
||||||
&:has(.action-card:only-child) {
|
|
||||||
.action-card {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action Card Base - Consistent card styling
|
|
||||||
.action-card {
|
|
||||||
@include design-tokens.ov-card;
|
|
||||||
@include design-tokens.ov-hover-lift(-4px);
|
|
||||||
@include design-tokens.ov-theme-transition;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
min-height: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
// Card Header
|
|
||||||
.card-header {
|
|
||||||
padding: var(--ov-meet-spacing-lg);
|
|
||||||
border-bottom: 1px solid var(--ov-meet-border-color-light);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--ov-meet-spacing-md);
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.card-icon {
|
|
||||||
@include design-tokens.ov-icon(lg);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title-group {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.mat-mdc-card-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--ov-meet-font-size-xl);
|
|
||||||
font-weight: var(--ov-meet-font-weight-semibold);
|
|
||||||
color: var(--ov-meet-text-primary);
|
|
||||||
line-height: var(--ov-meet-line-height-tight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-mdc-card-subtitle {
|
|
||||||
margin: var(--ov-meet-spacing-xs) 0 0 0;
|
|
||||||
font-size: var(--ov-meet-font-size-sm);
|
|
||||||
color: var(--ov-meet-text-secondary);
|
|
||||||
line-height: var(--ov-meet-line-height-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card Content
|
|
||||||
.card-content {
|
|
||||||
padding: var(--ov-meet-spacing-lg);
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primary Card - Join meeting styling
|
|
||||||
.primary-card {
|
|
||||||
.card-header {
|
|
||||||
background: linear-gradient(135deg, var(--ov-meet-surface-color) 0%, var(--ov-meet-color-primary-light) 180%);
|
|
||||||
color: var(--ov-meet-text-on-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.room-closed-card {
|
|
||||||
.card-header {
|
|
||||||
background: linear-gradient(135deg, var(--ov-meet-surface-color) 0%, var(--ov-meet-color-warning) 180%);
|
|
||||||
|
|
||||||
.mat-icon {
|
|
||||||
color: var(--ov-meet-color-warning) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-closed-message {
|
|
||||||
@include design-tokens.ov-flex-center;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--ov-meet-spacing-md);
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.warning-icon {
|
|
||||||
@include design-tokens.ov-icon(xl);
|
|
||||||
color: var(--ov-meet-color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--ov-meet-font-size-md);
|
|
||||||
color: var(--ov-meet-text-secondary);
|
|
||||||
line-height: var(--ov-meet-line-height-relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secondary Card - Recordings styling
|
|
||||||
.secondary-card {
|
|
||||||
.card-header {
|
|
||||||
background: linear-gradient(135deg, var(--ov-meet-surface-color) 0%, var(--ov-meet-color-accent) 180%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join Form - Form styling
|
|
||||||
.join-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--ov-meet-spacing-lg);
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.name-field {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.mat-mdc-form-field-icon-suffix {
|
|
||||||
color: var(--ov-meet-text-hint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-button {
|
|
||||||
@include design-tokens.ov-button-base;
|
|
||||||
height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--ov-meet-spacing-sm);
|
|
||||||
margin-top: auto;
|
|
||||||
background-color: var(--ov-meet-color-secondary);
|
|
||||||
color: var(--ov-meet-text-on-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recordings Info - Content for recordings card
|
|
||||||
.recordings-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--ov-meet-spacing-lg);
|
|
||||||
|
|
||||||
.recordings-description {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--ov-meet-font-size-md);
|
|
||||||
color: var(--ov-meet-text-secondary);
|
|
||||||
line-height: var(--ov-meet-line-height-relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.recordings-button {
|
|
||||||
@include design-tokens.ov-button-base;
|
|
||||||
height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--ov-meet-spacing-sm);
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick Actions - Footer actions
|
|
||||||
.quick-actions {
|
|
||||||
@include design-tokens.ov-flex-center;
|
|
||||||
margin-top: var(--ov-meet-spacing-xl);
|
|
||||||
|
|
||||||
.quick-action-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--ov-meet-spacing-sm);
|
|
||||||
color: var(--ov-meet-text-secondary);
|
|
||||||
@include design-tokens.ov-theme-transition;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--ov-meet-text-primary);
|
|
||||||
background-color: var(--ov-meet-surface-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Responsive adjustments
|
|
||||||
@include design-tokens.ov-mobile-down {
|
|
||||||
.room-access-container {
|
|
||||||
padding: 0;
|
|
||||||
padding-top: var(--ov-meet-spacing-sm);
|
|
||||||
margin-bottom: var(--ov-meet-spacing-xxl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-header {
|
|
||||||
margin-bottom: var(--ov-meet-spacing-xl);
|
|
||||||
|
|
||||||
.room-info .room-title {
|
|
||||||
font-size: var(--ov-meet-font-size-xxl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card {
|
|
||||||
min-height: auto;
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: var(--ov-meet-spacing-md);
|
|
||||||
|
|
||||||
.card-title-group {
|
|
||||||
.mat-mdc-card-title {
|
|
||||||
font-size: var(--ov-meet-font-size-lg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
|
||||||
padding: var(--ov-meet-spacing-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { ShareMeetingLinkComponent } from '../share-meeting-link/share-meeting-link.component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable component for the meeting lobby page.
|
|
||||||
* Displays the form to join the meeting and optional recordings card.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ov-meeting-lobby',
|
|
||||||
templateUrl: './meeting-lobby.component.html',
|
|
||||||
styleUrls: ['./meeting-lobby.component.scss'],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
MatCardModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatIconModule,
|
|
||||||
ShareMeetingLinkComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class MeetingLobbyComponent {
|
|
||||||
/**
|
|
||||||
* The room name to display
|
|
||||||
*/
|
|
||||||
@Input({ required: true }) roomName = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The meeting URL to share
|
|
||||||
*/
|
|
||||||
@Input() meetingUrl = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the room is closed
|
|
||||||
*/
|
|
||||||
@Input() roomClosed = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the recordings card
|
|
||||||
*/
|
|
||||||
@Input() showRecordingsCard = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the share meeting link component
|
|
||||||
*/
|
|
||||||
@Input() showShareLink = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the back button
|
|
||||||
*/
|
|
||||||
@Input() showBackButton = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Back button text
|
|
||||||
*/
|
|
||||||
@Input() backButtonText = 'Back';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The participant form group
|
|
||||||
*/
|
|
||||||
@Input({ required: true }) participantForm!: FormGroup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the form is submitted
|
|
||||||
*/
|
|
||||||
@Output() formSubmitted = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the view recordings button is clicked
|
|
||||||
*/
|
|
||||||
@Output() viewRecordingsClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the back button is clicked
|
|
||||||
*/
|
|
||||||
@Output() backClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the copy link button is clicked
|
|
||||||
*/
|
|
||||||
@Output() copyLinkClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when form is submitted
|
|
||||||
* When using NgComponentOutlet, use this instead of the @Output above
|
|
||||||
*/
|
|
||||||
@Input() formSubmittedFn?: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when view recordings is clicked
|
|
||||||
*/
|
|
||||||
@Input() viewRecordingsClickedFn?: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when back button is clicked
|
|
||||||
*/
|
|
||||||
@Input() backClickedFn?: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when copy link is clicked
|
|
||||||
*/
|
|
||||||
@Input() copyLinkClickedFn?: () => void;
|
|
||||||
|
|
||||||
onFormSubmit(): void {
|
|
||||||
if (this.formSubmittedFn) {
|
|
||||||
this.formSubmittedFn();
|
|
||||||
} else {
|
|
||||||
this.formSubmitted.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecordingsClick(): void {
|
|
||||||
if (this.viewRecordingsClickedFn) {
|
|
||||||
this.viewRecordingsClickedFn();
|
|
||||||
} else {
|
|
||||||
this.viewRecordingsClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBackClick(): void {
|
|
||||||
if (this.backClickedFn) {
|
|
||||||
this.backClickedFn();
|
|
||||||
} else {
|
|
||||||
this.backClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCopyLinkClick(): void {
|
|
||||||
if (this.copyLinkClickedFn) {
|
|
||||||
this.copyLinkClickedFn();
|
|
||||||
} else {
|
|
||||||
this.copyLinkClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
<div class="participant-item-container">
|
|
||||||
<ov-participant-panel-item [participant]="participant">
|
|
||||||
<!-- Moderator Badge -->
|
|
||||||
<ng-container *ovParticipantPanelParticipantBadge>
|
|
||||||
@if (showModeratorBadge) {
|
|
||||||
<span class="moderator-badge" [attr.id]="'moderator-badge-' + participant.sid">
|
|
||||||
<mat-icon [matTooltip]="moderatorBadgeTooltip" class="material-symbols-outlined">
|
|
||||||
shield_person
|
|
||||||
</mat-icon>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Moderation Controls -->
|
|
||||||
@if (showModerationControls) {
|
|
||||||
<div
|
|
||||||
*ovParticipantPanelItemElements
|
|
||||||
class="moderation-controls"
|
|
||||||
[attr.id]="'moderation-controls-' + participant.sid"
|
|
||||||
>
|
|
||||||
<!-- Make Moderator Button -->
|
|
||||||
@if (showMakeModerator) {
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
(click)="onMakeModeratorClick()"
|
|
||||||
[matTooltip]="makeModeratorTooltip"
|
|
||||||
class="make-moderator-btn"
|
|
||||||
[attr.id]="'make-moderator-btn-' + participant.sid"
|
|
||||||
>
|
|
||||||
<mat-icon class="material-symbols-outlined">add_moderator</mat-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Unmake Moderator Button -->
|
|
||||||
@if (showUnmakeModerator) {
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
(click)="onUnmakeModeratorClick()"
|
|
||||||
[matTooltip]="unmakeModeratorTooltip"
|
|
||||||
class="remove-moderator-btn"
|
|
||||||
[attr.id]="'remove-moderator-btn-' + participant.sid"
|
|
||||||
>
|
|
||||||
<mat-icon class="material-symbols-outlined">remove_moderator</mat-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Kick Participant Button -->
|
|
||||||
@if (showKickButton) {
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
(click)="onKickParticipantClick()"
|
|
||||||
[matTooltip]="kickParticipantTooltip"
|
|
||||||
class="force-disconnect-btn"
|
|
||||||
[attr.id]="'kick-participant-btn-' + participant.sid"
|
|
||||||
>
|
|
||||||
<mat-icon>call_end</mat-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</ov-participant-panel-item>
|
|
||||||
</div>
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
@use '../../../../../../src/assets/styles/design-tokens';
|
|
||||||
|
|
||||||
.participant-item-container {
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
::ng-deep .participant-container {
|
|
||||||
padding: 2px 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moderator-badge {
|
|
||||||
color: var(--ov-meet-color-warning);
|
|
||||||
|
|
||||||
mat-icon {
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.force-disconnect-btn,
|
|
||||||
.remove-moderator-btn {
|
|
||||||
color: var(--ov-meet-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.make-moderator-btn {
|
|
||||||
color: var(--ov-meet-color-warning);
|
|
||||||
}
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
||||||
import { OpenViduComponentsUiModule } from 'openvidu-components-angular';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable component for displaying participant panel items with moderation controls.
|
|
||||||
* This component is agnostic and configurable via inputs.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ov-meeting-participant-panel',
|
|
||||||
templateUrl: './meeting-participant-panel.component.html',
|
|
||||||
styleUrls: ['./meeting-participant-panel.component.scss'],
|
|
||||||
imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule, OpenViduComponentsUiModule]
|
|
||||||
})
|
|
||||||
export class MeetingParticipantPanelComponent {
|
|
||||||
/**
|
|
||||||
* The participant to display
|
|
||||||
*/
|
|
||||||
@Input({ required: true }) participant: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All participants in the meeting (used for determining moderation controls)
|
|
||||||
*/
|
|
||||||
@Input() allParticipants: any[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the moderator badge
|
|
||||||
*/
|
|
||||||
@Input() showModeratorBadge = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show moderation controls (make/unmake moderator, kick)
|
|
||||||
*/
|
|
||||||
@Input() showModerationControls = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "make moderator" button
|
|
||||||
*/
|
|
||||||
@Input() showMakeModerator = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "unmake moderator" button
|
|
||||||
*/
|
|
||||||
@Input() showUnmakeModerator = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "kick participant" button
|
|
||||||
*/
|
|
||||||
@Input() showKickButton = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moderator badge tooltip text
|
|
||||||
*/
|
|
||||||
@Input() moderatorBadgeTooltip = 'Moderator';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make moderator button tooltip text
|
|
||||||
*/
|
|
||||||
@Input() makeModeratorTooltip = 'Make participant moderator';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unmake moderator button tooltip text
|
|
||||||
*/
|
|
||||||
@Input() unmakeModeratorTooltip = 'Unmake participant moderator';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kick participant button tooltip text
|
|
||||||
*/
|
|
||||||
@Input() kickParticipantTooltip = 'Kick participant';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the make moderator button is clicked
|
|
||||||
*/
|
|
||||||
@Output() makeModeratorClicked = new EventEmitter<any>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the unmake moderator button is clicked
|
|
||||||
*/
|
|
||||||
@Output() unmakeModeratorClicked = new EventEmitter<any>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the kick participant button is clicked
|
|
||||||
*/
|
|
||||||
@Output() kickParticipantClicked = new EventEmitter<any>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when make moderator is clicked
|
|
||||||
* When using NgComponentOutlet, use this instead of the @Output above
|
|
||||||
*/
|
|
||||||
@Input() makeModeratorClickedFn?: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when unmake moderator is clicked
|
|
||||||
*/
|
|
||||||
@Input() unmakeModeratorClickedFn?: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when kick participant is clicked
|
|
||||||
*/
|
|
||||||
@Input() kickParticipantClickedFn?: () => void;
|
|
||||||
|
|
||||||
onMakeModeratorClick(): void {
|
|
||||||
if (this.makeModeratorClickedFn) {
|
|
||||||
this.makeModeratorClickedFn();
|
|
||||||
} else {
|
|
||||||
this.makeModeratorClicked.emit(this.participant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmakeModeratorClick(): void {
|
|
||||||
if (this.unmakeModeratorClickedFn) {
|
|
||||||
this.unmakeModeratorClickedFn();
|
|
||||||
} else {
|
|
||||||
this.unmakeModeratorClicked.emit(this.participant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onKickParticipantClick(): void {
|
|
||||||
if (this.kickParticipantClickedFn) {
|
|
||||||
this.kickParticipantClickedFn();
|
|
||||||
} else {
|
|
||||||
this.kickParticipantClicked.emit(this.participant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
@if (showOverlay) {
|
|
||||||
<div id="share-link-overlay" class="main-share-meeting-link-container fade-in-delayed-more OV_big">
|
|
||||||
<ov-share-meeting-link
|
|
||||||
class="main-share-meeting-link"
|
|
||||||
[title]="title"
|
|
||||||
[subtitle]="subtitle"
|
|
||||||
[titleSize]="titleSize"
|
|
||||||
[titleWeight]="titleWeight"
|
|
||||||
[meetingUrl]="meetingUrl"
|
|
||||||
(copyClicked)="onCopyClicked()"
|
|
||||||
></ov-share-meeting-link>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
@use '../../../../../../src/assets/styles/design-tokens';
|
|
||||||
|
|
||||||
.main-share-meeting-link-container {
|
|
||||||
background-color: var(--ov-surface-color); // Use ov-components variable
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--ov-meet-radius-md);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.main-share-meeting-link {
|
|
||||||
pointer-events: all;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in-delayed-more {
|
|
||||||
animation: fadeIn 0.5s ease-in 0.9s both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
import { ShareMeetingLinkComponent } from '../share-meeting-link/share-meeting-link.component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable component for displaying the share meeting link overlay
|
|
||||||
* when there are no remote participants in the meeting.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ov-meeting-share-link-overlay',
|
|
||||||
templateUrl: './meeting-share-link-overlay.component.html',
|
|
||||||
styleUrls: ['./meeting-share-link-overlay.component.scss'],
|
|
||||||
imports: [CommonModule, ShareMeetingLinkComponent]
|
|
||||||
})
|
|
||||||
export class MeetingShareLinkOverlayComponent {
|
|
||||||
/**
|
|
||||||
* Controls whether the overlay should be shown
|
|
||||||
*/
|
|
||||||
@Input() showOverlay = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The meeting URL to share
|
|
||||||
*/
|
|
||||||
@Input({ required: true }) meetingUrl = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title text for the overlay
|
|
||||||
*/
|
|
||||||
@Input() title = 'Start collaborating';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subtitle text for the overlay
|
|
||||||
*/
|
|
||||||
@Input() subtitle = 'Share this link to bring others into the meeting';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title size (sm, md, lg, xl)
|
|
||||||
*/
|
|
||||||
@Input() titleSize: 'sm' | 'md' | 'lg' | 'xl' = 'xl';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title weight (normal, bold)
|
|
||||||
*/
|
|
||||||
@Input() titleWeight: 'normal' | 'bold' = 'bold';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the copy button is clicked
|
|
||||||
*/
|
|
||||||
@Output() copyClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when copy button is clicked
|
|
||||||
* When using NgComponentOutlet, use this instead of the @Output above
|
|
||||||
*/
|
|
||||||
@Input() copyClickedFn?: () => void;
|
|
||||||
|
|
||||||
onCopyClicked(): void {
|
|
||||||
if (this.copyClickedFn) {
|
|
||||||
this.copyClickedFn();
|
|
||||||
} else {
|
|
||||||
this.copyClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
@if (showShareLink) {
|
|
||||||
<div class="share-meeting-link-container">
|
|
||||||
<ov-share-meeting-link [meetingUrl]="meetingUrl" (copyClicked)="onCopyClicked()"></ov-share-meeting-link>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
.share-meeting-link-container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
import { ShareMeetingLinkComponent } from '../share-meeting-link/share-meeting-link.component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable component for displaying the share meeting link panel
|
|
||||||
* inside the participants panel.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ov-meeting-share-link-panel',
|
|
||||||
templateUrl: './meeting-share-link-panel.component.html',
|
|
||||||
styleUrls: ['./meeting-share-link-panel.component.scss'],
|
|
||||||
imports: [CommonModule, ShareMeetingLinkComponent]
|
|
||||||
})
|
|
||||||
export class MeetingShareLinkPanelComponent {
|
|
||||||
/**
|
|
||||||
* Controls whether the share link panel should be shown
|
|
||||||
*/
|
|
||||||
@Input() showShareLink = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The meeting URL to share
|
|
||||||
*/
|
|
||||||
@Input({ required: true }) meetingUrl = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the copy button is clicked
|
|
||||||
*/
|
|
||||||
@Output() copyClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when copy button is clicked
|
|
||||||
* When using NgComponentOutlet, use this instead of the @Output above
|
|
||||||
*/
|
|
||||||
@Input() copyClickedFn?: () => void;
|
|
||||||
|
|
||||||
onCopyClicked(): void {
|
|
||||||
if (this.copyClickedFn) {
|
|
||||||
this.copyClickedFn();
|
|
||||||
} else {
|
|
||||||
this.copyClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
<!-- Copy Link Button -->
|
|
||||||
@if (showCopyLinkButton) {
|
|
||||||
@if (isMobile) {
|
|
||||||
<button id="copy-speaker-link" mat-menu-item (click)="onCopyLinkClick()" [disableRipple]="true">
|
|
||||||
<mat-icon>link</mat-icon>
|
|
||||||
<span class="button-text">{{ copyLinkText }}</span>
|
|
||||||
</button>
|
|
||||||
} @else {
|
|
||||||
<button
|
|
||||||
id="copy-speaker-link"
|
|
||||||
mat-icon-button
|
|
||||||
(click)="onCopyLinkClick()"
|
|
||||||
[disableRipple]="true"
|
|
||||||
[matTooltip]="copyLinkTooltip"
|
|
||||||
>
|
|
||||||
<mat-icon>link</mat-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Leave Menu -->
|
|
||||||
@if (showLeaveMenu) {
|
|
||||||
<button
|
|
||||||
id="leave-btn"
|
|
||||||
mat-icon-button
|
|
||||||
[matMenuTriggerFor]="leaveMenu"
|
|
||||||
[matTooltip]="leaveMenuTooltip"
|
|
||||||
[disableRipple]="true"
|
|
||||||
class="custom-leave-btn"
|
|
||||||
[class.mobile-btn]="isMobile"
|
|
||||||
>
|
|
||||||
<mat-icon>call_end</mat-icon>
|
|
||||||
</button>
|
|
||||||
<mat-menu #leaveMenu="matMenu">
|
|
||||||
<button mat-menu-item (click)="onLeaveMeetingClick()" id="leave-option">
|
|
||||||
<mat-icon>logout</mat-icon>
|
|
||||||
<span>{{ leaveOptionText }}</span>
|
|
||||||
</button>
|
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<button mat-menu-item (click)="onEndMeetingClick()" id="end-meeting-option">
|
|
||||||
<mat-icon>no_meeting_room</mat-icon>
|
|
||||||
<span>{{ endMeetingOptionText }}</span>
|
|
||||||
</button>
|
|
||||||
</mat-menu>
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
@use '../../../../../../src/assets/styles/design-tokens';
|
|
||||||
|
|
||||||
.button-text {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global styling for leave button when in toolbar
|
|
||||||
::ng-deep {
|
|
||||||
#media-buttons-container .custom-leave-btn {
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--ov-meet-color-error) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
border-radius: var(--ov-meet-radius-md) !important;
|
|
||||||
width: 65px;
|
|
||||||
margin: 6px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable component for meeting toolbar additional buttons.
|
|
||||||
* This component is agnostic and can be configured via inputs.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ov-meeting-toolbar-buttons',
|
|
||||||
templateUrl: './meeting-toolbar-buttons.component.html',
|
|
||||||
styleUrls: ['./meeting-toolbar-buttons.component.scss'],
|
|
||||||
imports: [CommonModule, MatButtonModule, MatIconModule, MatMenuModule, MatTooltipModule, MatDividerModule]
|
|
||||||
})
|
|
||||||
export class MeetingToolbarButtonsComponent {
|
|
||||||
/**
|
|
||||||
* Whether to show the copy link button
|
|
||||||
*/
|
|
||||||
@Input() showCopyLinkButton = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the leave menu with options
|
|
||||||
*/
|
|
||||||
@Input() showLeaveMenu = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the device is mobile (affects button style)
|
|
||||||
*/
|
|
||||||
@Input() isMobile = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy link button tooltip text
|
|
||||||
*/
|
|
||||||
@Input() copyLinkTooltip = 'Copy the meeting link';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy link button text (for mobile)
|
|
||||||
*/
|
|
||||||
@Input() copyLinkText = 'Copy meeting link';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leave menu tooltip text
|
|
||||||
*/
|
|
||||||
@Input() leaveMenuTooltip = 'Leave options';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leave option text
|
|
||||||
*/
|
|
||||||
@Input() leaveOptionText = 'Leave meeting';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* End meeting option text
|
|
||||||
*/
|
|
||||||
@Input() endMeetingOptionText = 'End meeting for all';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the copy link button is clicked
|
|
||||||
*/
|
|
||||||
@Output() copyLinkClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the leave meeting option is clicked
|
|
||||||
*/
|
|
||||||
@Output() leaveMeetingClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the end meeting option is clicked
|
|
||||||
*/
|
|
||||||
@Output() endMeetingClicked = new EventEmitter<void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when copy link button is clicked
|
|
||||||
* When using NgComponentOutlet, use this instead of the @Output above
|
|
||||||
*/
|
|
||||||
@Input() copyLinkClickedFn?: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when leave meeting is clicked
|
|
||||||
* When using NgComponentOutlet, use this instead of the @Output above
|
|
||||||
*/
|
|
||||||
@Input() leaveMeetingClickedFn?: () => Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to @Output: Function to call when end meeting is clicked
|
|
||||||
* When using NgComponentOutlet, use this instead of the @Output above
|
|
||||||
*/
|
|
||||||
@Input() endMeetingClickedFn?: () => Promise<void>;
|
|
||||||
|
|
||||||
onCopyLinkClick(): void {
|
|
||||||
if (this.copyLinkClickedFn) {
|
|
||||||
this.copyLinkClickedFn();
|
|
||||||
} else {
|
|
||||||
this.copyLinkClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onLeaveMeetingClick(): Promise<void> {
|
|
||||||
if (this.leaveMeetingClickedFn) {
|
|
||||||
await this.leaveMeetingClickedFn();
|
|
||||||
} else {
|
|
||||||
this.leaveMeetingClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onEndMeetingClick(): Promise<void> {
|
|
||||||
if (this.endMeetingClickedFn) {
|
|
||||||
await this.endMeetingClickedFn();
|
|
||||||
} else {
|
|
||||||
this.endMeetingClicked.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { InjectionToken, Type } from '@angular/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for registering custom components to be used in the meeting view.
|
|
||||||
* Each property represents a slot where a custom component can be injected.
|
|
||||||
*/
|
|
||||||
export interface MeetingComponentsPlugins {
|
|
||||||
/**
|
|
||||||
* Toolbar-related plugin components
|
|
||||||
*/
|
|
||||||
toolbar?: {
|
|
||||||
/**
|
|
||||||
* Additional buttons to show in the toolbar (e.g., copy link, settings)
|
|
||||||
*/
|
|
||||||
additionalButtons?: Type<any>;
|
|
||||||
/**
|
|
||||||
* Custom leave button component (only shown for moderators)
|
|
||||||
*/
|
|
||||||
leaveButton?: Type<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Participant panel-related plugin components
|
|
||||||
*/
|
|
||||||
participantPanel?: {
|
|
||||||
/**
|
|
||||||
* Custom component to render each participant item in the panel
|
|
||||||
*/
|
|
||||||
item?: Type<any>;
|
|
||||||
/**
|
|
||||||
* Component to show after the local participant in the panel
|
|
||||||
*/
|
|
||||||
afterLocalParticipant?: Type<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Layout-related plugin components
|
|
||||||
*/
|
|
||||||
layout?: {
|
|
||||||
/**
|
|
||||||
* Additional elements to show in the main layout (e.g., overlays, banners)
|
|
||||||
*/
|
|
||||||
additionalElements?: Type<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lobby-related plugin components
|
|
||||||
*/
|
|
||||||
lobby?: Type<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection token for registering meeting plugins.
|
|
||||||
* Apps (CE/PRO) should provide their custom components using this token.
|
|
||||||
*/
|
|
||||||
export const MEETING_COMPONENTS_TOKEN = new InjectionToken<MeetingComponentsPlugins>('MEETING_COMPONENTS_TOKEN');
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import { InjectionToken } from '@angular/core';
|
|
||||||
import { CustomParticipantModel } from '../../models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface defining the controls to show for a participant in the participant panel.
|
|
||||||
*/
|
|
||||||
export interface ParticipantControls {
|
|
||||||
/**
|
|
||||||
* Whether to show the moderator badge
|
|
||||||
*/
|
|
||||||
showModeratorBadge: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show moderation controls (make/unmake moderator, kick)
|
|
||||||
*/
|
|
||||||
showModerationControls: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "Make Moderator" button
|
|
||||||
*/
|
|
||||||
showMakeModerator: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "Remove Moderator" button
|
|
||||||
*/
|
|
||||||
showUnmakeModerator: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "Kick" button
|
|
||||||
*/
|
|
||||||
showKickButton: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class defining the actions that can be performed in a meeting.
|
|
||||||
* Apps (CE/PRO) must extend this class and provide their implementation.
|
|
||||||
*/
|
|
||||||
export abstract class MeetingActionHandler {
|
|
||||||
/**
|
|
||||||
* Room ID - will be set by MeetingComponent
|
|
||||||
*/
|
|
||||||
roomId = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Room secret - will be set by MeetingComponent
|
|
||||||
*/
|
|
||||||
roomSecret = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Local participant - will be set by MeetingComponent
|
|
||||||
*/
|
|
||||||
localParticipant?: CustomParticipantModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kicks a participant from the meeting
|
|
||||||
*/
|
|
||||||
abstract kickParticipant(participant: CustomParticipantModel): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a participant a moderator
|
|
||||||
*/
|
|
||||||
abstract makeModerator(participant: CustomParticipantModel): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes moderator role from a participant
|
|
||||||
*/
|
|
||||||
abstract unmakeModerator(participant: CustomParticipantModel): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the moderator link to clipboard
|
|
||||||
*/
|
|
||||||
abstract copyModeratorLink(): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the speaker link to clipboard
|
|
||||||
*/
|
|
||||||
abstract copySpeakerLink(): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the controls to show for a participant based on permissions and roles
|
|
||||||
*/
|
|
||||||
abstract getParticipantControls(participant: CustomParticipantModel): ParticipantControls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection token for the meeting action handler.
|
|
||||||
* Apps (CE/PRO) should provide their implementation using this token.
|
|
||||||
*/
|
|
||||||
export const MEETING_ACTION_HANDLER_TOKEN = new InjectionToken<MeetingActionHandler>('MEETING_ACTION_HANDLER_TOKEN');
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* Index file for customization exports
|
|
||||||
*/
|
|
||||||
export * from './components/meeting-components-plugins.token';
|
|
||||||
export * from './handlers/meeting-action-handler';
|
|
||||||
@ -4,4 +4,3 @@ export * from './navigation.model';
|
|||||||
export * from './notification.model';
|
export * from './notification.model';
|
||||||
export * from './sidenav.model';
|
export * from './sidenav.model';
|
||||||
export * from './wizard.model';
|
export * from './wizard.model';
|
||||||
export * from './lobby.model';
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { FormGroup } from '@angular/forms';
|
|
||||||
import { MeetRoom } from '@openvidu-meet/typings';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State interface representing the lobby state of a meeting
|
|
||||||
*/
|
|
||||||
export interface LobbyState {
|
|
||||||
room?: MeetRoom;
|
|
||||||
roomId: string;
|
|
||||||
roomSecret: string;
|
|
||||||
roomClosed: boolean;
|
|
||||||
hasRecordings: boolean;
|
|
||||||
showRecordingCard: boolean;
|
|
||||||
showBackButton: boolean;
|
|
||||||
backButtonText: string;
|
|
||||||
participantForm: FormGroup;
|
|
||||||
participantToken: string;
|
|
||||||
}
|
|
||||||
@ -13,10 +13,6 @@ export interface DialogOptions {
|
|||||||
forceCheckboxText?: string;
|
forceCheckboxText?: string;
|
||||||
forceCheckboxDescription?: string;
|
forceCheckboxDescription?: string;
|
||||||
forceConfirmCallback?: () => void;
|
forceConfirmCallback?: () => void;
|
||||||
// Action buttons visibility
|
|
||||||
showConfirmButton?: boolean;
|
|
||||||
showCancelButton?: boolean;
|
|
||||||
showActions?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteRoomDialogOptions {
|
export interface DeleteRoomDialogOptions {
|
||||||
|
|||||||
@ -1,19 +1,4 @@
|
|||||||
@if (showPrejoin) {
|
@if (showMeeting) {
|
||||||
<!-- Prejoin screen (Lobby) -->
|
|
||||||
@if (prejoinReady && plugins.lobby) {
|
|
||||||
<ng-container [ngComponentOutlet]="plugins.lobby" [ngComponentOutletInputs]="lobbyInputs()"> </ng-container>
|
|
||||||
} @else if (!prejoinReady) {
|
|
||||||
<div class="prejoin-loading-container">
|
|
||||||
<mat-spinner diameter="30"></mat-spinner>
|
|
||||||
<p class="prejoin-loading-text">Preparing your meeting...</p>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div class="prejoin-error-container">
|
|
||||||
<mat-icon class="prejoin-error-icon">error_outline</mat-icon>
|
|
||||||
<p class="prejoin-error-text">Unable to load the pre-join screen. Please try reloading the page.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
<ov-videoconference
|
<ov-videoconference
|
||||||
[token]="participantToken"
|
[token]="participantToken"
|
||||||
[prejoin]="true"
|
[prejoin]="true"
|
||||||
@ -48,62 +33,305 @@
|
|||||||
[showThemeSelector]="features().showThemeSelector"
|
[showThemeSelector]="features().showThemeSelector"
|
||||||
[showDisconnectionDialog]="false"
|
[showDisconnectionDialog]="false"
|
||||||
(onRoomCreated)="onRoomCreated($event)"
|
(onRoomCreated)="onRoomCreated($event)"
|
||||||
(onParticipantConnected)="eventHandler.onParticipantConnected($event)"
|
(onParticipantConnected)="onParticipantConnected($event)"
|
||||||
(onParticipantLeft)="eventHandler.onParticipantLeft($event)"
|
(onParticipantLeft)="onParticipantLeft($event)"
|
||||||
(onRecordingStartRequested)="eventHandler.onRecordingStartRequested($event)"
|
(onRecordingStartRequested)="onRecordingStartRequested($event)"
|
||||||
(onRecordingStopRequested)="eventHandler.onRecordingStopRequested($event)"
|
(onRecordingStopRequested)="onRecordingStopRequested($event)"
|
||||||
(onViewRecordingsClicked)="onViewRecordingsClicked()"
|
(onViewRecordingsClicked)="onViewRecordingsClicked()"
|
||||||
>
|
>
|
||||||
<!-- Toolbar Additional Buttons Plugin -->
|
|
||||||
@if (plugins.toolbar?.additionalButtons) {
|
|
||||||
<ng-container *ovToolbarAdditionalButtons>
|
<ng-container *ovToolbarAdditionalButtons>
|
||||||
<ng-container
|
<!-- Copy Link Button -->
|
||||||
[ngComponentOutlet]="plugins.toolbar!.additionalButtons!"
|
@if (features().canModerateRoom) {
|
||||||
[ngComponentOutletInputs]="toolbarAdditionalButtonsInputs()"
|
@if (isMobile) {
|
||||||
></ng-container>
|
<button id="copy-speaker-link" mat-menu-item (click)="copySpeakerLink()" [disableRipple]="true">
|
||||||
</ng-container>
|
<mat-icon>link</mat-icon>
|
||||||
|
<span class="button-text">Copy meeting link</span>
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
id="copy-speaker-link"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="copySpeakerLink()"
|
||||||
|
[disableRipple]="true"
|
||||||
|
matTooltip="Copy the meeting link"
|
||||||
|
>
|
||||||
|
<mat-icon>link</mat-icon>
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Toolbar Leave Button Plugin -->
|
|
||||||
@if (plugins.toolbar?.leaveButton) {
|
|
||||||
<ng-container *ovToolbarLeaveButton>
|
<ng-container *ovToolbarLeaveButton>
|
||||||
<ng-container
|
@if (features().canModerateRoom) {
|
||||||
[ngComponentOutlet]="plugins.toolbar!.leaveButton!"
|
<!-- Leave Button -->
|
||||||
[ngComponentOutletInputs]="toolbarLeaveButtonInputs()"
|
<button
|
||||||
></ng-container>
|
id="leave-btn"
|
||||||
</ng-container>
|
mat-icon-button
|
||||||
|
[matMenuTriggerFor]="leaveMenu"
|
||||||
|
matTooltip="Leave options"
|
||||||
|
[disableRipple]="true"
|
||||||
|
class="custom-leave-btn"
|
||||||
|
[class.mobile-btn]="isMobile"
|
||||||
|
>
|
||||||
|
<mat-icon>call_end</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #leaveMenu="matMenu">
|
||||||
|
<button mat-menu-item (click)="leaveMeeting()" id="leave-option">
|
||||||
|
<mat-icon>logout</mat-icon>
|
||||||
|
<span>Leave meeting</span>
|
||||||
|
</button>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<button mat-menu-item (click)="endMeeting()" id="end-meeting-option">
|
||||||
|
<mat-icon>no_meeting_room</mat-icon>
|
||||||
|
<span>End meeting for all</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
}
|
}
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Participant Panel After Local Participant Plugin -->
|
|
||||||
@if (plugins.participantPanel?.afterLocalParticipant) {
|
|
||||||
<ng-container *ovParticipantPanelAfterLocalParticipant>
|
<ng-container *ovParticipantPanelAfterLocalParticipant>
|
||||||
<ng-container
|
@if (features().canModerateRoom) {
|
||||||
[ngComponentOutlet]="plugins.participantPanel!.afterLocalParticipant!"
|
<div class="share-meeting-link-container">
|
||||||
[ngComponentOutletInputs]="participantPanelAfterLocalInputs()"
|
<ov-share-meeting-link
|
||||||
></ng-container>
|
[meetingUrl]="hostname + '/room/' + roomId"
|
||||||
</ng-container>
|
(copyClicked)="copySpeakerLink()"
|
||||||
|
></ov-share-meeting-link>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Layout Additional Elements Plugin -->
|
|
||||||
@if (plugins.layout?.additionalElements) {
|
|
||||||
<ng-container *ovLayoutAdditionalElements>
|
<ng-container *ovLayoutAdditionalElements>
|
||||||
@if (onlyModeratorIsPresent) {
|
@if (features().canModerateRoom && remoteParticipants.length === 0) {
|
||||||
<ng-container
|
<div class="main-share-meeting-link-container fade-in-delayed-more OV_big">
|
||||||
[ngComponentOutlet]="plugins.layout!.additionalElements!"
|
<ov-share-meeting-link
|
||||||
[ngComponentOutletInputs]="layoutAdditionalElementsInputs()"
|
class="main-share-meeting-link"
|
||||||
></ng-container>
|
[title]="'Start collaborating'"
|
||||||
|
[subtitle]="'Share this link to bring others into the meeting'"
|
||||||
|
[titleSize]="'xl'"
|
||||||
|
[titleWeight]="'bold'"
|
||||||
|
[meetingUrl]="hostname + '/room/' + roomId"
|
||||||
|
(copyClicked)="copySpeakerLink()"
|
||||||
|
></ov-share-meeting-link>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ovParticipantPanelItem="let participant">
|
||||||
|
<!-- If Meet participant is moderator -->
|
||||||
|
@if (features().canModerateRoom) {
|
||||||
|
<div class="participant-item-container">
|
||||||
|
<!-- Local participant -->
|
||||||
|
@if (participant.isLocal) {
|
||||||
|
<ov-participant-panel-item [participant]="participant">
|
||||||
|
<ng-container *ovParticipantPanelParticipantBadge>
|
||||||
|
<span class="moderator-badge">
|
||||||
|
<mat-icon matTooltip="Moderator" class="material-symbols-outlined">
|
||||||
|
shield_person
|
||||||
|
</mat-icon>
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
</ov-participant-panel-item>
|
||||||
|
} @else {
|
||||||
|
<!-- Remote participant -->
|
||||||
|
<ov-participant-panel-item [participant]="participant">
|
||||||
|
@if (participant.isModerator()) {
|
||||||
|
<ng-container *ovParticipantPanelParticipantBadge>
|
||||||
|
<span class="moderator-badge">
|
||||||
|
<mat-icon matTooltip="Moderator" class="material-symbols-outlined">
|
||||||
|
shield_person
|
||||||
|
</mat-icon>
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
}
|
||||||
|
<div *ovParticipantPanelItemElements>
|
||||||
|
<!-- Button to make moderator if not -->
|
||||||
|
@if (localParticipant!.isOriginalModerator()) {
|
||||||
|
@if (participant.isModerator() && !participant.isOriginalModerator()) {
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="unmakeModerator(participant)"
|
||||||
|
matTooltip="Unmake participant moderator"
|
||||||
|
class="remove-moderator-btn"
|
||||||
|
>
|
||||||
|
<mat-icon class="material-symbols-outlined">remove_moderator</mat-icon>
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
@if (!participant.isModerator()) {
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="makeModerator(participant)"
|
||||||
|
matTooltip="Make participant moderator"
|
||||||
|
class="make-moderator-btn"
|
||||||
|
>
|
||||||
|
<mat-icon class="material-symbols-outlined">add_moderator</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
@if (!participant.isModerator()) {
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="makeModerator(participant)"
|
||||||
|
matTooltip="Make participant moderator"
|
||||||
|
class="make-moderator-btn"
|
||||||
|
>
|
||||||
|
<mat-icon class="material-symbols-outlined">add_moderator</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Participant Panel Item Plugin -->
|
<!-- Button to kick participant -->
|
||||||
@if (plugins.participantPanel?.item) {
|
@if (!participant.isOriginalModerator()) {
|
||||||
<ng-container *ovParticipantPanelItem="let participant">
|
<button
|
||||||
<ng-container
|
mat-icon-button
|
||||||
[ngComponentOutlet]="plugins.participantPanel!.item!"
|
(click)="kickParticipant(participant)"
|
||||||
[ngComponentOutletInputs]="participantPanelItemInputsMap().get(participant.identity)"
|
matTooltip="Kick participant"
|
||||||
></ng-container>
|
class="force-disconnect-btn"
|
||||||
|
>
|
||||||
|
<mat-icon>call_end</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ov-participant-panel-item>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<!-- If I can't moderate the room -->
|
||||||
|
<div class="participant-item-container">
|
||||||
|
<ov-participant-panel-item [participant]="participant">
|
||||||
|
@if (participant.isModerator()) {
|
||||||
|
<ng-container *ovParticipantPanelParticipantBadge>
|
||||||
|
<span class="moderator-badge">
|
||||||
|
<mat-icon matTooltip="Moderator" class="material-symbols-outlined">
|
||||||
|
shield_person
|
||||||
|
</mat-icon>
|
||||||
|
</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
}
|
||||||
</ov-videoconference>
|
</ov-participant-panel-item>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
</ov-videoconference>
|
||||||
|
} @else {
|
||||||
|
<!-- Move this logic to lobby meeting page -->
|
||||||
|
<div class="ov-page-container">
|
||||||
|
<div class="room-access-container fade-in">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="room-header">
|
||||||
|
<mat-icon class="ov-room-icon room-icon">video_chat</mat-icon>
|
||||||
|
<div class="room-info">
|
||||||
|
<h1 class="room-title">{{ roomName }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Cards Grid -->
|
||||||
|
<div class="action-cards-grid">
|
||||||
|
<!-- Join Room Card -->
|
||||||
|
<mat-card class="action-card primary-card fade-in" [ngClass]="{ 'room-closed-card': roomClosed }">
|
||||||
|
<mat-card-header class="card-header">
|
||||||
|
<mat-icon class="ov-room-icon card-icon">{{ roomClosed ? 'lock' : 'meeting_room' }}</mat-icon>
|
||||||
|
<div class="card-title-group">
|
||||||
|
<mat-card-title>{{ roomClosed ? 'Room Closed' : 'Join Meeting' }}</mat-card-title>
|
||||||
|
<mat-card-subtitle>{{
|
||||||
|
roomClosed
|
||||||
|
? 'This room is not available for meetings'
|
||||||
|
: 'Enter the room and start connecting'
|
||||||
|
}}</mat-card-subtitle>
|
||||||
|
</div>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content class="card-content">
|
||||||
|
@if (!roomClosed) {
|
||||||
|
<form [formGroup]="participantForm" (ngSubmit)="submitAccessMeeting()" class="join-form">
|
||||||
|
<mat-form-field appearance="outline" class="name-field">
|
||||||
|
<mat-label>Your display name</mat-label>
|
||||||
|
<input
|
||||||
|
id="participant-name-input"
|
||||||
|
matInput
|
||||||
|
placeholder="Enter your name"
|
||||||
|
formControlName="name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<mat-icon matSuffix class="ov-action-icon">person</mat-icon>
|
||||||
|
@if (participantForm.get('name')?.hasError('required')) {
|
||||||
|
<mat-error> The name is <strong>required</strong> </mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
id="participant-name-submit"
|
||||||
|
type="submit"
|
||||||
|
class="join-button"
|
||||||
|
[disabled]="participantForm.invalid"
|
||||||
|
>
|
||||||
|
<span>Join Meeting</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
} @else {
|
||||||
|
<div class="room-closed-message">
|
||||||
|
<mat-icon class="warning-icon">warning</mat-icon>
|
||||||
|
<p>
|
||||||
|
Sorry, this room is closed. You cannot join at this time. Please contact the meeting
|
||||||
|
organizer for more information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- View Recordings Card -->
|
||||||
|
@if (showRecordingCard) {
|
||||||
|
<mat-card class="action-card secondary-card fade-in-delayed">
|
||||||
|
<mat-card-header class="card-header">
|
||||||
|
<mat-icon class="ov-recording-icon card-icon">video_library</mat-icon>
|
||||||
|
<div class="card-title-group">
|
||||||
|
<mat-card-title>View Recordings</mat-card-title>
|
||||||
|
<mat-card-subtitle>Browse and manage past recordings</mat-card-subtitle>
|
||||||
|
</div>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content class="card-content">
|
||||||
|
<div class="recordings-info">
|
||||||
|
<p class="recordings-description">
|
||||||
|
Access previously recorded meetings from this room. You can watch, download, or
|
||||||
|
manage existing recordings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="view-recordings-btn"
|
||||||
|
mat-stroked-button
|
||||||
|
color="accent"
|
||||||
|
(click)="goToRecordings()"
|
||||||
|
class="recordings-button"
|
||||||
|
>
|
||||||
|
<span>Browse Recordings</span>
|
||||||
|
</button>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Room URL Badge -->
|
||||||
|
@if (!roomClosed && features().canModerateRoom) {
|
||||||
|
<ov-share-meeting-link
|
||||||
|
[meetingUrl]="hostname + '/room/' + roomId"
|
||||||
|
(copyClicked)="copySpeakerLink()"
|
||||||
|
></ov-share-meeting-link>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
@if (showBackButton) {
|
||||||
|
<div class="quick-actions fade-in-delayed-more">
|
||||||
|
<button mat-button class="quick-action-button" (click)="goBack()">
|
||||||
|
<mat-icon>arrow_back</mat-icon>
|
||||||
|
<span>{{ backButtonText }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,327 @@
|
|||||||
@use '../../../../../../src/assets/styles/design-tokens';
|
@use '../../../../../../src/assets/styles/design-tokens';
|
||||||
|
|
||||||
.prejoin-loading-container,
|
// Room Access Container - Main layout using design tokens
|
||||||
.prejoin-error-container {
|
.room-access-container {
|
||||||
|
@include design-tokens.ov-container;
|
||||||
|
@include design-tokens.ov-page-content;
|
||||||
|
padding-top: var(--ov-meet-spacing-xxl);
|
||||||
|
background: var(--ov-meet-background-color);
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room Header - Clean title section
|
||||||
|
.room-header {
|
||||||
|
@include design-tokens.ov-flex-center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ov-meet-spacing-md);
|
||||||
|
margin-bottom: var(--ov-meet-spacing-xxl);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.room-icon {
|
||||||
|
@include design-tokens.ov-icon(xl);
|
||||||
|
color: var(--ov-meet-icon-rooms);
|
||||||
|
margin-bottom: var(--ov-meet-spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-info {
|
||||||
|
.room-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--ov-meet-font-size-hero);
|
||||||
|
font-weight: var(--ov-meet-font-weight-light);
|
||||||
|
color: var(--ov-meet-text-primary);
|
||||||
|
line-height: var(--ov-meet-line-height-tight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Cards Grid - Responsive layout
|
||||||
|
.action-cards-grid {
|
||||||
|
@include design-tokens.ov-grid-responsive(320px);
|
||||||
|
gap: var(--ov-meet-spacing-xl);
|
||||||
|
margin-bottom: var(--ov-meet-spacing-xxl);
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
// When there's only one card, limit its width to maintain visual consistency
|
||||||
|
&:has(.action-card:only-child) {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
.action-card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include design-tokens.ov-tablet-down {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--ov-meet-spacing-lg);
|
||||||
|
|
||||||
|
// On tablets and mobile, single cards should use full width
|
||||||
|
&:has(.action-card:only-child) {
|
||||||
|
.action-card {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Card Base - Consistent card styling
|
||||||
|
.action-card {
|
||||||
|
@include design-tokens.ov-card;
|
||||||
|
@include design-tokens.ov-hover-lift(-4px);
|
||||||
|
@include design-tokens.ov-theme-transition;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
// Card Header
|
||||||
|
.card-header {
|
||||||
|
padding: var(--ov-meet-spacing-lg);
|
||||||
|
border-bottom: 1px solid var(--ov-meet-border-color-light);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ov-meet-spacing-md);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
@include design-tokens.ov-icon(lg);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-group {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.mat-mdc-card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--ov-meet-font-size-xl);
|
||||||
|
font-weight: var(--ov-meet-font-weight-semibold);
|
||||||
|
color: var(--ov-meet-text-primary);
|
||||||
|
line-height: var(--ov-meet-line-height-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-card-subtitle {
|
||||||
|
margin: var(--ov-meet-spacing-xs) 0 0 0;
|
||||||
|
font-size: var(--ov-meet-font-size-sm);
|
||||||
|
color: var(--ov-meet-text-secondary);
|
||||||
|
line-height: var(--ov-meet-line-height-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card Content
|
||||||
|
.card-content {
|
||||||
|
padding: var(--ov-meet-spacing-lg);
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary Card - Join meeting styling
|
||||||
|
.primary-card {
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(135deg, var(--ov-meet-surface-color) 0%, var(--ov-meet-color-primary-light) 180%);
|
||||||
|
color: var(--ov-meet-text-on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.room-closed-card {
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(135deg, var(--ov-meet-surface-color) 0%, var(--ov-meet-color-warning) 180%);
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
color: var(--ov-meet-color-warning) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-closed-message {
|
||||||
|
@include design-tokens.ov-flex-center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ov-meet-spacing-md);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
@include design-tokens.ov-icon(xl);
|
||||||
|
color: var(--ov-meet-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--ov-meet-font-size-md);
|
||||||
|
color: var(--ov-meet-text-secondary);
|
||||||
|
line-height: var(--ov-meet-line-height-relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary Card - Recordings styling
|
||||||
|
.secondary-card {
|
||||||
|
.card-header {
|
||||||
|
background: linear-gradient(135deg, var(--ov-meet-surface-color) 0%, var(--ov-meet-color-accent) 180%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join Form - Form styling
|
||||||
|
.join-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ov-meet-spacing-lg);
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.name-field {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.mat-mdc-form-field-icon-suffix {
|
||||||
|
color: var(--ov-meet-text-hint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-button {
|
||||||
|
@include design-tokens.ov-button-base;
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--ov-meet-spacing-sm);
|
||||||
|
margin-top: auto;
|
||||||
|
background-color: var(--ov-meet-color-secondary);
|
||||||
|
color: var(--ov-meet-text-on-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recordings Info - Content for recordings card
|
||||||
|
.recordings-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ov-meet-spacing-lg);
|
||||||
|
|
||||||
|
.recordings-description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--ov-meet-font-size-md);
|
||||||
|
color: var(--ov-meet-text-secondary);
|
||||||
|
line-height: var(--ov-meet-line-height-relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recordings-button {
|
||||||
|
@include design-tokens.ov-button-base;
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--ov-meet-spacing-sm);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Actions - Footer actions
|
||||||
|
.quick-actions {
|
||||||
|
@include design-tokens.ov-flex-center;
|
||||||
|
margin-top: var(--ov-meet-spacing-xl);
|
||||||
|
|
||||||
|
.quick-action-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ov-meet-spacing-sm);
|
||||||
|
color: var(--ov-meet-text-secondary);
|
||||||
|
@include design-tokens.ov-theme-transition;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--ov-meet-text-primary);
|
||||||
|
background-color: var(--ov-meet-surface-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-meeting-link-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-share-meeting-link-container {
|
||||||
|
background-color: var(--ov-surface-color); // Use ov-components variable
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--ov-meet-radius-md);
|
||||||
|
|
||||||
|
.main-share-meeting-link {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive adjustments
|
||||||
|
@include design-tokens.ov-mobile-down {
|
||||||
|
.room-access-container {
|
||||||
|
padding: 0;
|
||||||
|
padding-top: var(--ov-meet-spacing-sm);
|
||||||
|
margin-bottom: var(--ov-meet-spacing-xxl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-header {
|
||||||
|
margin-bottom: var(--ov-meet-spacing-xl);
|
||||||
|
|
||||||
|
.room-info .room-title {
|
||||||
|
font-size: var(--ov-meet-font-size-xxl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
min-height: auto;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: var(--ov-meet-spacing-md);
|
||||||
|
|
||||||
|
.card-title-group {
|
||||||
|
.mat-mdc-card-title {
|
||||||
|
font-size: var(--ov-meet-font-size-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: var(--ov-meet-spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom leave button styling (existing functionality)
|
||||||
|
::ng-deep {
|
||||||
|
#media-buttons-container .custom-leave-btn {
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--ov-meet-color-error) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-radius: var(--ov-meet-radius-md) !important;
|
||||||
|
width: 65px;
|
||||||
|
margin: 6px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.force-disconnect-btn,
|
||||||
|
.remove-moderator-btn {
|
||||||
|
color: var(--ov-meet-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.make-moderator-btn {
|
||||||
|
color: var(--ov-meet-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-item-container {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
::ng-deep .participant-container {
|
||||||
|
padding: 2px 10px !important;
|
||||||
|
}
|
||||||
|
.moderator-badge {
|
||||||
|
color: var(--ov-meet-color-warning);
|
||||||
|
mat-icon {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,65 @@
|
|||||||
import { Clipboard } from '@angular/cdk/clipboard';
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
import { CommonModule, NgComponentOutlet } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, computed, effect, inject, OnInit, Signal, signal } from '@angular/core';
|
import { Component, effect, OnInit, Signal } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { CustomParticipantModel } from '../../models';
|
import { MatButtonModule, MatIconButton } from '@angular/material/button';
|
||||||
import { MeetingComponentsPlugins, MEETING_COMPONENTS_TOKEN, MEETING_ACTION_HANDLER_TOKEN } from '../../customization';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatRippleModule } from '@angular/material/core';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ShareMeetingLinkComponent } from '../../components';
|
||||||
|
import { CustomParticipantModel, ErrorReason } from '../../models';
|
||||||
import {
|
import {
|
||||||
|
AppDataService,
|
||||||
ApplicationFeatures,
|
ApplicationFeatures,
|
||||||
|
AuthService,
|
||||||
FeatureConfigurationService,
|
FeatureConfigurationService,
|
||||||
GlobalConfigService,
|
GlobalConfigService,
|
||||||
MeetingService,
|
MeetingService,
|
||||||
|
NavigationService,
|
||||||
NotificationService,
|
NotificationService,
|
||||||
ParticipantService,
|
ParticipantService,
|
||||||
WebComponentManagerService,
|
RecordingService,
|
||||||
MeetingEventHandlerService
|
RoomService,
|
||||||
|
SessionStorageService,
|
||||||
|
TokenStorageService,
|
||||||
|
WebComponentManagerService
|
||||||
} from '../../services';
|
} from '../../services';
|
||||||
import { MeetRoom, ParticipantRole } from '@openvidu-meet/typings';
|
import {
|
||||||
|
LeftEventReason,
|
||||||
|
MeetRoom,
|
||||||
|
MeetRoomStatus,
|
||||||
|
ParticipantRole,
|
||||||
|
WebComponentEvent,
|
||||||
|
WebComponentOutboundEventMessage,
|
||||||
|
MeetParticipantRoleUpdatedPayload,
|
||||||
|
MeetRoomConfigUpdatedPayload,
|
||||||
|
MeetSignalType
|
||||||
|
} from '@openvidu-meet/typings';
|
||||||
import {
|
import {
|
||||||
ParticipantService as ComponentParticipantService,
|
ParticipantService as ComponentParticipantService,
|
||||||
|
DataPacket_Kind,
|
||||||
OpenViduComponentsUiModule,
|
OpenViduComponentsUiModule,
|
||||||
OpenViduService,
|
OpenViduService,
|
||||||
OpenViduThemeMode,
|
OpenViduThemeMode,
|
||||||
OpenViduThemeService,
|
OpenViduThemeService,
|
||||||
|
ParticipantLeftEvent,
|
||||||
|
ParticipantLeftReason,
|
||||||
|
ParticipantModel,
|
||||||
|
RecordingStartRequestedEvent,
|
||||||
|
RecordingStopRequestedEvent,
|
||||||
|
RemoteParticipant,
|
||||||
Room,
|
Room,
|
||||||
|
RoomEvent,
|
||||||
Track,
|
Track,
|
||||||
ViewportService
|
ViewportService
|
||||||
} from 'openvidu-components-angular';
|
} from 'openvidu-components-angular';
|
||||||
import { combineLatest, Subject, takeUntil } from 'rxjs';
|
import { combineLatest, Subject, takeUntil } from 'rxjs';
|
||||||
import { MeetingLobbyService } from '../../services/meeting/meeting-lobby.service';
|
|
||||||
import { MeetingPluginManagerService } from '../../services/meeting/meeting-plugin-manager.service';
|
|
||||||
import { LobbyState } from '../../models/lobby.model';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ov-meeting',
|
selector: 'ov-meeting',
|
||||||
@ -38,56 +67,71 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|||||||
styleUrls: ['./meeting.component.scss'],
|
styleUrls: ['./meeting.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
OpenViduComponentsUiModule,
|
OpenViduComponentsUiModule,
|
||||||
|
// ApiDirectiveModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgComponentOutlet,
|
MatCardModule,
|
||||||
|
MatButtonModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatProgressSpinnerModule
|
MatIconButton,
|
||||||
],
|
MatMenuModule,
|
||||||
providers: [MeetingLobbyService, MeetingPluginManagerService, MeetingEventHandlerService]
|
MatDividerModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatRippleModule,
|
||||||
|
ShareMeetingLinkComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class MeetingComponent implements OnInit {
|
export class MeetingComponent implements OnInit {
|
||||||
lobbyState?: LobbyState;
|
participantForm = new FormGroup({
|
||||||
protected localParticipant = signal<CustomParticipantModel | undefined>(undefined);
|
name: new FormControl('', [Validators.required])
|
||||||
|
});
|
||||||
|
|
||||||
// Reactive signal for remote participants to trigger computed updates
|
hasRecordings = false;
|
||||||
protected remoteParticipants = signal<CustomParticipantModel[]>([]);
|
showRecordingCard = false;
|
||||||
|
roomClosed = false;
|
||||||
|
|
||||||
// Signal to track participant updates (role changes, etc.) that don't change array references
|
showBackButton = true;
|
||||||
protected participantsVersion = signal<number>(0);
|
backButtonText = 'Back';
|
||||||
|
|
||||||
showPrejoin = true;
|
room?: MeetRoom;
|
||||||
prejoinReady = false;
|
roomId = '';
|
||||||
|
roomSecret = '';
|
||||||
|
participantName = '';
|
||||||
|
participantToken = '';
|
||||||
|
localParticipant?: CustomParticipantModel;
|
||||||
|
remoteParticipants: CustomParticipantModel[] = [];
|
||||||
|
|
||||||
|
showMeeting = false;
|
||||||
features: Signal<ApplicationFeatures>;
|
features: Signal<ApplicationFeatures>;
|
||||||
|
meetingEndedByMe = false;
|
||||||
|
|
||||||
// Injected plugins
|
private destroy$ = new Subject<void>();
|
||||||
plugins: MeetingComponentsPlugins;
|
|
||||||
|
|
||||||
protected meetingService = inject(MeetingService);
|
constructor(
|
||||||
protected participantService = inject(ParticipantService);
|
protected route: ActivatedRoute,
|
||||||
protected featureConfService = inject(FeatureConfigurationService);
|
protected roomService: RoomService,
|
||||||
protected wcManagerService = inject(WebComponentManagerService);
|
protected meetingService: MeetingService,
|
||||||
protected openviduService = inject(OpenViduService);
|
protected participantService: ParticipantService,
|
||||||
protected ovComponentsParticipantService = inject(ComponentParticipantService);
|
protected recordingService: RecordingService,
|
||||||
protected viewportService = inject(ViewportService);
|
protected featureConfService: FeatureConfigurationService,
|
||||||
protected ovThemeService = inject(OpenViduThemeService);
|
protected authService: AuthService,
|
||||||
protected configService = inject(GlobalConfigService);
|
protected appDataService: AppDataService,
|
||||||
protected clipboard = inject(Clipboard);
|
protected sessionStorageService: SessionStorageService,
|
||||||
protected notificationService = inject(NotificationService);
|
protected wcManagerService: WebComponentManagerService,
|
||||||
protected lobbyService = inject(MeetingLobbyService);
|
protected openviduService: OpenViduService,
|
||||||
protected pluginManager = inject(MeetingPluginManagerService);
|
protected ovComponentsParticipantService: ComponentParticipantService,
|
||||||
|
protected navigationService: NavigationService,
|
||||||
// Public for direct template binding (uses arrow functions to preserve 'this' context)
|
protected notificationService: NotificationService,
|
||||||
public eventHandler = inject(MeetingEventHandlerService);
|
protected clipboard: Clipboard,
|
||||||
|
protected viewportService: ViewportService,
|
||||||
// Injected action handler (optional - falls back to default implementation)
|
protected ovThemeService: OpenViduThemeService,
|
||||||
protected actionHandler = inject(MEETING_ACTION_HANDLER_TOKEN, { optional: true });
|
protected configService: GlobalConfigService,
|
||||||
protected destroy$ = new Subject<void>();
|
protected tokenStorageService: TokenStorageService
|
||||||
|
) {
|
||||||
constructor() {
|
|
||||||
this.features = this.featureConfService.features;
|
this.features = this.featureConfService.features;
|
||||||
this.plugins = inject(MEETING_COMPONENTS_TOKEN, { optional: true }) || {};
|
|
||||||
|
|
||||||
// Change theme variables when custom theme is enabled
|
// Change theme variables when custom theme is enabled
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@ -107,125 +151,8 @@ export class MeetingComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed signals for plugin inputs
|
|
||||||
protected toolbarAdditionalButtonsInputs = computed(() =>
|
|
||||||
this.pluginManager.getToolbarAdditionalButtonsInputs(this.features().canModerateRoom, this.isMobile, () =>
|
|
||||||
this.handleCopySpeakerLink()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
protected toolbarLeaveButtonInputs = computed(() =>
|
|
||||||
this.pluginManager.getToolbarLeaveButtonInputs(
|
|
||||||
this.features().canModerateRoom,
|
|
||||||
this.isMobile,
|
|
||||||
() => this.openviduService.disconnectRoom(),
|
|
||||||
() => this.endMeeting()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
protected participantPanelAfterLocalInputs = computed(() =>
|
|
||||||
this.pluginManager.getParticipantPanelAfterLocalInputs(
|
|
||||||
this.features().canModerateRoom,
|
|
||||||
`${this.hostname}/room/${this.roomId}`,
|
|
||||||
() => this.handleCopySpeakerLink()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
protected layoutAdditionalElementsInputs = computed(() => {
|
|
||||||
const showOverlay = this.onlyModeratorIsPresent;
|
|
||||||
return this.pluginManager.getLayoutAdditionalElementsInputs(
|
|
||||||
showOverlay,
|
|
||||||
`${this.hostname}/room/${this.roomId}`,
|
|
||||||
() => this.handleCopySpeakerLink()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
protected lobbyInputs = computed(() => {
|
|
||||||
if (!this.lobbyState) return {};
|
|
||||||
return this.pluginManager.getLobbyInputs(
|
|
||||||
this.roomName,
|
|
||||||
`${this.hostname}/room/${this.roomId}`,
|
|
||||||
this.lobbyState.roomClosed,
|
|
||||||
this.lobbyState.showRecordingCard,
|
|
||||||
!this.lobbyState.roomClosed && this.features().canModerateRoom,
|
|
||||||
this.lobbyState.showBackButton,
|
|
||||||
this.lobbyState.backButtonText,
|
|
||||||
this.lobbyState.participantForm,
|
|
||||||
() => this.submitAccessMeeting(),
|
|
||||||
() => this.lobbyService.goToRecordings(),
|
|
||||||
() => this.lobbyService.goBack(),
|
|
||||||
() => this.handleCopySpeakerLink()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
protected participantPanelItemInputsMap = computed(() => {
|
|
||||||
const local = this.localParticipant();
|
|
||||||
const remotes = this.remoteParticipants();
|
|
||||||
// Force reactivity by reading participantsVersion signal
|
|
||||||
this.participantsVersion();
|
|
||||||
const allParticipants: CustomParticipantModel[] = local ? [local, ...remotes] : remotes;
|
|
||||||
|
|
||||||
const inputsMap = new Map<string, any>();
|
|
||||||
for (const participant of allParticipants) {
|
|
||||||
const inputs = this.pluginManager.getParticipantPanelItemInputs(
|
|
||||||
participant,
|
|
||||||
allParticipants,
|
|
||||||
(p) => this.handleMakeModerator(p),
|
|
||||||
(p) => this.handleUnmakeModerator(p),
|
|
||||||
(p) => this.handleKickParticipant(p)
|
|
||||||
);
|
|
||||||
inputsMap.set(participant.identity, inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputsMap;
|
|
||||||
});
|
|
||||||
|
|
||||||
get participantName(): string {
|
|
||||||
return this.lobbyService.participantName;
|
|
||||||
}
|
|
||||||
|
|
||||||
get participantToken(): string {
|
|
||||||
return this.lobbyState!.participantToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get room(): MeetRoom | undefined {
|
|
||||||
return this.lobbyState?.room;
|
|
||||||
}
|
|
||||||
|
|
||||||
get roomName(): string {
|
get roomName(): string {
|
||||||
return this.lobbyState?.room?.roomName || 'Room';
|
return this.room?.roomName || 'Room';
|
||||||
}
|
|
||||||
|
|
||||||
get roomId(): string {
|
|
||||||
return this.lobbyState?.roomId || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
get roomSecret(): string {
|
|
||||||
return this.lobbyState?.roomSecret || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
set roomSecret(value: string) {
|
|
||||||
if (this.lobbyState) {
|
|
||||||
this.lobbyState.roomSecret = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get onlyModeratorIsPresent(): boolean {
|
|
||||||
return this.features().canModerateRoom && !this.hasRemoteParticipants;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasRemoteParticipants(): boolean {
|
|
||||||
return this.remoteParticipants().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasRecordings(): boolean {
|
|
||||||
return this.lobbyState?.hasRecordings || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
set hasRecordings(value: boolean) {
|
|
||||||
if (this.lobbyState) {
|
|
||||||
this.lobbyState.hasRecordings = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hostname(): string {
|
get hostname(): string {
|
||||||
@ -237,18 +164,14 @@ export class MeetingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
this.roomId = this.roomService.getRoomId();
|
||||||
this.lobbyState = await this.lobbyService.initialize();
|
this.roomSecret = this.roomService.getRoomSecret();
|
||||||
this.prejoinReady = true;
|
this.room = await this.roomService.getRoom(this.roomId);
|
||||||
} catch (error) {
|
this.roomClosed = this.room.status === MeetRoomStatus.CLOSED;
|
||||||
console.error('Error initializing lobby state:', error);
|
|
||||||
this.notificationService.showDialog({
|
await this.setBackButtonText();
|
||||||
title: 'Error',
|
await this.checkForRecordings();
|
||||||
message: 'An error occurred while initializing the meeting lobby. Please try again later.',
|
await this.initializeParticipantName();
|
||||||
showCancelButton: false,
|
|
||||||
confirmText: 'OK'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -256,14 +179,129 @@ export class MeetingComponent implements OnInit {
|
|||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitAccessMeeting() {
|
/**
|
||||||
|
* Sets the back button text based on the application mode and user role
|
||||||
|
*/
|
||||||
|
private async setBackButtonText() {
|
||||||
|
const isStandaloneMode = this.appDataService.isStandaloneMode();
|
||||||
|
const redirection = this.navigationService.getLeaveRedirectURL();
|
||||||
|
const isAdmin = await this.authService.isAdmin();
|
||||||
|
|
||||||
|
if (isStandaloneMode && !redirection && !isAdmin) {
|
||||||
|
// If in standalone mode, no redirection URL and not an admin, hide the back button
|
||||||
|
this.showBackButton = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showBackButton = true;
|
||||||
|
this.backButtonText = isStandaloneMode && !redirection && isAdmin ? 'Back to Rooms' : 'Back';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there are recordings in the room and updates the visibility of the recordings card.
|
||||||
|
*
|
||||||
|
* It is necessary to previously generate a recording token in order to list the recordings.
|
||||||
|
* If token generation fails or the user does not have sufficient permissions to list recordings,
|
||||||
|
* the error will be caught and the recordings card will be hidden (`showRecordingCard` will be set to `false`).
|
||||||
|
*
|
||||||
|
* If recordings exist, sets `showRecordingCard` to `true`; otherwise, to `false`.
|
||||||
|
*/
|
||||||
|
private async checkForRecordings() {
|
||||||
try {
|
try {
|
||||||
await this.lobbyService.submitAccess();
|
const { canRetrieveRecordings } = await this.recordingService.generateRecordingToken(
|
||||||
|
this.roomId,
|
||||||
|
this.roomSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canRetrieveRecordings) {
|
||||||
|
this.showRecordingCard = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { recordings } = await this.recordingService.listRecordings({
|
||||||
|
maxItems: 1,
|
||||||
|
roomId: this.roomId,
|
||||||
|
fields: 'recordingId'
|
||||||
|
});
|
||||||
|
this.hasRecordings = recordings.length > 0;
|
||||||
|
this.showRecordingCard = this.hasRecordings;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking for recordings:', error);
|
||||||
|
this.showRecordingCard = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the participant name in the form control.
|
||||||
|
*
|
||||||
|
* Retrieves the participant name from the ParticipantTokenService first, and if not available,
|
||||||
|
* falls back to the authenticated username. Sets the retrieved name value in the
|
||||||
|
* participant form's 'name' control if a valid name is found.
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves when the participant name has been initialized
|
||||||
|
*/
|
||||||
|
private async initializeParticipantName() {
|
||||||
|
// Apply participant name from ParticipantTokenService if set, otherwise use authenticated username
|
||||||
|
const currentParticipantName = this.participantService.getParticipantName();
|
||||||
|
const username = await this.authService.getUsername();
|
||||||
|
const participantName = currentParticipantName || username;
|
||||||
|
|
||||||
|
if (participantName) {
|
||||||
|
this.participantForm.get('name')?.setValue(participantName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToRecordings() {
|
||||||
|
try {
|
||||||
|
await this.navigationService.navigateTo(`room/${this.roomId}/recordings`, { secret: this.roomSecret });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error navigating to recordings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the back button click event and navigates accordingly
|
||||||
|
* If in embedded mode, it closes the WebComponentManagerService
|
||||||
|
* If the redirect URL is set, it navigates to that URL
|
||||||
|
* If in standalone mode without a redirect URL, it navigates to the rooms page
|
||||||
|
*/
|
||||||
|
async goBack() {
|
||||||
|
if (this.appDataService.isEmbeddedMode()) {
|
||||||
|
this.wcManagerService.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectTo = this.navigationService.getLeaveRedirectURL();
|
||||||
|
if (redirectTo) {
|
||||||
|
// Navigate to the specified redirect URL
|
||||||
|
await this.navigationService.redirectToLeaveUrl();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.appDataService.isStandaloneMode()) {
|
||||||
|
// Navigate to rooms page
|
||||||
|
await this.navigationService.navigateTo('/rooms');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitAccessMeeting() {
|
||||||
|
const { valid, value } = this.participantForm;
|
||||||
|
if (!valid || !value.name?.trim()) {
|
||||||
|
// If the form is invalid, do not proceed
|
||||||
|
console.warn('Participant form is invalid. Cannot access meeting.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.participantName = value.name.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.generateParticipantToken();
|
||||||
|
await this.addParticipantNameToUrl();
|
||||||
|
await this.roomService.loadRoomConfig(this.roomId);
|
||||||
|
|
||||||
// The meeting view must be shown before loading the appearance config,
|
// The meeting view must be shown before loading the appearance config,
|
||||||
// as it contains theme information that might be applied immediately
|
// as it contains theme information that might be applied immediately
|
||||||
// when the meeting view is rendered
|
// when the meeting view is rendered
|
||||||
this.showPrejoin = false;
|
this.showMeeting = true;
|
||||||
await this.configService.loadRoomsAppearanceConfig();
|
await this.configService.loadRoomsAppearanceConfig();
|
||||||
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
@ -272,15 +310,8 @@ export class MeetingComponent implements OnInit {
|
|||||||
])
|
])
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe(([participants, local]) => {
|
.subscribe(([participants, local]) => {
|
||||||
this.remoteParticipants.set(participants as CustomParticipantModel[]);
|
this.remoteParticipants = participants as CustomParticipantModel[];
|
||||||
this.localParticipant.set(local as CustomParticipantModel);
|
this.localParticipant = local as CustomParticipantModel;
|
||||||
|
|
||||||
// Update action handler context if provided
|
|
||||||
if (this.actionHandler) {
|
|
||||||
this.actionHandler.roomId = this.roomId;
|
|
||||||
this.actionHandler.roomSecret = this.roomSecret;
|
|
||||||
this.actionHandler.localParticipant = this.localParticipant();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateVideoPinState();
|
this.updateVideoPinState();
|
||||||
});
|
});
|
||||||
@ -289,24 +320,205 @@ export class MeetingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomCreated(room: Room) {
|
/**
|
||||||
this.eventHandler.setupRoomListeners(room, {
|
* Centralized logic for managing video pinning based on
|
||||||
roomId: this.roomId,
|
* remote participants and local screen sharing state.
|
||||||
roomSecret: this.roomSecret,
|
*/
|
||||||
participantName: this.participantName,
|
private updateVideoPinState(): void {
|
||||||
localParticipant: () => this.localParticipant(),
|
if (!this.localParticipant) return;
|
||||||
remoteParticipants: () => this.remoteParticipants(),
|
|
||||||
onHasRecordingsChanged: (hasRecordings) => {
|
const hasRemote = this.remoteParticipants.length > 0;
|
||||||
this.hasRecordings = hasRecordings;
|
const isSharing = this.localParticipant.isScreenShareEnabled;
|
||||||
},
|
|
||||||
onRoomSecretChanged: (secret) => {
|
if (hasRemote && isSharing) {
|
||||||
this.roomSecret = secret;
|
// Pin the local screen share to appear bigger
|
||||||
},
|
this.localParticipant.setVideoPinnedBySource(Track.Source.ScreenShare, true);
|
||||||
onParticipantRoleUpdated: () => {
|
} else {
|
||||||
// Increment version to trigger reactivity in participant panel items
|
// Unpin everything if no remote participants or not sharing
|
||||||
this.participantsVersion.update((v) => v + 1);
|
this.localParticipant.setAllVideoPinned(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a participant token for joining a meeting.
|
||||||
|
*
|
||||||
|
* @throws When participant already exists in the room (status 409)
|
||||||
|
* @returns Promise that resolves when token is generated
|
||||||
|
*/
|
||||||
|
private async generateParticipantToken() {
|
||||||
|
try {
|
||||||
|
this.participantToken = await this.participantService.generateToken({
|
||||||
|
roomId: this.roomId,
|
||||||
|
secret: this.roomSecret,
|
||||||
|
participantName: this.participantName
|
||||||
});
|
});
|
||||||
|
this.participantName = this.participantService.getParticipantName()!;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error generating participant token:', error);
|
||||||
|
switch (error.status) {
|
||||||
|
case 400:
|
||||||
|
// Invalid secret
|
||||||
|
await this.navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM_SECRET, true);
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
// Room not found
|
||||||
|
await this.navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM, true);
|
||||||
|
break;
|
||||||
|
case 409:
|
||||||
|
// Room is closed
|
||||||
|
await this.navigationService.redirectToErrorPage(ErrorReason.CLOSED_ROOM, true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await this.navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Error generating participant token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add participant name as a query parameter to the URL
|
||||||
|
*/
|
||||||
|
private async addParticipantNameToUrl() {
|
||||||
|
await this.navigationService.updateQueryParamsFromUrl(this.route.snapshot.queryParams, {
|
||||||
|
'participant-name': this.participantName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRoomCreated(room: Room) {
|
||||||
|
room.on(
|
||||||
|
RoomEvent.DataReceived,
|
||||||
|
async (payload: Uint8Array, _participant?: RemoteParticipant, _kind?: DataPacket_Kind, topic?: string) => {
|
||||||
|
const event = JSON.parse(new TextDecoder().decode(payload));
|
||||||
|
|
||||||
|
switch (topic) {
|
||||||
|
case 'recordingStopped': {
|
||||||
|
// If a 'recordingStopped' event is received and there was no previous recordings,
|
||||||
|
// update the hasRecordings flag and refresh the recording token
|
||||||
|
if (this.hasRecordings) return;
|
||||||
|
|
||||||
|
this.hasRecordings = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.recordingService.generateRecordingToken(this.roomId, this.roomSecret);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing recording token:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MeetSignalType.MEET_ROOM_CONFIG_UPDATED: {
|
||||||
|
// Update room config
|
||||||
|
const { config } = event as MeetRoomConfigUpdatedPayload;
|
||||||
|
this.featureConfService.setRoomConfig(config);
|
||||||
|
|
||||||
|
// Refresh recording token if recording is enabled
|
||||||
|
if (config.recording.enabled) {
|
||||||
|
try {
|
||||||
|
await this.recordingService.generateRecordingToken(this.roomId, this.roomSecret);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing recording token:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED: {
|
||||||
|
// Update participant role
|
||||||
|
const { participantIdentity, newRole, secret } = event as MeetParticipantRoleUpdatedPayload;
|
||||||
|
|
||||||
|
if (participantIdentity === this.localParticipant!.identity) {
|
||||||
|
if (!secret) return;
|
||||||
|
|
||||||
|
this.roomSecret = secret;
|
||||||
|
this.roomService.setRoomSecret(secret, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.participantService.refreshParticipantToken({
|
||||||
|
roomId: this.roomId,
|
||||||
|
secret,
|
||||||
|
participantName: this.participantName,
|
||||||
|
participantIdentity
|
||||||
|
});
|
||||||
|
|
||||||
|
this.localParticipant!.meetRole = newRole;
|
||||||
|
this.notificationService.showSnackbar(`You have been assigned the role of ${newRole}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing participant token to update role:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const participant = this.remoteParticipants.find((p) => p.identity === participantIdentity);
|
||||||
|
if (participant) {
|
||||||
|
participant.meetRole = newRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onParticipantConnected(event: ParticipantModel) {
|
||||||
|
const message: WebComponentOutboundEventMessage<WebComponentEvent.JOINED> = {
|
||||||
|
event: WebComponentEvent.JOINED,
|
||||||
|
payload: {
|
||||||
|
roomId: event.getProperties().room?.name || '',
|
||||||
|
participantIdentity: event.identity
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.wcManagerService.sendMessageToParent(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onParticipantLeft(event: ParticipantLeftEvent) {
|
||||||
|
let leftReason = this.getReasonParamFromEvent(event.reason);
|
||||||
|
if (leftReason === LeftEventReason.MEETING_ENDED && this.meetingEndedByMe) {
|
||||||
|
leftReason = LeftEventReason.MEETING_ENDED_BY_SELF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send LEFT event to the parent component
|
||||||
|
const message: WebComponentOutboundEventMessage<WebComponentEvent.LEFT> = {
|
||||||
|
event: WebComponentEvent.LEFT,
|
||||||
|
payload: {
|
||||||
|
roomId: event.roomName,
|
||||||
|
participantIdentity: event.participantName,
|
||||||
|
reason: leftReason
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.wcManagerService.sendMessageToParent(message);
|
||||||
|
|
||||||
|
// Remove the moderator secret (and stored tokens) from session storage
|
||||||
|
// if the participant left for a reason other than browser unload
|
||||||
|
if (event.reason !== ParticipantLeftReason.BROWSER_UNLOAD) {
|
||||||
|
this.sessionStorageService.removeRoomSecret();
|
||||||
|
this.tokenStorageService.clearParticipantToken();
|
||||||
|
this.tokenStorageService.clearRecordingToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to the disconnected page with the reason
|
||||||
|
await this.navigationService.navigateTo('disconnected', { reason: leftReason }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps ParticipantLeftReason to LeftEventReason.
|
||||||
|
* This method translates the technical reasons for a participant leaving the room
|
||||||
|
* into user-friendly reasons that can be used in the UI or for logging purposes.
|
||||||
|
* @param reason The technical reason for the participant leaving the room.
|
||||||
|
* @returns The corresponding LeftEventReason.
|
||||||
|
*/
|
||||||
|
private getReasonParamFromEvent(reason: ParticipantLeftReason): LeftEventReason {
|
||||||
|
const reasonMap: Record<ParticipantLeftReason, LeftEventReason> = {
|
||||||
|
[ParticipantLeftReason.LEAVE]: LeftEventReason.VOLUNTARY_LEAVE,
|
||||||
|
[ParticipantLeftReason.BROWSER_UNLOAD]: LeftEventReason.VOLUNTARY_LEAVE,
|
||||||
|
[ParticipantLeftReason.NETWORK_DISCONNECT]: LeftEventReason.NETWORK_DISCONNECT,
|
||||||
|
[ParticipantLeftReason.SIGNAL_CLOSE]: LeftEventReason.NETWORK_DISCONNECT,
|
||||||
|
[ParticipantLeftReason.SERVER_SHUTDOWN]: LeftEventReason.SERVER_SHUTDOWN,
|
||||||
|
[ParticipantLeftReason.PARTICIPANT_REMOVED]: LeftEventReason.PARTICIPANT_KICKED,
|
||||||
|
[ParticipantLeftReason.ROOM_DELETED]: LeftEventReason.MEETING_ENDED,
|
||||||
|
[ParticipantLeftReason.DUPLICATE_IDENTITY]: LeftEventReason.UNKNOWN,
|
||||||
|
[ParticipantLeftReason.OTHER]: LeftEventReason.UNKNOWN
|
||||||
|
};
|
||||||
|
return reasonMap[reason] ?? LeftEventReason.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
async leaveMeeting() {
|
async leaveMeeting() {
|
||||||
@ -316,61 +528,32 @@ export class MeetingComponent implements OnInit {
|
|||||||
async endMeeting() {
|
async endMeeting() {
|
||||||
if (!this.participantService.isModeratorParticipant()) return;
|
if (!this.participantService.isModeratorParticipant()) return;
|
||||||
|
|
||||||
this.eventHandler.setMeetingEndedByMe(true);
|
this.meetingEndedByMe = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.meetingService.endMeeting(this.roomId);
|
await this.meetingService.endMeeting(this.roomId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error ending meeting:', error);
|
console.error('Error ending meeting:', error);
|
||||||
|
this.notificationService.showSnackbar('Failed to end meeting');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onViewRecordingsClicked() {
|
async kickParticipant(participant: CustomParticipantModel) {
|
||||||
window.open(`/room/${this.roomId}/recordings?secret=${this.roomSecret}`, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Centralized logic for managing video pinning based on
|
|
||||||
* remote participants and local screen sharing state.
|
|
||||||
*/
|
|
||||||
protected updateVideoPinState(): void {
|
|
||||||
if (!this.localParticipant) return;
|
|
||||||
|
|
||||||
const isSharing = this.localParticipant()?.isScreenShareEnabled;
|
|
||||||
|
|
||||||
if (this.hasRemoteParticipants && isSharing) {
|
|
||||||
// Pin the local screen share to appear bigger
|
|
||||||
this.localParticipant()?.setVideoPinnedBySource(Track.Source.ScreenShare, true);
|
|
||||||
} else {
|
|
||||||
// Unpin everything if no remote participants or not sharing
|
|
||||||
this.localParticipant()?.setAllVideoPinned(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event handler wrappers - delegates to actionHandler if provided, otherwise uses default implementation
|
|
||||||
*/
|
|
||||||
protected async handleKickParticipant(participant: CustomParticipantModel) {
|
|
||||||
if (this.actionHandler) {
|
|
||||||
await this.actionHandler.kickParticipant(participant);
|
|
||||||
} else {
|
|
||||||
// Default implementation
|
|
||||||
if (!this.participantService.isModeratorParticipant()) return;
|
if (!this.participantService.isModeratorParticipant()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.meetingService.kickParticipant(this.roomId, participant.identity);
|
await this.meetingService.kickParticipant(this.roomId, participant.identity);
|
||||||
console.log('Participant kicked successfully');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error kicking participant:', error);
|
console.error('Error kicking participant:', error);
|
||||||
}
|
this.notificationService.showSnackbar('Failed to kick participant');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleMakeModerator(participant: CustomParticipantModel) {
|
/**
|
||||||
if (this.actionHandler) {
|
* Makes a participant as moderator.
|
||||||
await this.actionHandler.makeModerator(participant);
|
* @param participant The participant to make as moderator.
|
||||||
} else {
|
*/
|
||||||
// Default implementation
|
async makeModerator(participant: CustomParticipantModel) {
|
||||||
if (!this.participantService.isModeratorParticipant()) return;
|
if (!this.participantService.isModeratorParticipant()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -379,62 +562,62 @@ export class MeetingComponent implements OnInit {
|
|||||||
participant.identity,
|
participant.identity,
|
||||||
ParticipantRole.MODERATOR
|
ParticipantRole.MODERATOR
|
||||||
);
|
);
|
||||||
console.log('Moderator assigned successfully');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error assigning moderator:', error);
|
console.error('Error making participant moderator:', error);
|
||||||
}
|
this.notificationService.showSnackbar('Failed to make participant moderator');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleUnmakeModerator(participant: CustomParticipantModel) {
|
/**
|
||||||
if (this.actionHandler) {
|
* Unmakes a participant as moderator.
|
||||||
await this.actionHandler.unmakeModerator(participant);
|
* @param participant The participant to unmake as moderator.
|
||||||
} else {
|
*/
|
||||||
// Default implementation
|
async unmakeModerator(participant: CustomParticipantModel) {
|
||||||
if (!this.participantService.isModeratorParticipant()) return;
|
if (!this.participantService.isModeratorParticipant()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.meetingService.changeParticipantRole(
|
await this.meetingService.changeParticipantRole(this.roomId, participant.identity, ParticipantRole.SPEAKER);
|
||||||
this.roomId,
|
|
||||||
participant.identity,
|
|
||||||
ParticipantRole.SPEAKER
|
|
||||||
);
|
|
||||||
console.log('Moderator unassigned successfully');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error unassigning moderator:', error);
|
console.error('Error unmaking participant moderator:', error);
|
||||||
}
|
this.notificationService.showSnackbar('Failed to unmake participant moderator');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private async handleCopyModeratorLink() {
|
async copyModeratorLink() {
|
||||||
// if (this.actionHandler) {
|
this.clipboard.copy(this.room!.moderatorUrl);
|
||||||
// await this.actionHandler.copyModeratorLink();
|
this.notificationService.showSnackbar('Moderator link copied to clipboard');
|
||||||
// } else {
|
}
|
||||||
// // Default implementation
|
|
||||||
// try {
|
|
||||||
// this.clipboard.copy(this.room!.moderatorUrl);
|
|
||||||
// this.notificationService.showSnackbar('Moderator link copied to clipboard');
|
|
||||||
|
|
||||||
// console.log('Moderator link copied to clipboard');
|
async copySpeakerLink() {
|
||||||
// } catch (error) {
|
this.clipboard.copy(this.room!.speakerUrl);
|
||||||
// console.error('Failed to copy moderator link:', error);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
protected async handleCopySpeakerLink() {
|
|
||||||
if (this.actionHandler) {
|
|
||||||
await this.actionHandler.copySpeakerLink();
|
|
||||||
} else {
|
|
||||||
// Default implementation
|
|
||||||
try {
|
|
||||||
const speakerLink = this.room!.speakerUrl;
|
|
||||||
this.clipboard.copy(speakerLink);
|
|
||||||
this.notificationService.showSnackbar('Speaker link copied to clipboard');
|
this.notificationService.showSnackbar('Speaker link copied to clipboard');
|
||||||
console.log('Speaker link copied to clipboard');
|
}
|
||||||
|
|
||||||
|
async onRecordingStartRequested(event: RecordingStartRequestedEvent) {
|
||||||
|
try {
|
||||||
|
await this.recordingService.startRecording(event.roomName);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if ((error as any).status === 503) {
|
||||||
|
console.error(
|
||||||
|
`No egress service was able to register a request.
|
||||||
|
Check your CPU usage or if there's any Media Node with enough CPU.
|
||||||
|
Remember that by default, a recording uses 4 CPUs for each room.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRecordingStopRequested(event: RecordingStopRequestedEvent) {
|
||||||
|
try {
|
||||||
|
await this.recordingService.stopRecording(event.recordingId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to copy speaker link:', error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onViewRecordingsClicked() {
|
||||||
|
window.open(`/room/${this.roomId}/recordings?secret=${this.roomSecret}`, '_blank');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,7 @@ export * from './auth.service';
|
|||||||
export * from './global-config.service';
|
export * from './global-config.service';
|
||||||
export * from './room.service';
|
export * from './room.service';
|
||||||
export * from './participant.service';
|
export * from './participant.service';
|
||||||
export * from './meeting/meeting.service';
|
export * from './meeting.service';
|
||||||
export * from './meeting/meeting-lobby.service';
|
|
||||||
export * from './meeting/meeting-plugin-manager.service';
|
|
||||||
export * from './meeting/meeting-event-handler.service';
|
|
||||||
export * from './feature-configuration.service';
|
export * from './feature-configuration.service';
|
||||||
export * from './recording.service';
|
export * from './recording.service';
|
||||||
export * from './webcomponent-manager.service';
|
export * from './webcomponent-manager.service';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpService, ParticipantService } from '..';
|
import { HttpService, ParticipantService } from '../services';
|
||||||
import { LoggerService } from 'openvidu-components-angular';
|
import { LoggerService } from 'openvidu-components-angular';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -1,359 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
Room,
|
|
||||||
RoomEvent,
|
|
||||||
DataPacket_Kind,
|
|
||||||
RemoteParticipant,
|
|
||||||
ParticipantLeftEvent,
|
|
||||||
ParticipantLeftReason,
|
|
||||||
RecordingStartRequestedEvent,
|
|
||||||
RecordingStopRequestedEvent,
|
|
||||||
ParticipantModel
|
|
||||||
} from 'openvidu-components-angular';
|
|
||||||
import {
|
|
||||||
FeatureConfigurationService,
|
|
||||||
RecordingService,
|
|
||||||
ParticipantService,
|
|
||||||
RoomService,
|
|
||||||
SessionStorageService,
|
|
||||||
TokenStorageService,
|
|
||||||
WebComponentManagerService,
|
|
||||||
NavigationService
|
|
||||||
} from '../../services';
|
|
||||||
import {
|
|
||||||
LeftEventReason,
|
|
||||||
MeetSignalType,
|
|
||||||
MeetParticipantRoleUpdatedPayload,
|
|
||||||
MeetRoomConfigUpdatedPayload,
|
|
||||||
WebComponentEvent,
|
|
||||||
WebComponentOutboundEventMessage
|
|
||||||
} from '@openvidu-meet/typings';
|
|
||||||
import { CustomParticipantModel } from '../../models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service that handles all LiveKit/OpenVidu room events.
|
|
||||||
*
|
|
||||||
* This service encapsulates all event handling logic previously in MeetingComponent,
|
|
||||||
* providing a clean separation of concerns and making the component more maintainable.
|
|
||||||
*
|
|
||||||
* Responsibilities:
|
|
||||||
* - Setup and manage room event listeners
|
|
||||||
* - Handle data received events (recording stopped, config updates, role changes)
|
|
||||||
* - Handle participant lifecycle events (connected, left)
|
|
||||||
* - Handle recording events (start, stop)
|
|
||||||
* - Map technical reasons to user-friendly reasons
|
|
||||||
* - Manage meeting ended state
|
|
||||||
* - Navigate to disconnected page with appropriate reason
|
|
||||||
*
|
|
||||||
* Benefits:
|
|
||||||
* - Reduces MeetingComponent size by ~200 lines
|
|
||||||
* - All event logic in one place (easier to test and maintain)
|
|
||||||
* - Clear API for event handling
|
|
||||||
* - Reusable across different components if needed
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class MeetingEventHandlerService {
|
|
||||||
// Injected services
|
|
||||||
protected featureConfService = inject(FeatureConfigurationService);
|
|
||||||
protected recordingService = inject(RecordingService);
|
|
||||||
protected participantService = inject(ParticipantService);
|
|
||||||
protected roomService = inject(RoomService);
|
|
||||||
protected sessionStorageService = inject(SessionStorageService);
|
|
||||||
protected tokenStorageService = inject(TokenStorageService);
|
|
||||||
protected wcManagerService = inject(WebComponentManagerService);
|
|
||||||
protected navigationService = inject(NavigationService);
|
|
||||||
|
|
||||||
// Internal state
|
|
||||||
private meetingEndedByMe = false;
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// PUBLIC METHODS - Room Event Handlers
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up all room event listeners when room is created.
|
|
||||||
* This is the main entry point for room event handling.
|
|
||||||
*
|
|
||||||
* @param room The LiveKit Room instance
|
|
||||||
* @param context Context object containing all necessary data and callbacks
|
|
||||||
*/
|
|
||||||
setupRoomListeners(
|
|
||||||
room: Room,
|
|
||||||
context: {
|
|
||||||
roomId: string;
|
|
||||||
roomSecret: string;
|
|
||||||
participantName: string;
|
|
||||||
localParticipant: () => CustomParticipantModel | undefined;
|
|
||||||
remoteParticipants: () => CustomParticipantModel[];
|
|
||||||
onHasRecordingsChanged: (hasRecordings: boolean) => void;
|
|
||||||
onRoomSecretChanged: (secret: string) => void;
|
|
||||||
onParticipantRoleUpdated?: () => void;
|
|
||||||
}
|
|
||||||
): void {
|
|
||||||
room.on(
|
|
||||||
RoomEvent.DataReceived,
|
|
||||||
async (
|
|
||||||
payload: Uint8Array,
|
|
||||||
_participant?: RemoteParticipant,
|
|
||||||
_kind?: DataPacket_Kind,
|
|
||||||
topic?: string
|
|
||||||
) => {
|
|
||||||
const event = JSON.parse(new TextDecoder().decode(payload));
|
|
||||||
|
|
||||||
switch (topic) {
|
|
||||||
case 'recordingStopped':
|
|
||||||
await this.handleRecordingStopped(
|
|
||||||
context.roomId,
|
|
||||||
context.roomSecret,
|
|
||||||
context.onHasRecordingsChanged
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
|
|
||||||
await this.handleRoomConfigUpdated(event, context.roomId, context.roomSecret);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED:
|
|
||||||
await this.handleParticipantRoleUpdated(
|
|
||||||
event,
|
|
||||||
context.roomId,
|
|
||||||
context.participantName,
|
|
||||||
context.localParticipant,
|
|
||||||
context.remoteParticipants,
|
|
||||||
context.onRoomSecretChanged,
|
|
||||||
context.onParticipantRoleUpdated
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles participant connected event.
|
|
||||||
* Sends JOINED event to parent window (for web component integration).
|
|
||||||
*
|
|
||||||
* Arrow function ensures correct 'this' binding when called from template.
|
|
||||||
*
|
|
||||||
* @param event Participant model from OpenVidu
|
|
||||||
*/
|
|
||||||
onParticipantConnected = (event: ParticipantModel): void => {
|
|
||||||
const message: WebComponentOutboundEventMessage<WebComponentEvent.JOINED> = {
|
|
||||||
event: WebComponentEvent.JOINED,
|
|
||||||
payload: {
|
|
||||||
roomId: event.getProperties().room?.name || '',
|
|
||||||
participantIdentity: event.identity
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.wcManagerService.sendMessageToParent(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles participant left event.
|
|
||||||
* - Maps technical reason to user-friendly reason
|
|
||||||
* - Sends LEFT event to parent window
|
|
||||||
* - Cleans up session storage (secrets, tokens)
|
|
||||||
* - Navigates to disconnected page
|
|
||||||
*
|
|
||||||
* Arrow function ensures correct 'this' binding when called from template.
|
|
||||||
*
|
|
||||||
* @param event Participant left event from OpenVidu
|
|
||||||
*/
|
|
||||||
onParticipantLeft = async (event: ParticipantLeftEvent): Promise<void> => {
|
|
||||||
let leftReason = this.mapLeftReason(event.reason);
|
|
||||||
|
|
||||||
// If meeting was ended by this user, update reason
|
|
||||||
if (leftReason === LeftEventReason.MEETING_ENDED && this.meetingEndedByMe) {
|
|
||||||
leftReason = LeftEventReason.MEETING_ENDED_BY_SELF;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send LEFT event to parent window
|
|
||||||
const message: WebComponentOutboundEventMessage<WebComponentEvent.LEFT> = {
|
|
||||||
event: WebComponentEvent.LEFT,
|
|
||||||
payload: {
|
|
||||||
roomId: event.roomName,
|
|
||||||
participantIdentity: event.participantName,
|
|
||||||
reason: leftReason
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.wcManagerService.sendMessageToParent(message);
|
|
||||||
|
|
||||||
// Clean up storage (except on browser unload)
|
|
||||||
if (event.reason !== ParticipantLeftReason.BROWSER_UNLOAD) {
|
|
||||||
this.sessionStorageService.removeRoomSecret();
|
|
||||||
this.tokenStorageService.clearParticipantToken();
|
|
||||||
this.tokenStorageService.clearRecordingToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to disconnected page
|
|
||||||
await this.navigationService.navigateTo('disconnected', { reason: leftReason }, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles recording start request event.
|
|
||||||
*
|
|
||||||
* Arrow function ensures correct 'this' binding when called from template.
|
|
||||||
*
|
|
||||||
* @param event Recording start requested event from OpenVidu
|
|
||||||
*/
|
|
||||||
onRecordingStartRequested = async (event: RecordingStartRequestedEvent): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await this.recordingService.startRecording(event.roomName);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.status === 503) {
|
|
||||||
console.error(
|
|
||||||
'No egress service available. Check CPU usage or Media Node capacity. ' +
|
|
||||||
'By default, a recording uses 2 CPUs per room.'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error('Error starting recording:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles recording stop request event.
|
|
||||||
*
|
|
||||||
* Arrow function ensures correct 'this' binding when called from template.
|
|
||||||
*
|
|
||||||
* @param event Recording stop requested event from OpenVidu
|
|
||||||
*/
|
|
||||||
onRecordingStopRequested = async (event: RecordingStopRequestedEvent): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await this.recordingService.stopRecording(event.recordingId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error stopping recording:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the "meeting ended by me" flag.
|
|
||||||
* This is used to differentiate between meeting ended by this user vs ended by someone else.
|
|
||||||
*
|
|
||||||
* @param value True if this user ended the meeting
|
|
||||||
*/
|
|
||||||
setMeetingEndedByMe(value: boolean): void {
|
|
||||||
this.meetingEndedByMe = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// PRIVATE METHODS - Event Handlers
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles recording stopped event.
|
|
||||||
* Updates hasRecordings flag and refreshes recording token.
|
|
||||||
*/
|
|
||||||
private async handleRecordingStopped(
|
|
||||||
roomId: string,
|
|
||||||
roomSecret: string,
|
|
||||||
onHasRecordingsChanged: (hasRecordings: boolean) => void
|
|
||||||
): Promise<void> {
|
|
||||||
// Notify that recordings are now available
|
|
||||||
onHasRecordingsChanged(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Refresh recording token to view recordings
|
|
||||||
await this.recordingService.generateRecordingToken(roomId, roomSecret);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing recording token:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles room config updated event.
|
|
||||||
* Updates feature config and refreshes recording token if needed.
|
|
||||||
*/
|
|
||||||
private async handleRoomConfigUpdated(
|
|
||||||
event: MeetRoomConfigUpdatedPayload,
|
|
||||||
roomId: string,
|
|
||||||
roomSecret: string
|
|
||||||
): Promise<void> {
|
|
||||||
const { config } = event;
|
|
||||||
|
|
||||||
// Update feature configuration
|
|
||||||
this.featureConfService.setRoomConfig(config);
|
|
||||||
|
|
||||||
// Refresh recording token if recording is enabled
|
|
||||||
if (config.recording.enabled) {
|
|
||||||
try {
|
|
||||||
await this.recordingService.generateRecordingToken(roomId, roomSecret);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing recording token:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles participant role updated event.
|
|
||||||
* Updates local or remote participant role and refreshes token if needed.
|
|
||||||
*/
|
|
||||||
private async handleParticipantRoleUpdated(
|
|
||||||
event: MeetParticipantRoleUpdatedPayload,
|
|
||||||
roomId: string,
|
|
||||||
participantName: string,
|
|
||||||
localParticipant: () => CustomParticipantModel | undefined,
|
|
||||||
remoteParticipants: () => CustomParticipantModel[],
|
|
||||||
onRoomSecretChanged: (secret: string) => void,
|
|
||||||
onParticipantRoleUpdated?: () => void
|
|
||||||
): Promise<void> {
|
|
||||||
const { participantIdentity, newRole, secret } = event;
|
|
||||||
const local = localParticipant();
|
|
||||||
|
|
||||||
// Check if the role update is for the local participant
|
|
||||||
if (local && participantIdentity === local.identity) {
|
|
||||||
if (!secret) return;
|
|
||||||
|
|
||||||
// Update room secret
|
|
||||||
onRoomSecretChanged(secret);
|
|
||||||
this.roomService.setRoomSecret(secret, false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Refresh participant token with new role
|
|
||||||
await this.participantService.refreshParticipantToken({
|
|
||||||
roomId,
|
|
||||||
secret,
|
|
||||||
participantName,
|
|
||||||
participantIdentity
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update local participant role
|
|
||||||
local.meetRole = newRole;
|
|
||||||
console.log(`You have been assigned the role of ${newRole}`);
|
|
||||||
|
|
||||||
// Notify component that participant role was updated
|
|
||||||
onParticipantRoleUpdated?.();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing participant token:', error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Update remote participant role
|
|
||||||
const participant = remoteParticipants().find((p) => p.identity === participantIdentity);
|
|
||||||
if (participant) {
|
|
||||||
participant.meetRole = newRole;
|
|
||||||
|
|
||||||
// Notify component that participant role was updated
|
|
||||||
onParticipantRoleUpdated?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps technical ParticipantLeftReason to user-friendly LeftEventReason.
|
|
||||||
* This provides better messaging to users about why they left the room.
|
|
||||||
*/
|
|
||||||
private mapLeftReason(reason: ParticipantLeftReason): LeftEventReason {
|
|
||||||
const reasonMap: Record<ParticipantLeftReason, LeftEventReason> = {
|
|
||||||
[ParticipantLeftReason.LEAVE]: LeftEventReason.VOLUNTARY_LEAVE,
|
|
||||||
[ParticipantLeftReason.BROWSER_UNLOAD]: LeftEventReason.VOLUNTARY_LEAVE,
|
|
||||||
[ParticipantLeftReason.NETWORK_DISCONNECT]: LeftEventReason.NETWORK_DISCONNECT,
|
|
||||||
[ParticipantLeftReason.SIGNAL_CLOSE]: LeftEventReason.NETWORK_DISCONNECT,
|
|
||||||
[ParticipantLeftReason.SERVER_SHUTDOWN]: LeftEventReason.SERVER_SHUTDOWN,
|
|
||||||
[ParticipantLeftReason.PARTICIPANT_REMOVED]: LeftEventReason.PARTICIPANT_KICKED,
|
|
||||||
[ParticipantLeftReason.ROOM_DELETED]: LeftEventReason.MEETING_ENDED,
|
|
||||||
[ParticipantLeftReason.DUPLICATE_IDENTITY]: LeftEventReason.UNKNOWN,
|
|
||||||
[ParticipantLeftReason.OTHER]: LeftEventReason.UNKNOWN
|
|
||||||
};
|
|
||||||
return reasonMap[reason] ?? LeftEventReason.UNKNOWN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,261 +0,0 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
|
||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|
||||||
import {
|
|
||||||
AuthService,
|
|
||||||
RecordingService,
|
|
||||||
RoomService,
|
|
||||||
ParticipantService,
|
|
||||||
NavigationService,
|
|
||||||
AppDataService,
|
|
||||||
WebComponentManagerService
|
|
||||||
} from '..';
|
|
||||||
import { MeetRoomStatus } from '@openvidu-meet/typings';
|
|
||||||
import { LobbyState } from '../../models/lobby.model';
|
|
||||||
import { ErrorReason } from '../../models';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service that manages the meeting lobby state and operations.
|
|
||||||
*
|
|
||||||
* Responsibilities:
|
|
||||||
* - Initialize and maintain lobby state
|
|
||||||
* - Validate participant information
|
|
||||||
* - Check for recordings availability
|
|
||||||
* - Handle navigation (back button, recordings)
|
|
||||||
*
|
|
||||||
* This service coordinates multiple domain services to provide
|
|
||||||
* a simplified interface for the MeetingComponent.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class MeetingLobbyService {
|
|
||||||
private state: LobbyState = {
|
|
||||||
roomId: '',
|
|
||||||
roomSecret: '',
|
|
||||||
roomClosed: false,
|
|
||||||
hasRecordings: false,
|
|
||||||
showRecordingCard: false,
|
|
||||||
showBackButton: true,
|
|
||||||
backButtonText: 'Back',
|
|
||||||
participantForm: new FormGroup({
|
|
||||||
name: new FormControl('', [Validators.required])
|
|
||||||
}),
|
|
||||||
participantToken: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
protected roomService: RoomService = inject(RoomService);
|
|
||||||
protected recordingService: RecordingService = inject(RecordingService);
|
|
||||||
protected authService: AuthService = inject(AuthService);
|
|
||||||
protected participantService: ParticipantService = inject(ParticipantService);
|
|
||||||
protected navigationService: NavigationService = inject(NavigationService);
|
|
||||||
protected appDataService: AppDataService = inject(AppDataService);
|
|
||||||
protected wcManagerService: WebComponentManagerService = inject(WebComponentManagerService);
|
|
||||||
protected route: ActivatedRoute = inject(ActivatedRoute);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current lobby state
|
|
||||||
*/
|
|
||||||
get lobbyState(): LobbyState {
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
set participantName(name: string) {
|
|
||||||
this.state.participantForm.get('name')?.setValue(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
get participantName(): string {
|
|
||||||
const { valid, value } = this.state.participantForm;
|
|
||||||
if (!valid || !value.name?.trim()) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return value.name.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the lobby state by fetching room data and configuring UI
|
|
||||||
*/
|
|
||||||
async initialize(): Promise<LobbyState> {
|
|
||||||
this.state.roomId = this.roomService.getRoomId();
|
|
||||||
this.state.roomSecret = this.roomService.getRoomSecret();
|
|
||||||
this.state.room = await this.roomService.getRoom(this.state.roomId);
|
|
||||||
this.state.roomClosed = this.state.room.status === MeetRoomStatus.CLOSED;
|
|
||||||
|
|
||||||
await this.setBackButtonText();
|
|
||||||
await this.checkForRecordings();
|
|
||||||
await this.initializeParticipantName();
|
|
||||||
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the back button click event and navigates accordingly
|
|
||||||
* If in embedded mode, it closes the WebComponentManagerService
|
|
||||||
* If the redirect URL is set, it navigates to that URL
|
|
||||||
* If in standalone mode without a redirect URL, it navigates to the rooms page
|
|
||||||
*/
|
|
||||||
async goBack() {
|
|
||||||
try {
|
|
||||||
if (this.appDataService.isEmbeddedMode()) {
|
|
||||||
this.wcManagerService.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirectTo = this.navigationService.getLeaveRedirectURL();
|
|
||||||
if (redirectTo) {
|
|
||||||
// Navigate to the specified redirect URL
|
|
||||||
await this.navigationService.redirectToLeaveUrl();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.appDataService.isStandaloneMode()) {
|
|
||||||
// Navigate to rooms page
|
|
||||||
await this.navigationService.navigateTo('/rooms');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error handling back navigation:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to recordings page
|
|
||||||
*/
|
|
||||||
async goToRecordings(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.navigationService.navigateTo(`room/${this.state.roomId}/recordings`, {
|
|
||||||
secret: this.state.roomSecret
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error navigating to recordings:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitAccess(): Promise<void> {
|
|
||||||
if (!this.participantName) {
|
|
||||||
console.error('Participant form is invalid. Cannot access meeting.');
|
|
||||||
throw new Error('Participant form is invalid');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.generateParticipantToken();
|
|
||||||
await this.addParticipantNameToUrl();
|
|
||||||
await this.roomService.loadRoomConfig(this.state.roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protected helper methods
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the back button text based on the application mode and user role
|
|
||||||
*/
|
|
||||||
protected async setBackButtonText(): Promise<void> {
|
|
||||||
const isStandaloneMode = this.appDataService.isStandaloneMode();
|
|
||||||
const redirection = this.navigationService.getLeaveRedirectURL();
|
|
||||||
const isAdmin = await this.authService.isAdmin();
|
|
||||||
|
|
||||||
if (isStandaloneMode && !redirection && !isAdmin) {
|
|
||||||
this.state.showBackButton = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.showBackButton = true;
|
|
||||||
this.state.backButtonText = isStandaloneMode && !redirection && isAdmin ? 'Back to Rooms' : 'Back';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there are recordings in the room and updates the visibility of the recordings card.
|
|
||||||
*
|
|
||||||
* It is necessary to previously generate a recording token in order to list the recordings.
|
|
||||||
* If token generation fails or the user does not have sufficient permissions to list recordings,
|
|
||||||
* the error will be caught and the recordings card will be hidden (`showRecordingCard` will be set to `false`).
|
|
||||||
*
|
|
||||||
* If recordings exist, sets `showRecordingCard` to `true`; otherwise, to `false`.
|
|
||||||
*/
|
|
||||||
protected async checkForRecordings(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const { canRetrieveRecordings } = await this.recordingService.generateRecordingToken(
|
|
||||||
this.state.roomId,
|
|
||||||
this.state.roomSecret
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!canRetrieveRecordings) {
|
|
||||||
this.state.showRecordingCard = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { recordings } = await this.recordingService.listRecordings({
|
|
||||||
maxItems: 1,
|
|
||||||
roomId: this.state.roomId,
|
|
||||||
fields: 'recordingId'
|
|
||||||
});
|
|
||||||
|
|
||||||
this.state.hasRecordings = recordings.length > 0;
|
|
||||||
this.state.showRecordingCard = this.state.hasRecordings;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking for recordings:', error);
|
|
||||||
this.state.showRecordingCard = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the participant name in the form control.
|
|
||||||
*
|
|
||||||
* Retrieves the participant name from the ParticipantTokenService first, and if not available,
|
|
||||||
* falls back to the authenticated username. Sets the retrieved name value in the
|
|
||||||
* participant form's 'name' control if a valid name is found.
|
|
||||||
*
|
|
||||||
* @returns A promise that resolves when the participant name has been initialized
|
|
||||||
*/
|
|
||||||
protected async initializeParticipantName(): Promise<void> {
|
|
||||||
// Apply participant name from ParticipantTokenService if set, otherwise use authenticated username
|
|
||||||
const currentParticipantName = this.participantService.getParticipantName();
|
|
||||||
const username = await this.authService.getUsername();
|
|
||||||
const participantName = currentParticipantName || username;
|
|
||||||
|
|
||||||
if (participantName) {
|
|
||||||
this.participantName = participantName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a participant token for joining a meeting.
|
|
||||||
*
|
|
||||||
* @throws When participant already exists in the room (status 409)
|
|
||||||
* @returns Promise that resolves when token is generated
|
|
||||||
*/
|
|
||||||
protected async generateParticipantToken() {
|
|
||||||
try {
|
|
||||||
this.state.participantToken = await this.participantService.generateToken({
|
|
||||||
roomId: this.state.roomId,
|
|
||||||
secret: this.state.roomSecret,
|
|
||||||
participantName: this.participantName
|
|
||||||
});
|
|
||||||
this.participantName = this.participantService.getParticipantName()!;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error generating participant token:', error);
|
|
||||||
switch (error.status) {
|
|
||||||
case 400:
|
|
||||||
// Invalid secret
|
|
||||||
await this.navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM_SECRET, true);
|
|
||||||
break;
|
|
||||||
case 404:
|
|
||||||
// Room not found
|
|
||||||
await this.navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM, true);
|
|
||||||
break;
|
|
||||||
case 409:
|
|
||||||
// Room is closed
|
|
||||||
await this.navigationService.redirectToErrorPage(ErrorReason.CLOSED_ROOM, true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
await this.navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Error generating participant token');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add participant name as a query parameter to the URL
|
|
||||||
*/
|
|
||||||
protected async addParticipantNameToUrl() {
|
|
||||||
await this.navigationService.updateQueryParamsFromUrl(this.route.snapshot.queryParams, {
|
|
||||||
'participant-name': this.participantName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
import { Injectable, Optional, Inject } from '@angular/core';
|
|
||||||
import { FormGroup } from '@angular/forms';
|
|
||||||
import { CustomParticipantModel } from '../../models';
|
|
||||||
import { MeetingActionHandler, MEETING_ACTION_HANDLER_TOKEN, ParticipantControls } from '../../customization';
|
|
||||||
import { ParticipantService } from '../participant.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service that manages plugin inputs and configurations for the MeetingComponent.
|
|
||||||
*
|
|
||||||
* Responsibilities:
|
|
||||||
* - Prepare input objects for toolbar plugins
|
|
||||||
* - Prepare input objects for participant panel plugins
|
|
||||||
* - Prepare input objects for layout plugins
|
|
||||||
* - Prepare input objects for lobby plugin
|
|
||||||
* - Calculate participant control visibility based on roles and permissions
|
|
||||||
*
|
|
||||||
* This service acts as a bridge between the MeetingComponent and the plugin components,
|
|
||||||
* encapsulating the logic for determining what inputs each plugin should receive.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class MeetingPluginManagerService {
|
|
||||||
constructor(
|
|
||||||
private participantService: ParticipantService,
|
|
||||||
@Optional() @Inject(MEETING_ACTION_HANDLER_TOKEN) private actionHandler?: MeetingActionHandler
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares inputs for the toolbar additional buttons plugin
|
|
||||||
*/
|
|
||||||
getToolbarAdditionalButtonsInputs(
|
|
||||||
canModerateRoom: boolean,
|
|
||||||
isMobile: boolean,
|
|
||||||
onCopyLink: () => void
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
showCopyLinkButton: canModerateRoom,
|
|
||||||
showLeaveMenu: false,
|
|
||||||
isMobile,
|
|
||||||
copyLinkClickedFn: onCopyLink
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares inputs for the toolbar leave button plugin
|
|
||||||
*/
|
|
||||||
getToolbarLeaveButtonInputs(
|
|
||||||
canModerateRoom: boolean,
|
|
||||||
isMobile: boolean,
|
|
||||||
onLeave: () => Promise<void>,
|
|
||||||
onEnd: () => Promise<void>
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
showCopyLinkButton: false,
|
|
||||||
showLeaveMenu: canModerateRoom,
|
|
||||||
isMobile,
|
|
||||||
leaveMeetingClickedFn: onLeave,
|
|
||||||
endMeetingClickedFn: onEnd
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares inputs for the participant panel "after local participant" plugin
|
|
||||||
*/
|
|
||||||
getParticipantPanelAfterLocalInputs(
|
|
||||||
canModerateRoom: boolean,
|
|
||||||
meetingUrl: string,
|
|
||||||
onCopyLink: () => void
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
showShareLink: canModerateRoom,
|
|
||||||
meetingUrl,
|
|
||||||
copyClickedFn: onCopyLink
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares inputs for the layout additional elements plugin
|
|
||||||
*/
|
|
||||||
getLayoutAdditionalElementsInputs(
|
|
||||||
showOverlay: boolean,
|
|
||||||
meetingUrl: string,
|
|
||||||
onCopyLink: () => void
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
showOverlay,
|
|
||||||
meetingUrl,
|
|
||||||
copyClickedFn: onCopyLink
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares inputs for the participant panel item plugin
|
|
||||||
*/
|
|
||||||
getParticipantPanelItemInputs(
|
|
||||||
participant: CustomParticipantModel,
|
|
||||||
allParticipants: CustomParticipantModel[],
|
|
||||||
onMakeModerator: (p: CustomParticipantModel) => void,
|
|
||||||
onUnmakeModerator: (p: CustomParticipantModel) => void,
|
|
||||||
onKick: (p: CustomParticipantModel) => void
|
|
||||||
) {
|
|
||||||
const controls = this.getParticipantControls(participant);
|
|
||||||
|
|
||||||
return {
|
|
||||||
participant,
|
|
||||||
allParticipants,
|
|
||||||
showModeratorBadge: controls.showModeratorBadge,
|
|
||||||
showModerationControls: controls.showModerationControls,
|
|
||||||
showMakeModerator: controls.showMakeModerator,
|
|
||||||
showUnmakeModerator: controls.showUnmakeModerator,
|
|
||||||
showKickButton: controls.showKickButton,
|
|
||||||
makeModeratorClickedFn: () => onMakeModerator(participant),
|
|
||||||
unmakeModeratorClickedFn: () => onUnmakeModerator(participant),
|
|
||||||
kickParticipantClickedFn: () => onKick(participant)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares inputs for the lobby plugin
|
|
||||||
*/
|
|
||||||
getLobbyInputs(
|
|
||||||
roomName: string,
|
|
||||||
meetingUrl: string,
|
|
||||||
roomClosed: boolean,
|
|
||||||
showRecordingCard: boolean,
|
|
||||||
showShareLink: boolean,
|
|
||||||
showBackButton: boolean,
|
|
||||||
backButtonText: string,
|
|
||||||
participantForm: FormGroup,
|
|
||||||
onFormSubmit: () => void,
|
|
||||||
onViewRecordings: () => void,
|
|
||||||
onBack: () => void,
|
|
||||||
onCopyLink: () => void
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
roomName,
|
|
||||||
meetingUrl,
|
|
||||||
roomClosed,
|
|
||||||
showRecordingsCard: showRecordingCard,
|
|
||||||
showShareLink,
|
|
||||||
showBackButton,
|
|
||||||
backButtonText,
|
|
||||||
participantForm,
|
|
||||||
formSubmittedFn: onFormSubmit,
|
|
||||||
viewRecordingsClickedFn: onViewRecordings,
|
|
||||||
backClickedFn: onBack,
|
|
||||||
copyLinkClickedFn: onCopyLink
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets participant controls based on action handler or default logic
|
|
||||||
*/
|
|
||||||
private getParticipantControls(participant: CustomParticipantModel): ParticipantControls {
|
|
||||||
if (this.actionHandler) {
|
|
||||||
return this.actionHandler.getParticipantControls(participant);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default implementation
|
|
||||||
return this.getDefaultParticipantControls(participant);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation for calculating participant control visibility.
|
|
||||||
*
|
|
||||||
* Rules:
|
|
||||||
* - Only moderators can see moderation controls
|
|
||||||
* - Local participant never sees controls on themselves
|
|
||||||
* - A moderator who was promoted (not original) cannot remove the moderator role from original moderators
|
|
||||||
* - A moderator who was promoted (not original) cannot kick original moderators
|
|
||||||
* - The moderator badge is shown based on the current role, not original role
|
|
||||||
*/
|
|
||||||
protected getDefaultParticipantControls(participant: CustomParticipantModel): ParticipantControls {
|
|
||||||
const isCurrentUser = participant.isLocal;
|
|
||||||
const currentUserIsModerator = this.participantService.isModeratorParticipant();
|
|
||||||
const participantIsModerator = participant.isModerator();
|
|
||||||
const participantIsOriginalModerator = participant.isOriginalModerator();
|
|
||||||
|
|
||||||
// Calculate if current moderator can revoke the moderator role from the target participant
|
|
||||||
// Only allow if target is not an original moderator
|
|
||||||
const canRevokeModeratorRole = currentUserIsModerator && !isCurrentUser && participantIsModerator && !participantIsOriginalModerator;
|
|
||||||
|
|
||||||
// Calculate if current moderator can kick the target participant
|
|
||||||
// Only allow if target is not an original moderator
|
|
||||||
const canKickParticipant = currentUserIsModerator && !isCurrentUser && !participantIsOriginalModerator;
|
|
||||||
|
|
||||||
return {
|
|
||||||
showModeratorBadge: participantIsModerator,
|
|
||||||
showModerationControls: currentUserIsModerator && !isCurrentUser,
|
|
||||||
showMakeModerator: currentUserIsModerator && !isCurrentUser && !participantIsModerator,
|
|
||||||
showUnmakeModerator: canRevokeModeratorRole,
|
|
||||||
showKickButton: canKickParticipant
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,4 +9,3 @@ export * from './lib/interceptors/index';
|
|||||||
export * from './lib/guards/index';
|
export * from './lib/guards/index';
|
||||||
export * from './lib/routes/base-routes';
|
export * from './lib/routes/base-routes';
|
||||||
export * from './lib/utils/index';
|
export * from './lib/utils/index';
|
||||||
export * from './lib/customization/index';
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { ceRoutes } from '@app/app.routes';
|
import { routes } from '@app/app.routes';
|
||||||
import { environment } from '@environment/environment';
|
import { environment } from '@environment/environment';
|
||||||
import { CustomParticipantModel, httpInterceptor, ThemeService } from '@openvidu-meet/shared-components';
|
import { CustomParticipantModel, httpInterceptor, ThemeService } from '@openvidu-meet/shared-components';
|
||||||
import { OpenViduComponentsConfig, OpenViduComponentsModule, ParticipantProperties } from 'openvidu-components-angular';
|
import { OpenViduComponentsConfig, OpenViduComponentsModule, ParticipantProperties } from 'openvidu-components-angular';
|
||||||
@ -30,7 +30,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
}),
|
}),
|
||||||
importProvidersFrom(OpenViduComponentsModule.forRoot(ovComponentsconfig)),
|
importProvidersFrom(OpenViduComponentsModule.forRoot(ovComponentsconfig)),
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(ceRoutes),
|
provideRouter(routes),
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
provideHttpClient(withInterceptors([httpInterceptor])),
|
provideHttpClient(withInterceptors([httpInterceptor])),
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,14 +1,4 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { baseRoutes, MeetingComponent } from '@openvidu-meet/shared-components';
|
import { baseRoutes } from '@openvidu-meet/shared-components';
|
||||||
import { MEETING_CE_PROVIDERS } from './customization';
|
|
||||||
|
|
||||||
/**
|
export const routes: Routes = baseRoutes;
|
||||||
* CE routes configure the plugin system using library components.
|
|
||||||
* The library's MeetingComponent uses NgComponentOutlet to render plugins dynamically.
|
|
||||||
*/
|
|
||||||
const routes = baseRoutes;
|
|
||||||
const meetingRoute = routes.find((route) => route.path === 'room/:room-id')!;
|
|
||||||
meetingRoute.component = MeetingComponent;
|
|
||||||
meetingRoute.providers = MEETING_CE_PROVIDERS;
|
|
||||||
|
|
||||||
export const ceRoutes: Routes = routes;
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from './meeting-ce.providers';
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { Provider } from '@angular/core';
|
|
||||||
import {
|
|
||||||
MEETING_COMPONENTS_TOKEN,
|
|
||||||
MeetingToolbarButtonsComponent,
|
|
||||||
MeetingParticipantPanelComponent,
|
|
||||||
MeetingShareLinkPanelComponent,
|
|
||||||
MeetingShareLinkOverlayComponent,
|
|
||||||
MeetingLobbyComponent
|
|
||||||
} from '@openvidu-meet/shared-components';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CE Meeting Providers
|
|
||||||
*
|
|
||||||
* Configures the plugin system using library components directly.
|
|
||||||
* No wrappers needed - library components receive @Input properties directly through NgComponentOutlet.
|
|
||||||
*
|
|
||||||
* The library's MeetingComponent:
|
|
||||||
* - Uses NgComponentOutlet to render plugins dynamically
|
|
||||||
* - Prepares inputs via helper methods (getToolbarAdditionalButtonsInputs, etc.)
|
|
||||||
* - Passes these inputs to plugins via [ngComponentOutletInputs]
|
|
||||||
*
|
|
||||||
* CE uses library components as plugins without any customization.
|
|
||||||
* PRO will later define its own custom components to override CE behavior.
|
|
||||||
*/
|
|
||||||
export const MEETING_CE_PROVIDERS: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: MEETING_COMPONENTS_TOKEN,
|
|
||||||
useValue: {
|
|
||||||
toolbar: {
|
|
||||||
additionalButtons: MeetingToolbarButtonsComponent,
|
|
||||||
leaveButton: MeetingToolbarButtonsComponent
|
|
||||||
},
|
|
||||||
participantPanel: {
|
|
||||||
item: MeetingParticipantPanelComponent,
|
|
||||||
afterLocalParticipant: MeetingShareLinkPanelComponent
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
additionalElements: MeetingShareLinkOverlayComponent
|
|
||||||
},
|
|
||||||
lobby: MeetingLobbyComponent
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// provide: MEETING_ACTION_HANDLER,
|
|
||||||
// useValue: {
|
|
||||||
// copySpeakerLink: () => {
|
|
||||||
// console.log('Copy speaker link clicked');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
];
|
|
||||||
@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"jest.jestCommandLine": "node --experimental-vm-modules ../../../node_modules/.bin/jest --config jest.config.mjs",
|
"jest.jestCommandLine": "node --experimental-vm-modules ./node_modules/.bin/jest --config jest.config.mjs",
|
||||||
"jest.rootPath": ".",
|
"jest.rootPath": "./",
|
||||||
"jest.nodeEnv": {
|
|
||||||
"NODE_OPTIONS": "--experimental-vm-modules"
|
|
||||||
},
|
|
||||||
"jest.runMode": "on-demand"
|
"jest.runMode": "on-demand"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,16 +6,11 @@ const jestConfig = {
|
|||||||
...createDefaultEsmPreset({
|
...createDefaultEsmPreset({
|
||||||
tsconfig: 'tsconfig.json'
|
tsconfig: 'tsconfig.json'
|
||||||
}),
|
}),
|
||||||
// Set the root directory to the webcomponent folder
|
|
||||||
rootDir: './',
|
|
||||||
resolver: 'ts-jest-resolver',
|
resolver: 'ts-jest-resolver',
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
|
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||||
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
|
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
|
||||||
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/tests/e2e/'],
|
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/tests/e2e/'],
|
||||||
transform: {
|
|
||||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }]
|
|
||||||
},
|
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
tsconfig: 'tsconfig.json'
|
tsconfig: 'tsconfig.json'
|
||||||
|
|||||||
49
meet-ce/frontend/webcomponent/test_localstorage_state.json
Normal file
49
meet-ce/frontend/webcomponent/test_localstorage_state.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"cookies": [
|
||||||
|
{
|
||||||
|
"name": "OvMeetParticipantToken",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiJ9.eyJtZXRhZGF0YSI6IntcImxpdmVraXRVcmxcIjpcIndzOi8vbG9jYWxob3N0Ojc4ODBcIixcInJvbGVzXCI6W3tcInJvbGVcIjpcInNwZWFrZXJcIixcInBlcm1pc3Npb25zXCI6e1wiY2FuUmVjb3JkXCI6ZmFsc2UsXCJjYW5DaGF0XCI6dHJ1ZSxcImNhbkNoYW5nZVZpcnR1YWxCYWNrZ3JvdW5kXCI6dHJ1ZX19XSxcInNlbGVjdGVkUm9sZVwiOlwic3BlYWtlclwifSIsIm5hbWUiOiJQLTFhYWdtYWkiLCJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6InRlc3Qtcm9vbS11cXJ3ajZsZjE3Nnk1anEiLCJjYW5QdWJsaXNoIjp0cnVlLCJjYW5TdWJzY3JpYmUiOnRydWUsImNhblB1Ymxpc2hEYXRhIjp0cnVlLCJjYW5VcGRhdGVPd25NZXRhZGF0YSI6dHJ1ZX0sImlzcyI6ImRldmtleSIsImV4cCI6MTc1ODczMTYyNiwibmJmIjowLCJzdWIiOiJQLTFhYWdtYWkifQ.pC8NFcp7U44kWosjPNlaV67Vgr_f8BlFd3Ni4x6_tR0",
|
||||||
|
"domain": "localhost",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": false,
|
||||||
|
"sameSite": "Strict"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"origins": [
|
||||||
|
{
|
||||||
|
"origin": "http://localhost:6080",
|
||||||
|
"localStorage": [
|
||||||
|
{
|
||||||
|
"name": "ovMeet-theme",
|
||||||
|
"value": "light"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ovComponents-tab_1758724425107_wtx80a2div_333_cameraEnabled",
|
||||||
|
"value": "{\"item\":true}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ovComponents-theme",
|
||||||
|
"value": "{\"item\":\"light\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ovComponents-virtualBg",
|
||||||
|
"value": "{\"item\":\"2\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ovComponents-tab_1758724425107_wtx80a2div_333_microphoneEnabled",
|
||||||
|
"value": "{\"item\":true}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ovComponents-activeTabs",
|
||||||
|
"value": "{\"item\":{\"tab_1758724425107_wtx80a2div_333\":1758724425108}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ovMeet-participantName",
|
||||||
|
"value": "P-1aagmai"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,2 +1 @@
|
|||||||
// Mock para imports de CSS en tests de Jest
|
|
||||||
module.exports = ''
|
module.exports = ''
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import {
|
|||||||
leaveRoom,
|
leaveRoom,
|
||||||
prepareForJoiningRoom
|
prepareForJoiningRoom
|
||||||
} from '../../helpers/function-helpers';
|
} from '../../helpers/function-helpers';
|
||||||
import { LeftEventReason } from '@openvidu-meet/typings';
|
|
||||||
|
|
||||||
let subscribedToAppErrors = false;
|
let subscribedToAppErrors = false;
|
||||||
|
|
||||||
@ -50,274 +49,49 @@ test.describe('Web Component E2E Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Event Handling', () => {
|
test.describe('Event Handling', () => {
|
||||||
test.describe('JOINED Event', () => {
|
test('should successfully join as moderator and receive joined event', async ({ page }) => {
|
||||||
test('should receive joined event when joining as moderator', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
await joinRoomAs('moderator', participantName, page);
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
await page.waitForSelector('.event-joined');
|
||||||
const joinElements = await page.locator('.event-joined').all();
|
const joinElements = await page.locator('.event-joined').all();
|
||||||
expect(joinElements.length).toBe(1);
|
expect(joinElements.length).toBe(1);
|
||||||
|
|
||||||
// Verify event payload contains required data
|
|
||||||
const eventText = await joinElements[0].textContent();
|
|
||||||
expect(eventText).toContain('roomId');
|
|
||||||
expect(eventText).toContain('participantIdentity');
|
|
||||||
expect(eventText).toContain(roomId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should receive joined event when joining as speaker', async ({ page }) => {
|
test('should successfully join as speaker and receive joined event', async ({ page }) => {
|
||||||
await joinRoomAs('speaker', participantName, page);
|
await joinRoomAs('speaker', participantName, page);
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
await page.waitForSelector('.event-joined');
|
||||||
const joinElements = await page.locator('.event-joined').all();
|
|
||||||
expect(joinElements.length).toBe(1);
|
|
||||||
|
|
||||||
// Verify event payload contains required data
|
|
||||||
const eventText = await joinElements[0].textContent();
|
|
||||||
expect(eventText).toContain('roomId');
|
|
||||||
expect(eventText).toContain('participantIdentity');
|
|
||||||
expect(eventText).toContain(roomId);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should receive only one joined event per join action', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Wait a bit to ensure no duplicate events
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const joinElements = await page.locator('.event-joined').all();
|
const joinElements = await page.locator('.event-joined').all();
|
||||||
expect(joinElements.length).toBe(1);
|
expect(joinElements.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('LEFT Event', () => {
|
test('should successfully join to room and receive left event when using leave command', async ({ page }) => {
|
||||||
test('should receive left event with voluntary_leave reason when using leave command', async ({
|
|
||||||
page
|
|
||||||
}) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
await joinRoomAs('moderator', participantName, page);
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await page.click('#leave-room-btn');
|
await page.click('#leave-room-btn');
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
await page.waitForSelector('.event-left');
|
||||||
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
const leftElements = await page.locator('.event-left').all();
|
||||||
expect(leftElements.length).toBe(1);
|
expect(leftElements.length).toBe(1);
|
||||||
|
|
||||||
// Verify event payload contains required data including reason
|
|
||||||
const eventText = await leftElements[0].textContent();
|
|
||||||
expect(eventText).toContain('roomId');
|
|
||||||
expect(eventText).toContain('participantIdentity');
|
|
||||||
expect(eventText).toContain('reason');
|
|
||||||
expect(eventText).toContain(LeftEventReason.VOLUNTARY_LEAVE);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should receive left event with voluntary_leave reason when using disconnect button', async ({
|
test('should successfully join to room and receive left event when using disconnect button', async ({
|
||||||
page
|
page
|
||||||
}) => {
|
}) => {
|
||||||
await joinRoomAs('moderator', participantName, page);
|
await joinRoomAs('moderator', participantName, page);
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await leaveRoom(page, 'moderator');
|
await leaveRoom(page, 'moderator');
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
await page.waitForSelector('.event-left');
|
||||||
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
const leftElements = await page.locator('.event-left').all();
|
||||||
expect(leftElements.length).toBe(1);
|
expect(leftElements.length).toBe(1);
|
||||||
|
|
||||||
// Verify event payload
|
|
||||||
const eventText = await leftElements[0].textContent();
|
|
||||||
expect(eventText).toContain('reason');
|
|
||||||
expect(eventText).toContain(LeftEventReason.VOLUNTARY_LEAVE);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should receive left event with meeting_ended reason when moderator ends meeting', async ({
|
test('should successfully join to room and receive left event when using end meeting command', async ({
|
||||||
page
|
page
|
||||||
}) => {
|
}) => {
|
||||||
await joinRoomAs('moderator', participantName, page);
|
await joinRoomAs('moderator', participantName, page);
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await page.click('#end-meeting-btn');
|
await page.click('#end-meeting-btn');
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
await page.waitForSelector('.event-left');
|
||||||
|
const meetingEndedElements = await page.locator('.event-left').all();
|
||||||
const leftElements = await page.locator('.event-left').all();
|
expect(meetingEndedElements.length).toBe(1);
|
||||||
expect(leftElements.length).toBe(1);
|
|
||||||
|
|
||||||
// Verify event payload contains meeting_ended_by_self reason
|
|
||||||
const eventText = await leftElements[0].textContent();
|
|
||||||
expect(eventText).toContain('reason');
|
|
||||||
expect(eventText).toContain(LeftEventReason.MEETING_ENDED);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should receive left event when speaker leaves room', async ({ page }) => {
|
|
||||||
await joinRoomAs('speaker', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await leaveRoom(page, 'speaker');
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
|
||||||
expect(leftElements.length).toBe(1);
|
|
||||||
|
|
||||||
// Verify event payload
|
|
||||||
const eventText = await leftElements[0].textContent();
|
|
||||||
expect(eventText).toContain('roomId');
|
|
||||||
expect(eventText).toContain('participantIdentity');
|
|
||||||
expect(eventText).toContain('reason');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('CLOSED Event', () => {
|
|
||||||
test('should receive closed event after leaving as moderator', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await page.click('#leave-room-btn');
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
|
|
||||||
// The closed event should be emitted after the left event
|
|
||||||
// Wait for a reasonable amount of time for the closed event
|
|
||||||
try {
|
|
||||||
await page.waitForSelector('.event-closed', { timeout: 5000 });
|
|
||||||
const closedElements = await page.locator('.event-closed').all();
|
|
||||||
expect(closedElements.length).toBeGreaterThanOrEqual(1);
|
|
||||||
} catch (e) {
|
|
||||||
// Closed event might not always be emitted depending on the flow
|
|
||||||
console.log('Closed event not received - this might be expected behavior');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should receive closed event after ending meeting', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await page.click('#end-meeting-btn');
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Wait for closed event after ending meeting
|
|
||||||
try {
|
|
||||||
await page.waitForSelector('.event-closed', { timeout: 5000 });
|
|
||||||
const closedElements = await page.locator('.event-closed').all();
|
|
||||||
expect(closedElements.length).toBeGreaterThanOrEqual(1);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Closed event not received - this might be expected behavior');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Event Sequences', () => {
|
|
||||||
test('should receive events in correct order: joined -> left', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Verify joined event is received first
|
|
||||||
let joinElements = await page.locator('.event-joined').all();
|
|
||||||
expect(joinElements.length).toBe(1);
|
|
||||||
|
|
||||||
await page.click('#leave-room-btn');
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Verify both events are present
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
|
||||||
expect(leftElements.length).toBe(1);
|
|
||||||
|
|
||||||
// Verify joined event is still present
|
|
||||||
joinElements = await page.locator('.event-joined').all();
|
|
||||||
expect(joinElements.length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Event Payload Validation', () => {
|
|
||||||
test('should include correct roomId in joined event payload', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
const joinElements = await page.locator('.event-joined').all();
|
|
||||||
const eventText = await joinElements[0].textContent();
|
|
||||||
|
|
||||||
// Parse the event text to extract the payload
|
|
||||||
expect(eventText).toContain(roomId);
|
|
||||||
expect(eventText).toContain('"roomId"');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should include participantIdentity in joined event payload', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
const joinElements = await page.locator('.event-joined').all();
|
|
||||||
const eventText = await joinElements[0].textContent();
|
|
||||||
|
|
||||||
expect(eventText).toContain('"participantIdentity"');
|
|
||||||
// The participantIdentity should be present (actual value may vary)
|
|
||||||
expect(eventText).toMatch(/participantIdentity.*:/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should include all required fields in left event payload', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await page.click('#leave-room-btn');
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
|
||||||
const eventText = await leftElements[0].textContent();
|
|
||||||
|
|
||||||
// Verify all required fields are present
|
|
||||||
expect(eventText).toContain('"roomId"');
|
|
||||||
expect(eventText).toContain('"participantIdentity"');
|
|
||||||
expect(eventText).toContain('"reason"');
|
|
||||||
expect(eventText).toContain(roomId);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have valid reason in left event payload', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
await page.click('#leave-room-btn');
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
|
||||||
const eventText = await leftElements[0].textContent();
|
|
||||||
|
|
||||||
// Check for valid reason values from LeftEventReason enum
|
|
||||||
const validReasons = Object.values(LeftEventReason);
|
|
||||||
|
|
||||||
const hasValidReason = validReasons.some((reason) => eventText.includes(reason));
|
|
||||||
expect(hasValidReason).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Event Error Handling', () => {
|
|
||||||
test('should handle joining and immediately leaving', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
|
|
||||||
// Leave immediately after join (without waiting for full connection)
|
|
||||||
await page.waitForTimeout(500); // Minimal wait
|
|
||||||
await page.click('#leave-room-btn');
|
|
||||||
|
|
||||||
// Should still receive left event
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
|
||||||
expect(leftElements.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not emit duplicate events on rapid actions', async ({ page }) => {
|
|
||||||
await joinRoomAs('moderator', participantName, page);
|
|
||||||
await page.waitForSelector('.event-joined', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Rapid clicking on leave button
|
|
||||||
await page.click('#leave-room-btn');
|
|
||||||
await page.click('#leave-room-btn').catch(() => {
|
|
||||||
/* Button might not be available */
|
|
||||||
});
|
|
||||||
await page.click('#leave-room-btn').catch(() => {
|
|
||||||
/* Button might not be available */
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.waitForSelector('.event-left', { timeout: 10000 });
|
|
||||||
await page.waitForTimeout(1000); // Wait for any potential duplicate events
|
|
||||||
|
|
||||||
// Should only have one left event
|
|
||||||
const leftElements = await page.locator('.event-left').all();
|
|
||||||
expect(leftElements.length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,387 +0,0 @@
|
|||||||
import { expect, test, Page, BrowserContext } from '@playwright/test';
|
|
||||||
import { MEET_TESTAPP_URL } from '../../config.js';
|
|
||||||
import {
|
|
||||||
createTestRoom,
|
|
||||||
deleteAllRecordings,
|
|
||||||
deleteAllRooms,
|
|
||||||
getIframeInShadowDom,
|
|
||||||
getLocalParticipantId,
|
|
||||||
getParticipantIdByName,
|
|
||||||
interactWithElementInIframe,
|
|
||||||
isShareLinkOverlayyHidden,
|
|
||||||
joinRoomAs,
|
|
||||||
leaveRoom,
|
|
||||||
makeParticipantModerator,
|
|
||||||
openParticipantsPanel,
|
|
||||||
prepareForJoiningRoom,
|
|
||||||
removeParticipantModerator,
|
|
||||||
waitForElementInIframe
|
|
||||||
} from '../../helpers/function-helpers.js';
|
|
||||||
|
|
||||||
let subscribedToAppErrors = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test suite for moderation features in OpenVidu Meet
|
|
||||||
* Tests moderator-specific functionality including share link overlay,
|
|
||||||
* moderator badges, and moderation controls (make/unmake moderator, kick participant)
|
|
||||||
*/
|
|
||||||
test.describe('Moderation Functionality Tests', () => {
|
|
||||||
let roomId: string;
|
|
||||||
let moderatorName: string;
|
|
||||||
let speakerName: string;
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// SETUP & TEARDOWN
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
// Create a test room before all tests
|
|
||||||
roomId = await createTestRoom('moderation-test-room');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
if (!subscribedToAppErrors) {
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
const type = msg.type();
|
|
||||||
const tag = type === 'error' ? 'ERROR' : type === 'warning' ? 'WARNING' : 'LOG';
|
|
||||||
console.log('[' + tag + ']', msg.text());
|
|
||||||
});
|
|
||||||
subscribedToAppErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
moderatorName = `Moderator-${Math.random().toString(36).substring(2, 9)}`;
|
|
||||||
speakerName = `Speaker-${Math.random().toString(36).substring(2, 9)}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterEach(async ({ context }) => {
|
|
||||||
// Save storage state after each test
|
|
||||||
await context.storageState({ path: 'test_localstorage_state.json' });
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterAll(async ({ browser }) => {
|
|
||||||
const tempContext = await browser.newContext();
|
|
||||||
const tempPage = await tempContext.newPage();
|
|
||||||
await deleteAllRooms(tempPage);
|
|
||||||
await deleteAllRecordings(tempPage);
|
|
||||||
|
|
||||||
await tempContext.close();
|
|
||||||
await tempPage.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// SHARE LINK OVERLAY TESTS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
test.describe('Share Link Overlay', () => {
|
|
||||||
test('should show share link overlay when moderator is alone in the room', async ({ page }) => {
|
|
||||||
// Moderator joins the room
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', moderatorName, page);
|
|
||||||
|
|
||||||
// Wait for session to be established
|
|
||||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Check that share link overlay is visible
|
|
||||||
const shareLinkOverlay = await waitForElementInIframe(page, '#share-link-overlay', {
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 5000
|
|
||||||
});
|
|
||||||
await expect(shareLinkOverlay).toBeVisible();
|
|
||||||
|
|
||||||
await leaveRoom(page, 'moderator');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should hide share link overlay when other participants join the room', async ({ page, browser }) => {
|
|
||||||
// Moderator joins the room
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', moderatorName, page);
|
|
||||||
|
|
||||||
// Wait for session and check overlay is visible
|
|
||||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
|
||||||
const shareLinkOverlay = await waitForElementInIframe(page, '#share-link-overlay', {
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 5000
|
|
||||||
});
|
|
||||||
await expect(shareLinkOverlay).toBeVisible();
|
|
||||||
|
|
||||||
// Second participant (speaker) joins
|
|
||||||
const speakerContext = await browser.newContext();
|
|
||||||
const speakerPage = await speakerContext.newPage();
|
|
||||||
await prepareForJoiningRoom(speakerPage, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('speaker', speakerName, speakerPage);
|
|
||||||
|
|
||||||
// Wait for remote participant to be visible in moderator's view
|
|
||||||
await waitForElementInIframe(page, '.OV_stream.remote', { state: 'visible', timeout: 10000 });
|
|
||||||
|
|
||||||
// Wait a moment for the overlay to hide (give it more time)
|
|
||||||
await page.waitForTimeout(3000);
|
|
||||||
|
|
||||||
// Check that share link overlay is no longer visible for moderator
|
|
||||||
const isHidden = await isShareLinkOverlayyHidden(page, '#share-link-overlay');
|
|
||||||
expect(isHidden).toBeTruthy();
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await leaveRoom(speakerPage);
|
|
||||||
await leaveRoom(page, 'moderator');
|
|
||||||
await speakerContext.close();
|
|
||||||
});
|
|
||||||
test('should not show share link overlay when user is not a moderator', async ({ page }) => {
|
|
||||||
// Speaker joins the room
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('speaker', speakerName, page);
|
|
||||||
|
|
||||||
// Wait for session to be established
|
|
||||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
// Check that share link overlay is not visible
|
|
||||||
const isHidden = await isShareLinkOverlayyHidden(page, '#share-link-overlay');
|
|
||||||
expect(isHidden).toBeTruthy();
|
|
||||||
|
|
||||||
await leaveRoom(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MODERATOR BADGE AND CONTROLS TESTS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
test.describe('Moderator Badge and Controls', () => {
|
|
||||||
test('should show moderator badge and controls when making participant a moderator', async ({
|
|
||||||
page,
|
|
||||||
browser
|
|
||||||
}) => {
|
|
||||||
// Moderator joins the room
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', moderatorName, page);
|
|
||||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Speaker joins the room
|
|
||||||
const speakerContext = await browser.newContext();
|
|
||||||
const speakerPage = await speakerContext.newPage();
|
|
||||||
await prepareForJoiningRoom(speakerPage, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('speaker', speakerName, speakerPage);
|
|
||||||
await waitForElementInIframe(speakerPage, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Wait for remote participant to appear in both views
|
|
||||||
await waitForElementInIframe(page, '.OV_stream.remote', { state: 'visible', timeout: 10000 });
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
// Moderator opens participants panel
|
|
||||||
await openParticipantsPanel(page);
|
|
||||||
|
|
||||||
// Get speaker's participant ID
|
|
||||||
const speakerParticipantId = await getParticipantIdByName(page, speakerName);
|
|
||||||
|
|
||||||
if (!speakerParticipantId) {
|
|
||||||
throw new Error(`Could not find speaker participant ID for: ${speakerName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make speaker a moderator
|
|
||||||
await makeParticipantModerator(page, speakerParticipantId);
|
|
||||||
|
|
||||||
// Speaker opens their participants panel
|
|
||||||
await openParticipantsPanel(speakerPage);
|
|
||||||
|
|
||||||
// Get speaker's own participant ID from their page
|
|
||||||
const speakerOwnParticipantId = await getLocalParticipantId(speakerPage);
|
|
||||||
|
|
||||||
if (!speakerOwnParticipantId) {
|
|
||||||
throw new Error('Could not find speaker own participant ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
const moderatorBadge = await waitForElementInIframe(
|
|
||||||
speakerPage,
|
|
||||||
`#moderator-badge-${speakerOwnParticipantId}`,
|
|
||||||
{
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await expect(moderatorBadge).toBeVisible();
|
|
||||||
|
|
||||||
// Speaker (now moderator) should be able to see moderation controls
|
|
||||||
// We verify by checking that at least one .moderation-controls div exists in the DOM
|
|
||||||
const frameLocator = await getIframeInShadowDom(speakerPage);
|
|
||||||
const moderationControlsCount = await frameLocator.locator('.moderation-controls').count();
|
|
||||||
|
|
||||||
// Should have at least 1 moderation-controls div (for the original moderator)
|
|
||||||
expect(moderationControlsCount).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await leaveRoom(speakerPage, 'moderator');
|
|
||||||
await leaveRoom(page, 'moderator');
|
|
||||||
await speakerContext.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should remove moderator badge and controls when revoking moderator role', async ({ page, browser }) => {
|
|
||||||
// Moderator joins the room
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', moderatorName, page);
|
|
||||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Speaker joins the room
|
|
||||||
const speakerContext = await browser.newContext();
|
|
||||||
const speakerPage = await speakerContext.newPage();
|
|
||||||
await prepareForJoiningRoom(speakerPage, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('speaker', speakerName, speakerPage);
|
|
||||||
await waitForElementInIframe(speakerPage, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Wait for remote participant to appear
|
|
||||||
await waitForElementInIframe(page, '.OV_stream.remote', { state: 'visible', timeout: 10000 });
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
// Moderator opens participants panel
|
|
||||||
await openParticipantsPanel(page);
|
|
||||||
|
|
||||||
// Get speaker's participant ID
|
|
||||||
const speakerParticipantId = await getParticipantIdByName(page, speakerName);
|
|
||||||
|
|
||||||
if (!speakerParticipantId) {
|
|
||||||
throw new Error(`Could not find speaker participant ID for: ${speakerName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make speaker a moderator
|
|
||||||
await makeParticipantModerator(page, speakerParticipantId);
|
|
||||||
|
|
||||||
// Verify speaker has moderator badge
|
|
||||||
await openParticipantsPanel(speakerPage);
|
|
||||||
|
|
||||||
const speakerOwnParticipantId = await getLocalParticipantId(speakerPage);
|
|
||||||
|
|
||||||
if (!speakerOwnParticipantId) {
|
|
||||||
throw new Error('Could not find speaker own participant ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
const moderatorBadge = await waitForElementInIframe(
|
|
||||||
speakerPage,
|
|
||||||
`#moderator-badge-${speakerOwnParticipantId}`,
|
|
||||||
{
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await expect(moderatorBadge).toBeVisible();
|
|
||||||
|
|
||||||
// Now revoke moderator role
|
|
||||||
await removeParticipantModerator(page, speakerParticipantId);
|
|
||||||
|
|
||||||
// Speaker should no longer see moderator badge
|
|
||||||
await waitForElementInIframe(speakerPage, `#moderator-badge-${speakerOwnParticipantId}`, {
|
|
||||||
state: 'hidden',
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Speaker should not see moderation controls (verify they can't see controls for the moderator)
|
|
||||||
const moderatorParticipantId = await getParticipantIdByName(speakerPage, moderatorName);
|
|
||||||
if (moderatorParticipantId) {
|
|
||||||
// If speaker is no longer moderator, moderation-controls div should be hidden
|
|
||||||
await waitForElementInIframe(speakerPage, `#moderation-controls-${moderatorParticipantId}`, {
|
|
||||||
state: 'hidden',
|
|
||||||
timeout: 5000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await leaveRoom(speakerPage);
|
|
||||||
await leaveRoom(page, 'moderator');
|
|
||||||
await speakerContext.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// ORIGINAL MODERATOR PROTECTION TESTS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
test.describe('Original Moderator Protection', () => {
|
|
||||||
test('should not allow removing moderator role from original moderator', async ({ page, browser }) => {
|
|
||||||
// Moderator joins the room
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', moderatorName, page);
|
|
||||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Speaker joins as second moderator
|
|
||||||
const speakerContext = await browser.newContext();
|
|
||||||
const speakerPage = await speakerContext.newPage();
|
|
||||||
await prepareForJoiningRoom(speakerPage, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', speakerName, speakerPage);
|
|
||||||
await waitForElementInIframe(speakerPage, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Wait for both participants to be in the session
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
// Second moderator opens participants panel
|
|
||||||
await openParticipantsPanel(speakerPage);
|
|
||||||
|
|
||||||
// Get original moderator's participant ID
|
|
||||||
const originalModParticipantId = await getParticipantIdByName(speakerPage, moderatorName);
|
|
||||||
|
|
||||||
if (!originalModParticipantId) {
|
|
||||||
throw new Error(`Could not find original moderator participant ID for: ${moderatorName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that "remove moderator" button is NOT present for original moderator
|
|
||||||
// The button should be in hidden state (not rendered)
|
|
||||||
try {
|
|
||||||
await waitForElementInIframe(speakerPage, `#remove-moderator-btn-${originalModParticipantId}`, {
|
|
||||||
state: 'hidden',
|
|
||||||
timeout: 2000
|
|
||||||
});
|
|
||||||
// If we get here, the button is correctly hidden
|
|
||||||
} catch (error) {
|
|
||||||
// If the element doesn't exist at all, that's also correct
|
|
||||||
console.log('✅ Remove moderator button not found for original moderator (as expected)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await leaveRoom(speakerPage, 'moderator');
|
|
||||||
await leaveRoom(page, 'moderator');
|
|
||||||
await speakerContext.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not allow kicking original moderator from the room', async ({ page, browser }) => {
|
|
||||||
// Moderator joins the room
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', moderatorName, page);
|
|
||||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Speaker joins as second moderator
|
|
||||||
const speakerContext = await browser.newContext();
|
|
||||||
const speakerPage = await speakerContext.newPage();
|
|
||||||
await prepareForJoiningRoom(speakerPage, MEET_TESTAPP_URL, roomId);
|
|
||||||
await joinRoomAs('moderator', speakerName, speakerPage);
|
|
||||||
await waitForElementInIframe(speakerPage, 'ov-session', { state: 'visible' });
|
|
||||||
|
|
||||||
// Wait for both participants to be in the session
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
// Second moderator opens participants panel
|
|
||||||
await openParticipantsPanel(speakerPage);
|
|
||||||
|
|
||||||
// Get original moderator's participant ID
|
|
||||||
const originalModParticipantId = await getParticipantIdByName(speakerPage, moderatorName);
|
|
||||||
|
|
||||||
if (!originalModParticipantId) {
|
|
||||||
throw new Error(`Could not find original moderator participant ID for: ${moderatorName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that "kick participant" button is NOT present for original moderator
|
|
||||||
// The button should be in hidden state (not rendered)
|
|
||||||
try {
|
|
||||||
await waitForElementInIframe(speakerPage, `#kick-participant-btn-${originalModParticipantId}`, {
|
|
||||||
state: 'hidden',
|
|
||||||
timeout: 2000
|
|
||||||
});
|
|
||||||
// If we get here, the button is correctly hidden
|
|
||||||
} catch (error) {
|
|
||||||
// If the element doesn't exist at all, that's also correct
|
|
||||||
console.log('✅ Kick participant button not found for original moderator (as expected)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await leaveRoom(speakerPage, 'moderator');
|
|
||||||
await leaveRoom(page, 'moderator');
|
|
||||||
await speakerContext.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -401,148 +401,3 @@ export const closeMoreOptionsMenu = async (page: Page) => {
|
|||||||
await interactWithElementInIframe(page, 'body', { action: 'click' });
|
await interactWithElementInIframe(page, 'body', { action: 'click' });
|
||||||
await page.waitForTimeout(500); // Wait for menu to close
|
await page.waitForTimeout(500); // Wait for menu to close
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MODERATION HELPER FUNCTIONS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the participant ID (sid) of a participant by name from a specific page view
|
|
||||||
* @param page - Playwright page object
|
|
||||||
* @param participantName - Name of the participant to find
|
|
||||||
* @returns Promise resolving to the participant ID (sid) or empty string if not found
|
|
||||||
*/
|
|
||||||
export const getParticipantIdByName = async (page: Page, participantName: string): Promise<string> => {
|
|
||||||
// Get iframe using the proper Playwright method
|
|
||||||
const frameLocator = await getIframeInShadowDom(page);
|
|
||||||
|
|
||||||
// Find all participant containers
|
|
||||||
const participantContainers = frameLocator.locator('[data-participant-id]');
|
|
||||||
const count = await participantContainers.count();
|
|
||||||
console.log(`🔍 Found ${count} participant containers`);
|
|
||||||
|
|
||||||
// Iterate through participants to find the matching name
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const container = participantContainers.nth(i);
|
|
||||||
const nameElement = container.locator('.participant-name-text');
|
|
||||||
const pName = await nameElement.textContent();
|
|
||||||
const pId = await container.getAttribute('data-participant-id');
|
|
||||||
|
|
||||||
console.log(`👤 Participant: "${pName?.trim()}" with ID: ${pId}`);
|
|
||||||
|
|
||||||
if (pName?.trim() === participantName) {
|
|
||||||
console.log(`✅ Found matching participant: ${participantName} with ID: ${pId}`);
|
|
||||||
return pId || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`❌ Could not find participant with name: ${participantName}`);
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current user's own participant ID (sid)
|
|
||||||
* @param page - Playwright page object
|
|
||||||
* @returns Promise resolving to the local participant's ID (sid) or empty string if not found
|
|
||||||
*/
|
|
||||||
export const getLocalParticipantId = async (page: Page): Promise<string> => {
|
|
||||||
// Get iframe using the proper Playwright method
|
|
||||||
const frameLocator = await getIframeInShadowDom(page);
|
|
||||||
|
|
||||||
// Find all participant containers
|
|
||||||
const participantContainers = frameLocator.locator('[data-participant-id]');
|
|
||||||
const count = await participantContainers.count();
|
|
||||||
console.log(`🔍 Found ${count} participant containers`);
|
|
||||||
|
|
||||||
// Iterate through participants to find the local one (has .local-indicator)
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const container = participantContainers.nth(i);
|
|
||||||
const youLabel = container.locator('.local-indicator');
|
|
||||||
const hasYouLabel = (await youLabel.count()) > 0;
|
|
||||||
|
|
||||||
if (hasYouLabel) {
|
|
||||||
const nameElement = container.locator('.participant-name-text');
|
|
||||||
const participantName = await nameElement.textContent();
|
|
||||||
const pId = await container.getAttribute('data-participant-id');
|
|
||||||
|
|
||||||
console.log(`✅ Found local participant: "${participantName?.trim()}" with ID: ${pId}`);
|
|
||||||
return pId || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('❌ Could not find local participant');
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the participants panel and waits for it to be visible
|
|
||||||
* @param page - Playwright page object
|
|
||||||
*/
|
|
||||||
export const openParticipantsPanel = async (page: Page): Promise<void> => {
|
|
||||||
await waitForElementInIframe(page, '#participants-panel-btn');
|
|
||||||
await interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' });
|
|
||||||
await waitForElementInIframe(page, 'ov-participants-panel', { state: 'visible' });
|
|
||||||
await page.waitForTimeout(1000); // Wait for panel to fully load
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a participant a moderator by clicking the make-moderator button
|
|
||||||
* @param page - Playwright page object (moderator's page)
|
|
||||||
* @param participantId - The participant ID (sid) to promote
|
|
||||||
*/
|
|
||||||
export const makeParticipantModerator = async (page: Page, participantId: string): Promise<void> => {
|
|
||||||
const makeModeratorbtn = await waitForElementInIframe(page, `#make-moderator-btn-${participantId}`, {
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
await makeModeratorbtn.click();
|
|
||||||
await page.waitForTimeout(2000); // Wait for role change to propagate
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes moderator role from a participant by clicking the remove-moderator button
|
|
||||||
* @param page - Playwright page object (moderator's page)
|
|
||||||
* @param participantId - The participant ID (sid) to demote
|
|
||||||
*/
|
|
||||||
export const removeParticipantModerator = async (page: Page, participantId: string): Promise<void> => {
|
|
||||||
const removeModeratorbtn = await waitForElementInIframe(page, `#remove-moderator-btn-${participantId}`, {
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
await removeModeratorbtn.click();
|
|
||||||
await page.waitForTimeout(2000); // Wait for role change to propagate
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an overlay element is hidden in the iframe
|
|
||||||
* An element is considered hidden if:
|
|
||||||
* - It doesn't exist in the DOM (removed)
|
|
||||||
* - Has display: none
|
|
||||||
* - Has visibility: hidden
|
|
||||||
* - Has opacity: 0
|
|
||||||
* @param page - Playwright page object
|
|
||||||
* @param overlaySelector - CSS selector for the overlay element
|
|
||||||
* @returns Promise resolving to true if the overlay is hidden, false otherwise
|
|
||||||
*/
|
|
||||||
export const isShareLinkOverlayyHidden = async (page: Page, overlaySelector: string): Promise<boolean> => {
|
|
||||||
const frameLocator = await getIframeInShadowDom(page);
|
|
||||||
const overlay = frameLocator.locator(overlaySelector);
|
|
||||||
const count = await overlay.count();
|
|
||||||
|
|
||||||
// Element doesn't exist in the DOM
|
|
||||||
if (count === 0) {
|
|
||||||
console.log('✅ Overlay element not found in DOM (removed)');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if element is hidden via CSS
|
|
||||||
const isVisible = await overlay.isVisible().catch(() => false);
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
console.log('✅ Overlay is hidden (display: none, visibility: hidden, or opacity: 0)');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('❌ Overlay is still visible');
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
import { describe, expect, it, jest } from '@jest/globals';
|
||||||
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
||||||
import '../../src/index';
|
import '../../src/index';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
import { describe, expect, it, jest } from '@jest/globals';
|
||||||
import { CommandsManager } from '../../src/components/CommandsManager';
|
import { CommandsManager } from '../../src/components/CommandsManager';
|
||||||
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
||||||
import '../../src/index';
|
import '../../src/index';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
import { describe, expect, it, jest } from '@jest/globals';
|
||||||
import { EventsManager } from '../../src/components/EventsManager';
|
import { EventsManager } from '../../src/components/EventsManager';
|
||||||
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
||||||
import '../../src/index';
|
import '../../src/index';
|
||||||
|
|||||||
7
meet.sh
7
meet.sh
@ -393,12 +393,6 @@ add_common_dev_commands() {
|
|||||||
CMD_NAMES+=("shared-meet-components")
|
CMD_NAMES+=("shared-meet-components")
|
||||||
CMD_COLORS+=("bgYellow.dark")
|
CMD_COLORS+=("bgYellow.dark")
|
||||||
CMD_COMMANDS+=("wait-on ${components_path} && pnpm --filter @openvidu-meet/frontend run lib:serve")
|
CMD_COMMANDS+=("wait-on ${components_path} && pnpm --filter @openvidu-meet/frontend run lib:serve")
|
||||||
|
|
||||||
# Testapp
|
|
||||||
CMD_NAMES+=("testapp")
|
|
||||||
CMD_COLORS+=("blue")
|
|
||||||
CMD_COMMANDS+=("node ./scripts/dev/watch-with-typings-guard.mjs 'pnpm run dev:testapp'")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: Add CE-specific commands (backend, frontend)
|
# Helper: Add CE-specific commands (backend, frontend)
|
||||||
@ -479,7 +473,6 @@ add_browsersync_commands() {
|
|||||||
const local = urls?.get('local') ?? 'undefined';
|
const local = urls?.get('local') ?? 'undefined';
|
||||||
const external = urls?.get('external') ?? 'undefined';
|
const external = urls?.get('external') ?? 'undefined';
|
||||||
console.log(chalk.cyanBright(' OpenVidu Meet: http://localhost:6080'));
|
console.log(chalk.cyanBright(' OpenVidu Meet: http://localhost:6080'));
|
||||||
console.log(chalk.cyanBright(' OpenVidu Meet Testapp: http://localhost:5080'));
|
|
||||||
console.log(chalk.cyanBright(' Live reload Local: ' + local));
|
console.log(chalk.cyanBright(' Live reload Local: ' + local));
|
||||||
console.log(chalk.cyanBright(' Live reload LAN: ' + external));
|
console.log(chalk.cyanBright(' Live reload LAN: ' + external));
|
||||||
|
|
||||||
|
|||||||
124
nginx.conf
124
nginx.conf
@ -1,124 +0,0 @@
|
|||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
# Configuración de logs
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
|
|
||||||
# Configuración básica
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
|
|
||||||
# Compresión
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_min_length 1024;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
text/xml
|
|
||||||
text/javascript
|
|
||||||
application/json
|
|
||||||
application/javascript
|
|
||||||
application/xml+rss
|
|
||||||
application/atom+xml
|
|
||||||
image/svg+xml;
|
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
||||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
|
||||||
|
|
||||||
# Upstream para OpenVidu Meet
|
|
||||||
upstream openvidu-backend {
|
|
||||||
server openvidu-meet:6080;
|
|
||||||
keepalive 32;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Servidor principal - Solo HTTP puerto 80
|
|
||||||
# EasyPanel/Traefik maneja SSL y subdominios
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# Headers para proxy (EasyPanel/Traefik)
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
|
|
||||||
# Timeouts
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_buffering off;
|
|
||||||
|
|
||||||
# WebSocket support
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
# API endpoints con rate limiting
|
|
||||||
location /api/ {
|
|
||||||
limit_req zone=api burst=20 nodelay;
|
|
||||||
proxy_pass http://openvidu-backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Login endpoint con rate limiting estricto
|
|
||||||
location /api/auth/login {
|
|
||||||
limit_req zone=login burst=5 nodelay;
|
|
||||||
proxy_pass http://openvidu-backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
# WebSocket para LiveKit
|
|
||||||
location /ws {
|
|
||||||
proxy_pass http://openvidu-backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Archivos estáticos con cache
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
proxy_pass http://openvidu-backend;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Todas las demás rutas
|
|
||||||
location / {
|
|
||||||
proxy_pass http://openvidu-backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Health check para EasyPanel
|
|
||||||
location /health {
|
|
||||||
access_log off;
|
|
||||||
return 200 "healthy\n";
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Security headers básicos
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
|
|
||||||
# Ocultar versión de nginx
|
|
||||||
server_tokens off;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -42,146 +42,20 @@
|
|||||||
"**/node_modules": true,
|
"**/node_modules": true,
|
||||||
"**/test-results": true,
|
"**/test-results": true,
|
||||||
},
|
},
|
||||||
"jest.disabledWorkspaceFolders": ["openvidu-meet (root)", "openvidu-components-angular", "shared-meet-components", "meet-testapp"],
|
"jest.disabledWorkspaceFolders": ["openvidu-meet", "typings", "frontend"],
|
||||||
"mochaExplorer.files": "tests/e2e/**/*.test.ts",
|
"mochaExplorer.files": "tests/e2e/**/*.test.ts",
|
||||||
"mochaExplorer.require": ".mocharc.js",
|
"mochaExplorer.require": ".mocharc.js",
|
||||||
"files.watcherExclude": {
|
"files.watcherExclude": {
|
||||||
"**/node_modules/**": true,
|
"**/node_modules/**": true,
|
||||||
"**/dist/**": true,
|
"**/dist/**": true,
|
||||||
},
|
},
|
||||||
},
|
// "mochaExplorer.files": "./frontend/tests/e2e/**/*.test.ts",
|
||||||
"launch": {
|
// "mochaExplorer.mochaPath": "./frontend/node_modules/mocha",
|
||||||
"version": "0.2.0",
|
// "mochaExplorer.require": "ts-node/register",
|
||||||
"configurations": [
|
// "mochaExplorer.configFile": "./frontend/.mocharc.js",
|
||||||
{
|
// "mochaExplorer.timeout": 30000,
|
||||||
"name": "Debug Jest Test (CE Backend)",
|
// "mochaExplorer.env": {
|
||||||
"type": "node",
|
// "NODE_ENV": "test"
|
||||||
"request": "launch",
|
// }
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest",
|
|
||||||
"args": [
|
|
||||||
"--config",
|
|
||||||
"${workspaceFolder:openvidu-meet (CE)}/backend/jest.config.mjs",
|
|
||||||
"--runInBand",
|
|
||||||
"--no-cache",
|
|
||||||
"${file}"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"windows": {
|
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest.cmd"
|
|
||||||
},
|
|
||||||
"runtimeArgs": [
|
|
||||||
"--experimental-vm-modules"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
},
|
|
||||||
"cwd": "${workspaceFolder:openvidu-meet (CE)}/backend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug Jest Test (PRO Backend)",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest",
|
|
||||||
"args": [
|
|
||||||
"--config",
|
|
||||||
"${workspaceFolder:openvidu-meet (PRO)}/backend/jest.config.mjs",
|
|
||||||
"--runInBand",
|
|
||||||
"--no-cache",
|
|
||||||
"${file}"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"windows": {
|
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest.cmd"
|
|
||||||
},
|
|
||||||
"runtimeArgs": [
|
|
||||||
"--experimental-vm-modules"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
},
|
|
||||||
"cwd": "${workspaceFolder:openvidu-meet (PRO)}/backend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug Jest Test (Webcomponent)",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest",
|
|
||||||
"args": [
|
|
||||||
"--config",
|
|
||||||
"${workspaceFolder:meet-webcomponent}/jest.config.mjs",
|
|
||||||
"--runInBand",
|
|
||||||
"--no-cache",
|
|
||||||
"${file}"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"windows": {
|
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest.cmd"
|
|
||||||
},
|
|
||||||
"runtimeArgs": [
|
|
||||||
"--experimental-vm-modules"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
},
|
|
||||||
"cwd": "${workspaceFolder:meet-webcomponent}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug Current Jest Test (CE Backend)",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest",
|
|
||||||
"args": [
|
|
||||||
"--config",
|
|
||||||
"${workspaceFolder:openvidu-meet (CE)}/backend/jest.config.mjs",
|
|
||||||
"--runInBand",
|
|
||||||
"--testNamePattern",
|
|
||||||
"${input:testName}",
|
|
||||||
"${file}"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"runtimeArgs": [
|
|
||||||
"--experimental-vm-modules"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
},
|
|
||||||
"cwd": "${workspaceFolder:openvidu-meet (CE)}/backend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug Current Jest Test (PRO Backend)",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder:openvidu-meet (root)}/node_modules/.bin/jest",
|
|
||||||
"args": [
|
|
||||||
"--config",
|
|
||||||
"${workspaceFolder:openvidu-meet (PRO)}/backend/jest.config.mjs",
|
|
||||||
"--runInBand",
|
|
||||||
"--testNamePattern",
|
|
||||||
"${input:testName}",
|
|
||||||
"${file}"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"runtimeArgs": [
|
|
||||||
"--experimental-vm-modules"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
},
|
|
||||||
"cwd": "${workspaceFolder:openvidu-meet (PRO)}/backend"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"id": "testName",
|
|
||||||
"description": "Test name pattern (optional, leave empty to run all tests in file)",
|
|
||||||
"default": "",
|
|
||||||
"type": "promptString"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -39,7 +39,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browser-sync": "3.0.4",
|
"browser-sync": "3.0.4",
|
||||||
"concurrently": "9.2.1",
|
"concurrently": "9.2.1",
|
||||||
"identity-obj-proxy": "3.0.0",
|
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"wait-on": "9.0.1"
|
"wait-on": "9.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
621
pnpm-lock.yaml
generated
621
pnpm-lock.yaml
generated
@ -14,9 +14,6 @@ importers:
|
|||||||
concurrently:
|
concurrently:
|
||||||
specifier: 9.2.1
|
specifier: 9.2.1
|
||||||
version: 9.2.1
|
version: 9.2.1
|
||||||
identity-obj-proxy:
|
|
||||||
specifier: 3.0.0
|
|
||||||
version: 3.0.0
|
|
||||||
tree-kill:
|
tree-kill:
|
||||||
specifier: 1.2.2
|
specifier: 1.2.2
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
@ -24,6 +21,42 @@ importers:
|
|||||||
specifier: 9.0.1
|
specifier: 9.0.1
|
||||||
version: 9.0.1
|
version: 9.0.1
|
||||||
|
|
||||||
|
../openvidu/openvidu-components-angular/projects/openvidu-components-angular:
|
||||||
|
dependencies:
|
||||||
|
'@angular/animations':
|
||||||
|
specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||||
|
version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||||
|
'@angular/cdk':
|
||||||
|
specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||||
|
version: 20.2.9(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||||
|
'@angular/common':
|
||||||
|
specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||||
|
version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||||
|
'@angular/core':
|
||||||
|
specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||||
|
version: 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||||
|
'@angular/forms':
|
||||||
|
specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||||
|
version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
||||||
|
'@angular/material':
|
||||||
|
specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||||
|
version: 20.2.9(b517547b325ffc8400ae4cda6a618bfd)
|
||||||
|
'@livekit/track-processors':
|
||||||
|
specifier: ^0.6.0
|
||||||
|
version: 0.6.1(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.11(@types/dom-mediacapture-record@1.0.22))
|
||||||
|
autolinker:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.1.5
|
||||||
|
buffer:
|
||||||
|
specifier: ^6.0.3
|
||||||
|
version: 6.0.3
|
||||||
|
livekit-client:
|
||||||
|
specifier: ^2.15.0
|
||||||
|
version: 2.15.11(@types/dom-mediacapture-record@1.0.22)
|
||||||
|
tslib:
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.8.1
|
||||||
|
|
||||||
meet-ce/backend:
|
meet-ce/backend:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-s3':
|
'@aws-sdk/client-s3':
|
||||||
@ -241,8 +274,8 @@ importers:
|
|||||||
specifier: 2.15.11
|
specifier: 2.15.11
|
||||||
version: 2.15.11(@types/dom-mediacapture-record@1.0.22)
|
version: 2.15.11(@types/dom-mediacapture-record@1.0.22)
|
||||||
openvidu-components-angular:
|
openvidu-components-angular:
|
||||||
specifier: 3.4.0
|
specifier: workspace:*
|
||||||
version: 3.4.0(3c63e791699a7778b296d869e22cd258)
|
version: link:../../../openvidu/openvidu-components-angular/projects/openvidu-components-angular
|
||||||
rxjs:
|
rxjs:
|
||||||
specifier: 7.8.2
|
specifier: 7.8.2
|
||||||
version: 7.8.2
|
version: 7.8.2
|
||||||
@ -374,8 +407,8 @@ importers:
|
|||||||
meet-ce/frontend/projects/shared-meet-components:
|
meet-ce/frontend/projects/shared-meet-components:
|
||||||
dependencies:
|
dependencies:
|
||||||
openvidu-components-angular:
|
openvidu-components-angular:
|
||||||
specifier: 3.4.0
|
specifier: workspace:*
|
||||||
version: 3.4.0(3c63e791699a7778b296d869e22cd258)
|
version: link:../../../../../openvidu/openvidu-components-angular/projects/openvidu-components-angular
|
||||||
tslib:
|
tslib:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
@ -462,6 +495,365 @@ importers:
|
|||||||
specifier: 5.9.2
|
specifier: 5.9.2
|
||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
|
|
||||||
|
meet-pro/backend:
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/client-s3':
|
||||||
|
specifier: 3.846.0
|
||||||
|
version: 3.846.0
|
||||||
|
'@azure/storage-blob':
|
||||||
|
specifier: 12.27.0
|
||||||
|
version: 12.27.0
|
||||||
|
'@google-cloud/storage':
|
||||||
|
specifier: 7.17.1
|
||||||
|
version: 7.17.1(encoding@0.1.13)
|
||||||
|
'@openvidu-meet-pro/typings':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../typings
|
||||||
|
'@openvidu-meet/backend':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../meet-ce/backend
|
||||||
|
'@sesamecare-oss/redlock':
|
||||||
|
specifier: 1.4.0
|
||||||
|
version: 1.4.0(ioredis@5.6.1)
|
||||||
|
archiver:
|
||||||
|
specifier: 7.0.1
|
||||||
|
version: 7.0.1
|
||||||
|
bcrypt:
|
||||||
|
specifier: 5.1.1
|
||||||
|
version: 5.1.1(encoding@0.1.13)
|
||||||
|
body-parser:
|
||||||
|
specifier: 2.2.0
|
||||||
|
version: 2.2.0
|
||||||
|
chalk:
|
||||||
|
specifier: 5.6.2
|
||||||
|
version: 5.6.2
|
||||||
|
cookie-parser:
|
||||||
|
specifier: 1.4.7
|
||||||
|
version: 1.4.7
|
||||||
|
cors:
|
||||||
|
specifier: 2.8.5
|
||||||
|
version: 2.8.5
|
||||||
|
cron:
|
||||||
|
specifier: 4.3.3
|
||||||
|
version: 4.3.3
|
||||||
|
dotenv:
|
||||||
|
specifier: 16.6.1
|
||||||
|
version: 16.6.1
|
||||||
|
express:
|
||||||
|
specifier: 4.21.2
|
||||||
|
version: 4.21.2
|
||||||
|
express-rate-limit:
|
||||||
|
specifier: 7.5.1
|
||||||
|
version: 7.5.1(express@4.21.2)
|
||||||
|
inversify:
|
||||||
|
specifier: 6.2.2
|
||||||
|
version: 6.2.2(reflect-metadata@0.2.2)
|
||||||
|
ioredis:
|
||||||
|
specifier: 5.6.1
|
||||||
|
version: 5.6.1
|
||||||
|
jwt-decode:
|
||||||
|
specifier: 4.0.0
|
||||||
|
version: 4.0.0
|
||||||
|
livekit-server-sdk:
|
||||||
|
specifier: 2.13.1
|
||||||
|
version: 2.13.1
|
||||||
|
ms:
|
||||||
|
specifier: 2.1.3
|
||||||
|
version: 2.1.3
|
||||||
|
uid:
|
||||||
|
specifier: 2.0.2
|
||||||
|
version: 2.0.2
|
||||||
|
winston:
|
||||||
|
specifier: 3.18.3
|
||||||
|
version: 3.18.3
|
||||||
|
yamljs:
|
||||||
|
specifier: 0.3.0
|
||||||
|
version: 0.3.0
|
||||||
|
zod:
|
||||||
|
specifier: 3.25.76
|
||||||
|
version: 3.25.76
|
||||||
|
devDependencies:
|
||||||
|
'@types/archiver':
|
||||||
|
specifier: 6.0.3
|
||||||
|
version: 6.0.3
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: 5.0.2
|
||||||
|
version: 5.0.2
|
||||||
|
'@types/cookie-parser':
|
||||||
|
specifier: 1.4.9
|
||||||
|
version: 1.4.9(@types/express@4.17.23)
|
||||||
|
'@types/cors':
|
||||||
|
specifier: 2.8.19
|
||||||
|
version: 2.8.19
|
||||||
|
'@types/express':
|
||||||
|
specifier: 4.17.23
|
||||||
|
version: 4.17.23
|
||||||
|
'@types/jest':
|
||||||
|
specifier: 29.5.14
|
||||||
|
version: 29.5.14
|
||||||
|
'@types/ms':
|
||||||
|
specifier: 2.1.0
|
||||||
|
version: 2.1.0
|
||||||
|
'@types/node':
|
||||||
|
specifier: 22.16.4
|
||||||
|
version: 22.16.4
|
||||||
|
'@types/supertest':
|
||||||
|
specifier: 6.0.3
|
||||||
|
version: 6.0.3
|
||||||
|
'@types/unzipper':
|
||||||
|
specifier: 0.10.11
|
||||||
|
version: 0.10.11
|
||||||
|
'@types/validator':
|
||||||
|
specifier: 13.15.2
|
||||||
|
version: 13.15.2
|
||||||
|
'@types/yamljs':
|
||||||
|
specifier: 0.2.34
|
||||||
|
version: 0.2.34
|
||||||
|
'@typescript-eslint/eslint-plugin':
|
||||||
|
specifier: 6.21.0
|
||||||
|
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
specifier: 6.21.0
|
||||||
|
version: 6.21.0(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
cross-env:
|
||||||
|
specifier: 7.0.3
|
||||||
|
version: 7.0.3
|
||||||
|
eslint:
|
||||||
|
specifier: 8.57.1
|
||||||
|
version: 8.57.1
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: 9.1.0
|
||||||
|
version: 9.1.0(eslint@8.57.1)
|
||||||
|
jest:
|
||||||
|
specifier: 29.7.0
|
||||||
|
version: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.2))
|
||||||
|
jest-fetch-mock:
|
||||||
|
specifier: 3.0.3
|
||||||
|
version: 3.0.3(encoding@0.1.13)
|
||||||
|
jest-junit:
|
||||||
|
specifier: 16.0.0
|
||||||
|
version: 16.0.0
|
||||||
|
nodemon:
|
||||||
|
specifier: 3.1.10
|
||||||
|
version: 3.1.10
|
||||||
|
openapi-generate-html:
|
||||||
|
specifier: 0.5.3
|
||||||
|
version: 0.5.3(@types/node@22.16.4)
|
||||||
|
prettier:
|
||||||
|
specifier: 3.6.2
|
||||||
|
version: 3.6.2
|
||||||
|
supertest:
|
||||||
|
specifier: 7.1.3
|
||||||
|
version: 7.1.3
|
||||||
|
ts-jest:
|
||||||
|
specifier: 29.4.0
|
||||||
|
version: 29.4.0(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.2)))(typescript@5.9.2)
|
||||||
|
ts-jest-resolver:
|
||||||
|
specifier: 2.0.1
|
||||||
|
version: 2.0.1
|
||||||
|
tsx:
|
||||||
|
specifier: 4.20.3
|
||||||
|
version: 4.20.3
|
||||||
|
typescript:
|
||||||
|
specifier: 5.9.2
|
||||||
|
version: 5.9.2
|
||||||
|
unzipper:
|
||||||
|
specifier: 0.12.3
|
||||||
|
version: 0.12.3
|
||||||
|
|
||||||
|
meet-pro/frontend:
|
||||||
|
dependencies:
|
||||||
|
'@angular/animations':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||||
|
'@angular/cdk':
|
||||||
|
specifier: 20.2.9
|
||||||
|
version: 20.2.9(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||||
|
'@angular/common':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||||
|
'@angular/compiler':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4
|
||||||
|
'@angular/core':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||||
|
'@angular/forms':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
||||||
|
'@angular/material':
|
||||||
|
specifier: 20.2.9
|
||||||
|
version: 20.2.9(b517547b325ffc8400ae4cda6a618bfd)
|
||||||
|
'@angular/platform-browser':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||||
|
'@angular/platform-browser-dynamic':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))
|
||||||
|
'@angular/router':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
||||||
|
'@livekit/track-processors':
|
||||||
|
specifier: 0.6.1
|
||||||
|
version: 0.6.1(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.11(@types/dom-mediacapture-record@1.0.22))
|
||||||
|
'@openvidu-meet/shared-components':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../meet-ce/frontend/projects/shared-meet-components
|
||||||
|
'@openvidu-meet/typings':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../meet-ce/typings
|
||||||
|
autolinker:
|
||||||
|
specifier: 4.1.5
|
||||||
|
version: 4.1.5
|
||||||
|
core-js:
|
||||||
|
specifier: 3.45.1
|
||||||
|
version: 3.45.1
|
||||||
|
jwt-decode:
|
||||||
|
specifier: 4.0.0
|
||||||
|
version: 4.0.0
|
||||||
|
livekit-client:
|
||||||
|
specifier: 2.15.11
|
||||||
|
version: 2.15.11(@types/dom-mediacapture-record@1.0.22)
|
||||||
|
openvidu-components-angular:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../openvidu/openvidu-components-angular/projects/openvidu-components-angular
|
||||||
|
rxjs:
|
||||||
|
specifier: 7.8.2
|
||||||
|
version: 7.8.2
|
||||||
|
tslib:
|
||||||
|
specifier: 2.8.1
|
||||||
|
version: 2.8.1
|
||||||
|
unique-names-generator:
|
||||||
|
specifier: 4.7.1
|
||||||
|
version: 4.7.1
|
||||||
|
zone.js:
|
||||||
|
specifier: 0.15.1
|
||||||
|
version: 0.15.1
|
||||||
|
devDependencies:
|
||||||
|
'@angular-builders/custom-webpack':
|
||||||
|
specifier: 20.0.0
|
||||||
|
version: 20.0.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
|
||||||
|
'@angular-devkit/build-angular':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(tsx@4.20.3)(typescript@5.9.2)
|
||||||
|
'@angular-eslint/builder':
|
||||||
|
specifier: 20.3.0
|
||||||
|
version: 20.3.0(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
'@angular-eslint/eslint-plugin':
|
||||||
|
specifier: 20.3.0
|
||||||
|
version: 20.3.0(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
'@angular-eslint/eslint-plugin-template':
|
||||||
|
specifier: 20.3.0
|
||||||
|
version: 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.46.1)(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
'@angular-eslint/schematics':
|
||||||
|
specifier: 20.3.0
|
||||||
|
version: 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.46.1)(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
'@angular-eslint/template-parser':
|
||||||
|
specifier: 20.3.0
|
||||||
|
version: 20.3.0(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
'@angular/cli':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@types/node@22.18.8)(chokidar@4.0.3)
|
||||||
|
'@angular/compiler-cli':
|
||||||
|
specifier: 20.3.4
|
||||||
|
version: 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||||
|
'@types/chai':
|
||||||
|
specifier: 4.3.20
|
||||||
|
version: 4.3.20
|
||||||
|
'@types/fluent-ffmpeg':
|
||||||
|
specifier: 2.1.27
|
||||||
|
version: 2.1.27
|
||||||
|
'@types/jasmine':
|
||||||
|
specifier: 5.1.9
|
||||||
|
version: 5.1.9
|
||||||
|
'@types/mocha':
|
||||||
|
specifier: 9.1.1
|
||||||
|
version: 9.1.1
|
||||||
|
'@types/node':
|
||||||
|
specifier: 22.18.8
|
||||||
|
version: 22.18.8
|
||||||
|
'@types/pixelmatch':
|
||||||
|
specifier: 5.2.6
|
||||||
|
version: 5.2.6
|
||||||
|
'@types/pngjs':
|
||||||
|
specifier: 6.0.5
|
||||||
|
version: 6.0.5
|
||||||
|
'@types/selenium-webdriver':
|
||||||
|
specifier: 4.35.1
|
||||||
|
version: 4.35.1
|
||||||
|
'@typescript-eslint/eslint-plugin':
|
||||||
|
specifier: 8.46.1
|
||||||
|
version: 8.46.1(@typescript-eslint/parser@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
specifier: 8.46.1
|
||||||
|
version: 8.46.1(eslint@8.57.1)(typescript@5.9.2)
|
||||||
|
chai:
|
||||||
|
specifier: 4.5.0
|
||||||
|
version: 4.5.0
|
||||||
|
chromedriver:
|
||||||
|
specifier: 141.0.0
|
||||||
|
version: 141.0.0
|
||||||
|
cross-env:
|
||||||
|
specifier: 7.0.3
|
||||||
|
version: 7.0.3
|
||||||
|
eslint:
|
||||||
|
specifier: 8.57.1
|
||||||
|
version: 8.57.1
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: 9.1.0
|
||||||
|
version: 9.1.0(eslint@8.57.1)
|
||||||
|
fluent-ffmpeg:
|
||||||
|
specifier: 2.1.3
|
||||||
|
version: 2.1.3
|
||||||
|
jasmine-core:
|
||||||
|
specifier: 5.6.0
|
||||||
|
version: 5.6.0
|
||||||
|
jasmine-spec-reporter:
|
||||||
|
specifier: 7.0.0
|
||||||
|
version: 7.0.0
|
||||||
|
karma:
|
||||||
|
specifier: 6.4.4
|
||||||
|
version: 6.4.4
|
||||||
|
karma-chrome-launcher:
|
||||||
|
specifier: 3.2.0
|
||||||
|
version: 3.2.0
|
||||||
|
karma-coverage:
|
||||||
|
specifier: 2.2.1
|
||||||
|
version: 2.2.1
|
||||||
|
karma-jasmine:
|
||||||
|
specifier: 5.1.0
|
||||||
|
version: 5.1.0(karma@6.4.4)
|
||||||
|
karma-jasmine-html-reporter:
|
||||||
|
specifier: 2.1.0
|
||||||
|
version: 2.1.0(jasmine-core@5.6.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4)
|
||||||
|
mocha:
|
||||||
|
specifier: 10.7.3
|
||||||
|
version: 10.7.3
|
||||||
|
ng-packagr:
|
||||||
|
specifier: 20.3.0
|
||||||
|
version: 20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2)
|
||||||
|
prettier:
|
||||||
|
specifier: 3.3.3
|
||||||
|
version: 3.3.3
|
||||||
|
selenium-webdriver:
|
||||||
|
specifier: 4.25.0
|
||||||
|
version: 4.25.0
|
||||||
|
ts-node:
|
||||||
|
specifier: 10.9.2
|
||||||
|
version: 10.9.2(@types/node@22.18.8)(typescript@5.9.2)
|
||||||
|
typescript:
|
||||||
|
specifier: 5.9.2
|
||||||
|
version: 5.9.2
|
||||||
|
|
||||||
|
meet-pro/typings:
|
||||||
|
devDependencies:
|
||||||
|
'@openvidu-meet/typings':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../meet-ce/typings
|
||||||
|
typescript:
|
||||||
|
specifier: 5.9.2
|
||||||
|
version: 5.9.2
|
||||||
|
|
||||||
testapp:
|
testapp:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@openvidu-meet/typings':
|
'@openvidu-meet/typings':
|
||||||
@ -7072,20 +7464,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-24epA6vxDX0hqEg+jKhMzeMZ9CvNoJlTcqat6+CL8U8bETM4Kc5i7wv2jhbqGFXHHw7kHLtPFz4QjgTooV5nHQ==}
|
resolution: {integrity: sha512-24epA6vxDX0hqEg+jKhMzeMZ9CvNoJlTcqat6+CL8U8bETM4Kc5i7wv2jhbqGFXHHw7kHLtPFz4QjgTooV5nHQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
openvidu-components-angular@3.4.0:
|
|
||||||
resolution: {integrity: sha512-WsJfBLBUjsnM6jDfjsOmtGlHgG1HFhIkk238f0u0VxqRdhwXlh/BK/kESxJVl6/RkhhIoU6GJ5Q3bCRXrXNzqg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@angular/animations': ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
'@angular/cdk': ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
'@angular/common': ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
'@angular/core': ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
'@angular/forms': ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
'@angular/material': ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
'@livekit/track-processors': ^0.6.0
|
|
||||||
autolinker: ^4.0.0
|
|
||||||
buffer: ^6.0.3
|
|
||||||
livekit-client: ^2.15.0
|
|
||||||
|
|
||||||
opn@5.3.0:
|
opn@5.3.0:
|
||||||
resolution: {integrity: sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==}
|
resolution: {integrity: sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -9221,6 +9599,59 @@ snapshots:
|
|||||||
- webpack-cli
|
- webpack-cli
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
'@angular-builders/custom-webpack@20.0.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)':
|
||||||
|
dependencies:
|
||||||
|
'@angular-builders/common': 4.0.0(@types/node@22.18.8)(chokidar@4.0.3)(typescript@5.9.2)
|
||||||
|
'@angular-devkit/architect': 0.2003.5(chokidar@4.0.3)
|
||||||
|
'@angular-devkit/build-angular': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(tsx@4.20.3)(typescript@5.9.2)
|
||||||
|
'@angular-devkit/core': 20.3.5(chokidar@4.0.3)
|
||||||
|
'@angular/build': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
|
||||||
|
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||||
|
lodash: 4.17.21
|
||||||
|
webpack-merge: 6.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@angular/compiler'
|
||||||
|
- '@angular/core'
|
||||||
|
- '@angular/localize'
|
||||||
|
- '@angular/platform-browser'
|
||||||
|
- '@angular/platform-server'
|
||||||
|
- '@angular/service-worker'
|
||||||
|
- '@angular/ssr'
|
||||||
|
- '@rspack/core'
|
||||||
|
- '@swc/core'
|
||||||
|
- '@swc/wasm'
|
||||||
|
- '@types/node'
|
||||||
|
- '@web/test-runner'
|
||||||
|
- browser-sync
|
||||||
|
- bufferutil
|
||||||
|
- chokidar
|
||||||
|
- debug
|
||||||
|
- html-webpack-plugin
|
||||||
|
- jest
|
||||||
|
- jest-environment-jsdom
|
||||||
|
- jiti
|
||||||
|
- karma
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- ng-packagr
|
||||||
|
- node-sass
|
||||||
|
- postcss
|
||||||
|
- protractor
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- tailwindcss
|
||||||
|
- terser
|
||||||
|
- tslib
|
||||||
|
- tsx
|
||||||
|
- typescript
|
||||||
|
- uglify-js
|
||||||
|
- utf-8-validate
|
||||||
|
- vitest
|
||||||
|
- webpack-cli
|
||||||
|
- yaml
|
||||||
|
|
||||||
'@angular-devkit/architect@0.2003.4(chokidar@4.0.3)':
|
'@angular-devkit/architect@0.2003.4(chokidar@4.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
||||||
@ -9239,7 +9670,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
||||||
'@angular-devkit/build-webpack': 0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2))(webpack@5.101.2)
|
'@angular-devkit/build-webpack': 0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)))(webpack@5.101.2(esbuild@0.25.9))
|
||||||
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
||||||
'@angular/build': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.0)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
|
'@angular/build': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.0)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
|
||||||
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||||
@ -9253,13 +9684,13 @@ snapshots:
|
|||||||
'@babel/preset-env': 7.28.3(@babel/core@7.28.3)
|
'@babel/preset-env': 7.28.3(@babel/core@7.28.3)
|
||||||
'@babel/runtime': 7.28.3
|
'@babel/runtime': 7.28.3
|
||||||
'@discoveryjs/json-ext': 0.6.3
|
'@discoveryjs/json-ext': 0.6.3
|
||||||
'@ngtools/webpack': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2)
|
'@ngtools/webpack': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))
|
||||||
ansi-colors: 4.1.3
|
ansi-colors: 4.1.3
|
||||||
autoprefixer: 10.4.21(postcss@8.5.6)
|
autoprefixer: 10.4.21(postcss@8.5.6)
|
||||||
babel-loader: 10.0.0(@babel/core@7.28.3)(webpack@5.101.2)
|
babel-loader: 10.0.0(@babel/core@7.28.3)(webpack@5.101.2(esbuild@0.25.9))
|
||||||
browserslist: 4.26.3
|
browserslist: 4.26.3
|
||||||
copy-webpack-plugin: 13.0.1(webpack@5.101.2)
|
copy-webpack-plugin: 13.0.1(webpack@5.101.2(esbuild@0.25.9))
|
||||||
css-loader: 7.1.2(webpack@5.101.2)
|
css-loader: 7.1.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
esbuild-wasm: 0.25.9
|
esbuild-wasm: 0.25.9
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
http-proxy-middleware: 3.0.5
|
http-proxy-middleware: 3.0.5
|
||||||
@ -9267,32 +9698,32 @@ snapshots:
|
|||||||
jsonc-parser: 3.3.1
|
jsonc-parser: 3.3.1
|
||||||
karma-source-map-support: 1.4.0
|
karma-source-map-support: 1.4.0
|
||||||
less: 4.4.0
|
less: 4.4.0
|
||||||
less-loader: 12.3.0(less@4.4.0)(webpack@5.101.2)
|
less-loader: 12.3.0(less@4.4.0)(webpack@5.101.2(esbuild@0.25.9))
|
||||||
license-webpack-plugin: 4.0.2(webpack@5.101.2)
|
license-webpack-plugin: 4.0.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
loader-utils: 3.3.1
|
loader-utils: 3.3.1
|
||||||
mini-css-extract-plugin: 2.9.4(webpack@5.101.2)
|
mini-css-extract-plugin: 2.9.4(webpack@5.101.2(esbuild@0.25.9))
|
||||||
open: 10.2.0
|
open: 10.2.0
|
||||||
ora: 8.2.0
|
ora: 8.2.0
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
piscina: 5.1.3
|
piscina: 5.1.3
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2)
|
postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))
|
||||||
resolve-url-loader: 5.0.0
|
resolve-url-loader: 5.0.0
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
sass: 1.90.0
|
sass: 1.90.0
|
||||||
sass-loader: 16.0.5(sass@1.90.0)(webpack@5.101.2)
|
sass-loader: 16.0.5(sass@1.90.0)(webpack@5.101.2(esbuild@0.25.9))
|
||||||
semver: 7.7.2
|
semver: 7.7.2
|
||||||
source-map-loader: 5.0.0(webpack@5.101.2)
|
source-map-loader: 5.0.0(webpack@5.101.2(esbuild@0.25.9))
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
terser: 5.43.1
|
terser: 5.43.1
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
webpack-dev-middleware: 7.4.2(webpack@5.101.2)
|
webpack-dev-middleware: 7.4.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
webpack-dev-server: 5.2.2(webpack@5.101.2)
|
webpack-dev-server: 5.2.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
webpack-merge: 6.0.1
|
webpack-merge: 6.0.1
|
||||||
webpack-subresource-integrity: 5.1.0(webpack@5.101.2)
|
webpack-subresource-integrity: 5.1.0(webpack@5.101.2(esbuild@0.25.9))
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@angular/core': 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
'@angular/core': 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||||
'@angular/platform-browser': 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
'@angular/platform-browser': 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||||
@ -9325,12 +9756,12 @@ snapshots:
|
|||||||
- webpack-cli
|
- webpack-cli
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
'@angular-devkit/build-webpack@0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2))(webpack@5.101.2)':
|
'@angular-devkit/build-webpack@0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)))(webpack@5.101.2(esbuild@0.25.9))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
webpack-dev-server: 5.2.2(webpack@5.101.2)
|
webpack-dev-server: 5.2.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
@ -9550,6 +9981,59 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
'@angular/build@20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)':
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
||||||
|
'@angular/compiler': 20.3.4
|
||||||
|
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||||
|
'@babel/core': 7.28.3
|
||||||
|
'@babel/helper-annotate-as-pure': 7.27.3
|
||||||
|
'@babel/helper-split-export-declaration': 7.24.7
|
||||||
|
'@inquirer/confirm': 5.1.14(@types/node@22.18.8)
|
||||||
|
'@vitejs/plugin-basic-ssl': 2.1.0(vite@7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3))
|
||||||
|
beasties: 0.3.5
|
||||||
|
browserslist: 4.26.3
|
||||||
|
esbuild: 0.25.9
|
||||||
|
https-proxy-agent: 7.0.6
|
||||||
|
istanbul-lib-instrument: 6.0.3
|
||||||
|
jsonc-parser: 3.3.1
|
||||||
|
listr2: 9.0.1
|
||||||
|
magic-string: 0.30.17
|
||||||
|
mrmime: 2.0.1
|
||||||
|
parse5-html-rewriting-stream: 8.0.0
|
||||||
|
picomatch: 4.0.3
|
||||||
|
piscina: 5.1.3
|
||||||
|
rollup: 4.52.3
|
||||||
|
sass: 1.90.0
|
||||||
|
semver: 7.7.2
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
tinyglobby: 0.2.14
|
||||||
|
tslib: 2.8.1
|
||||||
|
typescript: 5.9.2
|
||||||
|
vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
|
||||||
|
watchpack: 2.4.4
|
||||||
|
optionalDependencies:
|
||||||
|
'@angular/core': 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||||
|
'@angular/platform-browser': 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
||||||
|
karma: 6.4.4
|
||||||
|
less: 4.4.2
|
||||||
|
lmdb: 3.4.2
|
||||||
|
ng-packagr: 20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2)
|
||||||
|
postcss: 8.5.6
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- chokidar
|
||||||
|
- jiti
|
||||||
|
- lightningcss
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
- tsx
|
||||||
|
- yaml
|
||||||
|
|
||||||
'@angular/cdk@20.2.9(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)':
|
'@angular/cdk@20.2.9(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular/common': 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
'@angular/common': 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||||
@ -12039,7 +12523,7 @@ snapshots:
|
|||||||
'@napi-rs/nice-win32-x64-msvc': 1.1.1
|
'@napi-rs/nice-win32-x64-msvc': 1.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ngtools/webpack@20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2)':
|
'@ngtools/webpack@20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
@ -13364,6 +13848,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
|
vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
|
||||||
|
|
||||||
|
'@vitejs/plugin-basic-ssl@2.1.0(vite@7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3))':
|
||||||
|
dependencies:
|
||||||
|
vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
|
||||||
|
|
||||||
'@webassemblyjs/ast@1.14.1':
|
'@webassemblyjs/ast@1.14.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@webassemblyjs/helper-numbers': 1.13.2
|
'@webassemblyjs/helper-numbers': 1.13.2
|
||||||
@ -13735,7 +14223,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.101.2):
|
babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.3
|
'@babel/core': 7.28.3
|
||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
@ -14320,7 +14808,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-what: 3.14.1
|
is-what: 3.14.1
|
||||||
|
|
||||||
copy-webpack-plugin@13.0.1(webpack@5.101.2):
|
copy-webpack-plugin@13.0.1(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
glob-parent: 6.0.2
|
glob-parent: 6.0.2
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
@ -14431,7 +14919,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
|
|
||||||
css-loader@7.1.2(webpack@5.101.2):
|
css-loader@7.1.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
icss-utils: 5.1.0(postcss@8.5.6)
|
icss-utils: 5.1.0(postcss@8.5.6)
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@ -16980,7 +17468,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
|
|
||||||
less-loader@12.3.0(less@4.4.0)(webpack@5.101.2):
|
less-loader@12.3.0(less@4.4.0)(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
less: 4.4.0
|
less: 4.4.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -17021,7 +17509,7 @@ snapshots:
|
|||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
|
|
||||||
license-webpack-plugin@4.0.2(webpack@5.101.2):
|
license-webpack-plugin@4.0.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
webpack-sources: 3.3.3
|
webpack-sources: 3.3.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -17275,7 +17763,7 @@ snapshots:
|
|||||||
|
|
||||||
mimic-function@5.0.1: {}
|
mimic-function@5.0.1: {}
|
||||||
|
|
||||||
mini-css-extract-plugin@2.9.4(webpack@5.101.2):
|
mini-css-extract-plugin@2.9.4(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
schema-utils: 4.3.3
|
schema-utils: 4.3.3
|
||||||
tapable: 2.3.0
|
tapable: 2.3.0
|
||||||
@ -17680,20 +18168,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
|
|
||||||
openvidu-components-angular@3.4.0(3c63e791699a7778b296d869e22cd258):
|
|
||||||
dependencies:
|
|
||||||
'@angular/animations': 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
|
|
||||||
'@angular/cdk': 20.2.9(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
|
||||||
'@angular/common': 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
|
||||||
'@angular/core': 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
|
||||||
'@angular/forms': 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
|
||||||
'@angular/material': 20.2.9(b517547b325ffc8400ae4cda6a618bfd)
|
|
||||||
'@livekit/track-processors': 0.6.1(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.11(@types/dom-mediacapture-record@1.0.22))
|
|
||||||
autolinker: 4.1.5
|
|
||||||
buffer: 6.0.3
|
|
||||||
livekit-client: 2.15.11(@types/dom-mediacapture-record@1.0.22)
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
opn@5.3.0:
|
opn@5.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-wsl: 1.1.0
|
is-wsl: 1.1.0
|
||||||
@ -17957,7 +18431,7 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
ts-node: 10.9.2(@types/node@22.18.8)(typescript@5.7.3)
|
ts-node: 10.9.2(@types/node@22.18.8)(typescript@5.7.3)
|
||||||
|
|
||||||
postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2):
|
postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
cosmiconfig: 9.0.0(typescript@5.9.2)
|
cosmiconfig: 9.0.0(typescript@5.9.2)
|
||||||
jiti: 1.21.7
|
jiti: 1.21.7
|
||||||
@ -18534,7 +19008,7 @@ snapshots:
|
|||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
sass-loader@16.0.5(sass@1.90.0)(webpack@5.101.2):
|
sass-loader@16.0.5(sass@1.90.0)(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
neo-async: 2.6.2
|
neo-async: 2.6.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -18840,7 +19314,7 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
source-map-loader@5.0.0(webpack@5.101.2):
|
source-map-loader@5.0.0(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
@ -19606,6 +20080,23 @@ snapshots:
|
|||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
tsx: 4.20.3
|
tsx: 4.20.3
|
||||||
|
|
||||||
|
vite@7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3):
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.25.10
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
picomatch: 4.0.3
|
||||||
|
postcss: 8.5.6
|
||||||
|
rollup: 4.52.3
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 22.18.8
|
||||||
|
fsevents: 2.3.3
|
||||||
|
jiti: 1.21.7
|
||||||
|
less: 4.4.2
|
||||||
|
sass: 1.90.0
|
||||||
|
terser: 5.44.0
|
||||||
|
tsx: 4.20.3
|
||||||
|
|
||||||
void-elements@2.0.1: {}
|
void-elements@2.0.1: {}
|
||||||
|
|
||||||
w3c-xmlserializer@4.0.0:
|
w3c-xmlserializer@4.0.0:
|
||||||
@ -19642,7 +20133,7 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@7.0.0: {}
|
webidl-conversions@7.0.0: {}
|
||||||
|
|
||||||
webpack-dev-middleware@7.4.2(webpack@5.101.2):
|
webpack-dev-middleware@7.4.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
colorette: 2.0.20
|
colorette: 2.0.20
|
||||||
memfs: 4.49.0
|
memfs: 4.49.0
|
||||||
@ -19653,7 +20144,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
|
|
||||||
webpack-dev-server@5.2.2(webpack@5.101.2):
|
webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/bonjour': 3.5.13
|
'@types/bonjour': 3.5.13
|
||||||
'@types/connect-history-api-fallback': 1.5.4
|
'@types/connect-history-api-fallback': 1.5.4
|
||||||
@ -19681,7 +20172,7 @@ snapshots:
|
|||||||
serve-index: 1.9.1
|
serve-index: 1.9.1
|
||||||
sockjs: 0.3.24
|
sockjs: 0.3.24
|
||||||
spdy: 4.0.2
|
spdy: 4.0.2
|
||||||
webpack-dev-middleware: 7.4.2(webpack@5.101.2)
|
webpack-dev-middleware: 7.4.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
ws: 8.18.3
|
ws: 8.18.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
@ -19699,7 +20190,7 @@ snapshots:
|
|||||||
|
|
||||||
webpack-sources@3.3.3: {}
|
webpack-sources@3.3.3: {}
|
||||||
|
|
||||||
webpack-subresource-integrity@5.1.0(webpack@5.101.2):
|
webpack-subresource-integrity@5.1.0(webpack@5.101.2(esbuild@0.25.9)):
|
||||||
dependencies:
|
dependencies:
|
||||||
typed-assert: 1.0.9
|
typed-assert: 1.0.9
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
|
|||||||
@ -16,7 +16,6 @@ packages:
|
|||||||
- meet-ce/frontend/webcomponent
|
- meet-ce/frontend/webcomponent
|
||||||
- meet-ce/frontend/projects/shared-meet-components
|
- meet-ce/frontend/projects/shared-meet-components
|
||||||
- meet-ce/backend
|
- meet-ce/backend
|
||||||
- testapp
|
|
||||||
|
|
||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- chromedriver
|
- chromedriver
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Docker/CI Workspace Configuration
|
# Development Workspace Configuration
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# This workspace configuration is used for Docker builds and CI workflows.
|
# This workspace configuration is used for LOCAL DEVELOPMENT.
|
||||||
# It EXCLUDES external packages (like openvidu-components-angular) that
|
# It INCLUDES external packages (like openvidu-components-angular) that
|
||||||
# are not part of this repository.
|
# are located in sibling repositories for fast development with hot-reload.
|
||||||
#
|
#
|
||||||
# For local development, use pnpm-workspace.yaml instead.
|
# For Docker builds and CI, use pnpm-workspace.docker.yaml instead.
|
||||||
#
|
#
|
||||||
# See docs/ci-docker-dependencies-strategy.md for more information.
|
# See docs/ci-docker-dependencies-strategy.md for more information.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
- ../openvidu/openvidu-components-angular/projects/openvidu-components-angular
|
||||||
- meet-ce/typings
|
- meet-ce/typings
|
||||||
- meet-ce/frontend
|
- meet-ce/frontend
|
||||||
- meet-ce/frontend/webcomponent
|
- meet-ce/frontend/webcomponent
|
||||||
- meet-ce/frontend/projects/shared-meet-components
|
- meet-ce/frontend/projects/shared-meet-components
|
||||||
- meet-ce/backend
|
- meet-ce/backend
|
||||||
|
- meet-pro/frontend
|
||||||
|
- meet-pro/backend
|
||||||
|
- meet-pro/typings
|
||||||
- testapp
|
- testapp
|
||||||
|
|
||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🔄 REINICIANDO STACK COMPLETO OPENVIDU MEET"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
||||||
# Crear directorio de logs
|
|
||||||
mkdir -p /tmp/ovm-logs
|
|
||||||
|
|
||||||
echo -e "\n1️⃣ Matando procesos existentes..."
|
|
||||||
pkill -f "node.*dist/src/server.js" || true
|
|
||||||
pkill -f "livekit-server" || true
|
|
||||||
pkill -f "ng serve" || true
|
|
||||||
pkill -f "pnpm.*start" || true
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo -e "\n2️⃣ Reiniciando Backend..."
|
|
||||||
cd /home/xesar/Documentos/openvidu-meet/meet-ce/backend
|
|
||||||
pnpm run build
|
|
||||||
nohup env \
|
|
||||||
NODE_ENV=development \
|
|
||||||
LIVEKIT_URL=wss://livekit-server.bfzqqk.easypanel.host \
|
|
||||||
LIVEKIT_URL_PRIVATE=wss://livekit-server.bfzqqk.easypanel.host \
|
|
||||||
LIVEKIT_API_KEY=devkey \
|
|
||||||
LIVEKIT_API_SECRET=secretsecretsecretsecretsecretsecret \
|
|
||||||
MEET_REDIS_HOST=192.168.1.19 \
|
|
||||||
MEET_REDIS_PORT=6379 \
|
|
||||||
MEET_REDIS_PASSWORD=redispassword \
|
|
||||||
MEET_BLOB_STORAGE_MODE=s3 \
|
|
||||||
MEET_S3_SERVICE_ENDPOINT=http://192.168.1.19:9000 \
|
|
||||||
MEET_S3_ACCESS_KEY=minioadmin \
|
|
||||||
MEET_S3_SECRET_KEY=minioadmin \
|
|
||||||
MEET_S3_BUCKET=openvidu-appdata \
|
|
||||||
MEET_S3_WITH_PATH_STYLE_ACCESS=true \
|
|
||||||
MEET_INITIAL_ADMIN_USER=admin \
|
|
||||||
MEET_INITIAL_ADMIN_PASSWORD=admin \
|
|
||||||
node dist/src/server.js > /tmp/ovm-logs/backend.log 2>&1 &
|
|
||||||
|
|
||||||
echo "✅ Backend iniciado"
|
|
||||||
|
|
||||||
echo -e "\n3️⃣ Reiniciando LiveKit..."
|
|
||||||
cd /home/xesar/Documentos/openvidu-meet
|
|
||||||
nohup livekit-server --config livekit.yaml > /tmp/ovm-logs/livekit.log 2>&1 &
|
|
||||||
echo "✅ LiveKit iniciado"
|
|
||||||
|
|
||||||
|
|
||||||
echo -e "\n⏳ Esperando arranque de servicios (15s)..."
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
echo -e "\n🔍 Verificación final:"
|
|
||||||
echo "Puertos activos:"
|
|
||||||
ss -ltn | egrep '6080|7880|6379|9000' | sort
|
|
||||||
|
|
||||||
echo -e "\nTest conectividad:"
|
|
||||||
curl -s -o /dev/null -w "✅ Backend (6080): %{http_code}\n" https://openvidu.bfzqqk.easypanel.host || echo "❌ Backend no responde"
|
|
||||||
redis-cli -h 192.168.1.19 -p 6379 -a redispassword ping 2>/dev/null && echo "✅ Redis: PONG" || echo "❌ Redis no responde"
|
|
||||||
|
|
||||||
echo -e "\n🚀 STACK REINICIADO COMPLETAMENTE"
|
|
||||||
echo "📄 Logs en: /tmp/ovm-logs/"
|
|
||||||
echo -e "\n🌐 URLs:"
|
|
||||||
echo " • Frontend: http://192.168.1.19:4200"
|
|
||||||
echo " • Backend: https://openvidu.bfzqqk.easypanel.host"
|
|
||||||
echo " • LiveKit: wss://livekit-server.bfzqqk.easypanel.host"
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user