import { EgressStatus } from '@livekit/protocol'; import { MeetRecordingInfo, MeetRecordingStatus } from '@typings-ce'; import { EgressInfo } from 'livekit-server-sdk'; import { uid as secureUid } from 'uid/secure'; import { container } from '../config/index.js'; import { RoomService } from '../services/index.js'; export class RecordingHelper { private constructor() { // Prevent instantiation of this utility class } static async toRecordingInfo(egressInfo: EgressInfo): Promise { const status = RecordingHelper.extractOpenViduStatus(egressInfo.status); const size = RecordingHelper.extractSize(egressInfo); // const outputMode = RecordingHelper.extractOutputMode(egressInfo); const duration = RecordingHelper.extractDuration(egressInfo); const startDateMs = RecordingHelper.extractStartDate(egressInfo); const endDateMs = RecordingHelper.extractEndDate(egressInfo); const filename = RecordingHelper.extractFilename(egressInfo); const uid = RecordingHelper.extractUidFromFilename(filename); const { egressId, roomName: roomId, errorCode, error, details } = egressInfo; const roomService = container.get(RoomService); const { roomName } = await roomService.getMeetRoom(roomId); return { recordingId: `${roomId}--${egressId}--${uid}`, roomId, roomName, // outputMode, status, filename, startDate: startDateMs, endDate: endDateMs, duration, size, errorCode: errorCode ? Number(errorCode) : undefined, error: error ? String(error) : undefined, details: details ? String(details) : undefined }; } /** * Checks if the egress is for recording. * @param egress - The egress information. * @returns A boolean indicating if the egress is for recording. */ static isRecordingEgress(egress: EgressInfo): boolean { const { streamResults = [], fileResults = [] } = egress; return fileResults.length > 0 && streamResults.length === 0; } static canBeDeleted(recordingInfo: MeetRecordingInfo): boolean { const { status } = recordingInfo; const isFinished = [ MeetRecordingStatus.COMPLETE, MeetRecordingStatus.FAILED, MeetRecordingStatus.ABORTED, MeetRecordingStatus.LIMIT_REACHED ].includes(status); return isFinished; } static extractOpenViduStatus(status: EgressStatus | undefined): MeetRecordingStatus { switch (status) { case EgressStatus.EGRESS_STARTING: return MeetRecordingStatus.STARTING; case EgressStatus.EGRESS_ACTIVE: return MeetRecordingStatus.ACTIVE; case EgressStatus.EGRESS_ENDING: return MeetRecordingStatus.ENDING; case EgressStatus.EGRESS_COMPLETE: return MeetRecordingStatus.COMPLETE; case EgressStatus.EGRESS_FAILED: return MeetRecordingStatus.FAILED; case EgressStatus.EGRESS_ABORTED: return MeetRecordingStatus.ABORTED; case EgressStatus.EGRESS_LIMIT_REACHED: return MeetRecordingStatus.LIMIT_REACHED; default: return MeetRecordingStatus.FAILED; } } /** * Extracts the OpenVidu output mode based on the provided egress information. * If the egress information contains roomComposite, it returns RecordingOutputMode.COMPOSED. * Otherwise, it returns RecordingOutputMode.INDIVIDUAL. * * @param egressInfo - The egress information containing the roomComposite flag. * @returns The extracted OpenVidu output mode. */ // static extractOutputMode(egressInfo: EgressInfo): MeetRecordingOutputMode { // // if (egressInfo.request.case === 'roomComposite') { // // return MeetRecordingOutputMode.COMPOSED; // // } else { // // return MeetRecordingOutputMode.INDIVIDUAL; // // } // return MeetRecordingOutputMode.COMPOSED; // } /** * Extracts the filename/path for storing the recording. * For EgressInfo, returns the last segment of the fileResults. * For MeetRecordingInfo, returns a combination of roomId and filename. */ static extractFilename(recordingInfo: MeetRecordingInfo): string; static extractFilename(egressInfo: EgressInfo): string; static extractFilename(info: MeetRecordingInfo | EgressInfo): string { if ('request' in info) { // EgressInfo return info.fileResults[0]!.filename.split('/').pop()!; } else { // MeetRecordingInfo const { filename, roomId } = info; return `${roomId}/${filename}`; } } /** * Extracts the UID from the given filename. * * @param filename room-123--{uid}.mp4 * @returns */ static extractUidFromFilename(filename: string): string { const uidWithExtension = filename.split('--')[1]; return uidWithExtension.split('.')[0]; } /** * Extracts the room name, egressId, and UID from the given recordingId. * @param recordingId ${roomId}--${egressId}--${uid} */ static extractInfoFromRecordingId(recordingId: string): { roomId: string; egressId: string; uid: string } { const [roomId, egressId, uid] = recordingId.split('--'); if (!roomId || !egressId || !uid) { throw new Error(`Invalid recordingId format: ${recordingId}`); } return { roomId, egressId, uid }; } /** * Extracts the duration from the given egress information. * If the duration is not available, it returns 0. * @param egressInfo The egress information containing the file results. * @returns The duration in milliseconds. */ static extractDuration(egressInfo: EgressInfo): number | undefined { const duration = this.toSeconds(Number(egressInfo.fileResults?.[0]?.duration ?? 0)); return duration !== 0 ? duration : undefined; } /** * Extracts the endedAt value from the given EgressInfo object and converts it to milliseconds. * If the endedAt value is not provided, it defaults to 0. * * @param egressInfo - The EgressInfo object containing the endedAt value. * @returns The endedAt value converted to milliseconds. */ static extractEndDate(egressInfo: EgressInfo): number | undefined { const endDateMs = this.toMilliseconds(Number(egressInfo.endedAt ?? 0)); return endDateMs !== 0 ? endDateMs : undefined; } /** * Extracts the creation timestamp from the given EgressInfo object. * If the startedAt property is not defined, it returns 0. * @param egressInfo The EgressInfo object from which to extract the creation timestamp. * @returns The creation timestamp in milliseconds. */ static extractStartDate(egressInfo: EgressInfo): number { const { startedAt, updatedAt } = egressInfo; const createdAt = startedAt && Number(startedAt) !== 0 ? startedAt : (updatedAt ?? 0); return this.toMilliseconds(Number(createdAt)); } /** * Extracts the size from the given EgressInfo object. * If the size is not available, it returns 0. * * @param egressInfo - The EgressInfo object to extract the size from. * @returns The size extracted from the EgressInfo object, or 0 if not available. */ static extractSize(egressInfo: EgressInfo): number | undefined { const size = Number(egressInfo.fileResults?.[0]?.size ?? 0); return size !== 0 ? size : undefined; } /** * Builds the secrets for public and private access to recordings. * @returns An object containing public and private access secrets. */ static buildAccessSecrets(): { publicAccessSecret: string; privateAccessSecret: string } { return { publicAccessSecret: secureUid(10), privateAccessSecret: secureUid(10) }; } private static toSeconds(nanoseconds: number): number { const nanosecondsToSeconds = 1 / 1_000_000_000; return nanoseconds * nanosecondsToSeconds; } private static toMilliseconds(nanoseconds: number): number { const nanosecondsToMilliseconds = 1 / 1_000_000; return nanoseconds * nanosecondsToMilliseconds; } }