Skip to content

Commit

Permalink
feat: add configurable integration webrtc-server with loadbalancer mo…
Browse files Browse the repository at this point in the history
…dule.
  • Loading branch information
gabrielmatau79 committed Jan 30, 2025
1 parent 74667b1 commit 4052101
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 70 deletions.
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,19 @@ To configure and build the `ICE Server` you can be use following enviroment vari

Additional variables for configuring the `webrtc-server`:

| Variable | Description | Default Value |
| ------------------------ | --------------------------------------------------------- | ----------------------------------- |
| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` |
| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `<project_dir>/certs/fullchain.pem` |
| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `<project_dir>/certs/privkey.pem` |
| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | |
| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` |
| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` |
| `MEDIASOUP_LISTEN_IP` | The listening IP for audio/video in mediasoup. | `0.0.0.0` or `127.0.0.1` |
| `MEDIASOUP_ANNOUNCED_IP` | Public IP address for audio/video in mediasoup.. | |
| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /rooms response | |
| Variable | Description | Default Value |
| ------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` |
| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `<project_dir>/certs/fullchain.pem` |
| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `<project_dir>/certs/privkey.pem` |
| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | |
| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` |
| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` |
| `MEDIASOUP_LISTEN_IP` | The listening IP for audio/video in mediasoup. | `0.0.0.0` or `127.0.0.1` |
| `MEDIASOUP_ANNOUNCED_IP` | Public IP address for audio/video in mediasoup.. | |
| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /rooms response | |
| `LOADBALANCER_URL` | Specifies the URL of the load balancer responsible for distributing WebRTC rooms among available servers | |
| `SERVICE_URL` | Defines the base URL of the WebRTC server that registers itself with the load balancer | |

