backend: Enhance Livekit webhook handling with OpenVidu integration for recording events

This commit is contained in:
Carlos Santos 2025-03-14 20:03:33 +01:00
parent e644e1b434
commit 9f0877780e
4 changed files with 85 additions and 28 deletions

View File

@ -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';

View File

@ -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;
}

View File

@ -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}`);

View File

@ -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);