backend: Allows overriding recording layout
Enables users to override the default recording layout for a room when starting a recording. This allows customization of the recording appearance on a per-recording basis, instead of being tied solely to the room's configuration.
This commit is contained in:
parent
2fe720c90b
commit
1add921ce0
@ -9,3 +9,22 @@ content:
|
||||
type: string
|
||||
description: The unique identifier of the room to record.
|
||||
example: 'room-123'
|
||||
config:
|
||||
type: object
|
||||
description: |
|
||||
Optional configuration to override the room's recording configuration for this specific recording.
|
||||
If not provided, the recording will use the configuration defined in the room's config.
|
||||
properties:
|
||||
layout:
|
||||
type: string
|
||||
enum:
|
||||
- grid
|
||||
- speaker
|
||||
- single-speaker
|
||||
example: speaker
|
||||
description: |
|
||||
Defines the layout of the recording. This will override the room's default recording layout.
|
||||
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.
|
||||
|
||||
@ -8,6 +8,7 @@ content:
|
||||
roomId: 'room-123'
|
||||
roomName: 'room'
|
||||
status: 'active'
|
||||
layout: 'speaker'
|
||||
filename: 'room-123--XX445.mp4'
|
||||
startDate: 1600000000000
|
||||
headers:
|
||||
|
||||
@ -14,6 +14,7 @@ content:
|
||||
roomId: 'room-123'
|
||||
roomName: 'room'
|
||||
status: 'ending'
|
||||
layout: 'speaker'
|
||||
filename: 'room-123--XX445.mp4'
|
||||
startDate: 1600000000000
|
||||
details: 'End reason: StopEgress API'
|
||||
|
||||
@ -4,6 +4,14 @@
|
||||
summary: Start a recording
|
||||
description: >
|
||||
Start a new recording for an OpenVidu Meet room with the specified room ID.
|
||||
|
||||
|
||||
By default, the recording will use the configuration defined in the room's settings.
|
||||
However, you can optionally provide a configuration override in the request body to customize
|
||||
the recording settings (e.g., layout) for this specific recording session.
|
||||
|
||||
|
||||
If a configuration override is provided, those values will take precedence over the room's configuration.
|
||||
tags:
|
||||
- OpenVidu Meet - Recordings
|
||||
security:
|
||||
|
||||
@ -18,11 +18,11 @@ import { getBaseUrl } from '../utils/url.utils.js';
|
||||
export const startRecording = async (req: Request, res: Response) => {
|
||||
const logger = container.get(LoggerService);
|
||||
const recordingService = container.get(RecordingService);
|
||||
const { roomId } = req.body;
|
||||
const { roomId, config } = req.body;
|
||||
logger.info(`Starting recording in room '${roomId}'`);
|
||||
|
||||
try {
|
||||
const recordingInfo = await recordingService.startRecording(roomId);
|
||||
const recordingInfo = await recordingService.startRecording(roomId, config);
|
||||
res.setHeader(
|
||||
'Location',
|
||||
`${getBaseUrl()}${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingInfo.recordingId}`
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MeetRecordingFilters, MeetRecordingStatus } from '@openvidu-meet/typings';
|
||||
import { MeetRecordingFilters, MeetRecordingLayout, MeetRecordingStatus } from '@openvidu-meet/typings';
|
||||
import { z } from 'zod';
|
||||
import { nonEmptySanitizedRoomId } from './room.schema.js';
|
||||
|
||||
@ -50,7 +50,10 @@ export const nonEmptySanitizedRecordingId = (fieldName: string) =>
|
||||
);
|
||||
|
||||
export const StartRecordingReqSchema = z.object({
|
||||
roomId: nonEmptySanitizedRoomId('roomId')
|
||||
roomId: nonEmptySanitizedRoomId('roomId'),
|
||||
config: z.object({
|
||||
layout: z.nativeEnum(MeetRecordingLayout).optional()
|
||||
}).optional()
|
||||
});
|
||||
|
||||
export const RecordingFiltersSchema: z.ZodType<MeetRecordingFilters> = z.object({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
MeetRecordingFilters,
|
||||
MeetRecordingInfo,
|
||||
MeetRecordingLayout,
|
||||
MeetRecordingStatus,
|
||||
MeetRoom,
|
||||
MeetRoomConfig
|
||||
@ -51,7 +52,10 @@ export class RecordingService {
|
||||
@inject(LoggerService) protected logger: LoggerService
|
||||
) {}
|
||||
|
||||
async startRecording(roomId: string): Promise<MeetRecordingInfo> {
|
||||
async startRecording(
|
||||
roomId: string,
|
||||
configOverride?: { layout?: MeetRecordingLayout }
|
||||
): Promise<MeetRecordingInfo> {
|
||||
let acquiredLock: RedisLock | null = null;
|
||||
let eventListener!: (info: Record<string, unknown>) => void;
|
||||
let recordingId = '';
|
||||
@ -106,7 +110,7 @@ export class RecordingService {
|
||||
|
||||
const startRecordingPromise = (async (): Promise<MeetRecordingInfo> => {
|
||||
try {
|
||||
const options = this.generateCompositeOptionsFromRequest(room.config);
|
||||
const options = this.generateCompositeOptionsFromRequest(room.config, configOverride);
|
||||
const output = this.generateFileOutputFromRequest(roomId);
|
||||
const egressInfo = await this.livekitService.startRoomComposite(roomId, output, options);
|
||||
|
||||
@ -700,13 +704,20 @@ export class RecordingService {
|
||||
|
||||
/**
|
||||
* Generates composite options for recording based on the provided room configuration.
|
||||
* If configOverride is provided, its values will take precedence over room configuration.
|
||||
*
|
||||
* @param roomConfig The room configuration
|
||||
* @param configOverride Optional configuration override from the request
|
||||
* @returns The generated RoomCompositeOptions object.
|
||||
*/
|
||||
protected generateCompositeOptionsFromRequest({ recording }: MeetRoomConfig): RoomCompositeOptions {
|
||||
protected generateCompositeOptionsFromRequest(
|
||||
roomConfig: MeetRoomConfig,
|
||||
configOverride?: { layout?: MeetRecordingLayout }
|
||||
): RoomCompositeOptions {
|
||||
const roomRecordingConfig = roomConfig.recording;
|
||||
const layout = configOverride?.layout ?? roomRecordingConfig.layout;
|
||||
return {
|
||||
layout: recording.layout
|
||||
layout
|
||||
// customBaseUrl: customLayout,
|
||||
// audioOnly: false,
|
||||
// videoOnly: false
|
||||
|
||||
@ -588,13 +588,19 @@ export const endMeeting = async (roomId: string, moderatorToken: string) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const startRecording = async (roomId: string) => {
|
||||
export const startRecording = async (roomId: string, config?: { layout?: string }) => {
|
||||
checkAppIsRunning();
|
||||
|
||||
const body: { roomId: string; config?: { layout?: string } } = { roomId };
|
||||
|
||||
if (config) {
|
||||
body.config = config;
|
||||
}
|
||||
|
||||
return await request(app)
|
||||
.post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
||||
.send({ roomId });
|
||||
.send(body);
|
||||
};
|
||||
|
||||
export const stopRecording = async (recordingId: string) => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from '@jest/globals';
|
||||
import { MeetRoom } from '@openvidu-meet/typings';
|
||||
import { MeetRecordingLayout, MeetRoom } from '@openvidu-meet/typings';
|
||||
import { container } from '../../../../src/config/dependency-injector.config.js';
|
||||
import { setInternalConfig } from '../../../../src/config/internal-config.js';
|
||||
import { errorRoomNotFound } from '../../../../src/models/error.model.js';
|
||||
@ -207,4 +207,77 @@ describe('Recording API Tests', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Start Recording with Config Override', () => {
|
||||
beforeAll(async () => {
|
||||
// Create a room and join a participant
|
||||
context = await setupMultiRoomTestContext(1, true);
|
||||
({ room } = context.getRoomByIndex(0)!);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await disconnectFakeParticipants();
|
||||
await stopAllRecordings();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnectFakeParticipants();
|
||||
await Promise.all([deleteAllRooms(), deleteAllRecordings()]);
|
||||
context = null;
|
||||
});
|
||||
|
||||
it('should start recording with default room layout when no config override is provided', async () => {
|
||||
const response = await startRecording(room.roomId);
|
||||
const recordingId = response.body.recordingId;
|
||||
|
||||
expectValidStartRecordingResponse(response, room.roomId, room.roomName);
|
||||
// Verify the recording uses the room's default layout (grid)
|
||||
expect(response.body.layout).toBe(MeetRecordingLayout.GRID);
|
||||
|
||||
const stopResponse = await stopRecording(recordingId);
|
||||
expectValidStopRecordingResponse(stopResponse, recordingId, room.roomId, room.roomName);
|
||||
});
|
||||
|
||||
it('should override room layout when config with layout is provided', async () => {
|
||||
const response = await startRecording(room.roomId, { layout: MeetRecordingLayout.SPEAKER });
|
||||
const recordingId = response.body.recordingId;
|
||||
|
||||
expectValidStartRecordingResponse(response, room.roomId, room.roomName);
|
||||
// Verify the recording uses the overridden layout
|
||||
expect(response.body.layout).toBe(MeetRecordingLayout.SPEAKER);
|
||||
|
||||
const stopResponse = await stopRecording(recordingId);
|
||||
expectValidStopRecordingResponse(stopResponse, recordingId, room.roomId, room.roomName);
|
||||
});
|
||||
|
||||
it('should override room layout with single-speaker layout', async () => {
|
||||
const response = await startRecording(room.roomId, { layout: MeetRecordingLayout.SINGLE_SPEAKER });
|
||||
const recordingId = response.body.recordingId;
|
||||
|
||||
expectValidStartRecordingResponse(response, room.roomId, room.roomName);
|
||||
// Verify the recording uses the overridden layout
|
||||
expect(response.body.layout).toBe(MeetRecordingLayout.SINGLE_SPEAKER);
|
||||
|
||||
const stopResponse = await stopRecording(recordingId);
|
||||
expectValidStopRecordingResponse(stopResponse, recordingId, room.roomId, room.roomName);
|
||||
});
|
||||
|
||||
it('should reject invalid layout in config override', async () => {
|
||||
const response = await startRecording(room.roomId, { layout: 'invalid-layout' });
|
||||
|
||||
expectValidationError(response, 'config.layout', 'Invalid enum value');
|
||||
});
|
||||
|
||||
it('should accept empty config object and use room defaults', async () => {
|
||||
const response = await startRecording(room.roomId, {});
|
||||
const recordingId = response.body.recordingId;
|
||||
|
||||
expectValidStartRecordingResponse(response, room.roomId, room.roomName);
|
||||
// Verify the recording uses the room's default layout
|
||||
expect(response.body.layout).toBe(MeetRecordingLayout.GRID);
|
||||
|
||||
const stopResponse = await stopRecording(recordingId);
|
||||
expectValidStopRecordingResponse(stopResponse, recordingId, room.roomId, room.roomName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user