frontend: enhance feature configuration management and update video room component to use async feature flags

This commit is contained in:
Carlos Santos 2025-06-09 17:46:29 +02:00
parent acd0cb9b3d
commit 9e45716397
6 changed files with 336 additions and 52 deletions

View File

@ -52,17 +52,17 @@
<ov-videoconference
[token]="participantToken"
[participantName]="participantName"
[prejoin]="featureFlags.showPrejoin"
[prejoin]="(features$ | async)?.showPrejoin ?? true"
[prejoinDisplayParticipantName]="false"
[videoEnabled]="featureFlags.videoEnabled"
[audioEnabled]="featureFlags.audioEnabled"
[toolbarCameraButton]="featureFlags.showCamera"
[toolbarMicrophoneButton]="featureFlags.showMicrophone"
[toolbarScreenshareButton]="featureFlags.showScreenShare"
[toolbarChatPanelButton]="featureFlags.showChat"
[toolbarRecordingButton]="featureFlags.showRecording"
[videoEnabled]="(features$ | async)?.videoEnabled ?? true"
[audioEnabled]="(features$ | async)?.audioEnabled ?? true"
[toolbarCameraButton]="(features$ | async)?.showCamera ?? true"
[toolbarMicrophoneButton]="(features$ | async)?.showMicrophone ?? true"
[toolbarScreenshareButton]="(features$ | async)?.showScreenShare ?? true"
[toolbarChatPanelButton]="(features$ | async)?.showChat ?? true"
[toolbarRecordingButton]="(features$ | async)?.showRecording ?? true"
[toolbarBroadcastingButton]="false"
[toolbarBackgroundEffectsButton]="featureFlags.showBackgrounds"
[toolbarBackgroundEffectsButton]="(features$ | async)?.showBackgrounds ?? true"
[toolbarActivitiesPanelButton]="true"
[activitiesPanelRecordingActivity]="true"
[activitiesPanelBroadcastingActivity]="false"

View File

@ -1,4 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@ -27,6 +29,11 @@ import {
import { ParticipantTokenService } from '@lib/services/participant-token/participant-token.service';
import { RecordingManagerService } from '@lib/services/recording-manager/recording-manager.service';
import { NavigationService } from '@lib/services/navigation/navigation.service';
import {
ApplicationFeatures,
FeatureConfigurationService
} from '@lib/services/feature-configuration/feature-configuration.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-video-room',
@ -41,7 +48,8 @@ import { NavigationService } from '@lib/services/navigation/navigation.service';
FormsModule,
ReactiveFormsModule,
MatCardModule,
MatButtonModule
MatButtonModule,
AsyncPipe
]
})
export class VideoRoomComponent implements OnInit, OnDestroy {
@ -81,18 +89,25 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
showRecording: true,
showBackgrounds: true
};
features$!: Observable<ApplicationFeatures>;
constructor(
protected route: ActivatedRoute,
protected navigationService: NavigationService,
protected participantTokenService: ParticipantTokenService,
protected recManagerService: RecordingManagerService,
protected route: ActivatedRoute,
protected authService: AuthService,
protected ctxService: ContextService,
protected roomService: RoomService,
protected wcManagerService: WebComponentManagerService,
protected sessionStorageService: SessionStorageService
) {}
protected sessionStorageService: SessionStorageService,
protected featureConfService: FeatureConfigurationService
) {
this.featureConfService.features$.subscribe((features) => {
console.log('!!!!!!Feature flags updated:', features);
});
this.features$ = this.featureConfService.features$;
}
async ngOnInit() {
this.roomId = this.ctxService.getRoomId();
@ -120,7 +135,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
try {
await this.generateParticipantToken();
await this.replaceUrlQueryParams();
await this.loadRoomPreferences();
await this.roomService.loadPreferences(this.roomId);
this.showRoom = true;
} catch (error) {
console.error('Error accessing room:', error);
@ -281,27 +296,6 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
}
}
/**
* Loads the room preferences from the global preferences service and assigns them to the component.
*
* This method fetches the room preferences asynchronously and updates the component's properties
* based on the fetched preferences. It also updates the UI flags to show or hide certain features
* like chat, recording, and activity panel based on the preferences.
*
* @returns {Promise<void>} A promise that resolves when the room preferences have been loaded and applied.
*/
private async loadRoomPreferences() {
try {
this.roomPreferences = await this.roomService.getRoomPreferences();
} catch (error) {
console.error('Error loading room preferences:', error);
}
this.featureFlags.showChat = this.roomPreferences.chatPreferences.enabled;
this.featureFlags.showRecording = this.roomPreferences.recordingPreferences.enabled;
this.featureFlags.showBackgrounds = this.roomPreferences.virtualBackgroundPreferences.enabled;
}
/**
* Configures the feature flags based on participant permissions.
*/

