backend: Adds layout property to recording info

Adds the 'layout' property to recording information.

This allows tracking the layout used during a recording, enhancing recording metadata.

Updates recording schema and adds layout information to API responses.
This commit is contained in:
Carlos Santos 2026-01-14 18:46:38 +01:00
parent a1acc9ba22
commit 4ecd086f21
10 changed files with 90 additions and 11 deletions

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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:

View File

@ -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.

View File

@ -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}

View File

@ -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<MeetRecordingDocument>(
enum: Object.values(MeetRecordingStatus),
required: true
},
layout: {
type: String,
enum: Object.values(MeetRecordingLayout),
required: false
},
filename: {
type: String,
required: false

View File

@ -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) {

View File

@ -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);
}
});
});

View File

@ -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;