From 6f841eb2544bcb7247512cc56bea54d046c1b5bc Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Thu, 8 Jan 2026 10:41:38 +0100 Subject: [PATCH] Adds recording layout configuration Enables configuration of recording layouts. Specifies the recording layout in the room configuration. Now supports different layouts, such as grid, speaker, and single-speaker. Updated zod validation schemas Updated integration tests --- .../responses/success-get-room.yaml | 4 + .../responses/success-get-rooms.yaml | 4 + .../components/schemas/meet-room-config.yaml | 19 +++ meet-ce/backend/package.json | 2 + .../models/mongoose-schemas/room.schema.ts | 7 + .../src/models/zod-schemas/room.schema.ts | 56 ++++---- .../backend/src/services/recording.service.ts | 33 ++++- meet-ce/backend/src/services/room.service.ts | 9 +- .../tests/helpers/assertion-helpers.ts | 2 + .../integration/api/rooms/create-room.test.ts | 7 +- .../api/rooms/get-room-config.test.ts | 6 +- .../integration/api/rooms/get-room.test.ts | 3 +- .../recording-layout-room-config.test.ts | 94 +++++++++++++ .../api/rooms/update-room-config.test.ts | 38 ++---- .../selectable-card.component.scss | 7 +- .../recording-config.component.html | 2 +- .../recording-config.component.ts | 6 +- .../recording-layout.component.html | 4 +- .../recording-layout.component.ts | 124 ++++++++++-------- .../src/lib/services/wizard-state.service.ts | 20 ++- meet-ce/frontend/src/assets/layouts/grid.png | Bin 9788 -> 0 bytes .../frontend/src/assets/layouts/grid_dark.png | Bin 0 -> 1553 bytes .../src/assets/layouts/grid_light.png | Bin 0 -> 1595 bytes .../src/assets/layouts/single-speaker.png | Bin 6438 -> 0 bytes .../assets/layouts/single_speaker_dark.png | Bin 0 -> 1014 bytes .../assets/layouts/single_speaker_light.png | Bin 0 -> 1162 bytes .../frontend/src/assets/layouts/speaker.png | Bin 9808 -> 0 bytes .../src/assets/layouts/speaker_dark.png | Bin 0 -> 1496 bytes .../src/assets/layouts/speaker_light.png | Bin 0 -> 1637 bytes meet-ce/typings/src/recording.model.ts | 9 ++ meet-ce/typings/src/room-config.ts | 3 + pnpm-lock.yaml | 18 +++ 32 files changed, 347 insertions(+), 130 deletions(-) create mode 100644 meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts delete mode 100644 meet-ce/frontend/src/assets/layouts/grid.png create mode 100644 meet-ce/frontend/src/assets/layouts/grid_dark.png create mode 100644 meet-ce/frontend/src/assets/layouts/grid_light.png delete mode 100644 meet-ce/frontend/src/assets/layouts/single-speaker.png create mode 100644 meet-ce/frontend/src/assets/layouts/single_speaker_dark.png create mode 100644 meet-ce/frontend/src/assets/layouts/single_speaker_light.png delete mode 100644 meet-ce/frontend/src/assets/layouts/speaker.png create mode 100644 meet-ce/frontend/src/assets/layouts/speaker_dark.png create mode 100644 meet-ce/frontend/src/assets/layouts/speaker_light.png diff --git a/meet-ce/backend/openapi/components/responses/success-get-room.yaml b/meet-ce/backend/openapi/components/responses/success-get-room.yaml index 704a431e..7db0b12c 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-room.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-room.yaml @@ -18,6 +18,8 @@ content: config: recording: enabled: false + layout: grid + allowAccessTo: admin_moderator_speaker chat: enabled: true virtualBackground: @@ -78,6 +80,8 @@ content: config: recording: enabled: false + layout: grid + allowAccessTo: admin_moderator_speaker chat: enabled: true virtualBackground: diff --git a/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml b/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml index 8c86e2ce..9872b5b8 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml @@ -27,6 +27,8 @@ content: config: recording: enabled: false + layout: grid + allowAccessTo: admin_moderator_speaker chat: enabled: true virtualBackground: @@ -87,6 +89,8 @@ content: config: recording: enabled: true + layout: grid + allowAccessTo: admin_moderator_speaker chat: enabled: false virtualBackground: 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 860d7401..87f4d236 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml @@ -29,6 +29,25 @@ MeetRecordingConfig: default: true example: true description: If true, the room will be allowed to record the video of the participants. + layout: + type: string + enum: + - grid + - speaker + - single-speaker + - grid-light + - speaker-light + - single-speaker-light + default: grid + example: grid + description: | + Defines the layout of the recording. Options are: + - `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 allowAccessTo: type: string enum: diff --git a/meet-ce/backend/package.json b/meet-ce/backend/package.json index 2c80eb5b..97753f7c 100644 --- a/meet-ce/backend/package.json +++ b/meet-ce/backend/package.json @@ -73,6 +73,7 @@ "ioredis": "5.6.1", "jwt-decode": "4.0.0", "livekit-server-sdk": "2.13.3", + "lodash.merge": "4.6.2", "mongoose": "8.19.4", "ms": "2.1.3", "uid": "2.0.2", @@ -87,6 +88,7 @@ "@types/cors": "2.8.19", "@types/express": "4.17.25", "@types/jest": "29.5.14", + "@types/lodash.merge": "4.6.9", "@types/ms": "2.1.0", "@types/node": "22.16.5", "@types/supertest": "6.0.3", diff --git a/meet-ce/backend/src/models/mongoose-schemas/room.schema.ts b/meet-ce/backend/src/models/mongoose-schemas/room.schema.ts index 8843106e..4713e0b1 100644 --- a/meet-ce/backend/src/models/mongoose-schemas/room.schema.ts +++ b/meet-ce/backend/src/models/mongoose-schemas/room.schema.ts @@ -1,5 +1,6 @@ import { MeetRecordingAccess, + MeetRecordingLayout, MeetRoom, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, @@ -49,6 +50,12 @@ const MeetRecordingConfigSchema = new Schema( type: Boolean, required: true }, + layout: { + type: String, + enum: Object.values(MeetRecordingLayout), + required: true, + default: MeetRecordingLayout.GRID + }, allowAccessTo: { type: String, enum: Object.values(MeetRecordingAccess), diff --git a/meet-ce/backend/src/models/zod-schemas/room.schema.ts b/meet-ce/backend/src/models/zod-schemas/room.schema.ts index 9e9c6c45..804bbaee 100644 --- a/meet-ce/backend/src/models/zod-schemas/room.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/room.schema.ts @@ -5,6 +5,7 @@ import { MeetPermissions, MeetRecordingAccess, MeetRecordingConfig, + MeetRecordingLayout, MeetRoomAutoDeletionPolicy, MeetRoomConfig, MeetRoomDeletionPolicyWithMeeting, @@ -36,21 +37,11 @@ export const nonEmptySanitizedRoomId = (fieldName: string) => const RecordingAccessSchema: z.ZodType = z.nativeEnum(MeetRecordingAccess); -const RecordingConfigSchema: z.ZodType = z - .object({ - enabled: z.boolean(), - allowAccessTo: RecordingAccessSchema.optional() - }) - .refine( - (data) => { - // If recording is enabled, allowAccessTo must be provided - return !data.enabled || data.allowAccessTo !== undefined; - }, - { - message: 'allowAccessTo is required when recording is enabled', - path: ['allowAccessTo'] - } - ); +const RecordingConfigSchema: z.ZodType = z.object({ + enabled: z.boolean(), + layout: z.nativeEnum(MeetRecordingLayout).optional(), + allowAccessTo: RecordingAccessSchema.optional() +}); const ChatConfigSchema: z.ZodType = z.object({ enabled: z.boolean() @@ -119,16 +110,33 @@ const UpdateRoomConfigSchema: z.ZodType> = z /** * Schema for creating room config (applies defaults for missing fields) * Used when creating a new room - missing fields get default values + * + * IMPORTANT: Using functions in .default() to avoid shared mutable state. + * Each call creates a new object instance instead of reusing the same reference. */ const CreateRoomConfigSchema = z .object({ - recording: RecordingConfigSchema.optional().default({ enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }), - chat: ChatConfigSchema.optional().default({ enabled: true }), - virtualBackground: VirtualBackgroundConfigSchema.optional().default({ enabled: true }), - e2ee: E2EEConfigSchema.optional().default({ enabled: false }) + recording: RecordingConfigSchema.optional().default(() => ({ + enabled: true, + layout: MeetRecordingLayout.GRID, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER + })), + chat: ChatConfigSchema.optional().default(() => ({ enabled: true })), + virtualBackground: VirtualBackgroundConfigSchema.optional().default(() => ({ enabled: true })), + e2ee: E2EEConfigSchema.optional().default(() => ({ enabled: false })) // appearance: AppearanceConfigSchema, }) .transform((data) => { + // Apply default layout if not provided + if (data.recording.layout === undefined) { + data.recording.layout = MeetRecordingLayout.GRID; + } + + // Apply default allowAccessTo if not provided + if (data.recording.allowAccessTo === undefined) { + data.recording.allowAccessTo = MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER; + } + // Automatically disable recording when E2EE is enabled if (data.e2ee.enabled && data.recording.enabled) { data.recording = { @@ -169,10 +177,10 @@ export const RoomOptionsSchema: z.ZodType = z.object({ ) .optional(), autoDeletionPolicy: RoomAutoDeletionPolicySchema.optional() - .default({ + .default(() => ({ withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE - }) + })) .refine( (policy) => { return !policy || policy.withMeeting !== MeetRoomDeletionPolicyWithMeeting.FAIL; @@ -192,7 +200,11 @@ export const RoomOptionsSchema: z.ZodType = z.object({ } ), config: CreateRoomConfigSchema.optional().default({ - recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, + recording: { + enabled: true, + layout: MeetRecordingLayout.GRID, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER + }, chat: { enabled: true }, virtualBackground: { enabled: true }, e2ee: { enabled: false } diff --git a/meet-ce/backend/src/services/recording.service.ts b/meet-ce/backend/src/services/recording.service.ts index 235b1459..dee3b3ce 100644 --- a/meet-ce/backend/src/services/recording.service.ts +++ b/meet-ce/backend/src/services/recording.service.ts @@ -1,4 +1,10 @@ -import { MeetRecordingFilters, MeetRecordingInfo, MeetRecordingStatus } from '@openvidu-meet/typings'; +import { + MeetRecordingFilters, + MeetRecordingInfo, + MeetRecordingStatus, + MeetRoom, + MeetRoomConfig +} from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { EgressStatus, EncodedFileOutput, EncodedFileType, RoomCompositeOptions } from 'livekit-server-sdk'; import ms from 'ms'; @@ -58,7 +64,7 @@ export class RecordingService { if (!acquiredLock) throw errorRecordingAlreadyStarted(roomId); - await this.validateRoomForStartRecording(roomId); + const room = await this.validateRoomForStartRecording(roomId); // Manually send the recording signal to OpenVidu Components for avoiding missing event if timeout occurs // and the egress_started webhook is not received. @@ -100,7 +106,7 @@ export class RecordingService { const startRecordingPromise = (async (): Promise => { try { - const options = this.generateCompositeOptionsFromRequest(); + const options = this.generateCompositeOptionsFromRequest(room.config); const output = this.generateFileOutputFromRequest(roomId); const egressInfo = await this.livekitService.startRoomComposite(roomId, output, options); @@ -542,7 +548,14 @@ export class RecordingService { } } - protected async validateRoomForStartRecording(roomId: string): Promise { + /** + * Validates that a room exists and has participants before starting a recording. + * + * @param roomId + * @returns The MeetRoom object if validation passes. + * @throws Will throw an error if the room does not exist or has no participants. + */ + protected async validateRoomForStartRecording(roomId: string): Promise { const room = await this.roomRepository.findByRoomId(roomId); if (!room) throw errorRoomNotFound(roomId); @@ -550,6 +563,8 @@ export class RecordingService { const hasParticipants = await this.livekitService.roomHasParticipants(roomId); if (!hasParticipants) throw errorRoomHasNoParticipants(roomId); + + return room; } /** @@ -683,9 +698,15 @@ export class RecordingService { } } - protected generateCompositeOptionsFromRequest(layout = 'grid'): RoomCompositeOptions { + /** + * Generates composite options for recording based on the provided room configuration. + * + * @param roomConfig The room configuration + * @returns The generated RoomCompositeOptions object. + */ + protected generateCompositeOptionsFromRequest({ recording }: MeetRoomConfig): RoomCompositeOptions { return { - layout: layout + layout: recording.layout // customBaseUrl: customLayout, // audioOnly: false, // videoOnly: false diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index 783ec307..c0347877 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -13,6 +13,7 @@ import { } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { CreateOptions, Room } from 'livekit-server-sdk'; +import merge from 'lodash.merge'; import ms from 'ms'; import { uid as secureUid } from 'uid/secure'; import { uid } from 'uid/single'; @@ -33,6 +34,7 @@ import { LoggerService } from './logger.service.js'; import { RecordingService } from './recording.service.js'; import { RequestSessionService } from './request-session.service.js'; + /** * Service for managing OpenVidu Meet rooms. * @@ -131,11 +133,8 @@ export class RoomService { throw errorRoomActiveMeeting(roomId); } - // Merge the partial config with the existing config - room.config = { - ...room.config, - ...config - }; + // Merge existing config with new config (partial update) + room.config = merge({}, room.config, config); // Disable recording if E2EE is enabled if (room.config.e2ee.enabled && room.config.recording.enabled) { diff --git a/meet-ce/backend/tests/helpers/assertion-helpers.ts b/meet-ce/backend/tests/helpers/assertion-helpers.ts index 4184c897..c2b9d935 100644 --- a/meet-ce/backend/tests/helpers/assertion-helpers.ts +++ b/meet-ce/backend/tests/helpers/assertion-helpers.ts @@ -3,6 +3,7 @@ import { MeetingEndAction, MeetRecordingAccess, MeetRecordingInfo, + MeetRecordingLayout, MeetRecordingStatus, MeetRoom, MeetRoomAutoDeletionPolicy, @@ -155,6 +156,7 @@ export const expectValidRoom = ( expect(room.config).toEqual({ recording: { enabled: true, + layout: MeetRecordingLayout.GRID, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: true }, diff --git a/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts index d7827a35..de9e2997 100644 --- a/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts @@ -1,6 +1,7 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import { MeetRecordingAccess, + MeetRecordingLayout, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings } from '@openvidu-meet/typings'; @@ -60,6 +61,7 @@ describe('Room API Tests', () => { config: { recording: { enabled: false, + layout: MeetRecordingLayout.GRID, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: false }, @@ -95,7 +97,9 @@ describe('Room API Tests', () => { const expectedConfig = { recording: { - enabled: false + enabled: false, + layout: MeetRecordingLayout.GRID, // Default value + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER // Default value }, chat: { enabled: true }, // Default value virtualBackground: { enabled: true }, // Default value @@ -123,6 +127,7 @@ describe('Room API Tests', () => { const expectedConfig = { recording: { enabled: true, // Default value + layout: MeetRecordingLayout.GRID, // Default value allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER // Default value }, chat: { enabled: false }, diff --git a/meet-ce/backend/tests/integration/api/rooms/get-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-room-config.test.ts index 169fef1c..2a9b38a1 100644 --- a/meet-ce/backend/tests/integration/api/rooms/get-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/get-room-config.test.ts @@ -1,14 +1,15 @@ import { afterEach, beforeAll, describe, it } from '@jest/globals'; -import { MeetRecordingAccess } from '@openvidu-meet/typings'; +import { MeetRecordingAccess, MeetRecordingLayout } from '@openvidu-meet/typings'; +import { Response } from 'supertest'; import { expectSuccessRoomConfigResponse } from '../../../helpers/assertion-helpers.js'; import { deleteAllRooms, getRoomConfig, startTestServer } from '../../../helpers/request-helpers.js'; import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; -import { Response } from 'supertest'; describe('Room API Tests', () => { const DEFAULT_CONFIG = { recording: { enabled: true, + layout: MeetRecordingLayout.GRID, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: true }, @@ -40,6 +41,7 @@ describe('Room API Tests', () => { config: { recording: { enabled: true, + layout: MeetRecordingLayout.SPEAKER, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: true }, diff --git a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts index 1fad37a9..a00271c1 100644 --- a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; -import { MeetRecordingAccess } from '@openvidu-meet/typings'; +import { MeetRecordingAccess, MeetRecordingLayout } from '@openvidu-meet/typings'; import ms from 'ms'; import { expectSuccessRoomResponse, @@ -38,6 +38,7 @@ describe('Room API Tests', () => { config: { recording: { enabled: true, + layout: MeetRecordingLayout.SPEAKER, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: true }, diff --git a/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts new file mode 100644 index 00000000..91684c31 --- /dev/null +++ b/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts @@ -0,0 +1,94 @@ +import { afterAll, beforeAll, describe, it } from '@jest/globals'; +import { MeetRecordingAccess, MeetRecordingLayout } from '@openvidu-meet/typings'; +import { expectValidRoom } from '../../../helpers/assertion-helpers.js'; +import { createRoom, deleteAllRooms, startTestServer } from '../../../helpers/request-helpers.js'; + +describe('Room API Tests', () => { + beforeAll(async () => { + await startTestServer(); + }); + + afterAll(async () => { + await deleteAllRooms(); + }); + describe('Recording Layout Tests', () => { + it('Should create a room with default grid layout when layout is not specified', async () => { + const payload = { + roomName: 'Room with Default Layout', + config: { + recording: { + enabled: true + } + } + }; + + const room = await createRoom(payload); + + const expectedConfig = { + recording: { + enabled: true, + layout: MeetRecordingLayout.GRID, // Default value + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER + }, + chat: { enabled: true }, + virtualBackground: { enabled: true }, + e2ee: { enabled: false } + }; + expectValidRoom(room, 'Room with Default Layout', 'room_with_default_layout', expectedConfig); + }); + + it('Should create a room with speaker layout', async () => { + const payload = { + roomName: 'Speaker Layout Room', + config: { + recording: { + enabled: true, + layout: MeetRecordingLayout.SPEAKER, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER + } + } + }; + + const room = await createRoom(payload); + + const expectedConfig = { + recording: { + enabled: true, + layout: MeetRecordingLayout.SPEAKER, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER + }, + chat: { enabled: true }, + virtualBackground: { enabled: true }, + e2ee: { enabled: false } + }; + expectValidRoom(room, 'Speaker Layout Room', 'speaker_layout_room', expectedConfig); + }); + + it('Should create a room with single-speaker layout', async () => { + const payload = { + roomName: 'Single Speaker Layout Room', + config: { + recording: { + enabled: true, + layout: MeetRecordingLayout.SINGLE_SPEAKER, + allowAccessTo: MeetRecordingAccess.ADMIN + } + } + }; + + const room = await createRoom(payload); + + const expectedConfig = { + recording: { + enabled: true, + layout: MeetRecordingLayout.SINGLE_SPEAKER, + allowAccessTo: MeetRecordingAccess.ADMIN + }, + chat: { enabled: true }, + virtualBackground: { enabled: true }, + e2ee: { enabled: false } + }; + expectValidRoom(room, 'Single Speaker Layout Room', 'single_speaker_layout_room', expectedConfig); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts index 03f16816..04d7e378 100644 --- a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeAll, describe, expect, it, jest } from '@jest/globals'; -import { MeetRecordingAccess, MeetRoomConfig, MeetSignalType } from '@openvidu-meet/typings'; +import { MeetRecordingAccess, MeetRecordingLayout, MeetRoomConfig, MeetSignalType } from '@openvidu-meet/typings'; import { container } from '../../../../src/config/dependency-injector.config.js'; import { FrontendEventService } from '../../../../src/services/frontend-event.service.js'; import { @@ -61,7 +61,10 @@ describe('Room API Tests', () => { createdRoom.roomId, { roomId: createdRoom.roomId, - config: updatedConfig, + config: { + ...updatedConfig, + recording: { ...updatedConfig.recording, layout: MeetRecordingLayout.GRID } + }, timestamp: expect.any(Number) }, { @@ -76,7 +79,10 @@ describe('Room API Tests', () => { // Verify with a get request const getResponse = await getRoom(createdRoom.roomId); expect(getResponse.status).toBe(200); - expect(getResponse.body.config).toEqual(updatedConfig); + expect(getResponse.body.config).toEqual({ + ...updatedConfig, + recording: { ...updatedConfig.recording, layout: MeetRecordingLayout.GRID } // Layout remains unchanged + }); }); it('should allow partial config updates', async () => { @@ -86,7 +92,8 @@ describe('Room API Tests', () => { config: { recording: { enabled: true, - allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER + layout: MeetRecordingLayout.SPEAKER + // allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: true }, virtualBackground: { enabled: true }, @@ -112,7 +119,9 @@ describe('Room API Tests', () => { const expectedConfig: MeetRoomConfig = { recording: { - enabled: false + enabled: false, + layout: MeetRecordingLayout.SPEAKER, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: true }, virtualBackground: { enabled: true }, @@ -186,24 +195,5 @@ describe('Room API Tests', () => { expect(response.body.error).toContain('Unprocessable Entity'); expect(JSON.stringify(response.body.details)).toContain('recording.enabled'); }); - - it('should fail when recording is enabled but allowAccessTo is missing', async () => { - const createdRoom = await createRoom({ - roomName: 'missing-access' - }); - - const invalidConfig = { - recording: { - enabled: true // Missing allowAccessTo - }, - chat: { enabled: false }, - virtualBackground: { enabled: false } - }; - const response = await updateRoomConfig(createdRoom.roomId, invalidConfig); - - expect(response.status).toBe(422); - expect(response.body.error).toContain('Unprocessable Entity'); - expect(JSON.stringify(response.body.details)).toContain('recording.allowAccessTo'); - }); }); }); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.scss index 82b026aa..255d3b23 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.scss +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/selectable-card/selectable-card.component.scss @@ -10,6 +10,10 @@ min-height: 120px; display: flex; flex-direction: column; + height: -webkit-fill-available; + height: -moz-available; + height: fill-available; + margin-top: 0; &:hover:not(.no-hover):not(.selected) { @include design-tokens.ov-hover-lift(-2px); @@ -67,7 +71,7 @@ img { width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; display: block; @include design-tokens.ov-theme-transition; } @@ -125,6 +129,7 @@ color: var(--ov-meet-text-secondary); line-height: var(--ov-meet-line-height-normal); flex: 1; + text-align: center; } } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.html index e3962334..9288a10c 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.html @@ -3,7 +3,7 @@
video_library
-

Recording Config

+

Recording Configuration

Choose whether to enable recording capabilities for this room

diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.ts index b00655fc..93476691 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-config/recording-config.component.ts @@ -6,10 +6,10 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; +import { MeetRecordingAccess, MeetRoomOptions } from '@openvidu-meet/typings'; +import { Subject, takeUntil } from 'rxjs'; import { SelectableCardComponent, SelectableOption, SelectionEvent } from '../../../../../../components'; import { RoomWizardStateService } from '../../../../../../services'; -import { MeetRecordingAccess } from '@openvidu-meet/typings'; -import { Subject, takeUntil } from 'rxjs'; interface RecordingAccessOption { value: MeetRecordingAccess; @@ -88,7 +88,7 @@ export class RecordingConfigComponent implements OnDestroy { private saveFormData(formValue: any) { const enabled = formValue.recordingEnabled === 'enabled'; - const stepData: any = { + const stepData: Partial = { config: { recording: { enabled, diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html index b23fcc3e..833ddc12 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/recording-layout/recording-layout.component.html @@ -13,10 +13,10 @@
- @for (option of layoutOptions; track option.id) { + @for (option of layoutOptions(); track option.id) { = computed(() => { + return [ + { + id: 'grid', + title: 'Grid Layout', + description: 'Display participants in an equal-size grid', + imageUrl: `./assets/layouts/grid_${this.theme()}.png` + }, + { + id: 'speaker', + title: 'Speaker Layout', + description: 'Highlight the active speaker with other participants below', + imageUrl: `./assets/layouts/speaker_${this.theme()}.png`, + isPro: false, + disabled: false + // recommended: true + }, + { + id: 'single-speaker', + title: 'Single Speaker', + description: 'Show only the active speaker in the recording', + imageUrl: `./assets/layouts/single_speaker_${this.theme()}.png`, + isPro: false, + disabled: false + } + ]; + }); - private destroy$ = new Subject(); + private formValues: Signal; + selectedOption: Signal; - constructor(private wizardService: RoomWizardStateService) { + constructor() { const currentStep = this.wizardService.currentStep(); this.layoutForm = currentStep!.formGroup; - this.layoutForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + // Initialize formValues signal after layoutForm is created + this.formValues = toSignal(this.layoutForm.valueChanges, { + initialValue: this.layoutForm.value + }); + + // Initialize selectedOption computed signal + this.selectedOption = computed(() => { + const formValue = this.formValues(); + return formValue?.layout || MeetRecordingLayout.GRID; + }); + + // Subscribe to form changes to save data (using takeUntilDestroyed for automatic cleanup) + this.layoutForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { this.saveFormData(value); }); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - private saveFormData(formValue: any) { - // Note: Recording layout type is not part of MeetRoomOptions - // For now, just keep the form state + const roomOptions = this.wizardService.roomOptions(); + if (roomOptions.config?.recording) { + roomOptions.config.recording.layout = formValue.layout; + this.wizardService.updateStepData('recordingLayout', formValue); + } } onOptionSelect(event: SelectionEvent): void { this.layoutForm.patchValue({ - layoutType: event.optionId + layout: event.optionId }); } - - get selectedOption(): string { - return this.layoutForm.value.layoutType || 'grid'; - } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts index a0644d63..5b8bccff 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/wizard-state.service.ts @@ -1,18 +1,20 @@ import { computed, Injectable, signal } from '@angular/core'; import { AbstractControl, FormBuilder, ValidationErrors, Validators } from '@angular/forms'; -import { WizardNavigationConfig, WizardStep } from '../models'; import { MeetRecordingAccess, + MeetRecordingLayout, MeetRoomConfig, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomOptions } from '@openvidu-meet/typings'; +import { WizardNavigationConfig, WizardStep } from '../models'; // Default room config following the app's defaults const DEFAULT_CONFIG: MeetRoomConfig = { recording: { enabled: true, + layout: MeetRecordingLayout.GRID, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, chat: { enabled: true }, @@ -178,7 +180,7 @@ export class RoomWizardStateService { isActive: false, isVisible: false, // Initially hidden, will be shown based on recording settings formGroup: this.formBuilder.group({ - layoutType: 'grid' + layout: initialRoomOptions.config?.recording?.layout || MeetRecordingLayout.GRID }) }, { @@ -232,6 +234,7 @@ export class RoomWizardStateService { break; case 'recording': + case 'recordingLayout': updatedOptions = { ...currentOptions, config: { @@ -244,7 +247,6 @@ export class RoomWizardStateService { }; break; case 'recordingTrigger': - case 'recordingLayout': // These steps don't update room options updatedOptions = { ...currentOptions }; break; @@ -291,17 +293,23 @@ export class RoomWizardStateService { private updateStepsVisibility(): void { const currentSteps = this._steps(); const currentOptions = this._roomOptions(); - // TODO: Uncomment when recording config is fully implemented - const recordingEnabled = false; // currentOptions.config?.recording.enabled ?? false; + + const recordingEnabled = currentOptions.config?.recording?.enabled ?? false; // Update recording steps visibility based on recordingEnabled const updatedSteps = currentSteps.map((step) => { - if (step.id === 'recordingTrigger' || step.id === 'recordingLayout') { + if (step.id === 'recordingLayout') { return { ...step, isVisible: recordingEnabled // Only show if recording is enabled }; } + if (step.id === 'recordingTrigger') { + return { + ...step, + isVisible: false // TODO: Change to true when recording trigger config is implemented + }; + } return step; }); this._steps.set(updatedSteps); diff --git a/meet-ce/frontend/src/assets/layouts/grid.png b/meet-ce/frontend/src/assets/layouts/grid.png deleted file mode 100644 index c33fba553ce3ccf1ea4c61534cad42d5bc869366..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9788 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 Dxi1md literal 0 HcmV?d00001 diff --git a/meet-ce/frontend/src/assets/layouts/grid_light.png b/meet-ce/frontend/src/assets/layouts/grid_light.png new file mode 100644 index 0000000000000000000000000000000000000000..be3e484682cfc61fa6f51a9bb725af31d740e05e GIT binary patch literal 1595 zcmV-B2E_S^P)deX0FRH49UUF4tgLc!a>&TYIyyR5R#wXs za=-up010$bPE!D%fMCBM;6VFCcU+zT00pZ_L_t(|+U=cx^f7v#k zv`Cg^#1UcAvNM@D6J_<~bP_n(f!ol~(9qD(P!lONi+5^X6pvd-bRey#rIdX7{6Xw`$=&#pTJuhB$Om|P1ZDsY#vfp}^#PJ9_VY>CKSB3BQ zM*WmLydCGx%l<(q4_juw^(}SSG`jbed4UKV%;SVC&@?a4OhlbYYWR{1B2X-?cH}6Q zRGGHU-l|Fi(b)o-NYLVO+#pwOGCwP%IL|IY{%4`QdzkZS1eS?<#(0`tfPOOMpf?vf za7-d(6+2)#r}Y6#y$NBdU|&+jN((PmzL9OhXXl^RLazGGu}%2w{L^~qRlA$PKG?rG zyEiTxTfv@88ngQV(8q`6vnLbZlHG#tJl>JxvL};3t|2=@8b1t^-6Q^;x{z(g9w@zb z+l$bMJ;ohGjy-SEfPG&i1J7X(Zfnx2 z?zEk${$#O-ZR54-ciLV2+J2|l>^bAsCop;Tyoqh~d|!_xftX`cB%jJ2nvG3q6!!tP zls%c)zpon33A{rz)>{X%?D>ojcN3FL8qI+n?xLzWaKv4q=^WTcP>3-Q*z?F_0yLaQ zT7t8Go2Pg_lT7@|qz$`8@SE0m40en7b8FIq-NNnOLlm={M-zv5A$vmKgT_L33;SfO zx}2TD)46yVJKWO&?_zd?=VN3MyQ9pzv1{1r;Jv3UVNXZz8tEeTRQA0>C}59ACh=1C z7!QU(^5@y$Cy$+Jwg*80_^i1_Qmxf<%6x zg3qor90wiC+5{XEmrRP;Q*6iBqDPDgcyO9|Ci9KUo+8A>(#!7hu}qlkNot;o@bx4v zFiDKxG}<}XPinCLr|e1zF%@WjYO)l0h=s>qpQ)YH7nq1@b|vq5SW}<~=y_)niacBZ zw5nv6Aj^uwR+|LqI`$brGIVsa2piW@;nm_hoA9^T6Zp1cQCB zSF?L(Yz2EVY0mCa9G1hLObCd+CA$yzZ+J^JnOY)~Oq#J%NHGS>Wk+~NrZ!^rnwac? z($U{6Q)^*&j?FW*ftjhr$kZ0Hhxi4VT5=ncR_rls*}o!F+lt-&E16o)&eYn*nc96E zHh}wlUz;?|)Y|$?Es;+qE$1`j?C(!?nOaIFE$5LwR%dFHNo<>`y-$Gy0c5!B$%JCN zy8HMi1MAbvEc@QXnPg(yu=~xKydz$wHu$Yc3wDq2IID8S>=ton(jrqE_nzBW+ed8S?L8J&;OEBKewzB07TKwQ+nR`h%ttY|1WN%atO*=F+G&D3+q+j&E@$?!SIo90q!7sM zBP}&uMX-fJp?iCK`}_L`2M1t*Kp-kAD%RK6x3{-JL{wBXGcyy!%gf6*H#e7;mv?q{ zuvjb?7nhQf((39eA0OZE-@g|Z7kPMihKGl_xw%J1Mm9D!va+&3kdu=$BO`-@g9F@^ zmzM|SKp75)|NHmv&!0cQOF=mX^Sj02OE)WNK+?feA}XOK)v$fnma6Fc}#c3paupG7NB>~ z3-AR41hiladIYkgqocjOyx$4wwrH3fS4% z8yXrw#K*@+OiT4NQR@xTT|`1Lh1M!H}One-0i_z-DA*1S|lXhK2?p0XmU(#694- z-*kVbp#!;hd3rH^%yzzbY9%tk?jASiNrH2=w2ixOPi%KC_~DT#-+v|XufAS>7%hr( zBcGYFc+W%-?d4!|!p(_y9KBFmSz$Ibl&E=mF+q1aQBe8KiN;Vy{B*=qSAd<481MXG zcdjiX`TZM5%8Q+q6uSq|f!b&vDG}DQot43#dG8QR6j!SYK>;EhL`R2*|JG(&&=py6 zZ;(j-tHZsGzblK2gS|aXUyITcHNGU!}W{SD~-n#0qi6KEAwxS|jJ=KL@BHdqV&!>#@uRtJ_Cn^fEy52L}DTa0w zva~%t`r0x2V-Xh5sDvY#H$rJbBB`H1S*h6x)E_OES&-=#M@>D7rp_tzej!^NQonq# zs@+ZDqN|{N;Qb8g37c1$uwRK;9MZU?SD$;n8Jc?e-OYc|U-VO8>-?`|N^Y%enc&;^ zpp@Jn1jiyux3(OeHXTCJ;n^peiOI=wCCd!jx3r}8Wu21>OFEEtOPv{ddcRj*l@R@R zha!jBu0hAZK!<`=U>lk&zB4^LMj54GWNh5297i9KP+~VhOj^mA~&Cjq+~DU>KYx;guQqwDOw5{eb9KQ%&+?F?-3by5jJs z4x7PWX;knW78otZgg$piqoP$&*%C?FQ?l-=3g*uv_n9qzF+PwqTTnJqWR9P`XZ0rV zK3-8|uA1mT+ZE}9#8yI~*usS5k<;?BGRziPq7SF0z@2P-sxcA9i_od$NvY|d{lP!& z!|KU{`gl*tBI{PF4RF2MDk){^DBqrXIq+s{yixKov|5*c-+#BCARvSdsnFQ3u ztO{%Hx>OfGULRKs>6>)68l&^IRYd%9*<>QFDgJ>j!)JU$rmA```RzB(GY#CCl1=5$ zL+R~4>b|un-b%VlzIisAJs}-%ZKU@f(b!P2%C4P6rUQX-=?G{vaKkxqbpLUpLMMQ9B0lcd(1u<|Th zlB!ukF9>>Iv!X$?h(hs!qdITFx%t$Eybn>iA%=cG*zH_&z{a5;#XK-7;WiG-C zJ#hc#Ab`fVA#0x*F=>!?&=2F^eBU$60l&8)+iG3!m7(!gg)`Bq4R-nX@x9cl?iwU0Ub+ZaNt)J)WL1*ntqE0eQ zC{??ziI%G>Muxg=P4m~UNm`dYpWrE2HML7u5eaDjD5DzWP$EJ4fAMKo^ex}pr$`pO zJz|-nKuI>6d%LF#LQiq|6tCfw{UvQgomgh2POFEX23J$#QO4HHDACQeiCk>2l{2RL z%vx&8LuXSHW??4$sW6%N*{LSD)E;`P7Cij!~C6l=C;KN4{(im0}6&W^nRcAwr7V`6J_b~ z_F<6%o+W&d1kt&yy##Ct(IL>B_ig;&brgTIR#^m7$)+HoC_!>EN)PT$a#by(q7afR zBE}}&dY?W+uYcX$sux3zvqopTPtF4tVYQ5xw;DHxY+FSShlakLw@c-2POB@@T&>#A zPTOc0U`NO>!*eU}O9}Zbwi1l-yegg4#5#?sA-pkvdMkH0^|I*jhy#a>J*?=Ae{(eO zBtJq=@LC1FHnyV%4YsbEWqjKYkvFF~V z#7JSRNB_YRwyu*)E#kYYxw*8TQ$0-4?3lhPEIz6w_8U58fm~;t3Obop9`$6p(Oa=w z9$%G-!M2nU(H!g$7ashG-xu81dixNDL=Z;2-E zv0u5;xXs<}}P zg`b-x8l&}>&4tr+IO8AsL?fkTQZd5)Z zZ+*Qw#Pdw6{wir1p3;S>XvxE>)8w>0Z5GX+oY?m>^>h1vMM3NKM?`&Zb5co3dB%P0 zN6+G<%Ba^mr@un{nh|r(q15+|(hHX~yYmn?ifc;C@_SmB{V9C1E*~)gH|n|q5RRF&FaC*45nCZeF z2GJ%VA>S*J^BH~e?JmuTmau$^J^T3c2Sk#uWAb67ta|5`Ap#G!X7&a@99_PmXwb

