From 9f0877780e6f7f3ae379209ed517c4db0bb762df Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Fri, 14 Mar 2025 20:03:33 +0100 Subject: [PATCH] backend: Enhance Livekit webhook handling with OpenVidu integration for recording events --- .../controllers/livekit-webhook.controller.ts | 1 - backend/src/models/webhook.model.ts | 25 +++++++- .../src/services/livekit-webhook.service.ts | 27 ++++++--- .../src/services/openvidu-webhook.service.ts | 60 ++++++++++++++----- 4 files changed, 85 insertions(+), 28 deletions(-) diff --git a/backend/src/controllers/livekit-webhook.controller.ts b/backend/src/controllers/livekit-webhook.controller.ts index f138679..785a8d2 100644 --- a/backend/src/controllers/livekit-webhook.controller.ts +++ b/backend/src/controllers/livekit-webhook.controller.ts @@ -1,7 +1,6 @@ import { Request, Response } from 'express'; import { LoggerService } from '../services/logger.service.js'; import { LivekitWebhookService } from '../services/livekit-webhook.service.js'; -import { RoomService } from '../services/room.service.js'; import { WebhookEvent } from 'livekit-server-sdk'; import { OpenViduWebhookService } from '../services/openvidu-webhook.service.js'; import { container } from '../config/dependency-injector.config.js'; diff --git a/backend/src/models/webhook.model.ts b/backend/src/models/webhook.model.ts index 41c3b0f..c73dd2e 100644 --- a/backend/src/models/webhook.model.ts +++ b/backend/src/models/webhook.model.ts @@ -1,3 +1,24 @@ -export enum OpenViduWebhookEvent { - ROOM_FINISHED = 'room_finished', +import { RecordingStatus } from './recording.model.js'; + +export interface OpenViduWebhookEvent { + createdAt: number; + event: OpenViduWebhookEventType; + data: RoomWebhookData | RecordingWebhookData; +} + +export enum OpenViduWebhookEventType { + RECORDING_STARTED = 'recording_started', + RECORDING_STOPPED = 'recording_stopped', + ROOM_FINISHED = 'room_finished' +} + +export interface RecordingWebhookData { + recordingId: string; + filename?: string; + roomName: string; + status: RecordingStatus; +} + +export interface RoomWebhookData { + roomName: string; } diff --git a/backend/src/services/livekit-webhook.service.ts b/backend/src/services/livekit-webhook.service.ts index 0ea09e9..6b1f1fd 100644 --- a/backend/src/services/livekit-webhook.service.ts +++ b/backend/src/services/livekit-webhook.service.ts @@ -10,6 +10,7 @@ import { RoomService } from './room.service.js'; import { S3Service } from './s3.service.js'; import { RoomStatusData } from '../models/room.model.js'; import { RecordingService } from './recording.service.js'; +import { OpenViduWebhookService } from './openvidu-webhook.service.js'; @injectable() export class LivekitWebhookService { @@ -20,7 +21,8 @@ export class LivekitWebhookService { @inject(RecordingService) protected recordingService: RecordingService, @inject(LiveKitService) protected livekitService: LiveKitService, @inject(RoomService) protected roomService: RoomService, - @inject(LoggerService) protected logger: LoggerService + @inject(LoggerService) protected logger: LoggerService, + @inject(OpenViduWebhookService) protected openViduWebhookService: OpenViduWebhookService ) { this.webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET); } @@ -86,18 +88,24 @@ export class LivekitWebhookService { const { roomName } = egressInfo; - let payload: RecordingInfo | undefined = undefined; + let recordingInfo: RecordingInfo | undefined = undefined; this.logger.info(`Recording egress '${egressInfo.egressId}' updated: ${egressInfo.status}`); const topic: DataTopic = RecordingHelper.getDataTopicFromStatus(egressInfo); - payload = RecordingHelper.toRecordingInfo(egressInfo); + recordingInfo = RecordingHelper.toRecordingInfo(egressInfo); // Add recording metadata - const metadataPath = this.generateMetadataPath(payload); - await Promise.all([ - this.s3Service.saveObject(metadataPath, payload), - this.roomService.sendSignal(roomName, payload, { topic }) - ]); + const metadataPath = this.generateMetadataPath(recordingInfo); + const promises = [ + this.s3Service.saveObject(metadataPath, recordingInfo), + this.roomService.sendSignal(roomName, recordingInfo, { topic }) + ]; + + if(recordingInfo.status === RecordingStatus.STARTED) { + promises.push(this.openViduWebhookService.sendRecordingStartedWebhook(recordingInfo)); + } + + await Promise.all(promises); } catch (error) { this.logger.warn(`Error sending data on egress updated: ${error}`); } @@ -124,7 +132,8 @@ export class LivekitWebhookService { const metadataPath = this.generateMetadataPath(payload); await Promise.all([ this.s3Service.saveObject(metadataPath, payload), - this.roomService.sendSignal(roomName, payload, { topic }) + this.roomService.sendSignal(roomName, payload, { topic }), + this.openViduWebhookService.sendRecordingStoppedWebhook(payload) ]); } catch (error) { this.logger.warn(`Error sending data on egress ended: ${error}`); diff --git a/backend/src/services/openvidu-webhook.service.ts b/backend/src/services/openvidu-webhook.service.ts index 73041d8..9802df9 100644 --- a/backend/src/services/openvidu-webhook.service.ts +++ b/backend/src/services/openvidu-webhook.service.ts @@ -3,31 +3,59 @@ import { inject, injectable } from '../config/dependency-injector.config.js'; import { Room } from 'livekit-server-sdk'; import { LoggerService } from './logger.service.js'; import { MEET_API_KEY, MEET_WEBHOOK_ENABLED, MEET_WEBHOOK_URL } from '../environment.js'; -import { OpenViduWebhookEvent } from '../models/webhook.model.js'; +import { OpenViduWebhookEvent, OpenViduWebhookEventType } from '../models/webhook.model.js'; +import { RecordingInfo } from '../models/recording.model.js'; @injectable() export class OpenViduWebhookService { constructor(@inject(LoggerService) protected logger: LoggerService) {} async sendRoomFinishedWebhook(room: Room) { - await this.sendWebhookEvent(OpenViduWebhookEvent.ROOM_FINISHED, { - room: { - name: room.name + const data: OpenViduWebhookEvent = { + event: OpenViduWebhookEventType.ROOM_FINISHED, + createdAt: Date.now(), + data: { + roomName: room.name } - }); + }; + await this.sendWebhookEvent(data); } - private async sendWebhookEvent(eventType: OpenViduWebhookEvent, data: object) { + async sendRecordingStartedWebhook(recordingInfo: RecordingInfo) { + const data: OpenViduWebhookEvent = { + event: OpenViduWebhookEventType.RECORDING_STARTED, + createdAt: Date.now(), + data: { + recordingId: recordingInfo.id, + filename: recordingInfo.filename, + roomName: recordingInfo.roomName, + status: recordingInfo.status + } + }; + await this.sendWebhookEvent(data); + } + + async sendRecordingStoppedWebhook(recordingInfo: RecordingInfo) { + const data: OpenViduWebhookEvent = { + event: OpenViduWebhookEventType.RECORDING_STOPPED, + createdAt: Date.now(), + data: { + recordingId: recordingInfo.id, + filename: recordingInfo.filename, + roomName: recordingInfo.roomName, + status: recordingInfo.status + } + }; + await this.sendWebhookEvent(data); + } + + private async sendWebhookEvent(data: OpenViduWebhookEvent) { if (!this.isWebhookEnabled()) return; - const payload = { - event: eventType, - ...data - }; - const timestamp = Date.now(); - const signature = this.generateWebhookSignature(timestamp, payload); + const timestamp = data.createdAt; + const signature = this.generateWebhookSignature(timestamp, data); - this.logger.verbose(`Sending webhook event ${eventType}`); + this.logger.info(`Sending webhook event ${data.event}`); try { await this.fetchWithRetry(MEET_WEBHOOK_URL, { @@ -37,10 +65,10 @@ export class OpenViduWebhookService { 'X-Timestamp': timestamp.toString(), 'X-Signature': signature }, - body: JSON.stringify(payload) + body: JSON.stringify(data) }); } catch (error) { - this.logger.error(`Error sending webhook event ${eventType}: ${error}`); + this.logger.error(`Error sending webhook event ${data.event}: ${error}`); throw error; } } @@ -64,7 +92,7 @@ export class OpenViduWebhookService { throw new Error(`Request failed: ${error}`); } - this.logger.verbose(`Retrying in ${delay / 1000} seconds... (${retries} retries left)`); + this.logger.warn(`Retrying in ${delay / 1000} seconds... (${retries} retries left)`); await new Promise((resolve) => setTimeout(resolve, delay)); // Retry the request after a delay with exponential backoff return this.fetchWithRetry(url, options, retries - 1, delay * 2);