backend: implement room status handling and actions when meeting ends

This commit is contained in:
juancarmore 2025-08-28 18:32:59 +02:00
parent e7fae2b3be
commit 0125fc0934
7 changed files with 85 additions and 46 deletions

View File

@ -16,6 +16,7 @@ export class MeetRoomHelper {
return {
roomName: room.roomName,
autoDeletionDate: room.autoDeletionDate,
autoDeletionPolicy: room.autoDeletionPolicy,
preferences: room.preferences
// maxParticipants: room.maxParticipants
};

View File

@ -204,6 +204,10 @@ export const errorRoomNotFound = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Room Error', `Room '${roomId}' does not exist`, 404);
};
export const errorRoomClosed = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Room Error', `Room '${roomId}' is closed and cannot be joined`, 409);
};
export const errorRoomMetadataNotFound = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError(
'Room Error',
@ -226,28 +230,20 @@ export const errorParticipantNotFound = (participantIdentity: string, roomId: st
);
};
export const errorParticipantAlreadyExists = (participantIdentity: string, roomId: string): OpenViduMeetError => {
return new OpenViduMeetError(
'Participant Error',
`Participant '${participantIdentity}' already exists in room '${roomId}'`,
409
);
};
export const errorParticipantTokenNotPresent = (): OpenViduMeetError => {
return new OpenViduMeetError('Participant', 'No participant token provided', 400);
return new OpenViduMeetError('Participant Error', 'No participant token provided', 400);
};
export const errorInvalidParticipantToken = (): OpenViduMeetError => {
return new OpenViduMeetError('Participant', 'Invalid participant token', 400);
return new OpenViduMeetError('Participant Error', 'Invalid participant token', 400);
};
export const errorInvalidParticipantRole = (): OpenViduMeetError => {
return new OpenViduMeetError('Participant', 'No valid participant role provided', 400);
return new OpenViduMeetError('Participant Error', 'No valid participant role provided', 400);
};
export const errorParticipantIdentityNotProvided = (): OpenViduMeetError => {
return new OpenViduMeetError('Participant', 'No participant identity provided', 400);
return new OpenViduMeetError('Participant Error', 'No participant identity provided', 400);
};
// Handlers

View File