## Diagram of solution webrtc-server

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"scripts": {
"build": "nest build",
"format": "prettier --write \"packages/**/*.ts\" \"libs/**/*.ts\"",
"check-types": "eslint \"{src,apps,libs,test}/**/*.ts\" --ignore-pattern demo/",
"check-types": "eslint \"{src,packages,libs,test}/**/*.ts\" --ignore-pattern demo/",
"start": "nest start",
"start:dev": "LOG_LEVEL=3 nest start --watch",
"start:debug": "nest start --debug --watch",
Expand Down
1 change: 0 additions & 1 deletion packages/loadbalancer/src/config/logger.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { LogLevel, Logger } from '@nestjs/common'
import { warn } from 'console'

export function getLogLevels(): LogLevel[] {
const logger = new Logger('getLogLevels')
Expand Down
2 changes: 1 addition & 1 deletion packages/loadbalancer/src/dto/loadbalancer.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString, IsUrl, IsInt, IsUUID, Min, IsOptional } from 'class-validator'
import { IsString, IsUrl, IsInt, Min, IsOptional } from 'class-validator'

/**
* DTO for creating a room
Expand Down
12 changes: 9 additions & 3 deletions packages/loadbalancer/src/lib/HttpRequestService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { HttpService } from '@nestjs/axios'
import { firstValueFrom } from 'rxjs'
import { AxiosInstance } from 'axios'
import axios from 'axios'
import https from 'https'

@Injectable()
export class HttpRequestService {
private readonly logger = new Logger(HttpRequestService.name)
private readonly httpService: HttpService

constructor() {
const axiosInstance: AxiosInstance = axios.create()
const axiosInstance: AxiosInstance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
})

this.httpService = new HttpService(axiosInstance)
}

Expand All @@ -27,7 +33,7 @@ export class HttpRequestService {
this.logger.log(`[post] Request sent to ${uri}: ${response.status}`)
return response
} catch (error) {
this.logger.error(`[post] Failed to send POST request to ${uri}: ${error.message}`)
this.logger.error(`[post] Failed to send POST request to ${uri}: ${error}`)
}
} else {
this.logger.warn(`[post] URI is not defined, cannot send POST request: ${JSON.stringify(data)}`)
Expand All @@ -47,7 +53,7 @@ export class HttpRequestService {
this.logger.log(`[get] Request sent to ${uri}: ${response.status}`)
return response
} catch (error) {
this.logger.error(`[get] Failed to send GET request to ${uri}: ${error.message}`)
this.logger.error(`[get] Failed to send GET request to ${uri}: ${error}`)
throw error
}
} else {
Expand Down
1 change: 0 additions & 1 deletion packages/loadbalancer/src/lib/ServerHealthChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Redis from 'ioredis'
import { InjectRedis } from '@nestjs-modules/ioredis'
import { HttpRequestService } from './HttpRequestService'
import { ConfigService } from '@nestjs/config'
import { Server } from 'https'

@Injectable()
export class ServerHealthChecker implements OnModuleInit, OnModuleDestroy {
Expand Down
22 changes: 11 additions & 11 deletions packages/loadbalancer/src/loadbalancer.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LoadbalancerController } from './loadbalancer.controller';
import { LoadbalancerService } from './loadbalancer.service';
import { Test, TestingModule } from '@nestjs/testing'
import { LoadbalancerController } from './loadbalancer.controller'
import { LoadbalancerService } from './loadbalancer.service'

describe('LoadbalancerController', () => {
let loadbalancerController: LoadbalancerController;
let loadbalancerController: LoadbalancerController

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [LoadbalancerController],
providers: [LoadbalancerService],
}).compile();
}).compile()

loadbalancerController = app.get<LoadbalancerController>(LoadbalancerController);
});
loadbalancerController = app.get<LoadbalancerController>(LoadbalancerController)
})

describe('root', () => {
it('should return "Hello World!"', () => {
expect(loadbalancerController.getHello()).toBe('Hello World!');
});
});
});
expect(loadbalancerController.getHello()).toBe('Hello World!')
})
})
})
28 changes: 10 additions & 18 deletions packages/loadbalancer/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import { LoadbalancerModule } from './../src/loadbalancer.module'

describe('AppController (e2e)', () => {
let app: INestApplication;
let app: INestApplication

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
imports: [LoadbalancerModule],
}).compile()

app = moduleFixture.createNestApplication();
await app.init();
});

it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
app = moduleFixture.createNestApplication()
await app.init()
})
})
27 changes: 15 additions & 12 deletions packages/webrtc-server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,32 @@ services:
image: webrtc-server:test
environment:
- LOG_LEVEL=3
- MEDIASOUP_ANNOUNCED_IP=192.168.100.84
- MEDIASOUP_LISTEN_IP=192.168.100.84
- MEDIASOUP_ANNOUNCED_IP=192.168.10.18
- MEDIASOUP_LISTEN_IP=0.0.0.0
- PROTOO_LISTEN_PORT=4443
- MEDIASOUP_MIN_PORT=40000
- MEDIASOUP_MAX_PORT=50000
- HTTPS_CERT_FULLCHAIN=/config/certs/fullchain.pem
- HTTPS_CERT_PRIVKEY=/config/certs/privkey.pem
- MEDIASOUP_CLIENT_ENABLE_ICESERVER=yes
- MEDIASOUP_CLIENT_ICESERVER_HOST=192.168.100.84
- MEDIASOUP_CLIENT_ICESERVER_HOST=192.168.10.18
- MEDIASOUP_CLIENT_ICESERVER_PROTO=udp
- MEDIASOUP_CLIENT_ICESERVER_PORT=3478
- MEDIASOUP_CLIENT_ICESERVER_USER=test
- MEDIASOUP_CLIENT_ICESERVER_PASS=test123
- MEDIASOUP_CLIENT_PROTOOPORT=4443
- MEDIASOUP_CLIENT_PROTOOPORT=443
- MEDIASOUP_INGRESS_HOST=webrtc.prueba.2060.io
- REDIS_URL=redis://redis:6379
- LOADBALANCER_URL=http://192.168.10.18:3000
- SERVICE_URL=https://192.168.10.18
ports:
- '4443:4443'
- '443:4443'
networks:
- webrtc_network
volumes:
- ./certs/fullchain.pem:/config/certs/fullchain.pem
- ./certs/privkey.pem:/config/certs/privkey.pem

coturn:
image: coturn/coturn
environment:
Expand All @@ -35,11 +40,9 @@ services:
ports:
- '3478:3478'
- '3478:3478/udp'
networks:
- webrtc_network

redis:
container_name: redis
image: redis:alpine
restart: always
ports:
- 6379:6379
command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru
networks:
webrtc_network:
driver: bridge
17 changes: 17 additions & 0 deletions packages/webrtc-server/src/config/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,21 @@ export default registerAs('appConfig', () => ({
* @type {string | undefined}
*/
redisNatmap: process.env.REDIS_NATMAP,

/**
* Load balancer base URL.
* Specifies the URL of the load balancer responsible for distributing WebRTC rooms among available servers.
* - Must be a valid URL.
* - Typically, this is the entry point for WebRTC server registration and room allocation.
* - Example: "http://loadbalancer.example.com"
*/
loadbalancerUrl: process.env.LOADBALANCER_URL,

/**
* URL of the current service instance.
* - Defines the base URL of the WebRTC service or application that registers itself with the load balancer.
* - Used for health checks and room allocation.
* - Example: `http://webrtc-service.example.com`
*/
serviceUrl: process.env.SERVICE_URL,
}))
1 change: 1 addition & 0 deletions packages/webrtc-server/src/config/config.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const config = {
key: process.env.HTTPS_CERT_PRIVKEY || `${__dirname}/certs/privkey.pem`,
},
ingressHost: process.env.MEDIASOUP_INGRESS_HOST,
protooPort: process.env.MEDIASOUP_CLIENT_PROTOOPORT,
},
iceServers: [
{
Expand Down
9 changes: 9 additions & 0 deletions packages/webrtc-server/src/lib/Room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ export class Room extends EventEmitter {
}

await this.notificationService.sendNotification(eventNotificationUri, joinNotificationData)

if (process.env.LOADBALANCER_URL) {
this.logger.debug(`**Send notification loadBalancer***`)
const loadbalancerUrl = `${process.env.LOADBALANCER_URL}/room-closed`
await this.notificationService.post(loadbalancerUrl, {
serverId: config.https.ingressHost,
roomId: this.roomId,
})
}
}

// If the Peer was joined, notify all Peers.
Expand Down
21 changes: 21 additions & 0 deletions packages/webrtc-server/src/lib/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,25 @@ export class NotificationService {
)
}
}