Puf2B@{Uex&u)C!_ujiQZ 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 literal 0 HcmV?d00001 diff --git a/meet-ce/frontend/src/assets/layouts/single_speaker_light.png b/meet-ce/frontend/src/assets/layouts/single_speaker_light.png new file mode 100644 index 0000000000000000000000000000000000000000..b770428c4ab727e62b96c5be720ed6db9a0feb4e GIT binary patch literal 1162 zcmV;51alo70012w9adIWIyySY$jGd$ta5U4kB^TzIXUgf zsAvEH00?waPE!DYU?890zd(Sq=oJ6}1N%utK~#9!?Vaszqc9LfF~-0hf6@1U*|pPc zqizDhL|R{0KL6UJ(#hn;hG{e&I-O3Z)9L&l^fIcNjElbJt!5Blj4JgX4H9OaR86MY zATf<4=Gj{<0|8pAo6K+$b$c=mS{dM+kDh^X;38M^MC=#9P^9Gt+MJNrz&sG5r`Pf` zr+SiqX*R6BJ6xw|3V+j8DjNeP)DH{S;PJv`DH)tbPN0G+AhFAjo#mSCQJi2 z`-TR3;AYtk4SQs6q1kzWcVBv0-jP1;c_mzaox1|j4M$?fvE=u98Ede-@2*e{quz4Y z29OV1gO6;lue)nN!deS2N&d3?47*muTg+G8N619|c3m-H-engxVdj z&APjW>wcVfOYI&H{tD#o8xRhberk6Q*n_2?+C2w8?7k}QYl3}62e4m9X2-Zb5*oOE z&xM!pOdWyVJ!?vQqTay$>TNMZd}!B%7WxEvgUY23ck{&7Mt>&hIwZu}7_A`Tn(2!r zq5yYdQ~k&v>N!A-xO?}dEKYEvk)Gb)tjFj-fArgCKkez)_lU$jBKKZ!B2v41x3{Py zd+t6#IEscBKs>8^*e~t94bSNwUPao+EYIkE=6Wm3dp>u6Y9ZMG;d6KAmJtg?Hutmb zbuW-W<}QFgeP6}pUJ!*1h*a(XN^WSG+>3f%5eP)?0u-&Lj>mliY+3Kj)41mpl&z0@3|**6^1>>X6NZm z7@M7AxaYobPuRU-pnu)m&IabCCdDQvN@qsQ&CUm&F%GQ`ss(hb^RSPCv5*)3@B=wh zZ>`6t1}uzcOq@LwK*6xi^VY}eiHSAxIw$5BZ8$5Ryyg>6qB_jxFC$LnK`XGWHhRL@ zEnm@`fONlUUJ5=Jz&sM@Ypo0bvlPD8gfUZ1W@O4w{nFz^a9~*~p1S;&jTK=CZKu=e cbUG#c0Y*yV7|<0n@c;k-07*qoM6N<$g6-rlrT_o{ literal 0 HcmV?d00001 diff --git a/meet-ce/frontend/src/assets/layouts/speaker.png b/meet-ce/frontend/src/assets/layouts/speaker.png deleted file mode 100644 index 52591049dc83d0f0cb8cf224c4aaac0e1902dc8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9808 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 qIT`uymXi<K)>Iz?6hzI00m@8 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 literal 0 HcmV?d00001 diff --git a/meet-ce/typings/src/recording.model.ts b/meet-ce/typings/src/recording.model.ts index 7d5a0cee..8a552eb3 100644 --- a/meet-ce/typings/src/recording.model.ts +++ b/meet-ce/typings/src/recording.model.ts @@ -9,6 +9,14 @@ export enum MeetRecordingStatus { ABORTED = 'aborted', LIMIT_REACHED = 'limit_reached' } +export enum MeetRecordingLayout { + GRID = 'grid', + SPEAKER = 'speaker', + SINGLE_SPEAKER = 'single-speaker', + // GRID_LIGHT = 'grid-light', + // SPEAKER_LIGHT = 'speaker-light', + // SINGLE_SPEAKER_LIGHT = 'single-speaker-light' +} // export enum MeetRecordingOutputMode { // COMPOSED = 'composed', @@ -22,6 +30,7 @@ export interface MeetRecordingInfo { roomId: string; roomName: string; // outputMode: MeetRecordingOutputMode; + // layout: MeetRecordingLayout; status: MeetRecordingStatus; filename?: string; startDate?: number; diff --git a/meet-ce/typings/src/room-config.ts b/meet-ce/typings/src/room-config.ts index 448643bc..c8ae2104 100644 --- a/meet-ce/typings/src/room-config.ts +++ b/meet-ce/typings/src/room-config.ts @@ -1,3 +1,5 @@ +import { MeetRecordingLayout } from './recording.model'; + /** * Interface representing the config for a room. */ @@ -14,6 +16,7 @@ export interface MeetRoomConfig { */ export interface MeetRecordingConfig { enabled: boolean; + layout?: MeetRecordingLayout; allowAccessTo?: MeetRecordingAccess; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3857bb52..8afb8c65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,9 @@ importers: livekit-server-sdk: specifier: 2.13.3 version: 2.13.3 + lodash.merge: + specifier: 4.6.2 + version: 4.6.2 mongoose: specifier: 8.19.4 version: 8.19.4(socks@2.8.7) @@ -162,6 +165,9 @@ importers: '@types/jest': specifier: 29.5.14 version: 29.5.14 + '@types/lodash.merge': + specifier: 4.6.9 + version: 4.6.9 '@types/ms': specifier: 2.1.0 version: 2.1.0 @@ -3874,6 +3880,12 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/lodash.merge@4.6.9': + resolution: {integrity: sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==} + + '@types/lodash@4.17.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + '@types/lru-cache@4.1.3': resolution: {integrity: sha512-QjCOmf5kYwekcsfEKhcEHMK8/SvgnneuSDXFERBuC/DPca2KJIO/gpChTsVb35BoWLBpEAJWz1GFVEArSdtKtw==} @@ -13862,6 +13874,12 @@ snapshots: '@types/json5@0.0.29': {} + '@types/lodash.merge@4.6.9': + dependencies: + '@types/lodash': 4.17.21 + + '@types/lodash@4.17.21': {} + '@types/lru-cache@4.1.3': {} '@types/luxon@3.7.1': {}