diff --git a/openvidu-testapp/src/app/components/dialogs/events-dialog/events-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/events-dialog/events-dialog.component.ts index 996d95585..1ab109bf0 100644 --- a/openvidu-testapp/src/app/components/dialogs/events-dialog/events-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/events-dialog/events-dialog.component.ts @@ -6,40 +6,49 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatDividerModule } from '@angular/material/divider'; import { MatButtonModule } from '@angular/material/button'; +export interface EventGroup { + label: string; + eventCollection: Map; + eventArray: string[]; + checkAll: boolean; +} @Component({ selector: 'app-events-dialog', template: `

{{target}} events

- ALL - -
-
- @for (event of eventArray | slice:0:(eventArray.length/2); track event) { -
- {{event}} - -
- } + @for (group of eventGroups; track group.label) { +

{{group.label}}

+ ALL + +
+
+ @for (event of group.eventArray | slice:0:Math.ceil(group.eventArray.length/2); track event) { +
+ {{event}} + +
+ } +
+
+ @for (event of group.eventArray | slice:Math.ceil(group.eventArray.length/2):group.eventArray.length; track event) { +
+ {{event}} + +
+ } +
-
- @for (event of eventArray | slice:(eventArray.length/2 + 1):(eventArray.length); track event) { -
- {{event}} - -
- } -
-
+ } @@ -49,34 +58,50 @@ import { MatButtonModule } from '@angular/material/button'; 'mat-dialog-content { display: inline; }', 'mat-divider { margin-top: 5px; margin-bottom: 5px; }', '.col-50 {flex-basis: 50%; box-sizing: border-box; padding-left: 20px; }', - '.toggle { }' + '.toggle { }', + '.group-label { margin-top: 15px; margin-bottom: 5px; }', + '.group-label:first-child { margin-top: 0; }' ], imports: [SlicePipe, FormsModule, MatDialogModule, MatSlideToggleModule, MatDividerModule, MatButtonModule], }) export class EventsDialogComponent { + Math = Math; target = ''; - checkAll = true; - eventCollection: Map; - eventArray: string[]; + eventGroups: EventGroup[] = []; private dialogData = inject(MAT_DIALOG_DATA); constructor(public dialogRef: MatDialogRef) { const data = this.dialogData; this.target = data.target; - this.eventCollection = data.eventCollection; - this.eventArray = Array.from(this.eventCollection.keys()); + if (data.eventGroups) { + this.eventGroups = data.eventGroups.map((g: { label: string; eventCollection: Map }) => ({ + label: g.label, + eventCollection: g.eventCollection, + eventArray: Array.from(g.eventCollection.keys()), + checkAll: Array.from(g.eventCollection.values()).every(v => v), + })); + } else { + // Backward compatibility: single eventCollection + const eventCollection = data.eventCollection; + this.eventGroups = [{ + label: data.target, + eventCollection, + eventArray: Array.from(eventCollection.keys()), + checkAll: Array.from(eventCollection.values()).every(v => v), + }]; + } } - updateAll() { - this.eventCollection.forEach((value: boolean, key: string) => { - this.eventCollection.set(key, this.checkAll); + updateAll(group: EventGroup) { + group.eventCollection.forEach((_value: boolean, key: string) => { + group.eventCollection.set(key, group.checkAll); }); } - toggleEvent(event: any) { - this.eventCollection.set(event.source.name, event.checked); + toggleEvent(event: any, group: EventGroup) { + group.eventCollection.set(event.source.name, event.checked); } } diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html index 6efb264b7..893c259d1 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html @@ -27,7 +27,7 @@ cloud_circle @@ -89,10 +89,15 @@
+ [index]="index" [participantEvents]="participantEvents" [trackEvents]="trackEvents" + [earlyParticipantEvents]="earlyParticipantEvents" [earlyParticipantListeners]="earlyParticipantListeners" + [earlyTrackEvents]="earlyTrackEvents" [earlyTrackListeners]="earlyTrackListeners"> @for (participant of room.remoteParticipants | keyvalue; track participant) { } diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts index 0651817dc..45f4df757 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts @@ -28,6 +28,7 @@ import { LocalVideoTrack, MediaDeviceFailure, Participant, + ParticipantEvent, RemoteAudioTrack, RemoteDataTrack, RemoteParticipant, @@ -42,6 +43,7 @@ import { SubscriptionError, TextStreamReader, Track, + TrackEvent, TrackPublication, TrackPublishOptions, } from 'livekit-client'; @@ -58,6 +60,11 @@ import { InfoDialogComponent } from '../dialogs/info-dialog/info-dialog.componen import { ParticipantComponent } from '../participant/participant.component'; import { RoomEventCallbacks } from 'node_modules/livekit-client/dist/src/room/Room'; import PCTransport from 'node_modules/livekit-client/dist/src/room/PCTransport'; +import { + registerParticipantEventListeners, + registerTrackEventListeners, + removeAllManagedListeners, +} from 'src/app/utils/event-listener-utils'; @Component({ selector: 'app-openvidu-instance', @@ -74,6 +81,18 @@ export class OpenviduInstanceComponent { room?: Room; roomEvents: Map = new Map(); + participantEvents: Map = new Map(); + trackEvents: Map = new Map(); + + private roomEventListeners: Map void> = new Map(); + + // Early event registration: buffers events for participant/track components + // that don't yet exist when the SDK fires events during connect() + earlyParticipantEvents: Map = new Map(); + earlyParticipantListeners: Map void>> = new Map(); + earlyTrackEvents: Map = new Map(); + earlyTrackListeners: Map void>> = new Map(); + private earlyParticipantConnectedListener: ((...args: any[]) => void) | undefined; roomName: string = 'TestRoom'; participantName: string = 'TestParticipant'; @@ -142,6 +161,14 @@ export class OpenviduInstanceComponent { this.roomEvents.set(RoomEvent[event as keyof typeof RoomEvent], true); this.roomEvents.set(RoomEvent.ActiveSpeakersChanged, false); } + for (let event of Object.keys(ParticipantEvent)) { + this.participantEvents.set(ParticipantEvent[event as keyof typeof ParticipantEvent], true); + this.participantEvents.set(ParticipantEvent.IsSpeakingChanged, false); + } + for (let event of Object.keys(TrackEvent)) { + this.trackEvents.set(TrackEvent[event as keyof typeof TrackEvent], true); + this.trackEvents.set(TrackEvent.TimeSyncUpdate, false); + } this.participantName += this.index; if (this.roomConf.startSession) { const token = await this.roomApiService.createToken( @@ -177,6 +204,16 @@ export class OpenviduInstanceComponent { this.room = new Room(this.roomOptions); (window as any)['room_' + this.index] = this.room; + // Register early participant event listeners on local participant BEFORE connect + this.registerEarlyParticipantListeners(this.room.localParticipant); + + // Register early participant event listeners on remote participants as they connect + // This fires during connect() for participants already in the room + this.earlyParticipantConnectedListener = (participant: RemoteParticipant) => { + this.registerEarlyParticipantListeners(participant); + }; + this.room.addListener(RoomEvent.ParticipantConnected, this.earlyParticipantConnectedListener as any); + this.setupRoomEventListeners(new Map(), true); // connect to room @@ -216,6 +253,19 @@ export class OpenviduInstanceComponent { } } + private registerRoomListener(event: RoomEvent, listener: (...args: any[]) => void) { + this.room!.addListener(event, listener as any); + this.roomEventListeners.set(event, listener); + } + + private unregisterRoomListener(event: RoomEvent | string) { + const existing = this.roomEventListeners.get(event as string); + if (existing) { + this.room?.removeListener(event as RoomEvent, existing as any); + this.roomEventListeners.delete(event as string); + } + } + setupRoomEventListeners(oldValues: Map, firstTime: boolean) { // This is a link to the complete list of Room events let callbacks: RoomEventCallbacks; @@ -226,9 +276,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.Connected) !== oldValues.get(RoomEvent.Connected) ) { - this.room?.removeAllListeners(RoomEvent.Connected); + this.unregisterRoomListener(RoomEvent.Connected); if (this.roomEvents.get(RoomEvent.Connected)) { - this.room!.on(RoomEvent.Connected, () => { + this.registerRoomListener(RoomEvent.Connected, () => { this.updateEventList(RoomEvent.Connected, {}, ''); this.room!.remoteParticipants.forEach( (remoteParticipant: RemoteParticipant) => { @@ -251,22 +301,35 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.Reconnecting) !== oldValues.get(RoomEvent.Reconnecting) ) { - this.room?.removeAllListeners(RoomEvent.Reconnecting); + this.unregisterRoomListener(RoomEvent.Reconnecting); if (this.roomEvents.get(RoomEvent.Reconnecting)) { - this.room!.on(RoomEvent.Reconnecting, () => { + this.registerRoomListener(RoomEvent.Reconnecting, () => { this.updateEventList(RoomEvent.Reconnecting, {}, ''); }); } } + if ( + firstTime || + this.roomEvents.get(RoomEvent.SignalReconnecting) !== + oldValues.get(RoomEvent.SignalReconnecting) + ) { + this.unregisterRoomListener(RoomEvent.SignalReconnecting); + if (this.roomEvents.get(RoomEvent.SignalReconnecting)) { + this.registerRoomListener(RoomEvent.SignalReconnecting, () => { + this.updateEventList(RoomEvent.SignalReconnecting, {}, ''); + }); + } + } + if ( firstTime || this.roomEvents.get(RoomEvent.Reconnected) !== oldValues.get(RoomEvent.Reconnected) ) { - this.room?.removeAllListeners(RoomEvent.Reconnected); + this.unregisterRoomListener(RoomEvent.Reconnected); if (this.roomEvents.get(RoomEvent.Reconnected)) { - this.room!.on(RoomEvent.Reconnected, () => { + this.registerRoomListener(RoomEvent.Reconnected, () => { this.updateEventList(RoomEvent.Reconnected, {}, ''); }); } @@ -277,9 +340,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.Disconnected) !== oldValues.get(RoomEvent.Disconnected) ) { - this.room?.removeAllListeners(RoomEvent.Disconnected); + this.unregisterRoomListener(RoomEvent.Disconnected); if (this.roomEvents.get(RoomEvent.Disconnected)) { - this.room!.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => { + this.registerRoomListener(RoomEvent.Disconnected, (reason?: DisconnectReason) => { this.updateEventList( RoomEvent.Disconnected, {}, @@ -294,9 +357,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ConnectionStateChanged) !== oldValues.get(RoomEvent.ConnectionStateChanged) ) { - this.room?.removeAllListeners(RoomEvent.ConnectionStateChanged); + this.unregisterRoomListener(RoomEvent.ConnectionStateChanged); if (this.roomEvents.get(RoomEvent.ConnectionStateChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ConnectionStateChanged, (state: ConnectionState) => { this.updateEventList( @@ -309,14 +372,31 @@ export class OpenviduInstanceComponent { } } + if ( + firstTime || + this.roomEvents.get(RoomEvent.Moved) !== + oldValues.get(RoomEvent.Moved) + ) { + this.unregisterRoomListener(RoomEvent.Moved); + if (this.roomEvents.get(RoomEvent.Moved)) { + this.registerRoomListener(RoomEvent.Moved, (name: string) => { + this.updateEventList( + RoomEvent.Moved, + { name }, + `moved to room: ${name}` + ); + }); + } + } + if ( firstTime || this.roomEvents.get(RoomEvent.MediaDevicesChanged) !== oldValues.get(RoomEvent.MediaDevicesChanged) ) { - this.room?.removeAllListeners(RoomEvent.MediaDevicesChanged); + this.unregisterRoomListener(RoomEvent.MediaDevicesChanged); if (this.roomEvents.get(RoomEvent.MediaDevicesChanged)) { - this.room!.on(RoomEvent.MediaDevicesChanged, () => { + this.registerRoomListener(RoomEvent.MediaDevicesChanged, () => { this.updateEventList(RoomEvent.MediaDevicesChanged, {}, ''); }); } @@ -327,9 +407,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ParticipantConnected) !== oldValues.get(RoomEvent.ParticipantConnected) ) { - this.room?.removeAllListeners(RoomEvent.ParticipantConnected); + this.unregisterRoomListener(RoomEvent.ParticipantConnected); if (this.roomEvents.get(RoomEvent.ParticipantConnected)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ParticipantConnected, (participant: RemoteParticipant) => { this.updateEventList( @@ -347,9 +427,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ParticipantActive) !== oldValues.get(RoomEvent.ParticipantActive) ) { - this.room?.removeAllListeners(RoomEvent.ParticipantActive); + this.unregisterRoomListener(RoomEvent.ParticipantActive); if (this.roomEvents.get(RoomEvent.ParticipantActive)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ParticipantActive, (participant: Participant) => { this.updateEventList( @@ -367,9 +447,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ParticipantDisconnected) !== oldValues.get(RoomEvent.ParticipantDisconnected) ) { - this.room?.removeAllListeners(RoomEvent.ParticipantDisconnected); + this.unregisterRoomListener(RoomEvent.ParticipantDisconnected); if (this.roomEvents.get(RoomEvent.ParticipantDisconnected)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => { this.updateEventList( @@ -388,9 +468,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackPublished) !== oldValues.get(RoomEvent.TrackPublished) ) { - this.room?.removeAllListeners(RoomEvent.TrackPublished); + this.unregisterRoomListener(RoomEvent.TrackPublished); if (this.roomEvents.get(RoomEvent.TrackPublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackPublished, ( publication: RemoteTrackPublication, @@ -414,9 +494,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackSubscribed) !== oldValues.get(RoomEvent.TrackSubscribed) ) { - this.room?.removeAllListeners(RoomEvent.TrackSubscribed); + this.unregisterRoomListener(RoomEvent.TrackSubscribed); if (this.roomEvents.get(RoomEvent.TrackSubscribed)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackSubscribed, ( track: RemoteTrack, @@ -453,9 +533,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackSubscriptionFailed) !== oldValues.get(RoomEvent.TrackSubscriptionFailed) ) { - this.room?.removeAllListeners(RoomEvent.TrackSubscriptionFailed); + this.unregisterRoomListener(RoomEvent.TrackSubscriptionFailed); if (this.roomEvents.get(RoomEvent.TrackSubscriptionFailed)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackSubscriptionFailed, ( trackSid: string, @@ -479,9 +559,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackUnpublished) !== oldValues.get(RoomEvent.TrackUnpublished) ) { - this.room?.removeAllListeners(RoomEvent.TrackUnpublished); + this.unregisterRoomListener(RoomEvent.TrackUnpublished); if (this.roomEvents.get(RoomEvent.TrackUnpublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackUnpublished, ( publication: RemoteTrackPublication, @@ -502,9 +582,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackUnsubscribed) !== oldValues.get(RoomEvent.TrackUnsubscribed) ) { - this.room?.removeAllListeners(RoomEvent.TrackUnsubscribed); + this.unregisterRoomListener(RoomEvent.TrackUnsubscribed); if (this.roomEvents.get(RoomEvent.TrackUnsubscribed)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackUnsubscribed, ( track: Track, @@ -543,9 +623,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackMuted) !== oldValues.get(RoomEvent.TrackMuted) ) { - this.room?.removeAllListeners(RoomEvent.TrackMuted); + this.unregisterRoomListener(RoomEvent.TrackMuted); if (this.roomEvents.get(RoomEvent.TrackMuted)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackMuted, (publication: TrackPublication, participant: Participant) => { this.updateEventList( @@ -563,9 +643,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackUnmuted) !== oldValues.get(RoomEvent.TrackUnmuted) ) { - this.room?.removeAllListeners(RoomEvent.TrackUnmuted); + this.unregisterRoomListener(RoomEvent.TrackUnmuted); if (this.roomEvents.get(RoomEvent.TrackUnmuted)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackUnmuted, (publication: TrackPublication, participant: Participant) => { this.updateEventList( @@ -583,9 +663,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.LocalTrackPublished) !== oldValues.get(RoomEvent.LocalTrackPublished) ) { - this.room?.removeAllListeners(RoomEvent.LocalTrackPublished); + this.unregisterRoomListener(RoomEvent.LocalTrackPublished); if (this.roomEvents.get(RoomEvent.LocalTrackPublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.LocalTrackPublished, ( publication: LocalTrackPublication, @@ -611,9 +691,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.LocalTrackUnpublished) !== oldValues.get(RoomEvent.LocalTrackUnpublished) ) { - this.room?.removeAllListeners(RoomEvent.LocalTrackUnpublished); + this.unregisterRoomListener(RoomEvent.LocalTrackUnpublished); if (this.roomEvents.get(RoomEvent.LocalTrackUnpublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.LocalTrackUnpublished, ( publication: LocalTrackPublication, @@ -635,9 +715,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.LocalAudioSilenceDetected) !== oldValues.get(RoomEvent.LocalAudioSilenceDetected) ) { - this.room?.removeAllListeners(RoomEvent.LocalAudioSilenceDetected); + this.unregisterRoomListener(RoomEvent.LocalAudioSilenceDetected); if (this.roomEvents.get(RoomEvent.LocalAudioSilenceDetected)) { - this.room!.on( + this.registerRoomListener( RoomEvent.LocalAudioSilenceDetected, (publication: LocalTrackPublication) => { this.updateEventList( @@ -655,9 +735,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ParticipantMetadataChanged) !== oldValues.get(RoomEvent.ParticipantMetadataChanged) ) { - this.room?.removeAllListeners(RoomEvent.ParticipantMetadataChanged); + this.unregisterRoomListener(RoomEvent.ParticipantMetadataChanged); if (this.roomEvents.get(RoomEvent.ParticipantMetadataChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ParticipantMetadataChanged, (metadata: string | undefined, participant: Participant) => { this.updateEventList( @@ -675,9 +755,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ParticipantNameChanged) !== oldValues.get(RoomEvent.ParticipantNameChanged) ) { - this.room?.removeAllListeners(RoomEvent.ParticipantNameChanged); + this.unregisterRoomListener(RoomEvent.ParticipantNameChanged); if (this.roomEvents.get(RoomEvent.ParticipantNameChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ParticipantNameChanged, (name: string, participant: Participant) => { this.updateEventList( @@ -690,14 +770,37 @@ export class OpenviduInstanceComponent { } } + if ( + firstTime || + this.roomEvents.get(RoomEvent.ParticipantAttributesChanged) !== + oldValues.get(RoomEvent.ParticipantAttributesChanged) + ) { + this.unregisterRoomListener(RoomEvent.ParticipantAttributesChanged); + if (this.roomEvents.get(RoomEvent.ParticipantAttributesChanged)) { + this.registerRoomListener( + RoomEvent.ParticipantAttributesChanged, + ( + changedAttributes: Record, + participant: RemoteParticipant | LocalParticipant + ) => { + this.updateEventList( + RoomEvent.ParticipantAttributesChanged, + { changedAttributes, participant }, + `${participant.identity} ${JSON.stringify(changedAttributes)}` + ); + } + ); + } + } + if ( firstTime || this.roomEvents.get(RoomEvent.ParticipantPermissionsChanged) !== oldValues.get(RoomEvent.ParticipantPermissionsChanged) ) { - this.room?.removeAllListeners(RoomEvent.ParticipantPermissionsChanged); + this.unregisterRoomListener(RoomEvent.ParticipantPermissionsChanged); if (this.roomEvents.get(RoomEvent.ParticipantPermissionsChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ParticipantPermissionsChanged, ( prevPermissions: ParticipantPermission | undefined, @@ -722,9 +825,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ActiveSpeakersChanged) !== oldValues.get(RoomEvent.ActiveSpeakersChanged) ) { - this.room?.removeAllListeners(RoomEvent.ActiveSpeakersChanged); + this.unregisterRoomListener(RoomEvent.ActiveSpeakersChanged); if (this.roomEvents.get(RoomEvent.ActiveSpeakersChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => { this.updateEventList( @@ -746,9 +849,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.RoomMetadataChanged) !== oldValues.get(RoomEvent.RoomMetadataChanged) ) { - this.room?.removeAllListeners(RoomEvent.RoomMetadataChanged); + this.unregisterRoomListener(RoomEvent.RoomMetadataChanged); if (this.roomEvents.get(RoomEvent.RoomMetadataChanged)) { - this.room!.on(RoomEvent.RoomMetadataChanged, (metadata: string) => { + this.registerRoomListener(RoomEvent.RoomMetadataChanged, (metadata: string) => { this.updateEventList( RoomEvent.RoomMetadataChanged, { metadata }, @@ -763,9 +866,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.DataReceived) !== oldValues.get(RoomEvent.DataReceived) ) { - this.room?.removeAllListeners(RoomEvent.DataReceived); + this.unregisterRoomListener(RoomEvent.DataReceived); if (this.roomEvents.get(RoomEvent.DataReceived)) { - this.room!.on( + this.registerRoomListener( RoomEvent.DataReceived, ( payload: Uint8Array, @@ -785,14 +888,34 @@ export class OpenviduInstanceComponent { } } + if ( + firstTime || + this.roomEvents.get(RoomEvent.SipDTMFReceived) !== + oldValues.get(RoomEvent.SipDTMFReceived) + ) { + this.unregisterRoomListener(RoomEvent.SipDTMFReceived); + if (this.roomEvents.get(RoomEvent.SipDTMFReceived)) { + this.registerRoomListener( + RoomEvent.SipDTMFReceived, + (dtmf: any, participant?: RemoteParticipant) => { + this.updateEventList( + RoomEvent.SipDTMFReceived, + { dtmf, participant }, + `${participant?.identity ?? 'unknown'} ${JSON.stringify(dtmf)}` + ); + } + ); + } + } + if ( firstTime || this.roomEvents.get(RoomEvent.ConnectionQualityChanged) !== oldValues.get(RoomEvent.ConnectionQualityChanged) ) { - this.room?.removeAllListeners(RoomEvent.ConnectionQualityChanged); + this.unregisterRoomListener(RoomEvent.ConnectionQualityChanged); if (this.roomEvents.get(RoomEvent.ConnectionQualityChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ConnectionQualityChanged, (quality: ConnectionQuality, participant: Participant) => { this.updateEventList( @@ -810,9 +933,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.MediaDevicesError) !== oldValues.get(RoomEvent.MediaDevicesError) ) { - this.room?.removeAllListeners(RoomEvent.MediaDevicesError); + this.unregisterRoomListener(RoomEvent.MediaDevicesError); if (this.roomEvents.get(RoomEvent.MediaDevicesError)) { - this.room!.on(RoomEvent.MediaDevicesError, (error: Error) => { + this.registerRoomListener(RoomEvent.MediaDevicesError, (error: Error) => { this.updateEventList( RoomEvent.MediaDevicesError, { @@ -830,9 +953,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackStreamStateChanged) !== oldValues.get(RoomEvent.TrackStreamStateChanged) ) { - this.room?.removeAllListeners(RoomEvent.TrackStreamStateChanged); + this.unregisterRoomListener(RoomEvent.TrackStreamStateChanged); if (this.roomEvents.get(RoomEvent.TrackStreamStateChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackStreamStateChanged, ( publication: RemoteTrackPublication, @@ -854,11 +977,11 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackSubscriptionPermissionChanged) !== oldValues.get(RoomEvent.TrackSubscriptionPermissionChanged) ) { - this.room?.removeAllListeners( + this.unregisterRoomListener( RoomEvent.TrackSubscriptionPermissionChanged ); if (this.roomEvents.get(RoomEvent.TrackSubscriptionPermissionChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackSubscriptionPermissionChanged, ( publication: RemoteTrackPublication, @@ -880,9 +1003,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.TrackSubscriptionStatusChanged) !== oldValues.get(RoomEvent.TrackSubscriptionStatusChanged) ) { - this.room?.removeAllListeners(RoomEvent.TrackSubscriptionStatusChanged); + this.unregisterRoomListener(RoomEvent.TrackSubscriptionStatusChanged); if (this.roomEvents.get(RoomEvent.TrackSubscriptionStatusChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.TrackSubscriptionStatusChanged, ( publication: RemoteTrackPublication, @@ -904,9 +1027,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.AudioPlaybackStatusChanged) !== oldValues.get(RoomEvent.AudioPlaybackStatusChanged) ) { - this.room?.removeAllListeners(RoomEvent.AudioPlaybackStatusChanged); + this.unregisterRoomListener(RoomEvent.AudioPlaybackStatusChanged); if (this.roomEvents.get(RoomEvent.AudioPlaybackStatusChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.AudioPlaybackStatusChanged, (playing: boolean) => { this.updateEventList( @@ -919,14 +1042,34 @@ export class OpenviduInstanceComponent { } } + if ( + firstTime || + this.roomEvents.get(RoomEvent.VideoPlaybackStatusChanged) !== + oldValues.get(RoomEvent.VideoPlaybackStatusChanged) + ) { + this.unregisterRoomListener(RoomEvent.VideoPlaybackStatusChanged); + if (this.roomEvents.get(RoomEvent.VideoPlaybackStatusChanged)) { + this.registerRoomListener( + RoomEvent.VideoPlaybackStatusChanged, + (playing: boolean) => { + this.updateEventList( + RoomEvent.VideoPlaybackStatusChanged, + { playing }, + `canPlaybackVideo: ${playing}` + ); + } + ); + } + } + if ( firstTime || this.roomEvents.get(RoomEvent.SignalConnected) !== oldValues.get(RoomEvent.SignalConnected) ) { - this.room?.removeAllListeners(RoomEvent.SignalConnected); + this.unregisterRoomListener(RoomEvent.SignalConnected); if (this.roomEvents.get(RoomEvent.SignalConnected)) { - this.room!.on(RoomEvent.SignalConnected, () => { + this.registerRoomListener(RoomEvent.SignalConnected, () => { this.updateEventList(RoomEvent.SignalConnected, {}, ''); }); } @@ -937,9 +1080,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.RecordingStatusChanged) !== oldValues.get(RoomEvent.RecordingStatusChanged) ) { - this.room?.removeAllListeners(RoomEvent.RecordingStatusChanged); + this.unregisterRoomListener(RoomEvent.RecordingStatusChanged); if (this.roomEvents.get(RoomEvent.RecordingStatusChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.RecordingStatusChanged, (recording: boolean) => { this.updateEventList( @@ -957,11 +1100,11 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ParticipantEncryptionStatusChanged) !== oldValues.get(RoomEvent.ParticipantEncryptionStatusChanged) ) { - this.room?.removeAllListeners( + this.unregisterRoomListener( RoomEvent.ParticipantEncryptionStatusChanged ); if (this.roomEvents.get(RoomEvent.ParticipantEncryptionStatusChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ParticipantEncryptionStatusChanged, (encrypted: boolean, participant?: Participant) => { this.updateEventList( @@ -979,9 +1122,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.EncryptionError) !== oldValues.get(RoomEvent.EncryptionError) ) { - this.room?.removeAllListeners(RoomEvent.EncryptionError); + this.unregisterRoomListener(RoomEvent.EncryptionError); if (this.roomEvents.get(RoomEvent.EncryptionError)) { - this.room!.on(RoomEvent.EncryptionError, (error: Error) => { + this.registerRoomListener(RoomEvent.EncryptionError, (error: Error) => { this.updateEventList( RoomEvent.EncryptionError, { error: error.message }, @@ -996,9 +1139,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.DCBufferStatusChanged) !== oldValues.get(RoomEvent.DCBufferStatusChanged) ) { - this.room?.removeAllListeners(RoomEvent.DCBufferStatusChanged); + this.unregisterRoomListener(RoomEvent.DCBufferStatusChanged); if (this.roomEvents.get(RoomEvent.DCBufferStatusChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.DCBufferStatusChanged, (isLow: boolean, kind: any) => { this.updateEventList( @@ -1016,9 +1159,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.ActiveDeviceChanged) !== oldValues.get(RoomEvent.ActiveDeviceChanged) ) { - this.room?.removeAllListeners(RoomEvent.ActiveDeviceChanged); + this.unregisterRoomListener(RoomEvent.ActiveDeviceChanged); if (this.roomEvents.get(RoomEvent.ActiveDeviceChanged)) { - this.room!.on( + this.registerRoomListener( RoomEvent.ActiveDeviceChanged, (kind: MediaDeviceKind, deviceId: string) => { this.updateEventList( @@ -1036,9 +1179,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.LocalTrackSubscribed) !== oldValues.get(RoomEvent.LocalTrackSubscribed) ) { - this.room?.removeAllListeners(RoomEvent.LocalTrackSubscribed); + this.unregisterRoomListener(RoomEvent.LocalTrackSubscribed); if (this.roomEvents.get(RoomEvent.LocalTrackSubscribed)) { - this.room!.on( + this.registerRoomListener( RoomEvent.LocalTrackSubscribed, ( publication: LocalTrackPublication, @@ -1054,6 +1197,46 @@ export class OpenviduInstanceComponent { } } + if ( + firstTime || + this.roomEvents.get(RoomEvent.ChatMessage) !== + oldValues.get(RoomEvent.ChatMessage) + ) { + this.unregisterRoomListener(RoomEvent.ChatMessage); + if (this.roomEvents.get(RoomEvent.ChatMessage)) { + this.registerRoomListener( + RoomEvent.ChatMessage, + (message: any, participant?: RemoteParticipant | LocalParticipant) => { + this.updateEventList( + RoomEvent.ChatMessage, + { message, participant }, + `${participant?.identity ?? 'unknown'}: ${message.message}` + ); + } + ); + } + } + + if ( + firstTime || + this.roomEvents.get(RoomEvent.MetricsReceived) !== + oldValues.get(RoomEvent.MetricsReceived) + ) { + this.unregisterRoomListener(RoomEvent.MetricsReceived); + if (this.roomEvents.get(RoomEvent.MetricsReceived)) { + this.registerRoomListener( + RoomEvent.MetricsReceived, + (metrics: any, participant?: Participant) => { + this.updateEventList( + RoomEvent.MetricsReceived, + { metrics, participant }, + `${participant?.identity ?? 'unknown'}` + ); + } + ); + } + } + if ( firstTime || this.roomEvents.get(RoomEvent.TranscriptionReceived) !== @@ -1087,9 +1270,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.DataTrackPublished) !== oldValues.get(RoomEvent.DataTrackPublished) ) { - this.room?.removeAllListeners(RoomEvent.DataTrackPublished); + this.unregisterRoomListener(RoomEvent.DataTrackPublished); if (this.roomEvents.get(RoomEvent.DataTrackPublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.DataTrackPublished, (track: RemoteDataTrack) => { this.updateEventList( @@ -1107,9 +1290,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.DataTrackUnpublished) !== oldValues.get(RoomEvent.DataTrackUnpublished) ) { - this.room?.removeAllListeners(RoomEvent.DataTrackUnpublished); + this.unregisterRoomListener(RoomEvent.DataTrackUnpublished); if (this.roomEvents.get(RoomEvent.DataTrackUnpublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.DataTrackUnpublished, (sid: string) => { this.updateEventList( @@ -1127,9 +1310,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.LocalDataTrackPublished) !== oldValues.get(RoomEvent.LocalDataTrackPublished) ) { - this.room?.removeAllListeners(RoomEvent.LocalDataTrackPublished); + this.unregisterRoomListener(RoomEvent.LocalDataTrackPublished); if (this.roomEvents.get(RoomEvent.LocalDataTrackPublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.LocalDataTrackPublished, (track: LocalDataTrack) => { this.updateEventList( @@ -1147,9 +1330,9 @@ export class OpenviduInstanceComponent { this.roomEvents.get(RoomEvent.LocalDataTrackUnpublished) !== oldValues.get(RoomEvent.LocalDataTrackUnpublished) ) { - this.room?.removeAllListeners(RoomEvent.LocalDataTrackUnpublished); + this.unregisterRoomListener(RoomEvent.LocalDataTrackUnpublished); if (this.roomEvents.get(RoomEvent.LocalDataTrackUnpublished)) { - this.room!.on( + this.registerRoomListener( RoomEvent.LocalDataTrackUnpublished, (sid: string) => { this.updateEventList( @@ -1181,6 +1364,32 @@ export class OpenviduInstanceComponent { async disconnectRoom() { if (this.room) { + // Clean up early listeners that were never handed off + if (this.earlyParticipantConnectedListener) { + this.room.removeListener( + RoomEvent.ParticipantConnected, + this.earlyParticipantConnectedListener as any + ); + this.earlyParticipantConnectedListener = undefined; + } + for (const [key, listeners] of this.earlyParticipantListeners) { + const participant = + this.room.localParticipant.sid === key || this.room.localParticipant.identity === key + ? this.room.localParticipant + : this.room.remoteParticipants.get(key); + if (participant) { + removeAllManagedListeners(participant, listeners); + } + } + this.earlyParticipantEvents.clear(); + this.earlyParticipantListeners.clear(); + for (const [, listeners] of this.earlyTrackListeners) { + // Track listeners are cleaned up by disconnect, but clear maps + listeners.clear(); + } + this.earlyTrackEvents.clear(); + this.earlyTrackListeners.clear(); + await this.room.disconnect(); delete this.room; delete this.localTracks.audioTrack; @@ -1189,6 +1398,58 @@ export class OpenviduInstanceComponent { } } + private registerEarlyParticipantListeners(participant: Participant) { + const key = participant.sid || participant.identity; + const buffer: TestAppEvent[] = []; + this.earlyParticipantEvents.set(key, buffer); + + const listeners = registerParticipantEventListeners( + participant, + (eventType, eventContent, eventDescription) => { + if (this.participantEvents.size > 0 && !this.participantEvents.get(eventType)) return; + const event: TestAppEvent = { + eventType, + eventCategory: 'ParticipantEvent', + eventContent, + eventDescription, + }; + buffer.push(event); + this.testFeedService.pushNewEvent({ user: this.index, event }); + + // When a track becomes available during early registration, register early track listeners too + if (eventType === ParticipantEvent.TrackSubscribed && eventContent.track) { + this.registerEarlyTrackListeners(eventContent.track); + } else if (eventType === ParticipantEvent.LocalTrackPublished && eventContent.publication?.track) { + this.registerEarlyTrackListeners(eventContent.publication.track); + } + }, + this.decoder + ); + this.earlyParticipantListeners.set(key, listeners); + } + + private registerEarlyTrackListeners(track: Track) { + const key = track.sid || track.mediaStreamID; + const buffer: TestAppEvent[] = []; + this.earlyTrackEvents.set(key, buffer); + + const listeners = registerTrackEventListeners( + track, + (eventType, eventContent, eventDescription) => { + if (this.trackEvents.size > 0 && !this.trackEvents.get(eventType)) return; + const event: TestAppEvent = { + eventType, + eventCategory: 'TrackEvent', + eventContent, + eventDescription, + }; + buffer.push(event); + this.testFeedService.pushNewEvent({ user: this.index, event }); + } + ); + this.earlyTrackListeners.set(key, listeners); + } + async setCameraEnabled() { this.room!.localParticipant.setCameraEnabled(true); } @@ -1253,15 +1514,18 @@ export class OpenviduInstanceComponent { }); } - openRoomEventsDialog() { - const oldValues: Map = new Map( + openAllEventsDialog() { + const oldRoomValues: Map = new Map( JSON.parse(JSON.stringify([...this.roomEvents])) ); - const dialogRef = this.dialog.open(EventsDialogComponent, { data: { - eventCollection: this.roomEvents, - target: 'Session', + eventGroups: [ + { label: 'RoomEvent', eventCollection: this.roomEvents }, + { label: 'ParticipantEvent', eventCollection: this.participantEvents }, + { label: 'TrackEvent', eventCollection: this.trackEvents }, + ], + target: 'All', }, width: '800px', autoFocus: false, @@ -1272,9 +1536,9 @@ export class OpenviduInstanceComponent { if ( !!this.room && JSON.stringify(Array.from(this.roomEvents.entries())) !== - JSON.stringify(Array.from(oldValues.entries())) + JSON.stringify(Array.from(oldRoomValues.entries())) ) { - this.setupRoomEventListeners(oldValues, false); + this.setupRoomEventListeners(oldRoomValues, false); } }); } diff --git a/openvidu-testapp/src/app/components/participant/participant.component.html b/openvidu-testapp/src/app/components/participant/participant.component.html index e52368654..4a3fbe5c1 100644 --- a/openvidu-testapp/src/app/components/participant/participant.component.html +++ b/openvidu-testapp/src/app/components/participant/participant.component.html @@ -105,13 +105,17 @@ @for (trackPublication of participant.audioTrackPublications| keyvalue; track trackPublication) { + [localParticipant]="localParticipant" + [earlyTrackEvents]="earlyTrackEvents" [earlyTrackListeners]="earlyTrackListeners" + (newTrackEvent)="onTrackEvent($event)"> }
@for (trackPublication of participant.videoTrackPublications | keyvalue; track trackPublication) { + [localParticipant]="localParticipant" + [earlyTrackEvents]="earlyTrackEvents" [earlyTrackListeners]="earlyTrackListeners" + (newTrackEvent)="onTrackEvent($event)"> } } diff --git a/openvidu-testapp/src/app/components/participant/participant.component.ts b/openvidu-testapp/src/app/components/participant/participant.component.ts index 9f92c324b..fab800ed1 100644 --- a/openvidu-testapp/src/app/components/participant/participant.component.ts +++ b/openvidu-testapp/src/app/components/participant/participant.component.ts @@ -6,9 +6,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { MatExpansionModule } from '@angular/material/expansion'; import { AudioCaptureOptions, - ConnectionQuality, CreateLocalTracksOptions, - DataPacket_Kind, LocalAudioTrack, LocalDataTrack, LocalParticipant, @@ -23,7 +21,6 @@ import { Room, RoomEvent, ScreenShareCaptureOptions, - SubscriptionError, Track, TrackEvent, TrackPublication, @@ -33,7 +30,6 @@ import { createLocalScreenTracks, createLocalVideoTrack, } from 'livekit-client'; -import { ParticipantPermission } from 'livekit-server-sdk'; import { TestAppEvent, TestFeedService, @@ -42,7 +38,10 @@ import { OptionsDialogComponent } from '../dialogs/options-dialog/options-dialog import { VideoTrackComponent } from '../video-track/video-track.component'; import { AudioTrackComponent } from '../audio-track/audio-track.component'; import { DataTrackComponent } from '../data-track/data-track.component'; -import { ParticipantEventCallbacks } from 'node_modules/livekit-client/dist/src/room/participant/Participant'; +import { + registerParticipantEventListeners, + removeAllManagedListeners, +} from 'src/app/utils/event-listener-utils'; @Component({ selector: 'app-participant', @@ -60,6 +59,24 @@ export class ParticipantComponent { @Input() index: number; + @Input() + participantEvents: Map = new Map(); + + @Input() + trackEvents: Map = new Map(); + + @Input() + earlyParticipantEvents: Map = new Map(); + + @Input() + earlyParticipantListeners: Map void>> = new Map(); + + @Input() + earlyTrackEvents: Map = new Map(); + + @Input() + earlyTrackListeners: Map void>> = new Map(); + @Output() sendReliableDataToOneParticipant = new EventEmitter(); @@ -80,6 +97,8 @@ export class ParticipantComponent { trackPublishOptions?: TrackPublishOptions; private decoder = new TextDecoder(); + private participantEventListeners: Map void> = new Map(); + private roomListenersFromParticipant: Map void> = new Map(); private dialog = inject(MatDialog); @@ -89,6 +108,19 @@ export class ParticipantComponent { ) {} ngOnInit() { + // Drain early participant events buffered before this component existed + const key = this.participant.sid || this.participant.identity; + const earlyEvents = this.earlyParticipantEvents?.get(key); + if (earlyEvents) { + this.events.push(...earlyEvents); + this.earlyParticipantEvents.delete(key); + } + // Remove early listeners and replace with component-owned ones + const earlyListeners = this.earlyParticipantListeners?.get(key); + if (earlyListeners) { + removeAllManagedListeners(this.participant, earlyListeners); + this.earlyParticipantListeners.delete(key); + } this.setupParticipantEventListeners(); this.localParticipant = this.participant.isLocal ? (this.participant as LocalParticipant) @@ -106,7 +138,11 @@ export class ParticipantComponent { } onTrackEvent(event: TestAppEvent) { + if (this.trackEvents.size > 0 && !this.trackEvents.get(event.eventType)) { + return; + } this.events.push(event); + this.testFeedService.pushNewEvent({ user: this.index, event }); this.cdr.detectChanges(); } @@ -246,251 +282,16 @@ export class ParticipantComponent { * [ParticipantEventCallbacks] */ setupParticipantEventListeners() { - // This is a link to the complete list of Participant events - let callbacks: ParticipantEventCallbacks; - let events: ParticipantEvent; + // Remove any previous listeners + removeAllManagedListeners(this.participant, this.participantEventListeners); - this.participant - - .on( - ParticipantEvent.TrackPublished, - (publication: RemoteTrackPublication) => { - this.updateEventList( - ParticipantEvent.TrackPublished, - 'ParticipantEvent', - { publication }, - publication.source - ); - } - ) - - .on( - ParticipantEvent.TrackSubscribed, - (track: RemoteTrack, publication: RemoteTrackPublication) => { - this.updateEventList( - ParticipantEvent.TrackSubscribed, - 'ParticipantEvent', - { track, publication }, - publication.source - ); - } - ) - - .on( - ParticipantEvent.TrackSubscriptionFailed, - (trackSid: string, reason?: SubscriptionError) => { - this.updateEventList( - ParticipantEvent.TrackSubscriptionFailed, - 'ParticipantEvent', - { trackSid, reason }, - trackSid + - ' . Reason: ' + - (reason ? SubscriptionError[reason] : reason) - ); - } - ) - - .on( - ParticipantEvent.TrackUnpublished, - (publication: RemoteTrackPublication) => { - this.updateEventList( - ParticipantEvent.TrackUnpublished, - 'ParticipantEvent', - { publication }, - publication.source - ); - } - ) - - .on( - ParticipantEvent.TrackUnsubscribed, - (track: RemoteTrack, publication: RemoteTrackPublication) => { - this.updateEventList( - ParticipantEvent.TrackUnsubscribed, - 'ParticipantEvent', - { track, publication }, - track.source - ); - } - ) - - .on(ParticipantEvent.TrackMuted, (publication: TrackPublication) => { - this.updateEventList( - ParticipantEvent.TrackMuted, - 'ParticipantEvent', - { publication }, - publication.source - ); - }) - - .on(ParticipantEvent.TrackUnmuted, (publication: TrackPublication) => { - this.updateEventList( - ParticipantEvent.TrackUnmuted, - 'ParticipantEvent', - { publication }, - publication.source - ); - }) - - .on( - ParticipantEvent.LocalTrackPublished, - (publication: LocalTrackPublication) => { - this.updateEventList( - ParticipantEvent.LocalTrackPublished, - 'ParticipantEvent', - { publication }, - publication.source - ); - } - ) - - .on( - ParticipantEvent.LocalTrackUnpublished, - (publication: LocalTrackPublication) => { - this.updateEventList( - ParticipantEvent.LocalTrackUnpublished, - 'ParticipantEvent', - { publication }, - publication.source - ); - } - ) - - .on( - ParticipantEvent.ParticipantMetadataChanged, - (prevMetadata: string | undefined) => { - this.updateEventList( - ParticipantEvent.ParticipantMetadataChanged, - 'ParticipantEvent', - { prevMetadata }, - `previous: ${prevMetadata}, new: ${this.participant.metadata}` - ); - } - ) - - .on(ParticipantEvent.ParticipantNameChanged, (name: string) => { - this.updateEventList( - ParticipantEvent.ParticipantNameChanged, - 'ParticipantEvent', - { name }, - `${name}` - ); - }) - - .on( - ParticipantEvent.DataReceived, - (payload: Uint8Array, kind: DataPacket_Kind) => { - let decodedPayload = this.decoder.decode(payload); - decodedPayload += ` (kind: ${DataPacket_Kind[kind]})`; - this.updateEventList( - ParticipantEvent.DataReceived, - 'ParticipantEvent', - { payload: decodedPayload, kind }, - decodedPayload - ); - } - ) - - .on(ParticipantEvent.IsSpeakingChanged, (speaking: boolean) => { - this.updateEventList( - ParticipantEvent.IsSpeakingChanged, - 'ParticipantEvent', - { speaking }, - `${speaking}` - ); - }) - - .on( - ParticipantEvent.ConnectionQualityChanged, - (connectionQuality: ConnectionQuality) => { - this.updateEventList( - ParticipantEvent.ConnectionQualityChanged, - 'ParticipantEvent', - { connectionQuality }, - `${connectionQuality}` - ); - } - ) - - .on( - ParticipantEvent.TrackStreamStateChanged, - ( - publication: RemoteTrackPublication, - streamState: Track.StreamState - ) => { - this.updateEventList( - ParticipantEvent.TrackStreamStateChanged, - 'ParticipantEvent', - { publication, streamState }, - `${publication.source}: ${streamState}` - ); - } - ) - - .on( - ParticipantEvent.TrackSubscriptionPermissionChanged, - ( - publication: RemoteTrackPublication, - status: TrackPublication.PermissionStatus - ) => { - this.updateEventList( - ParticipantEvent.TrackSubscriptionPermissionChanged, - 'ParticipantEvent', - { publication, status }, - `${publication.source}: ${status}` - ); - } - ) - - .on(ParticipantEvent.MediaDevicesError, (error: Error) => { - this.updateEventList( - ParticipantEvent.MediaDevicesError, - 'ParticipantEvent', - { error }, - `${error.message}` - ); - }) - - .on( - ParticipantEvent.ParticipantPermissionsChanged, - (prevPermissions?: ParticipantPermission) => { - this.updateEventList( - ParticipantEvent.ParticipantPermissionsChanged, - 'ParticipantEvent', - { prevPermissions }, - `previous: ${prevPermissions}, new: ${JSON.stringify( - this.participant.permissions - )}` - ); - } - ) - - .on( - ParticipantEvent.TrackSubscriptionStatusChanged, - ( - publication: RemoteTrackPublication, - status: TrackPublication.SubscriptionStatus - ) => { - this.updateEventList( - ParticipantEvent.TrackSubscriptionStatusChanged, - 'ParticipantEvent', - { publication, status }, - `${publication.source}: ${status}` - ); - } - ) - - .on( - ParticipantEvent.LocalTrackSubscribed, - (trackPublication: LocalTrackPublication) => { - this.updateEventList( - ParticipantEvent.LocalTrackSubscribed, - 'ParticipantEvent', - { trackPublication }, - trackPublication.source - ); - } - ); + this.participantEventListeners = registerParticipantEventListeners( + this.participant, + (eventType, eventContent, eventDescription) => { + this.updateEventList(eventType, 'ParticipantEvent', eventContent, eventDescription); + }, + this.decoder + ); } updateEventList( @@ -499,6 +300,9 @@ export class ParticipantComponent { eventContent: any, eventDescription: string ) { + if (this.participantEvents.size > 0 && !this.participantEvents.get(eventType)) { + return; + } const event: TestAppEvent = { eventType, eventCategory, @@ -518,23 +322,22 @@ export class ParticipantComponent { } private setupDataTrackListeners() { - this.room.on( - RoomEvent.DataTrackPublished, - (track: RemoteDataTrack) => { - if (track.publisherIdentity === this.participant.identity) { - this.remoteDataTracks.push(track); - this.cdr.detectChanges(); - } - } - ); - this.room.on( - RoomEvent.DataTrackUnpublished, - (sid: string) => { - this.remoteDataTracks = this.remoteDataTracks.filter( - (t) => t.info.sid !== sid - ); + const publishedListener = (track: RemoteDataTrack) => { + if (track.publisherIdentity === this.participant.identity) { + this.remoteDataTracks.push(track); this.cdr.detectChanges(); } - ); + }; + this.room.addListener(RoomEvent.DataTrackPublished, publishedListener); + this.roomListenersFromParticipant.set(RoomEvent.DataTrackPublished, publishedListener as any); + + const unpublishedListener = (sid: string) => { + this.remoteDataTracks = this.remoteDataTracks.filter( + (t) => t.info.sid !== sid + ); + this.cdr.detectChanges(); + }; + this.room.addListener(RoomEvent.DataTrackUnpublished, unpublishedListener); + this.roomListenersFromParticipant.set(RoomEvent.DataTrackUnpublished, unpublishedListener as any); } } diff --git a/openvidu-testapp/src/app/components/track/track.component.ts b/openvidu-testapp/src/app/components/track/track.component.ts index 3cb8b0a94..b4121efbc 100644 --- a/openvidu-testapp/src/app/components/track/track.component.ts +++ b/openvidu-testapp/src/app/components/track/track.component.ts @@ -13,7 +13,6 @@ import { TrackEvent, LocalTrack, RemoteTrack, - TrackEventCallbacks, RemoteTrackPublication, AudioTrack, VideoTrack, @@ -22,6 +21,10 @@ import { TestAppEvent, TestFeedService, } from 'src/app/services/test-feed.service'; +import { + registerTrackEventListeners, + removeAllManagedListeners, +} from 'src/app/utils/event-listener-utils'; @Component({ selector: 'app-track', @@ -42,6 +45,12 @@ export class TrackComponent { @Input() localParticipant: LocalParticipant | undefined; + @Input() + earlyTrackEvents: Map = new Map(); + + @Input() + earlyTrackListeners: Map void>> = new Map(); + protected finalElementRefId: string = ''; private indexId: string; private trackId: string; @@ -52,6 +61,8 @@ export class TrackComponent { trackSubscribed: boolean = true; trackEnabled: boolean = true; + private trackEventListeners: Map void> = new Map(); + constructor(protected testFeedService: TestFeedService) {} @Input() set index(index: number) { @@ -62,6 +73,23 @@ export class TrackComponent { @Input() set track(track: AudioTrack | VideoTrack | undefined) { this._track = track; + // Drain early track events buffered before this component existed + if (this._track) { + const key = this._track.sid || this._track.mediaStreamID; + const earlyEvents = this.earlyTrackEvents?.get(key); + if (earlyEvents) { + for (const event of earlyEvents) { + this.newTrackEvent.emit(event as any); + } + this.earlyTrackEvents.delete(key); + } + const earlyListeners = this.earlyTrackListeners?.get(key); + if (earlyListeners) { + removeAllManagedListeners(this._track, earlyListeners); + this.earlyTrackListeners.delete(key); + } + } + this.setupTrackEventListeners(); this.trackId = `-${this.getTrackOrigin()}--${this._track?.kind}--${ @@ -115,93 +143,22 @@ export class TrackComponent { } protected setupTrackEventListeners() { - // This is a link to the complete list of Track events - let callbacks: TrackEventCallbacks; - let events: TrackEvent; + if (!this._track) return; - this._track - ?.on(TrackEvent.Message, () => { + // Clear previous listeners (in case track changed) + removeAllManagedListeners(this._track, this.trackEventListeners); + + this.trackEventListeners = registerTrackEventListeners( + this._track, + (eventType, eventContent, eventDescription) => { this.newTrackEvent.emit({ - eventType: TrackEvent.Message, + eventType, eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, + eventContent, + eventDescription, }); - }) - .on(TrackEvent.Muted, () => { - this.newTrackEvent.emit({ - eventType: TrackEvent.Muted, - eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, - }); - }) - .on(TrackEvent.Unmuted, () => { - this.newTrackEvent.emit({ - eventType: TrackEvent.Unmuted, - eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, - }); - }) - .on(TrackEvent.AudioSilenceDetected, () => { - this.newTrackEvent.emit({ - eventType: TrackEvent.AudioSilenceDetected, - eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, - }); - }) - .on(TrackEvent.Restarted, () => { - this.newTrackEvent.emit({ - eventType: TrackEvent.Restarted, - eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, - }); - }) - .on(TrackEvent.Ended, () => { - this.newTrackEvent.emit({ - eventType: TrackEvent.Ended, - eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, - }); - }) - .on(TrackEvent.VisibilityChanged, (visible: boolean) => { - this.newTrackEvent.emit({ - eventType: TrackEvent.VisibilityChanged, - eventCategory: 'TrackEvent', - eventContent: { visible, track: this._track }, - eventDescription: `${this._track!.source} is visible: ${visible}`, - }); - }) - .on(TrackEvent.VideoDimensionsChanged, (dimensions: Track.Dimensions) => { - this.newTrackEvent.emit({ - eventType: TrackEvent.VideoDimensionsChanged, - eventCategory: 'TrackEvent', - eventContent: { dimensions, track: this._track }, - eventDescription: `${this._track?.source} ${JSON.stringify( - dimensions - )}`, - }); - }) - .on(TrackEvent.UpstreamPaused, () => { - this.newTrackEvent.emit({ - eventType: TrackEvent.UpstreamPaused, - eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, - }); - }) - .on(TrackEvent.UpstreamResumed, () => { - this.newTrackEvent.emit({ - eventType: TrackEvent.UpstreamResumed, - eventCategory: 'TrackEvent', - eventContent: {}, - eventDescription: this._track!.source, - }); - }); + } + ); } protected getTrackOrigin(): string { diff --git a/openvidu-testapp/src/app/utils/event-listener-utils.ts b/openvidu-testapp/src/app/utils/event-listener-utils.ts new file mode 100644 index 000000000..8ce49c9fe --- /dev/null +++ b/openvidu-testapp/src/app/utils/event-listener-utils.ts @@ -0,0 +1,331 @@ +import { + ConnectionQuality, + DataPacket_Kind, + LocalTrackPublication, + LocalVideoTrack, + Participant, + ParticipantEvent, + RemoteTrack, + RemoteTrackPublication, + SubscriptionError, + Track, + TrackEvent, + TrackPublication, +} from 'livekit-client'; +import type { TranscriptionSegment, ChatMessage } from 'livekit-client'; +import { ParticipantPermission } from 'livekit-server-sdk'; + +export type ParticipantOnEvent = ( + eventType: ParticipantEvent, + eventContent: any, + eventDescription: string +) => void; + +export type TrackOnEvent = ( + eventType: TrackEvent, + eventContent: any, + eventDescription: string +) => void; + +/** + * Registers ALL participant event listeners on the given participant. + * Returns the listener map for later cleanup/replacement. + */ +export function registerParticipantEventListeners( + participant: Participant, + onEvent: ParticipantOnEvent, + decoder: TextDecoder +): Map void> { + const listeners = new Map void>(); + + const reg = (event: ParticipantEvent, listener: (...args: any[]) => void) => { + participant.addListener(event as any, listener as any); + listeners.set(event, listener); + }; + + reg(ParticipantEvent.TrackPublished, (publication: RemoteTrackPublication) => { + onEvent(ParticipantEvent.TrackPublished, { publication }, publication.source); + }); + + reg(ParticipantEvent.TrackSubscribed, (track: RemoteTrack, publication: RemoteTrackPublication) => { + onEvent(ParticipantEvent.TrackSubscribed, { track, publication }, publication.source); + }); + + reg(ParticipantEvent.TrackSubscriptionFailed, (trackSid: string, reason?: SubscriptionError) => { + onEvent( + ParticipantEvent.TrackSubscriptionFailed, + { trackSid, reason }, + trackSid + ' . Reason: ' + (reason ? SubscriptionError[reason] : reason) + ); + }); + + reg(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => { + onEvent(ParticipantEvent.TrackUnpublished, { publication }, publication.source); + }); + + reg(ParticipantEvent.TrackUnsubscribed, (track: RemoteTrack, publication: RemoteTrackPublication) => { + onEvent(ParticipantEvent.TrackUnsubscribed, { track, publication }, track.source); + }); + + reg(ParticipantEvent.TrackMuted, (publication: TrackPublication) => { + onEvent(ParticipantEvent.TrackMuted, { publication }, publication.source); + }); + + reg(ParticipantEvent.TrackUnmuted, (publication: TrackPublication) => { + onEvent(ParticipantEvent.TrackUnmuted, { publication }, publication.source); + }); + + reg(ParticipantEvent.LocalTrackPublished, (publication: LocalTrackPublication) => { + onEvent(ParticipantEvent.LocalTrackPublished, { publication }, publication.source); + }); + + reg(ParticipantEvent.LocalTrackUnpublished, (publication: LocalTrackPublication) => { + onEvent(ParticipantEvent.LocalTrackUnpublished, { publication }, publication.source); + }); + + reg(ParticipantEvent.ParticipantMetadataChanged, (prevMetadata: string | undefined) => { + onEvent( + ParticipantEvent.ParticipantMetadataChanged, + { prevMetadata }, + `previous: ${prevMetadata}, new: ${participant.metadata}` + ); + }); + + reg(ParticipantEvent.ParticipantNameChanged, (name: string) => { + onEvent(ParticipantEvent.ParticipantNameChanged, { name }, `${name}`); + }); + + reg(ParticipantEvent.DataReceived, (payload: Uint8Array, kind: DataPacket_Kind) => { + let decodedPayload = decoder.decode(payload); + decodedPayload += ` (kind: ${DataPacket_Kind[kind]})`; + onEvent(ParticipantEvent.DataReceived, { payload: decodedPayload, kind }, decodedPayload); + }); + + reg(ParticipantEvent.IsSpeakingChanged, (speaking: boolean) => { + onEvent(ParticipantEvent.IsSpeakingChanged, { speaking }, `${speaking}`); + }); + + reg(ParticipantEvent.ConnectionQualityChanged, (connectionQuality: ConnectionQuality) => { + onEvent(ParticipantEvent.ConnectionQualityChanged, { connectionQuality }, `${connectionQuality}`); + }); + + reg( + ParticipantEvent.TrackStreamStateChanged, + (publication: RemoteTrackPublication, streamState: Track.StreamState) => { + onEvent( + ParticipantEvent.TrackStreamStateChanged, + { publication, streamState }, + `${publication.source}: ${streamState}` + ); + } + ); + + reg( + ParticipantEvent.TrackSubscriptionPermissionChanged, + (publication: RemoteTrackPublication, status: TrackPublication.PermissionStatus) => { + onEvent( + ParticipantEvent.TrackSubscriptionPermissionChanged, + { publication, status }, + `${publication.source}: ${status}` + ); + } + ); + + reg(ParticipantEvent.MediaDevicesError, (error: Error) => { + onEvent(ParticipantEvent.MediaDevicesError, { error }, `${error.message}`); + }); + + reg(ParticipantEvent.ParticipantPermissionsChanged, (prevPermissions?: ParticipantPermission) => { + onEvent( + ParticipantEvent.ParticipantPermissionsChanged, + { prevPermissions }, + `previous: ${prevPermissions}, new: ${JSON.stringify(participant.permissions)}` + ); + }); + + reg( + ParticipantEvent.TrackSubscriptionStatusChanged, + (publication: RemoteTrackPublication, status: TrackPublication.SubscriptionStatus) => { + onEvent( + ParticipantEvent.TrackSubscriptionStatusChanged, + { publication, status }, + `${publication.source}: ${status}` + ); + } + ); + + reg(ParticipantEvent.LocalTrackSubscribed, (trackPublication: LocalTrackPublication) => { + onEvent(ParticipantEvent.LocalTrackSubscribed, { trackPublication }, trackPublication.source); + }); + + reg( + ParticipantEvent.LocalTrackCpuConstrained, + (track: LocalVideoTrack, publication: LocalTrackPublication) => { + onEvent(ParticipantEvent.LocalTrackCpuConstrained, { track, publication }, publication.source); + } + ); + + reg(ParticipantEvent.SipDTMFReceived, (dtmf: any) => { + onEvent(ParticipantEvent.SipDTMFReceived, { dtmf }, JSON.stringify(dtmf)); + }); + + reg( + ParticipantEvent.TranscriptionReceived, + (transcription: TranscriptionSegment[], publication?: TrackPublication) => { + onEvent( + ParticipantEvent.TranscriptionReceived, + { transcription, publication }, + `segments: ${transcription.length}, source: ${publication?.source ?? 'unknown'}` + ); + } + ); + + reg(ParticipantEvent.AudioStreamAcquired, () => { + onEvent(ParticipantEvent.AudioStreamAcquired, {}, ''); + }); + + reg(ParticipantEvent.AttributesChanged, (changedAttributes: Record) => { + onEvent(ParticipantEvent.AttributesChanged, { changedAttributes }, JSON.stringify(changedAttributes)); + }); + + reg(ParticipantEvent.ChatMessage, (msg: ChatMessage) => { + onEvent(ParticipantEvent.ChatMessage, { msg }, msg.message); + }); + + reg(ParticipantEvent.Active, () => { + onEvent(ParticipantEvent.Active, {}, ''); + }); + + return listeners; +} + +/** + * Removes all listeners in the given map from the target emitter. + */ +export function removeAllManagedListeners( + target: any, + listeners: Map void> +) { + for (const [event, listener] of listeners) { + target.removeListener(event, listener); + } + listeners.clear(); +} + +/** + * Registers ALL track event listeners on the given track. + * Returns the listener map for later cleanup/replacement. + */ +export function registerTrackEventListeners( + track: Track, + onEvent: TrackOnEvent +): Map void> { + const listeners = new Map void>(); + + const reg = (event: TrackEvent, listener: (...args: any[]) => void) => { + track.addListener(event as any, listener as any); + listeners.set(event, listener); + }; + + reg(TrackEvent.Message, () => { + onEvent(TrackEvent.Message, {}, track.source); + }); + + reg(TrackEvent.Muted, () => { + onEvent(TrackEvent.Muted, {}, track.source); + }); + + reg(TrackEvent.Unmuted, () => { + onEvent(TrackEvent.Unmuted, {}, track.source); + }); + + reg(TrackEvent.Restarted, () => { + onEvent(TrackEvent.Restarted, {}, track.source); + }); + + reg(TrackEvent.Ended, () => { + onEvent(TrackEvent.Ended, {}, track.source); + }); + + reg(TrackEvent.CpuConstrained, () => { + onEvent(TrackEvent.CpuConstrained, {}, track.source); + }); + + reg(TrackEvent.UpdateSettings, () => { + onEvent(TrackEvent.UpdateSettings, {}, track.source); + }); + + reg(TrackEvent.UpdateSubscription, () => { + onEvent(TrackEvent.UpdateSubscription, {}, track.source); + }); + + reg(TrackEvent.AudioPlaybackStarted, () => { + onEvent(TrackEvent.AudioPlaybackStarted, {}, track.source); + }); + + reg(TrackEvent.AudioPlaybackFailed, (error?: Error) => { + onEvent(TrackEvent.AudioPlaybackFailed, { error }, `${track.source} ${error?.message ?? ''}`); + }); + + reg(TrackEvent.AudioSilenceDetected, () => { + onEvent(TrackEvent.AudioSilenceDetected, {}, track.source); + }); + + reg(TrackEvent.VisibilityChanged, (visible: boolean) => { + onEvent(TrackEvent.VisibilityChanged, { visible, track }, `${track.source} is visible: ${visible}`); + }); + + reg(TrackEvent.VideoDimensionsChanged, (dimensions: Track.Dimensions) => { + onEvent( + TrackEvent.VideoDimensionsChanged, + { dimensions, track }, + `${track.source} ${JSON.stringify(dimensions)}` + ); + }); + + reg(TrackEvent.VideoPlaybackStarted, () => { + onEvent(TrackEvent.VideoPlaybackStarted, {}, track.source); + }); + + reg(TrackEvent.VideoPlaybackFailed, (error?: Error) => { + onEvent(TrackEvent.VideoPlaybackFailed, { error }, `${track.source} ${error?.message ?? ''}`); + }); + + reg(TrackEvent.ElementAttached, (element: HTMLMediaElement) => { + onEvent(TrackEvent.ElementAttached, { element }, track.source); + }); + + reg(TrackEvent.ElementDetached, (element: HTMLMediaElement) => { + onEvent(TrackEvent.ElementDetached, { element }, track.source); + }); + + reg(TrackEvent.UpstreamPaused, () => { + onEvent(TrackEvent.UpstreamPaused, {}, track.source); + }); + + reg(TrackEvent.UpstreamResumed, () => { + onEvent(TrackEvent.UpstreamResumed, {}, track.source); + }); + + reg(TrackEvent.TrackProcessorUpdate, (processor?: any) => { + onEvent(TrackEvent.TrackProcessorUpdate, { processor }, track.source); + }); + + reg(TrackEvent.AudioTrackFeatureUpdate, (_track: any, feature: any, enabled: boolean) => { + onEvent( + TrackEvent.AudioTrackFeatureUpdate, + { feature, enabled }, + `${track.source} feature: ${feature}, enabled: ${enabled}` + ); + }); + + reg(TrackEvent.TimeSyncUpdate, (update: { timestamp: number; rtpTimestamp: number }) => { + onEvent(TrackEvent.TimeSyncUpdate, { update }, `${track.source} timestamp: ${update.timestamp}`); + }); + + reg(TrackEvent.PreConnectBufferFlushed, (buffer: any) => { + onEvent(TrackEvent.PreConnectBufferFlushed, { buffer }, track.source); + }); + + return listeners; +}