View File

@ -2,7 +2,13 @@ import { Injectable } from '@angular/core';
import { jwtDecode } from 'jwt-decode';
import { ApplicationMode, ContextData, Edition } from '../../models/context.model';
import { LoggerService } from 'openvidu-components-angular';
import { AuthMode, HttpService, ParticipantRole } from 'projects/shared-meet-components/src/public-api';
import {
AuthMode,
HttpService,
OpenViduMeetPermissions,
ParticipantRole
} from 'projects/shared-meet-components/src/public-api';
import { FeatureConfigurationService } from '../feature-configuration/feature-configuration.service';
@Injectable({
providedIn: 'root'
@ -36,14 +42,15 @@ export class ContextService {
leaveRedirectUrl: ''
};
private log;
protected log;
/**
* Initializes a new instance of the ContextService class.
*/
constructor(
private loggerService: LoggerService,
private httpService: HttpService
protected loggerService: LoggerService,
protected featureConfService: FeatureConfigurationService,
protected httpService: HttpService
) {
this.log = this.loggerService.get('OpenVidu Meet - ContextService');
}
@ -126,24 +133,26 @@ export class ContextService {
setParticipantTokenAndUpdateContext(token: string): void {
try {
const decodedToken = this.getValidDecodedToken(token);
this.context.participantToken = token;
this.context.participantPermissions = decodedToken.metadata.permissions;
this.context.participantRole = decodedToken.metadata.role;
// Update feature configuration based on the new token
// this.updateFeatureConfiguration();
this.setParticipantToken(token);
this.setParticipantPermissions(decodedToken.metadata.permissions);
this.setParticipantRole(decodedToken.metadata.role);
} catch (error: any) {
this.log.e('Error setting token in context', error);
throw new Error('Error setting token', error);
}
}
setParticipantToken(token: string): void {
this.context.participantToken = token;
}
getParticipantToken(): string {
return this.context.participantToken;
}
setParticipantRole(participantRole: ParticipantRole): void {
this.context.participantRole = participantRole;
this.featureConfService.setParticipantRole(participantRole);
}
getParticipantRole(): ParticipantRole {
@ -154,6 +163,11 @@ export class ContextService {
return this.context.participantRole === ParticipantRole.MODERATOR;
}
setParticipantPermissions(permissions: OpenViduMeetPermissions): void {
this.context.participantPermissions = permissions;
this.featureConfService.setParticipantPermissions(permissions);
}
getParticipantPermissions() {
return this.context.participantPermissions;
}
@ -162,6 +176,7 @@ export class ContextService {
try {
const decodedToken = this.getValidDecodedToken(token);
this.context.recordingPermissions = decodedToken.metadata.recordingPermissions;
this.featureConfService.setRecordingPermissions(decodedToken.metadata.recordingPermissions);
} catch (error: any) {
this.log.e('Error setting recording token in context', error);
throw new Error('Error setting recording token', error);

View File

@ -0,0 +1,15 @@
import { TestBed } from '@angular/core/testing';
import { FeatureConfigurationService } from './feature-configuration.service';
describe('FeatureConfigurationService', () => {
let service: FeatureConfigurationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(FeatureConfigurationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,249 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, combineLatest, map } from 'rxjs';
import { LoggerService } from 'openvidu-components-angular';
import {
MeetRoomPreferences,
GlobalPreferences,
OpenViduMeetPermissions,
ParticipantRole,
RecordingPermissions
} from '@lib/typings/ce';
import { GlobalPreferencesService } from '../global-preferences/global-preferences.service';
/**
* Interface that defines all available features in the application
*/
export interface ApplicationFeatures {
// Video Room Features
videoEnabled: boolean;
audioEnabled: boolean;
showMicrophone: boolean;
showCamera: boolean;
showScreenShare: boolean;
showPrejoin: boolean;
// Communication Features
showChat: boolean;
showRecording: boolean;
showBackgrounds: boolean;
// UI Features
showParticipantList: boolean;
showSettings: boolean;
showFullscreen: boolean;
// Admin Features
canModerateRoom: boolean;
canManageRecordings: boolean;
canAccessConsole: boolean;
// Recording Features
canDeleteRecordings: boolean;
canRetrieveRecordings: boolean;
}
/**
* Base configuration for default features
*/
const DEFAULT_FEATURES: ApplicationFeatures = {
videoEnabled: true,
audioEnabled: true,
showMicrophone: true,
showCamera: true,
showScreenShare: true,
showPrejoin: true,
showChat: true,
showRecording: true,
showBackgrounds: true,
showParticipantList: true,
showSettings: true,
showFullscreen: true,
canModerateRoom: false,
canManageRecordings: false,
canAccessConsole: false,
canDeleteRecordings: false,
canRetrieveRecordings: false
};
/**
* Centralized service to manage feature configuration
* based on global preferences, room preferences, and participant permissions
*/
@Injectable({
providedIn: 'root'
})
export class FeatureConfigurationService {
protected log;
// Subjects to handle reactive state
protected globalPreferencesSubject = new BehaviorSubject<GlobalPreferences | null>(null);
protected roomPreferencesSubject = new BehaviorSubject<MeetRoomPreferences | null>(null);
protected participantPermissionsSubject = new BehaviorSubject<OpenViduMeetPermissions | null>(null);
protected recordingPermissionsSubject = new BehaviorSubject<RecordingPermissions | null>(null);
protected participantRoleSubject = new BehaviorSubject<ParticipantRole | null>(null);
// Observable that combines all configurations
public readonly features$: Observable<ApplicationFeatures>;
constructor(
protected loggerService: LoggerService,
protected globalPreferencesService: GlobalPreferencesService
) {
this.log = this.loggerService.get('OpenVidu Meet - FeatureConfigurationService');
// Configure the combined observable
this.features$ = combineLatest([
this.globalPreferencesSubject.asObservable(),
this.roomPreferencesSubject.asObservable(),
this.participantPermissionsSubject.asObservable(),
this.recordingPermissionsSubject.asObservable(),
this.participantRoleSubject.asObservable()
]).pipe(
map(([globalPrefs, roomPrefs, participantPerms, recordingPerms, role]) =>
this.calculateFeatures(globalPrefs, roomPrefs, participantPerms, recordingPerms, role)
)
);
}
/**
* Updates global preferences
*/
setGlobalPreferences(preferences: GlobalPreferences | null): void {
this.log.d('Updating global preferences', preferences);
this.globalPreferencesSubject.next(preferences);
}
/**
* Updates room preferences
*/
setRoomPreferences(preferences: MeetRoomPreferences | null): void {
this.log.d('Updating room preferences', preferences);
this.roomPreferencesSubject.next(preferences);
}
/**
* Updates participant permissions
*/
setParticipantPermissions(permissions: OpenViduMeetPermissions | null): void {
this.log.d('Updating participant permissions', permissions);
this.participantPermissionsSubject.next(permissions);
}
/**
* Updates recording permissions
*/
setRecordingPermissions(permissions: RecordingPermissions | null): void {
this.log.d('Updating recording permissions', permissions);
this.recordingPermissionsSubject.next(permissions);
}
/**
* Updates participant role
*/
setParticipantRole(role: ParticipantRole | null): void {
this.log.d('Updating participant role', role);
this.participantRoleSubject.next(role);
}
/**
* Gets the current feature configuration synchronously
*/
getCurrentFeatures(): ApplicationFeatures {
return this.calculateFeatures(
this.globalPreferencesSubject.value,
this.roomPreferencesSubject.value,
this.participantPermissionsSubject.value,
this.recordingPermissionsSubject.value,
this.participantRoleSubject.value
);
}
/**
* Checks if a specific feature is enabled
*/
isFeatureEnabled(featureName: keyof ApplicationFeatures): boolean {
return this.getCurrentFeatures()[featureName];
}
/**
* Core logic to calculate features based on all configurations
*/
protected calculateFeatures(
globalPrefs: GlobalPreferences | null,
roomPrefs: MeetRoomPreferences | null,
participantPerms: OpenViduMeetPermissions | null,
recordingPerms: RecordingPermissions | null,
role: ParticipantRole | null
): ApplicationFeatures {
// Start with default configuration
const features: ApplicationFeatures = { ...DEFAULT_FEATURES };
// Apply global preferences restrictions
if (globalPrefs) {
}
// Apply room configurations
if (roomPrefs) {
features.showChat = roomPrefs.chatPreferences.enabled;
features.showRecording = roomPrefs.recordingPreferences.enabled && role === ParticipantRole.MODERATOR;
features.showBackgrounds = roomPrefs.virtualBackgroundPreferences.enabled;
}
// Apply participant permissions (these can restrict enabled features)
if (participantPerms) {
// Only restrict if the feature is already enabled
if (features.showChat) {
features.showChat = participantPerms.canChat;
}
if (features.showRecording) {
features.showRecording = participantPerms.canRecord;
}
if (features.showBackgrounds) {
features.showBackgrounds = participantPerms.canChangeVirtualBackground;
}
if (features.showScreenShare) {
features.showScreenShare = participantPerms.canPublishScreen;
}
}
if (recordingPerms) {
// Apply recording permissions
features.canDeleteRecordings = recordingPerms.canDeleteRecordings;
features.canRetrieveRecordings = recordingPerms.canRetrieveRecordings;
}
// Apply role-based configurations
if (role) {
features.canModerateRoom = role === ParticipantRole.MODERATOR;
features.canManageRecordings = role === ParticipantRole.MODERATOR;
features.canAccessConsole = role === ParticipantRole.MODERATOR;
}
this.log.d('Calculated features', features);
return features;
}
/**
* Loads initial preferences from services
*/
async initializeConfiguration(): Promise<void> {
try {
this.log.d('Initializing feature configuration');
// Load global preferences if available
// (this will depend on your GlobalPreferencesService implementation)
} catch (error) {
this.log.e('Error initializing feature configuration', error);
}
}
/**
* Resets all configurations to their initial values
*/
reset(): void {
this.globalPreferencesSubject.next(null);
this.roomPreferencesSubject.next(null);
this.participantPermissionsSubject.next(null);
this.participantRoleSubject.next(null);
}
}

View File

@ -3,6 +3,7 @@ import { MeetRoomPreferences } from '@lib/typings/ce';
import { LoggerService } from 'openvidu-components-angular';
import { HttpService } from '../http/http.service';
import { MeetRoom, MeetRoomOptions } from 'projects/shared-meet-components/src/lib/typings/ce/room';
import { FeatureConfigurationService } from '../feature-configuration/feature-configuration.service';
@Injectable({
providedIn: 'root'
@ -12,7 +13,8 @@ export class RoomService {
protected roomPreferences: MeetRoomPreferences | undefined;
constructor(
protected loggerService: LoggerService,
protected httpService: HttpService
protected httpService: HttpService,
protected featureConfService: FeatureConfigurationService
) {
this.log = this.loggerService.get('OpenVidu Meet - RoomService');
}
@ -39,14 +41,23 @@ export class RoomService {
return this.httpService.getRoom(roomId);
}
async getRoomPreferences(): Promise<MeetRoomPreferences> {
if (!this.roomPreferences) {
this.log.d('Room preferences not found, fetching from server');
// Fetch the room preferences from the server
this.roomPreferences = await this.httpService.getRoomPreferences();
async loadPreferences(roomId: string, forceUpdate: boolean = false): Promise<MeetRoomPreferences> {
if (this.roomPreferences && !forceUpdate) {
this.log.d('Returning cached room preferences');
return this.roomPreferences;
}
return this.roomPreferences;
this.log.d('Fetching room preferences from server');
try {
const room = await this.getRoom(roomId);
this.roomPreferences = room.preferences! as MeetRoomPreferences;
this.featureConfService.setRoomPreferences(this.roomPreferences);
console.log('Room preferences loaded:', this.roomPreferences);
return this.roomPreferences;
} catch (error) {
this.log.e('Error loading room preferences', error);
throw new Error('Failed to load room preferences');
}
}
/**