backend: Enhance Livekit webhook handling with OpenVidu integration for recording events
This commit is contained in:
parent
e644e1b434
commit
9f0877780e
@ -1,7 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { LoggerService } from '../services/logger.service.js';
|
import { LoggerService } from '../services/logger.service.js';
|
||||||
import { LivekitWebhookService } from '../services/livekit-webhook.service.js';
|
import { LivekitWebhookService } from '../services/livekit-webhook.service.js';
|
||||||
import { RoomService } from '../services/room.service.js';
|
|
||||||
import { WebhookEvent } from 'livekit-server-sdk';
|
import { WebhookEvent } from 'livekit-server-sdk';
|
||||||
import { OpenViduWebhookService } from '../services/openvidu-webhook.service.js';
|
import { OpenViduWebhookService } from '../services/openvidu-webhook.service.js';
|
||||||
import { container } from '../config/dependency-injector.config.js';
|
import { container } from '../config/dependency-injector.config.js';
|
||||||
|
|||||||
@ -1,3 +1,24 @@
|
|||||||
export enum OpenViduWebhookEvent {
|
import { RecordingStatus } from './recording.model.js';
|
||||||
ROOM_FINISHED = 'room_finished',
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { RoomService } from './room.service.js';
|
|||||||
import { S3Service } from './s3.service.js';
|
import { S3Service } from './s3.service.js';
|
||||||
import { RoomStatusData } from '../models/room.model.js';
|
import { RoomStatusData } from '../models/room.model.js';
|
||||||
import { RecordingService } from './recording.service.js';
|
import { RecordingService } from './recording.service.js';
|
||||||
|
import { OpenViduWebhookService } from './openvidu-webhook.service.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LivekitWebhookService {
|
export class LivekitWebhookService {
|
||||||
@ -20,7 +21,8 @@ export class LivekitWebhookService {
|
|||||||
@inject(RecordingService) protected recordingService: RecordingService,
|
@inject(RecordingService) protected recordingService: RecordingService,
|
||||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||||
@inject(RoomService) protected roomService: RoomService,
|
@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);
|
this.webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||||
}
|
}
|
||||||
@ -86,18 +88,24 @@ export class LivekitWebhookService {
|
|||||||
|
|
||||||
const { roomName } = egressInfo;
|
const { roomName } = egressInfo;
|
||||||
|
|
||||||
let payload: RecordingInfo | undefined = undefined;
|
let recordingInfo: RecordingInfo | undefined = undefined;
|
||||||
|
|
||||||
this.logger.info(`Recording egress '${egressInfo.egressId}' updated: ${egressInfo.status}`);
|
this.logger.info(`Recording egress '${egressInfo.egressId}' updated: ${egressInfo.status}`);
|
||||||
const topic: DataTopic = RecordingHelper.getDataTopicFromStatus(egressInfo);
|
const topic: DataTopic = RecordingHelper.getDataTopicFromStatus(egressInfo);
|
||||||
payload = RecordingHelper.toRecordingInfo(egressInfo);
|
recordingInfo = RecordingHelper.toRecordingInfo(egressInfo);
|
||||||
|
|
||||||
// Add recording metadata
|
// Add recording metadata
|
||||||
const metadataPath = this.generateMetadataPath(payload);
|
const metadataPath = this.generateMetadataPath(recordingInfo);
|
||||||
await Promise.all([
|
const promises = [
|
||||||
this.s3Service.saveObject(metadataPath, payload),
|
this.s3Service.saveObject(metadataPath, recordingInfo),
|
||||||
this.roomService.sendSignal(roomName, payload, { topic })
|
this.roomService.sendSignal(roomName, recordingInfo, { topic })
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if(recordingInfo.status === RecordingStatus.STARTED) {
|
||||||
|
promises.push(this.openViduWebhookService.sendRecordingStartedWebhook(recordingInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`Error sending data on egress updated: ${error}`);
|
this.logger.warn(`Error sending data on egress updated: ${error}`);
|
||||||
}
|
}
|
||||||
@ -124,7 +132,8 @@ export class LivekitWebhookService {
|
|||||||
const metadataPath = this.generateMetadataPath(payload);
|
const metadataPath = this.generateMetadataPath(payload);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.s3Service.saveObject(metadataPath, payload),
|
this.s3Service.saveObject(metadataPath, payload),
|
||||||
this.roomService.sendSignal(roomName, payload, { topic })
|
this.roomService.sendSignal(roomName, payload, { topic }),
|
||||||
|
this.openViduWebhookService.sendRecordingStoppedWebhook(payload)
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`Error sending data on egress ended: ${error}`);
|
this.logger.warn(`Error sending data on egress ended: ${error}`);
|
||||||
|
|||||||
@ -3,31 +3,59 @@ import { inject, injectable } from '../config/dependency-injector.config.js';
|
|||||||
import { Room } from 'livekit-server-sdk';
|
import { Room } from 'livekit-server-sdk';
|
||||||
import { LoggerService } from './logger.service.js';
|
import { LoggerService } from './logger.service.js';
|
||||||
import { MEET_API_KEY, MEET_WEBHOOK_ENABLED, MEET_WEBHOOK_URL } from '../environment.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()
|
@injectable()
|
||||||
export class OpenViduWebhookService {
|
export class OpenViduWebhookService {
|
||||||
constructor(@inject(LoggerService) protected logger: LoggerService) {}
|
constructor(@inject(LoggerService) protected logger: LoggerService) {}
|
||||||
|
|
||||||
async sendRoomFinishedWebhook(room: Room) {
|
async sendRoomFinishedWebhook(room: Room) {
|
||||||
await this.sendWebhookEvent(OpenViduWebhookEvent.ROOM_FINISHED, {
|
const data: OpenViduWebhookEvent = {
|
||||||
room: {
|
event: OpenViduWebhookEventType.ROOM_FINISHED,
|
||||||
name: room.name
|
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;
|
if (!this.isWebhookEnabled()) return;
|
||||||
|
|
||||||
const payload = {
|
const timestamp = data.createdAt;
|
||||||
event: eventType,
|
const signature = this.generateWebhookSignature(timestamp, data);
|
||||||
...data
|
|
||||||
};
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const signature = this.generateWebhookSignature(timestamp, payload);
|
|
||||||
|
|
||||||
this.logger.verbose(`Sending webhook event ${eventType}`);
|
this.logger.info(`Sending webhook event ${data.event}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.fetchWithRetry(MEET_WEBHOOK_URL, {
|
await this.fetchWithRetry(MEET_WEBHOOK_URL, {
|
||||||
@ -37,10 +65,10 @@ export class OpenViduWebhookService {
|
|||||||
'X-Timestamp': timestamp.toString(),
|
'X-Timestamp': timestamp.toString(),
|
||||||
'X-Signature': signature
|
'X-Signature': signature
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error sending webhook event ${eventType}: ${error}`);
|
this.logger.error(`Error sending webhook event ${data.event}: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +92,7 @@ export class OpenViduWebhookService {
|
|||||||
throw new Error(`Request failed: ${error}`);
|
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));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
// Retry the request after a delay with exponential backoff
|
// Retry the request after a delay with exponential backoff
|
||||||
return this.fetchWithRetry(url, options, retries - 1, delay * 2);
|
return this.fetchWithRetry(url, options, retries - 1, delay * 2);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user