@ -1,10 +1,13 @@
import { MeetRecordingInfo, MeetRecordingStatus } from '@typings-ce';
import { MeetingEndAction, MeetRecordingInfo, MeetRecordingStatus, MeetRoomStatus } from '@typings-ce';
import { inject, injectable } from 'inversify';
import { EgressInfo, ParticipantInfo, Room, WebhookEvent, WebhookReceiver } from 'livekit-server-sdk';
import ms from 'ms';
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } from '../environment.js';
import { MeetLock, MeetRoomHelper, RecordingHelper } from '../helpers/index.js';
import { DistributedEventType } from '../models/distributed-event.model.js';
import { FrontendEventService } from './frontend-event.service.js';
import {
DistributedEventService,
LiveKitService,
LoggerService,
MeetStorageService,
@ -12,11 +15,8 @@ import {
OpenViduWebhookService,
ParticipantService,
RecordingService,
RoomService,
DistributedEventService
RoomService
} from './index.js';
import { FrontendEventService } from './frontend-event.service.js';
import ms from 'ms';
@injectable()
export class LivekitWebhookService {
@ -183,57 +183,89 @@ export class LivekitWebhookService {
* Handles a room started event from LiveKit.
*
* This method retrieves the corresponding meet room from the room service using the LiveKit room name.
* If the meet room is found, it sends a webhook notification indicating that the meeting has started.
* If the meet room is not found, it logs a warning message.
* If the meet room is found, it updates the room status to ACTIVE_MEETING,
* and sends a webhook notification indicating that the meeting has started.
*
* @param {Room} room - The room object that has started.
*/
async handleRoomStarted(room: Room) {
async handleRoomStarted({ name: roomId }: Room) {
try {
const meetRoom = await this.roomService.getMeetRoom(room.name);
const meetRoom = await this.roomService.getMeetRoom(roomId);
if (!meetRoom) {
this.logger.warn(`Room ${room.name} not found in OpenVidu Meet.`);
this.logger.warn(`Room '${roomId}' not found in OpenVidu Meet.`);
return;
}
this.logger.info(`Processing room_started event for room: ${roomId}`);
// Update Meet room status to ACTIVE_MEETING
meetRoom.status = MeetRoomStatus.ACTIVE_MEETING;
await this.storageService.saveMeetRoom(meetRoom);
// Send webhook notification
this.openViduWebhookService.sendMeetingStartedWebhook(meetRoom);
} catch (error) {
this.logger.error('Error sending meeting started webhook:', error);
this.logger.error('Error handling room started event:', error);
}
}
/**
* Handles the event when a room is finished.
* Handles a room finished event from LiveKit.
*
* This method sends a webhook notification indicating that the room has finished.
* If an error occurs while sending the webhook, it logs the error.
* This method retrieves the corresponding meet room from the room service using the LiveKit room name.
* If the meet room is found, it processes the room based on its meeting end action:
*
* - If the action is DELETE, it deletes the room and all associated recordings.
* - If the action is CLOSE, it closes the room without deleting it.
* - If the action is NONE, it simply updates the room status to OPEN.
*
* Then, it sends a webhook notification indicating that the meeting has ended,
* and cleans up any resources associated with the room.
*
* @param {Room} room - The room object that has finished.
* @returns {Promise<void>} A promise that resolves when the webhook has been sent.
*/
async handleRoomFinished({ name: roomName }: Room): Promise<void> {
async handleRoomFinished({ name: roomId }: Room): Promise<void> {
try {
const meetRoom = await this.roomService.getMeetRoom(roomName);
const meetRoom = await this.roomService.getMeetRoom(roomId);
if (!meetRoom) {
this.logger.warn(`Room ${roomName} not found in OpenVidu Meet.`);
this.logger.warn(`Room '${roomId}' not found in OpenVidu Meet.`);
return;
}
this.logger.info(`Processing room_finished event for room: ${roomName}`);
this.openViduWebhookService.sendMeetingEndedWebhook(meetRoom);
this.logger.info(`Processing room_finished event for room: ${roomId}`);
const tasks = [];
if (meetRoom.markedForDeletion) {
// If the room is marked for deletion, we need to delete it
this.logger.info(`Deleting room ${roomName} after meeting finished because it was marked for deletion`);
tasks.push(this.roomService.bulkDeleteRooms([roomName], true));
switch (meetRoom.meetingEndAction) {
case MeetingEndAction.DELETE:
// TODO: Delete also all recordings associated with the room
this.logger.info(
`Deleting room '${roomId}' after meeting finished because it was scheduled to be deleted`
);
tasks.push(this.roomService.bulkDeleteRooms([roomId], true));
break;
case MeetingEndAction.CLOSE:
this.logger.info(
`Closing room '${roomId}' after meeting finished because it was scheduled to be closed`
);
meetRoom.status = MeetRoomStatus.CLOSED;
meetRoom.meetingEndAction = MeetingEndAction.NONE;
tasks.push(this.storageService.saveMeetRoom(meetRoom));
break;
default:
// Update Meet room status to OPEN
meetRoom.status = MeetRoomStatus.OPEN;
meetRoom.meetingEndAction = MeetingEndAction.NONE;
tasks.push(this.storageService.saveMeetRoom(meetRoom));
}
// Send webhook notification
this.openViduWebhookService.sendMeetingEndedWebhook(meetRoom);
tasks.push(
this.participantService.cleanupParticipantNames(roomName),
this.recordingService.releaseRecordingLockIfNoEgress(roomName)
this.participantService.cleanupParticipantNames(roomId),
this.recordingService.releaseRecordingLockIfNoEgress(roomId)
);
await Promise.all(tasks);
} catch (error) {

View File

@ -1,4 +1,5 @@
import {
MeetRoomStatus,
MeetTokenMetadata,
OpenViduMeetPermissions,
ParticipantOptions,
@ -9,7 +10,11 @@ import { inject, injectable } from 'inversify';
import { ParticipantInfo } from 'livekit-server-sdk';
import { MeetRoomHelper } from '../helpers/room.helper.js';
import { validateMeetTokenMetadata } from '../middlewares/index.js';
import { errorParticipantIdentityNotProvided, errorParticipantNotFound } from '../models/error.model.js';
import {
errorParticipantIdentityNotProvided,
errorParticipantNotFound,
errorRoomClosed
} from '../models/error.model.js';
import {
FrontendEventService,
LiveKitService,
@ -40,6 +45,13 @@ export class ParticipantService {
let finalParticipantOptions: ParticipantOptions = participantOptions;
if (participantName) {
// Check that room is open
const room = await this.roomService.getMeetRoom(roomId);
if (room.status === MeetRoomStatus.CLOSED) {
throw errorRoomClosed(roomId);
}
if (refresh) {
if (!participantIdentity) {
throw errorParticipantIdentityNotProvided();

View File

@ -454,7 +454,7 @@ export class RoomService {
if (result.status === 'fulfilled' && result.value) {
const room = result.value;
room.markedForDeletion = true;
room.meetingEndAction = MeetingEndAction.DELETE;
roomsToUpdate.push({ roomId, room });
successfulRoomIds.push(roomId);
} else {

View File

@ -13,7 +13,6 @@ import { AccessToken, AccessTokenOptions, ClaimGrants, TokenVerifier, VideoGrant
import INTERNAL_CONFIG from '../config/internal-config.js';
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from '../environment.js';
import { LoggerService } from './index.js';
import { uid } from 'uid';
@injectable()
export class TokenService {

View File

@ -36,9 +36,8 @@ export const enum MeetRoomStatus {
export const enum MeetingEndAction {
NONE = 'none', // No action is taken when the meeting ends
CLOSE = 'close', // The room is closed when the meeting ends
DELETE = 'delete', // The room is deleted when the meeting ends
DELETE_ALL = 'delete_all' // The room and its recordings are deleted when the meeting ends
CLOSE = 'close', // The room will be closed when the meeting ends
DELETE = 'delete' // The room (and its recordings if any) will be deleted when the meeting ends
}
export interface MeetRoomAutoDeletionPolicy {