import { MeetRoomStatus, MeetTokenMetadata, OpenViduMeetPermissions, ParticipantOptions, ParticipantPermissions, ParticipantRole } from '@typings-ce'; 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, errorRoomClosed } from '../models/error.model.js'; import { FrontendEventService, LiveKitService, LoggerService, ParticipantNameService, RoomService, TokenService } from './index.js'; @injectable() export class ParticipantService { constructor( @inject(LoggerService) protected logger: LoggerService, @inject(RoomService) protected roomService: RoomService, @inject(LiveKitService) protected livekitService: LiveKitService, @inject(FrontendEventService) protected frontendEventService: FrontendEventService, @inject(TokenService) protected tokenService: TokenService, @inject(ParticipantNameService) protected participantNameService: ParticipantNameService ) {} async generateOrRefreshParticipantToken( participantOptions: ParticipantOptions, currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[], refresh = false ): Promise { const { roomId, secret, participantName, participantIdentity } = participantOptions; let finalParticipantName = participantName; 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); } // Create the Livekit room if it doesn't exist await this.roomService.createLivekitRoom(roomId); if (refresh) { if (!participantIdentity) { throw errorParticipantIdentityNotProvided(); } this.logger.verbose(`Refreshing participant token for '${participantIdentity}' in room '${roomId}'`); // Check if participant with same participantIdentity exists in the room const participantExists = await this.participantExists(roomId, participantIdentity, 'identity'); if (!participantExists) { this.logger.verbose(`Participant '${participantIdentity}' does not exist in room '${roomId}'`); throw errorParticipantNotFound(participantIdentity, roomId); } } else { this.logger.verbose(`Generating participant token for '${participantName}' in room '${roomId}'`); try { // Reserve a unique name for the participant finalParticipantName = await this.participantNameService.reserveUniqueName(roomId, participantName); this.logger.verbose(`Reserved unique name '${finalParticipantName}' for room '${roomId}'`); } catch (error) { this.logger.error( `Failed to reserve unique name '${participantName}' for room '${roomId}':`, error ); throw error; } // Update participantOptions with the final participant name finalParticipantOptions = { ...participantOptions, participantName: finalParticipantName }; } } const role = await this.roomService.getRoomRoleBySecret(roomId, secret); const token = await this.generateParticipantToken(finalParticipantOptions, role, currentRoles); this.logger.verbose( `Participant token generated for room '${roomId}'` + (finalParticipantName ? ` with name '${finalParticipantName}'` : '') ); return token; } protected async generateParticipantToken( participantOptions: ParticipantOptions, role: ParticipantRole, currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] ): Promise { const { roomId, participantName } = participantOptions; const permissions = this.getParticipantPermissions(roomId, role, !!participantName); if (!currentRoles.some((r) => r.role === role)) { currentRoles.push({ role, permissions: permissions.openvidu }); } return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles, role); } async getParticipant(roomId: string, participantIdentity: string): Promise { this.logger.verbose(`Fetching participant '${participantIdentity}'`); return this.livekitService.getParticipant(roomId, participantIdentity); } async participantExists( roomId: string, participantNameOrIdentity: string, participantField: 'name' | 'identity' = 'identity' ): Promise { this.logger.verbose(`Checking if participant '${participantNameOrIdentity}' exists in room '${roomId}'`); return this.livekitService.participantExists(roomId, participantNameOrIdentity, participantField); } async deleteParticipant(roomId: string, participantIdentity: string): Promise { this.logger.verbose(`Deleting participant '${participantIdentity}' from room '${roomId}'`); return this.livekitService.deleteParticipant(roomId, participantIdentity); } getParticipantPermissions(roomId: string, role: ParticipantRole, addJoinPermission = true): ParticipantPermissions { switch (role) { case ParticipantRole.MODERATOR: return this.generateModeratorPermissions(roomId, addJoinPermission); case ParticipantRole.SPEAKER: return this.generateSpeakerPermissions(roomId, addJoinPermission); default: throw new Error(`Role ${role} not supported`); } } async updateParticipantRole(roomId: string, participantIdentity: string, newRole: ParticipantRole): Promise { try { const meetRoom = await this.roomService.getMeetRoom(roomId); const participant = await this.getParticipant(roomId, participantIdentity); const metadata: MeetTokenMetadata = this.parseMetadata(participant.metadata); // Update selected role and roles array metadata.selectedRole = newRole; const currentRoles = metadata.roles; if (!currentRoles.some((r) => r.role === newRole)) { const { openvidu } = this.getParticipantPermissions(roomId, newRole); currentRoles.push({ role: newRole, permissions: openvidu }); } await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata)); const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom); const secret = newRole === ParticipantRole.MODERATOR ? moderatorSecret : speakerSecret; await this.frontendEventService.sendParticipantRoleUpdatedSignal( roomId, participantIdentity, newRole, secret ); } catch (error) { this.logger.error('Error updating participant role:', error); throw error; } } parseMetadata(metadata: string): MeetTokenMetadata { try { const parsedMetadata = JSON.parse(metadata); return validateMeetTokenMetadata(parsedMetadata); } catch (error) { this.logger.error('Failed to parse participant metadata:', error); throw new Error('Invalid participant metadata format'); } } protected generateModeratorPermissions(roomId: string, addJoinPermission = true): ParticipantPermissions { return { livekit: { roomJoin: addJoinPermission, room: roomId, canPublish: true, canSubscribe: true, canPublishData: true, canUpdateOwnMetadata: true }, openvidu: { canRecord: true, canChat: true, canChangeVirtualBackground: true } }; } protected generateSpeakerPermissions(roomId: string, addJoinPermission = true): ParticipantPermissions { return { livekit: { roomJoin: addJoinPermission, room: roomId, canPublish: true, canSubscribe: true, canPublishData: true, canUpdateOwnMetadata: true }, openvidu: { canRecord: false, canChat: true, canChangeVirtualBackground: true } }; } /** * Releases a participant's reserved name when they disconnect. * This should be called when a participant leaves the room to free up the name. * * @param roomId - The room identifier * @param participantName - The participant name to release */ async releaseParticipantName(roomId: string, participantName: string): Promise { try { await this.participantNameService.releaseName(roomId, participantName); this.logger.verbose(`Released participant name '${participantName}' for room '${roomId}'`); } catch (error) { this.logger.warn(`Error releasing participant name '${participantName}' for room '${roomId}':`, error); } } /** * Gets all currently reserved participant names in a room. * Useful for debugging and monitoring. * * @param roomId - The room identifier * @returns Promise - Array of reserved participant names */ async getReservedNames(roomId: string): Promise { return await this.participantNameService.getReservedNames(roomId); } /** * Cleans up expired participant name reservations for a room. * This can be called during room cleanup or periodically. * * @param roomId - The room identifier */ async cleanupParticipantNames(roomId: string): Promise { await this.participantNameService.cleanupExpiredReservations(roomId); } }