frontend: enhance feature configuration management and update video room component to use async feature flags
This commit is contained in:
parent
acd0cb9b3d
commit
9e45716397
@ -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"
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user