diff --git a/meet-ce/backend/openapi/components/responses/success-get-recording.yaml b/meet-ce/backend/openapi/components/responses/success-get-recording.yaml index d2bd8850..9c7f7167 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-recording.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-recording.yaml @@ -11,6 +11,7 @@ content: roomId: 'room-123' roomName: 'room' status: 'complete' + layout: 'grid' filename: 'room-123--XX445.mp4' startDate: 1600000000000 endDate: 1600000003600 @@ -25,5 +26,6 @@ content: roomId: 'room-456' roomName: 'room' status: 'active' + layout: 'grid' filename: 'room-456--QR789.mp4' startDate: 1682500000000 diff --git a/meet-ce/backend/openapi/components/responses/success-get-recordings.yaml b/meet-ce/backend/openapi/components/responses/success-get-recordings.yaml index 897772af..4838ebdb 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-recordings.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-recordings.yaml @@ -19,6 +19,7 @@ content: roomId: 'room-123' roomName: 'room' status: 'active' + layout: 'grid' filename: 'room-123--XX445.mp4' startDate: 1620000000000 endDate: 1620000003600 @@ -29,6 +30,7 @@ content: roomId: 'room-456' roomName: 'room' status: 'complete' + layout: 'grid' filename: 'room-456--XX678.mp4' startDate: 1625000000000 endDate: 1625000007200 diff --git a/meet-ce/backend/openapi/components/schemas/meet-recording.yaml b/meet-ce/backend/openapi/components/schemas/meet-recording.yaml index 709bde2a..bf265e75 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-recording.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-recording.yaml @@ -22,6 +22,10 @@ properties: enum: ['starting', 'active', 'ending', 'complete', 'failed', 'aborted', 'limit_reached'] example: 'active' description: The status of the recording. + layout: + type: string + example: 'grid' + description: The layout of the recording. filename: type: string example: 'room-123--XX445.mp4' diff --git a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml index 87f4d236..f9d599a6 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml @@ -35,9 +35,9 @@ MeetRecordingConfig: - grid - speaker - single-speaker - - grid-light - - speaker-light - - single-speaker-light + # - grid-light + # - speaker-light + # - single-speaker-light default: grid example: grid description: | @@ -45,9 +45,9 @@ MeetRecordingConfig: - `grid`: All participants are shown in a grid layout. - `speaker`: The active speaker is shown prominently, with other participants in smaller thumbnails. - `single-speaker`: Only the active speaker is shown in the recording. - - `grid-light`: Similar to `grid` but with a light-themed background. - - `speaker-light`: Similar to `speaker` but with a light-themed background. - - `single-speaker-light`: Similar to `single-speaker` but with a light + # - `grid-light`: Similar to `grid` but with a light-themed background. + # - `speaker-light`: Similar to `speaker` but with a light-themed background. + # - `single-speaker-light`: Similar to `single-speaker` but with a light-themed background. allowAccessTo: type: string enum: diff --git a/meet-ce/backend/openapi/components/schemas/recording-base.yaml b/meet-ce/backend/openapi/components/schemas/recording-base.yaml index 4b535a29..7698be96 100644 --- a/meet-ce/backend/openapi/components/schemas/recording-base.yaml +++ b/meet-ce/backend/openapi/components/schemas/recording-base.yaml @@ -22,6 +22,11 @@ properties: status: type: string description: The status of the recording. + example: active + layout: + type: string + description: The layout of the recording. + example: grid filename: type: string description: The name of the recording file. diff --git a/meet-ce/backend/src/helpers/recording.helper.ts b/meet-ce/backend/src/helpers/recording.helper.ts index eca1e1aa..44369fff 100644 --- a/meet-ce/backend/src/helpers/recording.helper.ts +++ b/meet-ce/backend/src/helpers/recording.helper.ts @@ -1,5 +1,5 @@ import { EgressStatus } from '@livekit/protocol'; -import { MeetRecordingInfo, MeetRecordingStatus } from '@openvidu-meet/typings'; +import { MeetRecordingInfo, MeetRecordingLayout, MeetRecordingStatus } from '@openvidu-meet/typings'; import { EgressInfo } from 'livekit-server-sdk'; import { container } from '../config/dependency-injector.config.js'; import { RoomService } from '../services/room.service.js'; @@ -19,7 +19,7 @@ export class RecordingHelper { const filename = RecordingHelper.extractFilename(egressInfo); const recordingId = RecordingHelper.extractRecordingIdFromEgress(egressInfo); const { roomName: roomId, errorCode, error, details } = egressInfo; - + const layout = RecordingHelper.extractRecordingLayout(egressInfo); const roomService = container.get(RoomService); const { roomName } = await roomService.getMeetRoom(roomId); @@ -28,6 +28,7 @@ export class RecordingHelper { roomId, roomName, // outputMode, + layout, status, filename, startDate: startDateMs, @@ -138,6 +139,23 @@ export class RecordingHelper { return `${meetRoomId}--${egressId}--${uid}`; } + static extractRecordingLayout(egressInfo: EgressInfo): MeetRecordingLayout | undefined { + if (egressInfo.request.case !== 'roomComposite') return undefined; + + const { layout } = egressInfo.request.value; + + switch (layout) { + case 'grid': + return MeetRecordingLayout.GRID; + case 'speaker': + return MeetRecordingLayout.SPEAKER; + case 'single-speaker': + return MeetRecordingLayout.SINGLE_SPEAKER; + default: + return MeetRecordingLayout.GRID; // Default layout + } + } + /** * Extracts the room name, egressId, and UID from the given recordingId. * @param recordingId ${roomId}--${egressId}--${uid} diff --git a/meet-ce/backend/src/models/mongoose-schemas/recording.schema.ts b/meet-ce/backend/src/models/mongoose-schemas/recording.schema.ts index e2890c3e..d1034d7e 100644 --- a/meet-ce/backend/src/models/mongoose-schemas/recording.schema.ts +++ b/meet-ce/backend/src/models/mongoose-schemas/recording.schema.ts @@ -1,4 +1,4 @@ -import { MeetRecordingInfo, MeetRecordingStatus } from '@openvidu-meet/typings'; +import { MeetRecordingInfo, MeetRecordingLayout, MeetRecordingStatus } from '@openvidu-meet/typings'; import { Document, model, Schema } from 'mongoose'; import { INTERNAL_CONFIG } from '../../config/internal-config.js'; @@ -43,6 +43,11 @@ const MeetRecordingSchema = new Schema( enum: Object.values(MeetRecordingStatus), required: true }, + layout: { + type: String, + enum: Object.values(MeetRecordingLayout), + required: false + }, filename: { type: String, required: false diff --git a/meet-ce/backend/tests/helpers/assertion-helpers.ts b/meet-ce/backend/tests/helpers/assertion-helpers.ts index c2b9d935..0547f9e1 100644 --- a/meet-ce/backend/tests/helpers/assertion-helpers.ts +++ b/meet-ce/backend/tests/helpers/assertion-helpers.ts @@ -195,6 +195,12 @@ export const expectValidRecording = ( expect(recording.status).toBe(status); expect(recording.filename).toBeDefined(); expect(recording.details).toBeDefined(); + expect(recording.layout).toBeDefined(); + + // Validate layout is a valid value + if (recording.layout !== undefined) { + expect(Object.values(MeetRecordingLayout)).toContain(recording.layout); + } }; export const expectValidRoomWithFields = (room: MeetRoom, fields: string[] = []) => { @@ -373,9 +379,15 @@ export const expectValidStartRecordingResponse = (response: Response, roomId: st expect(response.body).toHaveProperty('startDate'); expect(response.body).toHaveProperty('status', 'active'); expect(response.body).toHaveProperty('filename'); + expect(response.body).toHaveProperty('layout'); expect(response.body).not.toHaveProperty('duration'); expect(response.body).not.toHaveProperty('endDate'); expect(response.body).not.toHaveProperty('size'); + + // Validate layout is a valid value + if (response.body.layout !== undefined) { + expect(Object.values(MeetRecordingLayout)).toContain(response.body.layout); + } }; export const expectValidStopRecordingResponse = ( @@ -393,6 +405,12 @@ export const expectValidStopRecordingResponse = ( expect(response.body).toHaveProperty('filename'); expect(response.body).toHaveProperty('startDate'); expect(response.body).toHaveProperty('duration', expect.any(Number)); + expect(response.body).toHaveProperty('layout'); + + // Validate layout is a valid value + if (response.body.layout !== undefined) { + expect(Object.values(MeetRecordingLayout)).toContain(response.body.layout); + } expectValidRecordingLocationHeader(response); }; @@ -432,6 +450,16 @@ export const expectValidGetRecordingResponse = ( }) ); + // Validate layout property + expect(body).toHaveProperty('layout'); + + if (body.layout !== undefined) { + expect(body.layout).toBeDefined(); + expect(typeof body.layout).toBe('string'); + // Validate it's a valid MeetRecordingLayout value + expect(Object.values(MeetRecordingLayout)).toContain(body.layout); + } + expect(body.status).toBeDefined(); if (status !== undefined) { diff --git a/meet-ce/backend/tests/integration/webhooks/webhook.test.ts b/meet-ce/backend/tests/integration/webhooks/webhook.test.ts index a1b25876..3e8f966b 100644 --- a/meet-ce/backend/tests/integration/webhooks/webhook.test.ts +++ b/meet-ce/backend/tests/integration/webhooks/webhook.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; -import { MeetRecordingInfo, MeetRecordingStatus, MeetWebhookEvent, MeetWebhookEventType } from '@openvidu-meet/typings'; +import { MeetRecordingInfo, MeetRecordingLayout, MeetRecordingStatus, MeetWebhookEvent, MeetWebhookEventType } from '@openvidu-meet/typings'; import { Request } from 'express'; import http from 'http'; import { @@ -148,6 +148,11 @@ describe('Webhook Integration Tests', () => { expect(recordingStartedWebhook?.headers['x-timestamp']).toBeDefined(); expect(recordingStartedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_STARTED); expect(data.status).toBe(MeetRecordingStatus.STARTING); + expect(data.layout).toBeDefined(); + + if (data.layout) { + expect(Object.values(MeetRecordingLayout)).toContain(data.layout); + } // Check recording_updated webhook const recordingUpdatedWebhook = receivedWebhooks.find( @@ -163,6 +168,11 @@ describe('Webhook Integration Tests', () => { expect(recordingUpdatedWebhook?.headers['x-timestamp']).toBeDefined(); expect(recordingUpdatedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_UPDATED); expect(data.status).toBe(MeetRecordingStatus.ACTIVE); + expect(data.layout).toBeDefined(); + + if (data.layout) { + expect(Object.values(MeetRecordingLayout)).toContain(data.layout); + } // Check recording_ended webhook const recordingEndedWebhook = receivedWebhooks.find( @@ -179,5 +189,10 @@ describe('Webhook Integration Tests', () => { expect(recordingEndedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_ENDED); expect(data.status).not.toBe(MeetRecordingStatus.ENDING); expect(data.status).toBe(MeetRecordingStatus.COMPLETE); + expect(data.layout).toBeDefined(); + + if (data.layout) { + expect(Object.values(MeetRecordingLayout)).toContain(data.layout); + } }); }); diff --git a/meet-ce/typings/src/recording.model.ts b/meet-ce/typings/src/recording.model.ts index 8a552eb3..f0e1f24f 100644 --- a/meet-ce/typings/src/recording.model.ts +++ b/meet-ce/typings/src/recording.model.ts @@ -30,8 +30,8 @@ export interface MeetRecordingInfo { roomId: string; roomName: string; // outputMode: MeetRecordingOutputMode; - // layout: MeetRecordingLayout; status: MeetRecordingStatus; + layout?: MeetRecordingLayout; filename?: string; startDate?: number; endDate?: number;