/**
* Sends a POST request to the specified URI with the given data.
*
* @param {string} uri - The URI to which the POST request will be sent.
* @param {Object} data - The payload to send in the POST request body.
*/
public async post(uri: string, data: object): Promise<any> {
if (uri) {
try {
const response = await firstValueFrom(this.httpService.post(uri, data))
this.logger.log(`[post] Request sent to ${uri}: ${response.status}`)
return response
} catch (error) {
this.logger.error(`[post] Failed to send POST request to ${uri}: ${error.message}`)
return error
}
} else {
this.logger.warn(`[post] URI is not defined, cannot send POST request: ${JSON.stringify(data)}`)
}
}
}
24 changes: 24 additions & 0 deletions packages/webrtc-server/src/rooms/rooms.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ export class RoomsController {

constructor(private readonly roomsService: RoomsService) {}

/**
* Health check endpoint for the WebRTC Server.
* - Used by the Load Balancer's `ServerHealthChecker` to verify server availability.
* - Returns `200 OK` if the WebRTC server is running correctly.
* - Can be expanded to check additional dependencies (e.g., WebRTC workers, database connections).
*
* @returns {Promise<{ status: string }>} - Returns `{ status: 'ok' }` if the server is healthy.
*/
@Get('health')
@ApiOperation({ summary: 'Health Check', description: 'Checks if the WebRTC server is running.' })
@ApiResponse({
status: 200,
description: 'The server is healthy.',
schema: { example: { status: 'ok' } },
})
@ApiResponse({
status: 500,
description: 'Server error or dependency failure.',
})
async checkHealth(): Promise<{ status: string }> {
this.logger.debug('Health check requested')
return this.roomsService.getHealthStatus()
}

/**
* Endpoint to create or retrieve a room.
* Delegates the logic to the RoomsService.
Expand Down
Loading

0 comments on commit 4052101

Please sign in to comment.