frontend: Enables user-controlled live captions
Allows users to toggle live captions on or off. Introduces a room configuration setting to enable/disable the captions feature. The captions button visibility is now controlled by the 'showCaptions' feature flag.
This commit is contained in:
parent
cb12d9a8fe
commit
0a56a74433
@ -1,8 +1,8 @@
|
||||
@if (meetingContextService.lkRoom()) {
|
||||
<div class="main-container" [ngClass]="{ withFooter: shouldShowCaptions() }">
|
||||
<div class="main-container" [ngClass]="{ withFooter: areCaptionsEnabledByUser() }">
|
||||
<ov-layout
|
||||
[ovRemoteParticipants]="visibleRemoteParticipants()"
|
||||
[ngClass]="{ withFooter: shouldShowCaptions() }"
|
||||
[ngClass]="{ withFooter: areCaptionsEnabledByUser() }"
|
||||
>
|
||||
@if (shouldShowLinkOverlay()) {
|
||||
<ng-container *ovLayoutAdditionalElements>
|
||||
@ -43,7 +43,7 @@
|
||||
</ov-layout>
|
||||
</div>
|
||||
<!-- Live Captions Component -->
|
||||
@if (shouldShowCaptions()) {
|
||||
@if (areCaptionsEnabledByUser()) {
|
||||
<ov-meeting-captions [captions]="captions()"></ov-meeting-captions>
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ export class MeetingCustomLayoutComponent {
|
||||
return this.meetingContextService.canModerateRoom() && hasNoRemotes;
|
||||
});
|
||||
|
||||
protected readonly shouldShowCaptions = computed(() => this.captionsService.areCaptionsEnabled());
|
||||
protected readonly areCaptionsEnabledByUser = computed(() => this.captionsService.areCaptionsEnabledByUser());
|
||||
|
||||
protected readonly captions = computed(() => this.captionsService.captions());
|
||||
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
<!-- Captions button -->
|
||||
@if (showCaptionsButton()) {
|
||||
@if (isMobile()) {
|
||||
<!-- On mobile, the captions button will be inside a menu -->
|
||||
<button id="captions-button" mat-menu-item (click)="onCaptionsClick()" [disableRipple]="true">
|
||||
<mat-icon class="material-symbols-outlined">{{
|
||||
areCaptionsEnabled() ? 'subtitles_off' : 'subtitles'
|
||||
}}</mat-icon>
|
||||
<mat-icon class="material-symbols-outlined">subtitles</mat-icon>
|
||||
<span class="button-text">
|
||||
{{ areCaptionsEnabled() ? 'Disable live captions' : 'Enable live captions' }}
|
||||
{{ areCaptionsEnabledByUser() ? 'Disable live captions' : 'Enable live captions' }}
|
||||
</span>
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
id="captions-button"
|
||||
[ngClass]="areCaptionsEnabled() ? 'active' : ''"
|
||||
[ngClass]="areCaptionsEnabledByUser() ? 'active' : ''"
|
||||
mat-icon-button
|
||||
(click)="onCaptionsClick()"
|
||||
[disableRipple]="true"
|
||||
[matTooltip]="areCaptionsEnabled() ? 'Disable live captions' : 'Enable live captions'"
|
||||
[matTooltip]="areCaptionsEnabledByUser() ? 'Disable live captions' : 'Enable live captions'"
|
||||
>
|
||||
<mat-icon>{{ areCaptionsEnabled() ? 'subtitles_off' : 'subtitles' }}</mat-icon>
|
||||
<mat-icon>subtitles</mat-icon>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Copy Link Button -->
|
||||
@if (showCopyLinkButton()) {
|
||||
|
||||
@ -33,12 +33,17 @@ export class MeetingToolbarExtraButtonsComponent {
|
||||
*/
|
||||
protected showCopyLinkButton = computed(() => this.meetingContextService.canModerateRoom());
|
||||
|
||||
/**
|
||||
* Whether to show the captions button
|
||||
*/
|
||||
protected showCaptionsButton = computed(() => this.meetingContextService.areCaptionsAllowed());
|
||||
|
||||
/**
|
||||
* Whether the device is mobile (affects button style)
|
||||
*/
|
||||
protected isMobile = computed(() => this.meetingContextService.isMobile());
|
||||
|
||||
protected areCaptionsEnabled = computed(() => this.captionService.areCaptionsEnabled());
|
||||
protected areCaptionsEnabledByUser = computed(() => this.captionService.areCaptionsEnabledByUser());
|
||||
|
||||
onCopyLinkClick(): void {
|
||||
const room = this.meetingContextService.meetRoom();
|
||||
@ -51,6 +56,6 @@ export class MeetingToolbarExtraButtonsComponent {
|
||||
}
|
||||
|
||||
onCaptionsClick(): void {
|
||||
this.captionService.areCaptionsEnabled() ? this.captionService.disable() : this.captionService.enable();
|
||||
this.captionService.areCaptionsEnabledByUser() ? this.captionService.disable() : this.captionService.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,11 +37,16 @@ export class MeetingCaptionsService {
|
||||
|
||||
// Reactive state
|
||||
private readonly _captions = signal<Caption[]>([]);
|
||||
private readonly _isEnabled = signal<boolean>(false);
|
||||
private readonly _areCaptionsEnabledByUser = signal<boolean>(false);
|
||||
|
||||
// Public readonly signals
|
||||
/**
|
||||
* Current list of active captions
|
||||
*/
|
||||
readonly captions = this._captions.asReadonly();
|
||||
readonly areCaptionsEnabled = this._isEnabled.asReadonly();
|
||||
/**
|
||||
* Whether captions are enabled by the user
|
||||
*/
|
||||
readonly areCaptionsEnabledByUser = this._areCaptionsEnabledByUser.asReadonly();
|
||||
|
||||
// Map to track expiration timeouts
|
||||
private expirationTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
@ -83,7 +88,7 @@ export class MeetingCaptionsService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._isEnabled()) {
|
||||
if (this._areCaptionsEnabledByUser()) {
|
||||
this.logger.d('Captions already enabled');
|
||||
return;
|
||||
}
|
||||
@ -91,7 +96,7 @@ export class MeetingCaptionsService {
|
||||
// Register the LiveKit transcription handler
|
||||
this.room.registerTextStreamHandler('lk.transcription', this.handleTranscription.bind(this));
|
||||
|
||||
this._isEnabled.set(true);
|
||||
this._areCaptionsEnabledByUser.set(true);
|
||||
this.logger.d('Captions enabled');
|
||||
}
|
||||
|
||||
@ -100,7 +105,7 @@ export class MeetingCaptionsService {
|
||||
* This is called when the user deactivates captions.
|
||||
*/
|
||||
disable(): void {
|
||||
if (!this._isEnabled()) {
|
||||
if (!this._areCaptionsEnabledByUser()) {
|
||||
this.logger.d('Captions already disabled');
|
||||
return;
|
||||
}
|
||||
@ -108,7 +113,7 @@ export class MeetingCaptionsService {
|
||||
// Clear all active captions
|
||||
this.clearAllCaptions();
|
||||
|
||||
this._isEnabled.set(false);
|
||||
this._areCaptionsEnabledByUser.set(false);
|
||||
this.room?.unregisterTextStreamHandler('lk.transcription');
|
||||
this.logger.d('Captions disabled');
|
||||
}
|
||||
@ -119,7 +124,7 @@ export class MeetingCaptionsService {
|
||||
destroy(): void {
|
||||
this.clearAllCaptions();
|
||||
this.room = null;
|
||||
this._isEnabled.set(false);
|
||||
this._areCaptionsEnabledByUser.set(false);
|
||||
this.logger.d('Meeting Captions service destroyed');
|
||||
}
|
||||
|
||||
|
||||
@ -186,6 +186,14 @@ export class MeetingContextService {
|
||||
return this._e2eeKey().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether captions feature is allowed in the room
|
||||
* @returns true if captions feature is allowed, false otherwise
|
||||
*/
|
||||
areCaptionsAllowed(): boolean {
|
||||
return this.featureConfigService.features().showCaptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the room secret in context
|
||||
* @param secret The room secret
|
||||
|
||||
@ -61,6 +61,30 @@
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Captions Card -->
|
||||
<mat-card class="config-card">
|
||||
<mat-card-content>
|
||||
<div class="card-header">
|
||||
<div class="icon-title-group">
|
||||
<mat-icon class="feature-icon">closed_caption</mat-icon>
|
||||
<div class="title-group">
|
||||
<h4 class="card-title">Captions</h4>
|
||||
<p class="card-description">
|
||||
Enable live transcription to display captions during the meeting
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mat-slide-toggle
|
||||
[checked]="captionsEnabled"
|
||||
(change)="onCaptionsToggleChange($event)"
|
||||
color="primary"
|
||||
class="feature-toggle"
|
||||
>
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Chat Settings Card -->
|
||||
<mat-card class="config-card">
|
||||
<mat-card-content>
|
||||
|
||||
@ -44,6 +44,9 @@ export class RoomConfigComponent implements OnDestroy {
|
||||
},
|
||||
e2ee: {
|
||||
enabled: formValue.e2eeEnabled ?? false
|
||||
},
|
||||
captions: {
|
||||
enabled: formValue.captionsEnabled ?? false
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -102,6 +105,11 @@ export class RoomConfigComponent implements OnDestroy {
|
||||
this.configForm.patchValue({ virtualBackgroundEnabled: isEnabled });
|
||||
}
|
||||
|
||||
onCaptionsToggleChange(event: any): void {
|
||||
const isEnabled = event.checked;
|
||||
this.configForm.patchValue({ captionsEnabled: isEnabled });
|
||||
}
|
||||
|
||||
get chatEnabled(): boolean {
|
||||
return this.configForm.value.chatEnabled || false;
|
||||
}
|
||||
@ -113,4 +121,8 @@ export class RoomConfigComponent implements OnDestroy {
|
||||
get e2eeEnabled(): boolean {
|
||||
return this.configForm.value.e2eeEnabled ?? false;
|
||||
}
|
||||
|
||||
get captionsEnabled(): boolean {
|
||||
return this.configForm.value.captionsEnabled ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,8 @@ const DEFAULT_CONFIG: MeetRoomConfig = {
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
e2ee: { enabled: false },
|
||||
captions: { enabled: false }
|
||||
};
|
||||
|
||||
/**
|
||||
@ -192,7 +193,8 @@ export class RoomWizardStateService {
|
||||
formGroup: this.formBuilder.group({
|
||||
chatEnabled: initialRoomOptions.config!.chat!.enabled,
|
||||
virtualBackgroundEnabled: initialRoomOptions.config!.virtualBackground!.enabled,
|
||||
e2eeEnabled: initialRoomOptions.config!.e2ee!.enabled
|
||||
e2eeEnabled: initialRoomOptions.config!.e2ee!.enabled,
|
||||
captionsEnabled: initialRoomOptions.config!.captions!.enabled
|
||||
})
|
||||
}
|
||||
];
|
||||
@ -267,6 +269,10 @@ export class RoomWizardStateService {
|
||||
...currentOptions.config?.e2ee,
|
||||
...stepData.config?.e2ee
|
||||
},
|
||||
captions: {
|
||||
...currentOptions.config?.captions,
|
||||
...stepData.config?.captions
|
||||
},
|
||||
recording: {
|
||||
...currentOptions.config?.recording,
|
||||
// If recording is explicitly set in stepData, use it
|
||||
|
||||
@ -31,6 +31,7 @@ export interface ApplicationFeatures {
|
||||
showRecordingPanel: boolean;
|
||||
showChat: boolean;
|
||||
showBackgrounds: boolean;
|
||||
showCaptions: boolean;
|
||||
showParticipantList: boolean;
|
||||
showSettings: boolean;
|
||||
showFullscreen: boolean;
|
||||
|
||||
@ -22,6 +22,7 @@ const DEFAULT_FEATURES: ApplicationFeatures = {
|
||||
showRecordingPanel: true,
|
||||
showChat: true,
|
||||
showBackgrounds: true,
|
||||
showCaptions: false,
|
||||
showParticipantList: true,
|
||||
showSettings: true,
|
||||
showFullscreen: true,
|
||||
@ -115,6 +116,7 @@ export class FeatureConfigurationService {
|
||||
features.showRecordingPanel = roomConfig.recording.enabled;
|
||||
features.showChat = roomConfig.chat.enabled;
|
||||
features.showBackgrounds = roomConfig.virtualBackground.enabled;
|
||||
features.showCaptions = roomConfig.captions?.enabled ?? false;
|
||||
}
|
||||
|
||||
// Apply room member permissions (these can restrict enabled features)
|
||||
@ -130,7 +132,6 @@ export class FeatureConfigurationService {
|
||||
if (features.showBackgrounds) {
|
||||
features.showBackgrounds = permissions.meet.canChangeVirtualBackground;
|
||||
}
|
||||
|
||||
// Media features
|
||||
const canPublish = permissions.livekit.canPublish;
|
||||
const canPublishSources = permissions.livekit.canPublishSources ?? [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user