From b92630ecd8f2843987eb5d3901ae445e39b98b4b Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Wed, 30 Apr 2025 12:12:20 +0200 Subject: [PATCH] ov-components: enhance participant disconnection handling with reasons and refactor disconnect logic --- .../components/session/session.component.ts | 73 ++++++++++++++----- .../components/toolbar/toolbar.component.ts | 16 ++-- .../src/lib/models/participant.model.ts | 19 ++++- .../lib/services/openvidu/openvidu.service.ts | 32 +++----- 4 files changed, 90 insertions(+), 50 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts index 2ad7501a..d4bfb7e0 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts @@ -46,7 +46,7 @@ import { RoomEvent, Track } from 'livekit-client'; -import { ParticipantLeftEvent, ParticipantModel } from '../../models/participant.model'; +import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; /** * @internal @@ -79,7 +79,8 @@ export class SessionComponent implements OnInit, OnDestroy { @Output() onRoomReconnected: EventEmitter = new EventEmitter(); /** - * Provides event notifications that fire when Room is disconnected for the local participant. + * Provides event notifications that fire when participant is disconnected from Room. + * @deprecated Use onParticipantLeft instead */ @Output() onRoomDisconnected: EventEmitter = new EventEmitter(); @@ -129,7 +130,7 @@ export class SessionComponent implements OnInit, OnDestroy { @HostListener('window:beforeunload') beforeunloadHandler() { - this.disconnectRoom(); + this.disconnectRoom(ParticipantLeftReason.BROWSER_UNLOAD); } @HostListener('window:resize') @@ -185,6 +186,7 @@ export class SessionComponent implements OnInit, OnDestroy { } async ngOnInit() { + this.shouldDisconnectRoomWhenComponentIsDestroyed = true; this.room = this.openviduService.getRoom(); // this.subscribeToCaptionLanguage(); @@ -208,13 +210,6 @@ export class SessionComponent implements OnInit, OnDestroy { } try { await this.participantService.connect(); - this.openviduService.setDisconnectCallback(() => { - const event: ParticipantLeftEvent = { - roomName: this.openviduService.getRoomName(), - participantName: this.participantService.getLocalParticipant()?.identity || '' - }; - this.onParticipantLeft.emit(event); - }); // Send room created after participant connect for avoiding to send incomplete room payload this.onRoomCreated.emit(this.room); this.cd.markForCheck(); @@ -233,7 +228,7 @@ export class SessionComponent implements OnInit, OnDestroy { async ngOnDestroy() { if (this.shouldDisconnectRoomWhenComponentIsDestroyed) { - await this.disconnectRoom(); + await this.disconnectRoom(ParticipantLeftReason.LEAVE); } if (this.room) this.room.removeAllListeners(); this.participantService.clear(); @@ -243,10 +238,16 @@ export class SessionComponent implements OnInit, OnDestroy { // if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe(); } - async disconnectRoom() { + async disconnectRoom(reason: ParticipantLeftReason) { // Mark session as disconnected for avoiding to do it again in ngOnDestroy this.shouldDisconnectRoomWhenComponentIsDestroyed = false; - await this.openviduService.disconnectRoom(); + await this.openviduService.disconnectRoom(() => { + this.onParticipantLeft.emit({ + roomName: this.openviduService.getRoomName(), + participantName: this.participantService.getLocalParticipant()?.identity || '', + reason + }); + }); } private subscribeToTogglingMenu() { @@ -483,14 +484,46 @@ export class SessionComponent implements OnInit, OnDestroy { }); this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => { - if (reason === DisconnectReason.SERVER_SHUTDOWN) { - this.log.e('Room Disconnected', reason); - this.actionService.openConnectionDialog( - this.translateService.translate('ERRORS.CONNECTION'), - this.translateService.translate('ERRORS.RECONNECT') - ); - this.onRoomDisconnected.emit(); + const participantLeftEvent: ParticipantLeftEvent = { + roomName: this.openviduService.getRoomName(), + participantName: this.participantService.getLocalParticipant()?.identity || '', + reason: ParticipantLeftReason.NETWORK_DISCONNECT + }; + const messageErrorKey = 'ERRORS.DISCONNECT'; + let descriptionErrorKey = 'ERRORS.NETWORK_DISCONNECT'; + + switch (reason) { + case DisconnectReason.CLIENT_INITIATED: + // Skip disconnect reason if the user has left the room + return; + case DisconnectReason.DUPLICATE_IDENTITY: + participantLeftEvent.reason = ParticipantLeftReason.DUPLICATE_IDENTITY; + descriptionErrorKey = 'ERRORS.DUPLICATE_IDENTITY'; + break; + case DisconnectReason.SERVER_SHUTDOWN: + descriptionErrorKey = 'ERRORS.SERVER_SHUTDOWN'; + participantLeftEvent.reason = ParticipantLeftReason.SERVER_SHUTDOWN; + break; + case DisconnectReason.PARTICIPANT_REMOVED: + participantLeftEvent.reason = ParticipantLeftReason.PARTICIPANT_REMOVED; + descriptionErrorKey = 'ERRORS.PARTICIPANT_REMOVED'; + break; + case DisconnectReason.ROOM_DELETED: + participantLeftEvent.reason = ParticipantLeftReason.ROOM_DELETED; + descriptionErrorKey = 'ERRORS.ROOM_DELETED'; + break; + case DisconnectReason.SIGNAL_CLOSE: + participantLeftEvent.reason = ParticipantLeftReason.SIGNAL_CLOSE; + descriptionErrorKey = 'ERRORS.SIGNAL_CLOSE'; + break; } + this.log.e('Room Disconnected', participantLeftEvent.reason); + this.onParticipantLeft.emit(participantLeftEvent); + this.onRoomDisconnected.emit(); + this.actionService.openDialog( + this.translateService.translate(messageErrorKey), + this.translateService.translate(descriptionErrorKey) + ); // await this.disconnectRoom(); }); } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/toolbar/toolbar.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/toolbar/toolbar.component.ts index c96e4894..cfd4db09 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/toolbar/toolbar.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/toolbar/toolbar.component.ts @@ -46,7 +46,7 @@ import { RecordingService } from '../../services/recording/recording.service'; import { StorageService } from '../../services/storage/storage.service'; import { TranslateService } from '../../services/translate/translate.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; -import { ParticipantLeftEvent, ParticipantModel } from '../../models/participant.model'; +import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; import { Room, RoomEvent } from 'livekit-client'; import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model'; @@ -512,16 +512,18 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit { } /** + * The participant leaves the room voluntarily. * @ignore */ async disconnect() { - const event: ParticipantLeftEvent = { - roomName: this.openviduService.getRoomName(), - participantName: this.participantService.getLocalParticipant()?.identity || '' - }; try { - await this.openviduService.disconnectRoom(); - this.onParticipantLeft.emit(event); + await this.openviduService.disconnectRoom(() => + this.onParticipantLeft.emit({ + roomName: this.openviduService.getRoomName(), + participantName: this.participantService.getLocalParticipant()?.identity || '', + reason: ParticipantLeftReason.LEAVE + }) + ); } catch (error) { this.log.e('There was an error disconnecting:', error.code, error.message); this.actionService.openDialog(this.translateService.translate('ERRORS.DISCONNECT'), error?.error || error?.message || error); diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts index 629b09e5..8d693de8 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts @@ -1,7 +1,6 @@ import { DeviceType } from './device.model'; import { AudioCaptureOptions, - DataPacket_Kind, DataPublishOptions, LocalParticipant, LocalTrack, @@ -19,8 +18,26 @@ import { export interface ParticipantLeftEvent { roomName: string; participantName: string; + reason: ParticipantLeftReason; } +export enum ParticipantLeftReason { + // User-initiated disconnections + LEAVE = 'LEAVE', // The participant left the room voluntarily + BROWSER_UNLOAD = 'browser_unload', // The participant was disconnected due to a browser unload event + + // Network-related disconnections + NETWORK_DISCONNECT = 'network_disconnect', // The participant was disconnected due to a network error + SIGNAL_CLOSE = 'websocket_closed', // The participant was disconnected due to a websocket error + + // Server-initiated disconnections + SERVER_SHUTDOWN = 'server_shutdown', // The server was shut down + PARTICIPANT_REMOVED = 'participant_removed', // The participant was removed from the room + ROOM_DELETED = 'room_deleted', // The room was deleted + + // Permission/policy-based disconnections + DUPLICATE_IDENTITY = 'duplicate_identity' // The participant was disconnected due to a duplicate identity +} /** * Interface that defines the properties of the participant track publication. */ diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts index 279e35a2..76b3e0ce 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts @@ -38,7 +38,6 @@ export class OpenViduService { private localTracks: LocalTrack[] = []; private livekitToken = ''; private livekitUrl = ''; - private disconnectCallback: () => void; private log: ILogger; /** @@ -102,33 +101,22 @@ export class OpenViduService { } /** - * Disconnects local participant from the room + * Disconnects from the current room. + * + * This method will check if there's an active connection to a room before attempting to disconnect. + * If the room is connected, it will perform the disconnection and call the optional callback function. + * + * @param callback - Optional function to be executed after a successful disconnection + * @returns A Promise that resolves once the disconnection is complete */ - async disconnectRoom(): Promise { + async disconnectRoom(callback?: () => void): Promise { if (this.isRoomConnected()) { - this.log.d('Disconnecting room'); + this.log.d('Disconnecting from room'); await this.room.disconnect(); - if (this.disconnectCallback) { - this.disconnectCallback(); - } + if (callback) callback(); } } - /** - * Sets a callback function that triggers when a participant is disconnected - * from the session using the OpenViduService.disconnectRoom() method. - * - * This is particularly useful in cases where the disconnection occurs externally, - * outside of this component, ensuring that the parent component is notified - * even when the service is used directly. - * - * @param callback - The function to be executed upon disconnection. - * @internal - */ - setDisconnectCallback(callback: () => void): void { - this.disconnectCallback = callback; - } - /** * @returns Room instance */