From 04b8b741e24d0c5bcf586e7a6be5553458de631a Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Tue, 22 Jul 2025 18:17:57 +0200 Subject: [PATCH] ov-components: Refactor videoconference component to use centralized state management --- .../videoconference.component.html | 14 +- .../videoconference.component.ts | 182 ++++++++++++------ .../lib/models/videoconference-state.model.ts | 98 ++++++++++ 3 files changed, 233 insertions(+), 61 deletions(-) create mode 100644 openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/videoconference-state.model.ts diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.html index 166a0a4e..e98f47cb 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.html @@ -1,16 +1,16 @@
-
- +
+ {{ 'PREJOIN.PREPARING' | translate }}
-
+
-
+
error - {{ errorMessage }} + {{ componentState.error?.message }}
-
+
(); private log: ILogger; private latestParticipantName: string | undefined; + // Expose constants to template + get spinnerDiameter(): number { + return VideoconferenceComponent.SPINNER_DIAMETER; + } + + /** + * @internal + * Updates the component state + */ + private updateComponentState(newState: Partial): void { + this.componentState = { ...this.componentState, ...newState }; + this.log.d(`State updated to: ${this.componentState.state}`, this.componentState); + } + + /** + * @internal + * Checks if user has initiated the join process + */ + private hasUserInitiatedJoin(): boolean { + return ( + this.componentState.state === VideoconferenceState.JOINING || + this.componentState.state === VideoconferenceState.READY_TO_CONNECT || + this.componentState.state === VideoconferenceState.CONNECTED + ); + } + /** * @internal */ @@ -426,6 +448,17 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { private libService: OpenViduComponentsConfigService ) { this.log = this.loggerSrv.get('VideoconferenceComponent'); + + // Initialize state + this.updateComponentState({ + state: VideoconferenceState.INITIALIZING, + showPrejoin: true, + isRoomReady: false, + wasPrejoinShown: false, + isLoading: true, + error: { hasError: false } + }); + this.subscribeToVideconferenceDirectives(); } @@ -441,7 +474,11 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { ngAfterViewInit() { this.addMaterialIconsIfNeeded(); this.setupTemplates(); - this.deviceSrv.initializeDevices().then(() => (this.loading = false)); + this.deviceSrv.initializeDevices().then(() => { + this.updateComponentState({ + isLoading: false + }); + }); } /** @@ -449,10 +486,10 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { */ private addMaterialIconsIfNeeded(): void { //Add material icons to the page if not already present - const existingLink = document.querySelector('link[href*="Material+Symbols+Outlined"]'); + const existingLink = document.querySelector(VideoconferenceComponent.MATERIAL_ICONS_SELECTOR); if (!existingLink) { const link = document.createElement('link'); - link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=background_replace,keep_off'; + link.href = VideoconferenceComponent.MATERIAL_ICONS_URL; link.rel = 'stylesheet'; document.head.appendChild(link); } @@ -630,17 +667,23 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { try { // Mark that user has initiated the join process - this.hasUserInitiatedJoin = true; + this.updateComponentState({ + state: VideoconferenceState.JOINING, + wasPrejoinShown: this.componentState.showPrejoin + }); // Always initialize the room when ready to join this.openviduService.initRoom(); const participantName = this.latestParticipantName; - if (this.isRoomReady) { + if (this.componentState.isRoomReady) { // Room is ready, hide prejoin and proceed this.log.d('Room is ready, proceeding to join'); - this.showPrejoin = false; + this.updateComponentState({ + state: VideoconferenceState.READY_TO_CONNECT, + showPrejoin: false + }); } else { // Room not ready, request token if we have a participant name if (participantName) { @@ -651,26 +694,34 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { } } - const wasPrejoinShown = this.showPrejoin; - // Emit onReadyToJoin event only if prejoin page was actually shown // This ensures the event semantics are correct - if (wasPrejoinShown) { + if (this.componentState.wasPrejoinShown) { this.log.d('Emitting onReadyToJoin event (prejoin was shown)'); this.onReadyToJoin.emit(); } } catch (error) { this.log.e('Error during ready to join process', error); - // Could emit an error event or handle gracefully based on requirements + this.updateComponentState({ + state: VideoconferenceState.ERROR, + error: { + hasError: true, + message: 'Error during ready to join process' + } + }); } } /** * @internal */ _onParticipantLeft(event: ParticipantLeftEvent) { - this.isRoomReady = false; - // Reset join initiation flag to allow prejoin to show again if needed - this.hasUserInitiatedJoin = false; + // Reset to disconnected state to allow prejoin to show again if needed + this.updateComponentState({ + state: VideoconferenceState.DISCONNECTED, + isRoomReady: false, + showPrejoin: this.libService.showPrejoin() + }); + this.onParticipantLeft.emit(event); } @@ -685,20 +736,34 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { const livekitUrl = this.libService.getLivekitUrl(); this.openviduService.initializeAndSetToken(token, livekitUrl); this.log.d('Token has been successfully set. Room is ready to join'); - this.isRoomReady = true; // Only update showPrejoin if user hasn't initiated join process yet // This prevents prejoin from showing again after user clicked join - if (!this.hasUserInitiatedJoin) { - this.showPrejoin = this.libService.showPrejoin(); + if (!this.hasUserInitiatedJoin()) { + this.updateComponentState({ + state: VideoconferenceState.PREJOIN_SHOWN, + isRoomReady: true, + showPrejoin: this.libService.showPrejoin() + }); } else { // User has initiated join, proceed to hide prejoin and continue this.log.d('User has initiated join, hiding prejoin and proceeding'); - this.showPrejoin = false; + this.updateComponentState({ + state: VideoconferenceState.READY_TO_CONNECT, + isRoomReady: true, + showPrejoin: false + }); } } catch (error) { this.log.e('Error trying to set token', error); - this._tokenError = error; + this.updateComponentState({ + state: VideoconferenceState.ERROR, + error: { + hasError: true, + message: 'Error setting token', + tokenError: error + } + }); } }); @@ -706,16 +771,26 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { if (!error) return; this.log.e('Token error received', error); - this._tokenError = error; + this.updateComponentState({ + state: VideoconferenceState.ERROR, + error: { + hasError: true, + message: 'Token error', + tokenError: error + } + }); - if (!this.showPrejoin) { + if (!this.componentState.showPrejoin) { this.actionService.openDialog(error.name, error.message, false); } }); this.libService.prejoin$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { - this.showPrejoin = value; - if (!this.showPrejoin) { + this.updateComponentState({ + showPrejoin: value + }); + + if (!value) { // Emit token ready if the prejoin page won't be shown // Ensure we have a participant name before proceeding with the join @@ -747,10 +822,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit { } this._onReadyToJoin(); } - }, 1000); + }, VideoconferenceComponent.PARTICIPANT_NAME_TIMEOUT_MS); } } - // this.cd.markForCheck(); }); this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => { diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/videoconference-state.model.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/videoconference-state.model.ts new file mode 100644 index 00000000..8389f667 --- /dev/null +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/videoconference-state.model.ts @@ -0,0 +1,98 @@ +/** + * Enum representing the possible states of the videoconference component + */ +export enum VideoconferenceState { + /** + * Initial state when the component is loading + */ + INITIALIZING = 'INITIALIZING', + + /** + * Prejoin page is being shown to the user + */ + PREJOIN_SHOWN = 'PREJOIN_SHOWN', + + /** + * User has initiated the join process, waiting for token + */ + JOINING = 'JOINING', + + /** + * Token received and room is ready to connect + */ + READY_TO_CONNECT = 'READY_TO_CONNECT', + + /** + * Successfully connected to the room + */ + CONNECTED = 'CONNECTED', + + /** + * Disconnected from the room + */ + DISCONNECTED = 'DISCONNECTED', + + /** + * Error state + */ + ERROR = 'ERROR' +} + +/** + * Interface representing the state information of the videoconference component + */ +export interface VideoconferenceStateInfo { + /** + * Current state of the videoconference + */ + state: VideoconferenceState; + + /** + * Whether prejoin page should be visible + */ + showPrejoin: boolean; + + /** + * Whether room is ready for connection + */ + isRoomReady: boolean; + + /** + * Whether user is connected to the room + */ + isConnected: boolean; + + /** + * Whether audio devices are available + */ + hasAudioDevices: boolean; + + /** + * Whether video devices are available + */ + hasVideoDevices: boolean; + + /** + * Whether user has initiated the join process + */ + hasUserInitiatedJoin: boolean; + + /** + * Whether prejoin was shown to the user at least once + */ + wasPrejoinShown: boolean; + + /** + * Whether the component is in loading state + */ + isLoading: boolean; + + /** + * Error information if any + */ + error?: { + hasError: boolean; + message?: string; + tokenError?: any; + }; +}