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 7db0b12c..6fcb7a69 100644
--- a/meet-ce/backend/openapi/components/responses/success-get-room.yaml
+++ b/meet-ce/backend/openapi/components/responses/success-get-room.yaml
@@ -26,6 +26,8 @@ content:
enabled: true
e2ee:
enabled: false
+ captions:
+ enabled: false
roles:
moderator:
permissions:
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 9872b5b8..fb54e370 100644
--- a/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml
+++ b/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml
@@ -35,6 +35,8 @@ content:
enabled: true
e2ee:
enabled: false
+ captions:
+ enabled: false
roles:
moderator:
permissions:
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 f9d599a6..9c794afa 100644
--- a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml
+++ b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml
@@ -13,6 +13,9 @@ MeetRoomConfig:
e2ee:
$ref: '#/MeetE2EEConfig'
description: Config for End-to-End Encryption (E2EE) in the room.
+ captions:
+ $ref: '#/MeetCaptionsConfig'
+ description: Config for live captions in the room.
MeetChatConfig:
type: object
properties:
@@ -80,3 +83,13 @@ MeetE2EEConfig:
If true, the room will have End-to-End Encryption (E2EE) enabled.
This ensures that the media streams are encrypted from the sender to the receiver, providing enhanced privacy and security for the participants.
**Enabling E2EE will disable the recording feature for the room**.
+MeetCaptionsConfig:
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ default: false
+ example: false
+ description: >
+ If true, the room will have live captions enabled.
+ This allows participants to see real-time captions of the all participants' speech during the meeting.
diff --git a/meet-ce/backend/src/config/internal-config.ts b/meet-ce/backend/src/config/internal-config.ts
index d7b95776..cf4e709c 100644
--- a/meet-ce/backend/src/config/internal-config.ts
+++ b/meet-ce/backend/src/config/internal-config.ts
@@ -49,6 +49,8 @@ export const INTERNAL_CONFIG = {
PARTICIPANT_MAX_CONCURRENT_NAME_REQUESTS: '20', // Maximum number of request by the same name at the same time allowed
PARTICIPANT_NAME_RESERVATION_TTL: '12h' as StringValue, // Time-to-live for participant name reservations
+ CAPTIONS_AGENT_NAME: 'agent-meet-captions',
+
// MongoDB Schema Versions
// These define the current schema version for each collection
// Increment when making breaking changes to the schema structure
diff --git a/meet-ce/backend/src/environment.ts b/meet-ce/backend/src/environment.ts
index d1ec064d..43fcd499 100644
--- a/meet-ce/backend/src/environment.ts
+++ b/meet-ce/backend/src/environment.ts
@@ -85,7 +85,7 @@ export const MEET_ENV = {
ENABLED_MODULES: process.env.ENABLED_MODULES ?? '',
// Agent Speech Processing configuration
- AGENT_SPEECH_PROCESSING_NAME: process.env.MEET_AGENT_SPEECH_PROCESSING_NAME || '',
+ CAPTIONS_ENABLED: process.env.MEET_CAPTIONS || 'true',
};
export function checkModuleEnabled() {
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 4713e0b1..c09cdf0d 100644
--- a/meet-ce/backend/src/models/mongoose-schemas/room.schema.ts
+++ b/meet-ce/backend/src/models/mongoose-schemas/room.schema.ts
@@ -105,6 +105,20 @@ const MeetE2EEConfigSchema = new Schema(
{ _id: false }
);
+/**
+ * Mongoose schema for MeetRoom captions configuration.
+ */
+const MeetCaptionsConfigSchema = new Schema(
+ {
+ enabled: {
+ type: Boolean,
+ required: true,
+ default: false
+ }
+ },
+ { _id: false }
+);
+
/**
* Sub-schema for room theme configuration.
*/
@@ -181,6 +195,11 @@ const MeetRoomConfigSchema = new Schema(
type: MeetE2EEConfigSchema,
required: true,
default: { enabled: false }
+ },
+ captions: {
+ type: MeetCaptionsConfigSchema,
+ required: true,
+ default: { enabled: false }
}
},
{ _id: false }
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 804bbaee..06a7b998 100644
--- a/meet-ce/backend/src/models/zod-schemas/room.schema.ts
+++ b/meet-ce/backend/src/models/zod-schemas/room.schema.ts
@@ -7,6 +7,7 @@ import {
MeetRecordingConfig,
MeetRecordingLayout,
MeetRoomAutoDeletionPolicy,
+ MeetRoomCaptionsConfig,
MeetRoomConfig,
MeetRoomDeletionPolicyWithMeeting,
MeetRoomDeletionPolicyWithRecordings,
@@ -55,6 +56,10 @@ const E2EEConfigSchema: z.ZodType = z.object({
enabled: z.boolean()
});
+const CaptionsConfigSchema: z.ZodType = z.object({
+ enabled: z.boolean()
+});
+
const ThemeModeSchema: z.ZodType = z.nativeEnum(MeetRoomThemeMode);
const hexColorSchema = z
@@ -92,7 +97,8 @@ const UpdateRoomConfigSchema: z.ZodType> = z
recording: RecordingConfigSchema.optional(),
chat: ChatConfigSchema.optional(),
virtualBackground: VirtualBackgroundConfigSchema.optional(),
- e2ee: E2EEConfigSchema.optional()
+ e2ee: E2EEConfigSchema.optional(),
+ captions: CaptionsConfigSchema.optional()
// appearance: AppearanceConfigSchema,
})
.transform((data: Partial) => {
@@ -123,7 +129,8 @@ const CreateRoomConfigSchema = z
})),
chat: ChatConfigSchema.optional().default(() => ({ enabled: true })),
virtualBackground: VirtualBackgroundConfigSchema.optional().default(() => ({ enabled: true })),
- e2ee: E2EEConfigSchema.optional().default(() => ({ enabled: false }))
+ e2ee: E2EEConfigSchema.optional().default(() => ({ enabled: false })),
+ captions: CaptionsConfigSchema.optional().default(() => ({ enabled: false }))
// appearance: AppearanceConfigSchema,
})
.transform((data) => {
@@ -207,7 +214,8 @@ export const RoomOptionsSchema: z.ZodType = z.object({
},
chat: { enabled: true },
virtualBackground: { enabled: true },
- e2ee: { enabled: false }
+ e2ee: { enabled: false },
+ captions: { enabled: false }
})
// maxParticipants: z
// .number()
diff --git a/meet-ce/backend/src/services/livekit-webhook.service.ts b/meet-ce/backend/src/services/livekit-webhook.service.ts
index 74fb7bf9..dadaa7a0 100644
--- a/meet-ce/backend/src/services/livekit-webhook.service.ts
+++ b/meet-ce/backend/src/services/livekit-webhook.service.ts
@@ -163,8 +163,8 @@ export class LivekitWebhookService {
* @param participant - Information about the newly joined participant.
*/
async handleParticipantJoined(room: Room, participant: ParticipantInfo) {
- // Skip if the participant is an egress participant
- if (this.livekitService.isEgressParticipant(participant)) return;
+ // Skip if the participant is not a standard participant
+ if (!this.livekitService.isStandardParticipant(participant)) return;
try {
const { recordings } = await this.recordingService.getAllRecordings({ roomId: room.name });
@@ -185,8 +185,8 @@ export class LivekitWebhookService {
* @param participant - Information about the participant who left.
*/
async handleParticipantLeft(room: Room, participant: ParticipantInfo) {
- // Skip if the participant is an egress participant
- if (this.livekitService.isEgressParticipant(participant)) return;
+ // Skip if the participant is not a standard participant
+ if (!this.livekitService.isStandardParticipant(participant)) return;
try {
// Release the participant's reserved name
diff --git a/meet-ce/backend/src/services/livekit.service.ts b/meet-ce/backend/src/services/livekit.service.ts
index 630da29b..72debf17 100644
--- a/meet-ce/backend/src/services/livekit.service.ts
+++ b/meet-ce/backend/src/services/livekit.service.ts
@@ -1,3 +1,4 @@
+import { ParticipantInfo_Kind } from '@livekit/protocol';
import { inject, injectable } from 'inversify';
import {
CreateOptions,
@@ -400,8 +401,11 @@ export class LiveKitService {
}
}
- isEgressParticipant(participant: ParticipantInfo): boolean {
- // TODO: Remove deprecated warning by using ParticipantInfo_Kind: participant.kind === ParticipantInfo_Kind.EGRESS;
- return participant.identity.startsWith('EG_') && participant.permission?.recorder === true;
+ /**
+ * Checks if a participant is a standard participant (web clients).
+ * @param participant
+ */
+ isStandardParticipant(participant: ParticipantInfo): boolean {
+ return participant.kind === ParticipantInfo_Kind.STANDARD;
}
}
diff --git a/meet-ce/backend/src/services/room-member.service.ts b/meet-ce/backend/src/services/room-member.service.ts
index 6fcee613..950ae899 100644
--- a/meet-ce/backend/src/services/room-member.service.ts
+++ b/meet-ce/backend/src/services/room-member.service.ts
@@ -133,9 +133,10 @@ export class RoomMemberService {
// Get participant permissions (with join meeting)
const permissions = await this.getRoomMemberPermissions(roomId, role, true);
+ const withCaptions = room.config.captions.enabled ?? false;
// Generate token with participant name
- return this.tokenService.generateRoomMemberToken(role, permissions, participantName, participantIdentity);
+ return this.tokenService.generateRoomMemberToken(role, permissions, participantName, participantIdentity, withCaptions);
}
/**
diff --git a/meet-ce/backend/src/services/token.service.ts b/meet-ce/backend/src/services/token.service.ts
index 632838e3..84fc6c47 100644
--- a/meet-ce/backend/src/services/token.service.ts
+++ b/meet-ce/backend/src/services/token.service.ts
@@ -42,7 +42,8 @@ export class TokenService {
role: MeetRoomMemberRole,
permissions: MeetRoomMemberPermissions,
participantName?: string,
- participantIdentity?: string
+ participantIdentity?: string,
+ roomWithCaptions = false
): Promise {
const metadata: MeetRoomMemberTokenMetadata = {
livekitUrl: MEET_ENV.LIVEKIT_URL,
@@ -56,23 +57,36 @@ export class TokenService {
ttl: INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION,
metadata: JSON.stringify(metadata)
};
- return await this.generateJwtToken(tokenOptions, permissions.livekit as VideoGrant);
+ return await this.generateJwtToken(tokenOptions, permissions.livekit as VideoGrant, roomWithCaptions);
}
- private async generateJwtToken(tokenOptions: AccessTokenOptions, grants?: VideoGrant): Promise {
+ private async generateJwtToken(
+ tokenOptions: AccessTokenOptions,
+ grants?: VideoGrant,
+ roomWithCaptions = false
+ ): Promise {
const at = new AccessToken(MEET_ENV.LIVEKIT_API_KEY, MEET_ENV.LIVEKIT_API_SECRET, tokenOptions);
if (grants) {
at.addGrant(grants);
}
- if (MEET_ENV.AGENT_SPEECH_PROCESSING_NAME) {
+ const captionsEnabledInEnv = MEET_ENV.CAPTIONS_ENABLED === 'true';
+ const captionsEnabledInRoom = Boolean(roomWithCaptions);
- this.logger.debug('Adding speech processing agent dispatch to token', MEET_ENV.AGENT_SPEECH_PROCESSING_NAME);
+ // Warn if configuration is inconsistent
+ if (!captionsEnabledInEnv && captionsEnabledInRoom) {
+ this.logger.warn(
+ `Captions feature is disabled in environment but Room is created with captions enabled. Please enable captions in environment by setting MEET_CAPTIONS_ENABLED=true to ensure proper functionality.`
+ );
+ }
+
+ if (captionsEnabledInEnv && captionsEnabledInRoom) {
+ this.logger.debug('Activating Captions Agent. Configuring Room Agent Dispatch.');
at.roomConfig = new RoomConfiguration({
agents: [
new RoomAgentDispatch({
- agentName: MEET_ENV.AGENT_SPEECH_PROCESSING_NAME
+ agentName: INTERNAL_CONFIG.CAPTIONS_AGENT_NAME
})
]
});
diff --git a/meet-ce/typings/src/room-config.ts b/meet-ce/typings/src/room-config.ts
index c8ae2104..01ccfa12 100644
--- a/meet-ce/typings/src/room-config.ts
+++ b/meet-ce/typings/src/room-config.ts
@@ -4,56 +4,60 @@ import { MeetRecordingLayout } from './recording.model';
* Interface representing the config for a room.
*/
export interface MeetRoomConfig {
- chat: MeetChatConfig;
- recording: MeetRecordingConfig;
- virtualBackground: MeetVirtualBackgroundConfig;
- e2ee: MeetE2EEConfig;
- // appearance: MeetAppearanceConfig;
+ chat: MeetChatConfig;
+ recording: MeetRecordingConfig;
+ virtualBackground: MeetVirtualBackgroundConfig;
+ e2ee: MeetE2EEConfig;
+ captions: MeetRoomCaptionsConfig;
+ // appearance: MeetAppearanceConfig;
}
/**
* Interface representing the config for recordings in a room.
*/
export interface MeetRecordingConfig {
- enabled: boolean;
- layout?: MeetRecordingLayout;
- allowAccessTo?: MeetRecordingAccess;
+ enabled: boolean;
+ layout?: MeetRecordingLayout;
+ allowAccessTo?: MeetRecordingAccess;
}
export enum MeetRecordingAccess {
- ADMIN = 'admin', // Only admins can access the recording
- ADMIN_MODERATOR = 'admin_moderator', // Admins and moderators can access
- ADMIN_MODERATOR_SPEAKER = 'admin_moderator_speaker' // Admins, moderators and speakers can access
+ ADMIN = 'admin', // Only admins can access the recording
+ ADMIN_MODERATOR = 'admin_moderator', // Admins and moderators can access
+ ADMIN_MODERATOR_SPEAKER = 'admin_moderator_speaker', // Admins, moderators and speakers can access
}
export interface MeetChatConfig {
- enabled: boolean;
+ enabled: boolean;
}
export interface MeetVirtualBackgroundConfig {
- enabled: boolean;
+ enabled: boolean;
}
export interface MeetE2EEConfig {
- enabled: boolean;
+ enabled: boolean;
+}
+export interface MeetRoomCaptionsConfig {
+ enabled: boolean;
}
export interface MeetAppearanceConfig {
- themes: MeetRoomTheme[];
+ themes: MeetRoomTheme[];
}
export interface MeetRoomTheme {
- name: string;
- enabled: boolean;
- baseTheme: MeetRoomThemeMode;
- backgroundColor?: string;
- primaryColor?: string;
- secondaryColor?: string;
- accentColor?: string;
- surfaceColor?: string;
+ name: string;
+ enabled: boolean;
+ baseTheme: MeetRoomThemeMode;
+ backgroundColor?: string;
+ primaryColor?: string;
+ secondaryColor?: string;
+ accentColor?: string;
+ surfaceColor?: string;
}
export enum MeetRoomThemeMode {
- LIGHT = 'light',
- DARK = 'dark'
+ LIGHT = 'light',
+ DARK = 'dark',
}