backend: implement room status handling and actions when meeting ends
This commit is contained in:
parent
e7fae2b3be
commit
0125fc0934
@ -16,6 +16,7 @@ export class MeetRoomHelper {
|
|||||||
return {
|
return {
|
||||||
roomName: room.roomName,
|
roomName: room.roomName,
|
||||||
autoDeletionDate: room.autoDeletionDate,
|
autoDeletionDate: room.autoDeletionDate,
|
||||||
|
autoDeletionPolicy: room.autoDeletionPolicy,
|
||||||
preferences: room.preferences
|
preferences: room.preferences
|
||||||
// maxParticipants: room.maxParticipants
|
// maxParticipants: room.maxParticipants
|
||||||
};
|
};
|
||||||
|
|||||||
@ -204,6 +204,10 @@ export const errorRoomNotFound = (roomId: string): OpenViduMeetError => {
|
|||||||
return new OpenViduMeetError('Room Error', `Room '${roomId}' does not exist`, 404);
|
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 => {
|
export const errorRoomMetadataNotFound = (roomId: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError(
|
return new OpenViduMeetError(
|
||||||
'Room Error',
|
'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 => {
|
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 => {
|
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 => {
|
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 => {
|
export const errorParticipantIdentityNotProvided = (): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('Participant', 'No participant identity provided', 400);
|
return new OpenViduMeetError('Participant Error', 'No participant identity provided', 400);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { MeetRecordingInfo, MeetRecordingStatus } from '@typings-ce';
|
import { MeetingEndAction, MeetRecordingInfo, MeetRecordingStatus, MeetRoomStatus } from '@typings-ce';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import { EgressInfo, ParticipantInfo, Room, WebhookEvent, WebhookReceiver } from 'livekit-server-sdk';
|
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 { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } from '../environment.js';
|
||||||
import { MeetLock, MeetRoomHelper, RecordingHelper } from '../helpers/index.js';
|
import { MeetLock, MeetRoomHelper, RecordingHelper } from '../helpers/index.js';
|
||||||
import { DistributedEventType } from '../models/distributed-event.model.js';
|
import { DistributedEventType } from '../models/distributed-event.model.js';
|
||||||
|
import { FrontendEventService } from './frontend-event.service.js';
|
||||||
import {
|
import {
|
||||||
|
DistributedEventService,
|
||||||
LiveKitService,
|
LiveKitService,
|
||||||
LoggerService,
|
LoggerService,
|
||||||
MeetStorageService,
|
MeetStorageService,
|
||||||
@ -12,11 +15,8 @@ import {
|
|||||||
OpenViduWebhookService,
|
OpenViduWebhookService,
|
||||||
ParticipantService,
|
ParticipantService,
|
||||||
RecordingService,
|
RecordingService,
|
||||||
RoomService,
|
RoomService
|
||||||
DistributedEventService
|
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
import { FrontendEventService } from './frontend-event.service.js';
|
|
||||||
import ms from 'ms';
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LivekitWebhookService {
|
export class LivekitWebhookService {
|
||||||
@ -183,57 +183,89 @@ export class LivekitWebhookService {
|
|||||||
* Handles a room started event from LiveKit.
|
* Handles a room started event from LiveKit.
|
||||||
*
|
*
|
||||||
* This method retrieves the corresponding meet room from the room service using the LiveKit room name.
|
* 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 found, it updates the room status to ACTIVE_MEETING,
|
||||||
* If the meet room is not found, it logs a warning message.
|
* 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 {
|
try {
|
||||||
const meetRoom = await this.roomService.getMeetRoom(room.name);
|
const meetRoom = await this.roomService.getMeetRoom(roomId);
|
||||||
|
|
||||||
if (!meetRoom) {
|
if (!meetRoom) {
|
||||||
this.logger.warn(`Room ${room.name} not found in OpenVidu Meet.`);
|
this.logger.warn(`Room '${roomId}' not found in OpenVidu Meet.`);
|
||||||
return;
|
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);
|
this.openViduWebhookService.sendMeetingStartedWebhook(meetRoom);
|
||||||
} catch (error) {
|
} 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.
|
* This method retrieves the corresponding meet room from the room service using the LiveKit room name.
|
||||||
* If an error occurs while sending the webhook, it logs the error.
|
* 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.
|
* @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 {
|
try {
|
||||||
const meetRoom = await this.roomService.getMeetRoom(roomName);
|
const meetRoom = await this.roomService.getMeetRoom(roomId);
|
||||||
|
|
||||||
if (!meetRoom) {
|
if (!meetRoom) {
|
||||||
this.logger.warn(`Room ${roomName} not found in OpenVidu Meet.`);
|
this.logger.warn(`Room '${roomId}' not found in OpenVidu Meet.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`Processing room_finished event for room: ${roomName}`);
|
this.logger.info(`Processing room_finished event for room: ${roomId}`);
|
||||||
|
|
||||||
this.openViduWebhookService.sendMeetingEndedWebhook(meetRoom);
|
|
||||||
|
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
|
|
||||||
if (meetRoom.markedForDeletion) {
|
switch (meetRoom.meetingEndAction) {
|
||||||
// If the room is marked for deletion, we need to delete it
|
case MeetingEndAction.DELETE:
|
||||||
this.logger.info(`Deleting room ${roomName} after meeting finished because it was marked for deletion`);
|
// TODO: Delete also all recordings associated with the room
|
||||||
tasks.push(this.roomService.bulkDeleteRooms([roomName], true));
|
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(
|
tasks.push(
|
||||||
this.participantService.cleanupParticipantNames(roomName),
|
this.participantService.cleanupParticipantNames(roomId),
|
||||||
this.recordingService.releaseRecordingLockIfNoEgress(roomName)
|
this.recordingService.releaseRecordingLockIfNoEgress(roomId)
|
||||||
);
|
);
|
||||||
await Promise.all(tasks);
|
await Promise.all(tasks);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
MeetRoomStatus,
|
||||||
MeetTokenMetadata,
|
MeetTokenMetadata,
|
||||||
OpenViduMeetPermissions,
|
OpenViduMeetPermissions,
|
||||||
ParticipantOptions,
|
ParticipantOptions,
|
||||||
@ -9,7 +10,11 @@ import { inject, injectable } from 'inversify';
|
|||||||
import { ParticipantInfo } from 'livekit-server-sdk';
|
import { ParticipantInfo } from 'livekit-server-sdk';
|
||||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||||
import { validateMeetTokenMetadata } from '../middlewares/index.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 {
|
import {
|
||||||
FrontendEventService,
|
FrontendEventService,
|
||||||
LiveKitService,
|
LiveKitService,
|
||||||
@ -40,6 +45,13 @@ export class ParticipantService {
|
|||||||
let finalParticipantOptions: ParticipantOptions = participantOptions;
|
let finalParticipantOptions: ParticipantOptions = participantOptions;
|
||||||
|
|
||||||
if (participantName) {
|
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 (refresh) {
|
||||||
if (!participantIdentity) {
|
if (!participantIdentity) {
|
||||||
throw errorParticipantIdentityNotProvided();
|
throw errorParticipantIdentityNotProvided();
|
||||||
|
|||||||
@ -454,7 +454,7 @@ export class RoomService {
|
|||||||
|
|
||||||
if (result.status === 'fulfilled' && result.value) {
|
if (result.status === 'fulfilled' && result.value) {
|
||||||
const room = result.value;
|
const room = result.value;
|
||||||
room.markedForDeletion = true;
|
room.meetingEndAction = MeetingEndAction.DELETE;
|
||||||
roomsToUpdate.push({ roomId, room });
|
roomsToUpdate.push({ roomId, room });
|
||||||
successfulRoomIds.push(roomId);
|
successfulRoomIds.push(roomId);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { AccessToken, AccessTokenOptions, ClaimGrants, TokenVerifier, VideoGrant
|
|||||||
import INTERNAL_CONFIG from '../config/internal-config.js';
|
import INTERNAL_CONFIG from '../config/internal-config.js';
|
||||||
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from '../environment.js';
|
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from '../environment.js';
|
||||||
import { LoggerService } from './index.js';
|
import { LoggerService } from './index.js';
|
||||||
import { uid } from 'uid';
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class TokenService {
|
export class TokenService {
|
||||||
|
|||||||
@ -36,9 +36,8 @@ export const enum MeetRoomStatus {
|
|||||||
|
|
||||||
export const enum MeetingEndAction {
|
export const enum MeetingEndAction {
|
||||||
NONE = 'none', // No action is taken when the meeting ends
|
NONE = 'none', // No action is taken when the meeting ends
|
||||||
CLOSE = 'close', // The room is closed when the meeting ends
|
CLOSE = 'close', // The room will be closed when the meeting ends
|
||||||
DELETE = 'delete', // The room is deleted when the meeting ends
|
DELETE = 'delete' // The room (and its recordings if any) will be deleted when the meeting ends
|
||||||
DELETE_ALL = 'delete_all' // The room and its recordings are deleted when the meeting ends
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MeetRoomAutoDeletionPolicy {
|
export interface MeetRoomAutoDeletionPolicy {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user