openvidu-testapp: update and refactor all RoomEvent, ParticipantEvent and TrackEvent management

- Updates all available events to latest
- Refactor event listeners to use shared utilities with early registration and safe add/remove pattern
pull/900/head
pabloFuente 2026-05-26 14:29:42 +02:00
parent 052b110776
commit f037f31da1
7 changed files with 871 additions and 482 deletions

View File

@ -6,21 +6,29 @@ 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<string, boolean>;
eventArray: string[];
checkAll: boolean;
}
@Component({
selector: 'app-events-dialog',
template: `
<h2 mat-dialog-title>{{target}} events</h2>
<mat-dialog-content>
<mat-slide-toggle [(ngModel)]="checkAll" (change)="updateAll()" [color]="'warn'"><i>ALL</i></mat-slide-toggle>
@for (group of eventGroups; track group.label) {
<h3 class="group-label">{{group.label}}</h3>
<mat-slide-toggle [(ngModel)]="group.checkAll" (change)="updateAll(group)" [color]="'warn'"><i>ALL</i></mat-slide-toggle>
<mat-divider></mat-divider>
<div class="row no-wrap-row">
<div class="col-50">
@for (event of eventArray | slice:0:(eventArray.length/2); track event) {
@for (event of group.eventArray | slice:0:Math.ceil(group.eventArray.length/2); track event) {
<div class="toggle">
<mat-slide-toggle
(change)="toggleEvent($event)"
[checked]="eventCollection.get(event)"
(change)="toggleEvent($event, group)"
[checked]="group.eventCollection.get(event)"
[name]="event"
color="warn">{{event}}
</mat-slide-toggle>
@ -28,11 +36,11 @@ import { MatButtonModule } from '@angular/material/button';
}
</div>
<div class="col-50">
@for (event of eventArray | slice:(eventArray.length/2 + 1):(eventArray.length); track event) {
@for (event of group.eventArray | slice:Math.ceil(group.eventArray.length/2):group.eventArray.length; track event) {
<div class="toggle">
<mat-slide-toggle
(change)="toggleEvent($event)"
[checked]="eventCollection.get(event)"
(change)="toggleEvent($event, group)"
[checked]="group.eventCollection.get(event)"
[name]="event"
color="warn">{{event}}
</mat-slide-toggle>
@ -40,6 +48,7 @@ import { MatButtonModule } from '@angular/material/button';
}
</div>
</div>
}
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button id="close-dialog-btn" mat-dialog-close="">CLOSE</button>
@ -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<string, boolean>;
eventArray: string[];
eventGroups: EventGroup[] = [];
private dialogData = inject(MAT_DIALOG_DATA);
constructor(public dialogRef: MatDialogRef<EventsDialogComponent>) {
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<string, boolean> }) => ({
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);
}
}

View File

@ -27,7 +27,7 @@
<mat-icon class="mat-icon-custom-ic" aria-label="Room API button">cloud_circle</mat-icon>
</button>
<button mat-icon-button title="Room events" [id]="'room-events-btn-' + index"
class="mat-icon-custom" (click)="openRoomEventsDialog()">
class="mat-icon-custom" (click)="openAllEventsDialog()">
<mat-icon class="mat-icon-custom-ic"
aria-label="Room events button">notifications</mat-icon>
</button>
@ -89,10 +89,15 @@
</div>
<div>
<app-participant class="local-participant" [participant]="room.localParticipant" [room]="room"
[index]="index"></app-participant>
[index]="index" [participantEvents]="participantEvents" [trackEvents]="trackEvents"
[earlyParticipantEvents]="earlyParticipantEvents" [earlyParticipantListeners]="earlyParticipantListeners"
[earlyTrackEvents]="earlyTrackEvents" [earlyTrackListeners]="earlyTrackListeners"></app-participant>
@for (participant of room.remoteParticipants | keyvalue; track participant) {
<app-participant class="remote-participant"
[participant]="participant.value" [room]="room" [index]="index"
[participantEvents]="participantEvents" [trackEvents]="trackEvents"
[earlyParticipantEvents]="earlyParticipantEvents" [earlyParticipantListeners]="earlyParticipantListeners"
[earlyTrackEvents]="earlyTrackEvents" [earlyTrackListeners]="earlyTrackListeners"
(sendReliableDataToOneParticipant)="sendDataReliable($event)"
(sendLossyDataToOneParticipant)="sendDataLossy($event)"></app-participant>
}

View File

@ -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<RoomEvent, Boolean> = new Map<RoomEvent, boolean>();
participantEvents: Map<ParticipantEvent, boolean> = new Map<ParticipantEvent, boolean>();
trackEvents: Map<TrackEvent, boolean> = new Map<TrackEvent, boolean>();
private roomEventListeners: Map<string, (...args: any[]) => 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<string, TestAppEvent[]> = new Map();
earlyParticipantListeners: Map<string, Map<string, (...args: any[]) => void>> = new Map();
earlyTrackEvents: Map<string, TestAppEvent[]> = new Map();
earlyTrackListeners: Map<string, Map<string, (...args: any[]) => 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<string, boolean>, 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<string, string>,
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<string, boolean> = new Map(
openAllEventsDialog() {
const oldRoomValues: Map<string, boolean> = 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);
}
});
}

View File

@ -105,13 +105,17 @@
@for (trackPublication of participant.audioTrackPublications| keyvalue; track trackPublication) {
<app-audio-track
[index]="index" [trackPublication]="trackPublication.value" [track]="trackPublication.value.audioTrack"
[localParticipant]="localParticipant" (newTrackEvent)="onTrackEvent($event)"></app-audio-track>
[localParticipant]="localParticipant"
[earlyTrackEvents]="earlyTrackEvents" [earlyTrackListeners]="earlyTrackListeners"
(newTrackEvent)="onTrackEvent($event)"></app-audio-track>
}
</div>
@for (trackPublication of participant.videoTrackPublications | keyvalue; track trackPublication) {
<app-video-track
[index]="index" [trackPublication]="trackPublication.value" [track]="trackPublication.value.videoTrack"
[localParticipant]="localParticipant" (newTrackEvent)="onTrackEvent($event)"></app-video-track>
[localParticipant]="localParticipant"
[earlyTrackEvents]="earlyTrackEvents" [earlyTrackListeners]="earlyTrackListeners"
(newTrackEvent)="onTrackEvent($event)"></app-video-track>
}
</div>
}

View File

@ -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<string, boolean> = new Map();
@Input()
trackEvents: Map<string, boolean> = new Map();
@Input()
earlyParticipantEvents: Map<string, TestAppEvent[]> = new Map();
@Input()
earlyParticipantListeners: Map<string, Map<string, (...args: any[]) => void>> = new Map();
@Input()
earlyTrackEvents: Map<string, TestAppEvent[]> = new Map();
@Input()
earlyTrackListeners: Map<string, Map<string, (...args: any[]) => void>> = new Map();
@Output()
sendReliableDataToOneParticipant = new EventEmitter<string>();
@ -80,6 +97,8 @@ export class ParticipantComponent {
trackPublishOptions?: TrackPublishOptions;
private decoder = new TextDecoder();
private participantEventListeners: Map<string, (...args: any[]) => void> = new Map();
private roomListenersFromParticipant: Map<string, (...args: any[]) => 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,250 +282,15 @@ 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
);
}
@ -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) => {
const publishedListener = (track: RemoteDataTrack) => {
if (track.publisherIdentity === this.participant.identity) {
this.remoteDataTracks.push(track);
this.cdr.detectChanges();
}
}
);
this.room.on(
RoomEvent.DataTrackUnpublished,
(sid: string) => {
};
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);
}
}

View File

@ -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<string, TestAppEvent[]> = new Map();
@Input()
earlyTrackListeners: Map<string, Map<string, (...args: any[]) => 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<string, (...args: any[]) => 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,94 +143,23 @@ 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,
});
})
.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,
});
eventContent,
eventDescription,
});
}
);
}
protected getTrackOrigin(): string {
let origin: string;

View File

@ -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<string, (...args: any[]) => void> {
const listeners = new Map<string, (...args: any[]) => 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<string, string>) => {
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<string, (...args: any[]) => 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<string, (...args: any[]) => void> {
const listeners = new Map<string, (...args: any[]) => 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;
}