frontend: update recording permissions handling and improve UI feature flags

This commit is contained in:
juancarmore 2025-08-13 23:10:12 +02:00
parent 5f71b0c28a
commit 2478845fb6
4 changed files with 97 additions and 28 deletions

View File

@ -11,16 +11,15 @@
[toolbarScreenshareButton]="features().showScreenShare" [toolbarScreenshareButton]="features().showScreenShare"
[toolbarLeaveButton]="!features().canModerateRoom" [toolbarLeaveButton]="!features().canModerateRoom"
[toolbarRecordingButton]="features().canRecordRoom" [toolbarRecordingButton]="features().canRecordRoom"
[toolbarViewRecordingsButton]="features().showRecordings" [toolbarViewRecordingsButton]="features().canRetrieveRecordings && hasRecordings"
[toolbarBroadcastingButton]="false" [toolbarBroadcastingButton]="false"
[toolbarChatPanelButton]="features().showChat" [toolbarChatPanelButton]="features().showChat"
[toolbarBackgroundEffectsButton]="features().showBackgrounds" [toolbarBackgroundEffectsButton]="features().showBackgrounds"
[toolbarParticipantsPanelButton]="features().showParticipantList" [toolbarParticipantsPanelButton]="features().showParticipantList"
[toolbarSettingsButton]="features().showSettings" [toolbarSettingsButton]="features().showSettings"
[toolbarFullscreenButton]="features().showFullscreen" [toolbarFullscreenButton]="features().showFullscreen"
[toolbarActivitiesPanelButton]="features().showRecordings" [toolbarActivitiesPanelButton]="features().showRecordingPanel"
[activitiesPanelRecordingActivity]="features().showRecordings" [activitiesPanelRecordingActivity]="features().showRecordingPanel"
[recordingActivityReadOnly]="!features().canRecordRoom"
[recordingActivityShowControls]="{ [recordingActivityShowControls]="{
play: false, play: false,
download: false, download: false,
@ -28,7 +27,7 @@
externalView: true externalView: true
}" }"
[recordingActivityStartStopRecordingButton]="features().canRecordRoom" [recordingActivityStartStopRecordingButton]="features().canRecordRoom"
[recordingActivityViewRecordingsButton]="features().showRecordings" [recordingActivityViewRecordingsButton]="features().canRetrieveRecordings && hasRecordings"
[recordingActivityShowRecordingsList]="false" [recordingActivityShowRecordingsList]="false"
[activitiesPanelBroadcastingActivity]="false" [activitiesPanelBroadcastingActivity]="false"
[showDisconnectionDialog]="false" [showDisconnectionDialog]="false"
@ -37,8 +36,7 @@
(onParticipantLeft)="onParticipantLeft($event)" (onParticipantLeft)="onParticipantLeft($event)"
(onRecordingStartRequested)="onRecordingStartRequested($event)" (onRecordingStartRequested)="onRecordingStartRequested($event)"
(onRecordingStopRequested)="onRecordingStopRequested($event)" (onRecordingStopRequested)="onRecordingStopRequested($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked(undefined)" (onViewRecordingsClicked)="onViewRecordingsClicked()"
(onViewRecordingClicked)="onViewRecordingsClicked($event)"
> >
@if (features().canModerateRoom) { @if (features().canModerateRoom) {
<div *ovToolbarAdditionalButtons> <div *ovToolbarAdditionalButtons>

View File

@ -84,6 +84,8 @@ export class MeetingComponent implements OnInit {
participantForm = new FormGroup({ participantForm = new FormGroup({
name: new FormControl('', [Validators.required, Validators.minLength(4)]) name: new FormControl('', [Validators.required, Validators.minLength(4)])
}); });
hasRecordings = false;
showRecordingCard = false; showRecordingCard = false;
showBackButton = true; showBackButton = true;
@ -175,13 +177,23 @@ export class MeetingComponent implements OnInit {
*/ */
private async checkForRecordings() { private async checkForRecordings() {
try { try {
await this.recordingService.generateRecordingToken(this.roomId, this.roomSecret); const { canRetrieveRecordings } = await this.recordingService.generateRecordingToken(
this.roomId,
this.roomSecret
);
if (!canRetrieveRecordings) {
this.showRecordingCard = false;
return;
}
const { recordings } = await this.recordingService.listRecordings({ const { recordings } = await this.recordingService.listRecordings({
maxItems: 1, maxItems: 1,
roomId: this.roomId, roomId: this.roomId,
fields: 'recordingId' fields: 'recordingId'
}); });
this.showRecordingCard = recordings.length > 0; this.hasRecordings = recordings.length > 0;
this.showRecordingCard = this.hasRecordings;
} catch (error) { } catch (error) {
console.error('Error checking for recordings:', error); console.error('Error checking for recordings:', error);
this.showRecordingCard = false; this.showRecordingCard = false;
@ -326,10 +338,34 @@ export class MeetingComponent implements OnInit {
const event = JSON.parse(new TextDecoder().decode(payload)); const event = JSON.parse(new TextDecoder().decode(payload));
switch (topic) { switch (topic) {
case 'recordingStopped': {
// If a 'recordingStopped' event is received and there was no previous recordings,
// update the hasRecordings flag and refresh the recording token
if (this.hasRecordings) return;
this.hasRecordings = true;
try {
await this.recordingService.generateRecordingToken(this.roomId, this.roomSecret);
} catch (error) {
console.error('Error refreshing recording token:', error);
}
break;
}
case MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED: { case MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED: {
// Update room preferences // Update room preferences
const { preferences } = event as MeetRoomPreferencesUpdatedPayload; const { preferences } = event as MeetRoomPreferencesUpdatedPayload;
this.featureConfService.setRoomPreferences(preferences); this.featureConfService.setRoomPreferences(preferences);
// Refresh recording token if recording is enabled
if (preferences.recordingPreferences.enabled) {
try {
await this.recordingService.generateRecordingToken(this.roomId, this.roomSecret);
} catch (error) {
console.error('Error refreshing recording token:', error);
}
}
break; break;
} }
case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED: { case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED: {
@ -504,11 +540,7 @@ Remember that by default, a recording uses 4 CPUs for each room.`
} }
} }
async onViewRecordingsClicked(recordingId?: any) { async onViewRecordingsClicked() {
if (recordingId) { window.open(`/room/${this.roomId}/recordings?secret=${this.roomSecret}`, '_blank');
await this.recordingService.playRecording(recordingId);
} else {
window.open(`/room/${this.roomId}/recordings?secret=${this.roomSecret}`, '_blank');
}
} }
} }

View File

@ -1,5 +1,11 @@
import { computed, Injectable, signal } from '@angular/core'; import { computed, Injectable, signal } from '@angular/core';
import { MeetRoomPreferences, ParticipantPermissions, ParticipantRole, TrackSource } from '@lib/typings/ce'; import {
MeetRoomPreferences,
ParticipantPermissions,
ParticipantRole,
RecordingPermissions,
TrackSource
} from '@lib/typings/ce';
import { LoggerService } from 'openvidu-components-angular'; import { LoggerService } from 'openvidu-components-angular';
/** /**
@ -14,7 +20,7 @@ export interface ApplicationFeatures {
showScreenShare: boolean; showScreenShare: boolean;
// UI Features // UI Features
showRecordings: boolean; showRecordingPanel: boolean;
showChat: boolean; showChat: boolean;
showBackgrounds: boolean; showBackgrounds: boolean;
showParticipantList: boolean; showParticipantList: boolean;
@ -24,6 +30,7 @@ export interface ApplicationFeatures {
// Permissions // Permissions
canModerateRoom: boolean; canModerateRoom: boolean;
canRecordRoom: boolean; canRecordRoom: boolean;
canRetrieveRecordings: boolean;
} }
/** /**
@ -36,7 +43,7 @@ const DEFAULT_FEATURES: ApplicationFeatures = {
showMicrophone: true, showMicrophone: true,
showScreenShare: true, showScreenShare: true,
showRecordings: true, showRecordingPanel: true,
showChat: true, showChat: true,
showBackgrounds: true, showBackgrounds: true,
showParticipantList: true, showParticipantList: true,
@ -44,7 +51,8 @@ const DEFAULT_FEATURES: ApplicationFeatures = {
showFullscreen: true, showFullscreen: true,
canModerateRoom: false, canModerateRoom: false,
canRecordRoom: false canRecordRoom: false,
canRetrieveRecordings: false
}; };
/** /**
@ -61,10 +69,16 @@ export class FeatureConfigurationService {
protected roomPreferences = signal<MeetRoomPreferences | undefined>(undefined); protected roomPreferences = signal<MeetRoomPreferences | undefined>(undefined);
protected participantPermissions = signal<ParticipantPermissions | undefined>(undefined); protected participantPermissions = signal<ParticipantPermissions | undefined>(undefined);
protected participantRole = signal<ParticipantRole | undefined>(undefined); protected participantRole = signal<ParticipantRole | undefined>(undefined);
protected recordingPermissions = signal<RecordingPermissions | undefined>(undefined);
// Computed signal to derive features based on current configurations // Computed signal to derive features based on current configurations
public readonly features = computed<ApplicationFeatures>(() => public readonly features = computed<ApplicationFeatures>(() =>
this.calculateFeatures(this.roomPreferences(), this.participantPermissions(), this.participantRole()) this.calculateFeatures(
this.roomPreferences(),
this.participantPermissions(),
this.participantRole(),
this.recordingPermissions()
)
); );
constructor(protected loggerService: LoggerService) { constructor(protected loggerService: LoggerService) {
@ -95,6 +109,14 @@ export class FeatureConfigurationService {
this.participantRole.set(role); this.participantRole.set(role);
} }
/**
* Updates recording permissions
*/
setRecordingPermissions(permissions: RecordingPermissions): void {
this.log.d('Updating recording permissions', permissions);
this.recordingPermissions.set(permissions);
}
/** /**
* Checks if a specific feature is enabled * Checks if a specific feature is enabled
*/ */
@ -108,14 +130,15 @@ export class FeatureConfigurationService {
protected calculateFeatures( protected calculateFeatures(
roomPrefs?: MeetRoomPreferences, roomPrefs?: MeetRoomPreferences,
participantPerms?: ParticipantPermissions, participantPerms?: ParticipantPermissions,
role?: ParticipantRole role?: ParticipantRole,
recordingPerms?: RecordingPermissions
): ApplicationFeatures { ): ApplicationFeatures {
// Start with default configuration // Start with default configuration
const features: ApplicationFeatures = { ...DEFAULT_FEATURES }; const features: ApplicationFeatures = { ...DEFAULT_FEATURES };
// Apply room configurations // Apply room configurations
if (roomPrefs) { if (roomPrefs) {
features.showRecordings = roomPrefs.recordingPreferences.enabled; features.showRecordingPanel = roomPrefs.recordingPreferences.enabled;
features.showChat = roomPrefs.chatPreferences.enabled; features.showChat = roomPrefs.chatPreferences.enabled;
features.showBackgrounds = roomPrefs.virtualBackgroundPreferences.enabled; features.showBackgrounds = roomPrefs.virtualBackgroundPreferences.enabled;
} }
@ -123,8 +146,7 @@ export class FeatureConfigurationService {
// Apply participant permissions (these can restrict enabled features) // Apply participant permissions (these can restrict enabled features)
if (participantPerms) { if (participantPerms) {
// Only restrict if the feature is already enabled // Only restrict if the feature is already enabled
if (features.showRecordings) { if (features.showRecordingPanel) {
// features.showRecordings = !!recordingRole;
features.canRecordRoom = participantPerms.openvidu.canRecord; features.canRecordRoom = participantPerms.openvidu.canRecord;
} }
if (features.showChat) { if (features.showChat) {
@ -149,6 +171,11 @@ export class FeatureConfigurationService {
features.canModerateRoom = role === ParticipantRole.MODERATOR; features.canModerateRoom = role === ParticipantRole.MODERATOR;
} }
// Apply recording permissions
if (recordingPerms) {
features.canRetrieveRecordings = recordingPerms.canRetrieveRecordings;
}
this.log.d('Calculated features', features); this.log.d('Calculated features', features);
return features; return features;
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ShareRecordingDialogComponent } from '@lib/components'; import { ShareRecordingDialogComponent } from '@lib/components';
import { AuthService, HttpService, ParticipantService } from '@lib/services'; import { AuthService, FeatureConfigurationService, HttpService, ParticipantService } from '@lib/services';
import { MeetRecordingFilters, MeetRecordingInfo, RecordingPermissions } from '@lib/typings/ce'; import { MeetRecordingFilters, MeetRecordingInfo, RecordingPermissions } from '@lib/typings/ce';
import { getValidDecodedToken } from '@lib/utils'; import { getValidDecodedToken } from '@lib/utils';
import { LoggerService } from 'openvidu-components-angular'; import { LoggerService } from 'openvidu-components-angular';
@ -25,6 +25,7 @@ export class RecordingService {
private httpService: HttpService, private httpService: HttpService,
protected participantService: ParticipantService, protected participantService: ParticipantService,
protected authService: AuthService, protected authService: AuthService,
protected featureConfService: FeatureConfigurationService,
protected dialog: MatDialog protected dialog: MatDialog
) { ) {
this.log = this.loggerService.get('OpenVidu Meet - RecordingManagerService'); this.log = this.loggerService.get('OpenVidu Meet - RecordingManagerService');
@ -159,10 +160,18 @@ export class RecordingService {
*/ */
async generateRecordingToken(roomId: string, secret: string): Promise<RecordingPermissions> { async generateRecordingToken(roomId: string, secret: string): Promise<RecordingPermissions> {
const path = `${HttpService.INTERNAL_API_PATH_PREFIX}/rooms/${roomId}/recording-token`; const path = `${HttpService.INTERNAL_API_PATH_PREFIX}/rooms/${roomId}/recording-token`;
const { token } = await this.httpService.postRequest<{ token: string }>(path, { secret });
this.setRecordingPermissionsFromToken(token); try {
return this.recordingPermissions; const { token } = await this.httpService.postRequest<{ token: string }>(path, { secret });
this.setRecordingPermissionsFromToken(token);
return this.recordingPermissions;
} catch (error) {
this.featureConfService.setRecordingPermissions({
canRetrieveRecordings: false,
canDeleteRecordings: false
});
throw error;
}
} }
/** /**
@ -174,6 +183,9 @@ export class RecordingService {
try { try {
const decodedToken = getValidDecodedToken(token); const decodedToken = getValidDecodedToken(token);
this.recordingPermissions = decodedToken.metadata.recordingPermissions; this.recordingPermissions = decodedToken.metadata.recordingPermissions;
// Update feature configuration
this.featureConfService.setRecordingPermissions(this.recordingPermissions);
} catch (error) { } catch (error) {
this.log.e('Error setting recording permissions from token', error); this.log.e('Error setting recording permissions from token', error);
throw new Error('Error setting recording permissions from token'); throw new Error('Error setting recording permissions from token');