@observertc/observer-js 1.0.1-4daea92.0 → 1.0.1-5a1b94b.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +535 -1
  2. package/lib/ObservedCall.d.ts +18 -2
  3. package/lib/ObservedCall.d.ts.map +1 -1
  4. package/lib/ObservedCall.js +65 -11
  5. package/lib/ObservedCallEventMonitor.d.ts +14 -13
  6. package/lib/ObservedCallEventMonitor.d.ts.map +1 -1
  7. package/lib/ObservedCallEventMonitor.js +3 -1
  8. package/lib/ObservedClient.d.ts +9 -9
  9. package/lib/ObservedClient.d.ts.map +1 -1
  10. package/lib/ObservedClient.js +181 -15
  11. package/lib/ObservedClientEventMonitor.d.ts +54 -52
  12. package/lib/ObservedClientEventMonitor.d.ts.map +1 -1
  13. package/lib/ObservedClientEventMonitor.js +7 -0
  14. package/lib/ObservedTURN.d.ts +1 -1
  15. package/lib/ObservedTURN.d.ts.map +1 -1
  16. package/lib/ObservedTURN.js +1 -0
  17. package/lib/ObservedTurnServer.d.ts +2 -2
  18. package/lib/ObservedTurnServer.d.ts.map +1 -1
  19. package/lib/Observer.d.ts +6 -5
  20. package/lib/Observer.d.ts.map +1 -1
  21. package/lib/Observer.js +13 -20
  22. package/lib/ObserverEventMonitor.d.ts +45 -13
  23. package/lib/ObserverEventMonitor.d.ts.map +1 -1
  24. package/lib/ObserverEventMonitor.js +114 -1
  25. package/lib/common/ObserverSummary.d.ts.map +1 -0
  26. package/lib/common/types.d.ts +20 -0
  27. package/lib/common/types.d.ts.map +1 -1
  28. package/lib/index.d.ts +17 -17
  29. package/lib/index.d.ts.map +1 -1
  30. package/lib/index.js +16 -16
  31. package/lib/mediasoup/ObservedMediasoupRouter.d.ts +7 -0
  32. package/lib/mediasoup/ObservedMediasoupRouter.d.ts.map +1 -0
  33. package/lib/mediasoup/ObservedMediasoupRouter.js +12 -0
  34. package/lib/schema/ClientEventTypes.d.ts +147 -79
  35. package/lib/schema/ClientEventTypes.d.ts.map +1 -1
  36. package/lib/schema/ClientEventTypes.js +5 -2
  37. package/lib/schema/ClientSample.d.ts +1 -1
  38. package/lib/schema/ClientSample.d.ts.map +1 -1
  39. package/lib/updaters/OnAnyCallObserverUpdater.d.ts.map +1 -1
  40. package/lib/utils/MediasoupRemoteTrackResolver.d.ts +2 -2
  41. package/lib/utils/MediasoupRemoteTrackResolver.d.ts.map +1 -1
  42. package/lib/utils/RemoteTrackResolver.d.ts +2 -2
  43. package/lib/utils/RemoteTrackResolver.d.ts.map +1 -1
  44. package/lib/{ObservedCertificate.d.ts → webrtc/ObservedCertificate.d.ts} +1 -1
  45. package/lib/webrtc/ObservedCertificate.d.ts.map +1 -0
  46. package/lib/{ObservedCodec.d.ts → webrtc/ObservedCodec.d.ts} +1 -1
  47. package/lib/webrtc/ObservedCodec.d.ts.map +1 -0
  48. package/lib/{ObservedDataChannel.d.ts → webrtc/ObservedDataChannel.d.ts} +1 -1
  49. package/lib/webrtc/ObservedDataChannel.d.ts.map +1 -0
  50. package/lib/{ObservedIceCandidate.d.ts → webrtc/ObservedIceCandidate.d.ts} +1 -1
  51. package/lib/webrtc/ObservedIceCandidate.d.ts.map +1 -0
  52. package/lib/{ObservedIceCandidatePair.d.ts → webrtc/ObservedIceCandidatePair.d.ts} +3 -2
  53. package/lib/webrtc/ObservedIceCandidatePair.d.ts.map +1 -0
  54. package/lib/{ObservedIceCandidatePair.js → webrtc/ObservedIceCandidatePair.js} +17 -1
  55. package/lib/{ObservedIceTransport.d.ts → webrtc/ObservedIceTransport.d.ts} +1 -1
  56. package/lib/webrtc/ObservedIceTransport.d.ts.map +1 -0
  57. package/lib/{ObservedInboundRtp.d.ts → webrtc/ObservedInboundRtp.d.ts} +2 -2
  58. package/lib/webrtc/ObservedInboundRtp.d.ts.map +1 -0
  59. package/lib/{ObservedInboundTrack.d.ts → webrtc/ObservedInboundTrack.d.ts} +5 -4
  60. package/lib/webrtc/ObservedInboundTrack.d.ts.map +1 -0
  61. package/lib/{ObservedInboundTrack.js → webrtc/ObservedInboundTrack.js} +2 -1
  62. package/lib/{ObservedMediaPlayout.d.ts → webrtc/ObservedMediaPlayout.d.ts} +2 -2
  63. package/lib/webrtc/ObservedMediaPlayout.d.ts.map +1 -0
  64. package/lib/{ObservedMediaSource.d.ts → webrtc/ObservedMediaSource.d.ts} +2 -2
  65. package/lib/webrtc/ObservedMediaSource.d.ts.map +1 -0
  66. package/lib/{ObservedOutboundRtp.d.ts → webrtc/ObservedOutboundRtp.d.ts} +2 -2
  67. package/lib/webrtc/ObservedOutboundRtp.d.ts.map +1 -0
  68. package/lib/{ObservedOutboundTrack.d.ts → webrtc/ObservedOutboundTrack.d.ts} +5 -4
  69. package/lib/webrtc/ObservedOutboundTrack.d.ts.map +1 -0
  70. package/lib/{ObservedOutboundTrack.js → webrtc/ObservedOutboundTrack.js} +2 -1
  71. package/lib/{ObservedPeerConnection.d.ts → webrtc/ObservedPeerConnection.d.ts} +26 -4
  72. package/lib/webrtc/ObservedPeerConnection.d.ts.map +1 -0
  73. package/lib/{ObservedPeerConnection.js → webrtc/ObservedPeerConnection.js} +80 -17
  74. package/lib/{ObservedPeerConnectionTransport.d.ts → webrtc/ObservedPeerConnectionTransport.d.ts} +1 -1
  75. package/lib/webrtc/ObservedPeerConnectionTransport.d.ts.map +1 -0
  76. package/lib/{ObservedRemoteInboundRtp.d.ts → webrtc/ObservedRemoteInboundRtp.d.ts} +2 -2
  77. package/lib/webrtc/ObservedRemoteInboundRtp.d.ts.map +1 -0
  78. package/lib/{ObservedRemoteOutboundRtp.d.ts → webrtc/ObservedRemoteOutboundRtp.d.ts} +2 -2
  79. package/lib/webrtc/ObservedRemoteOutboundRtp.d.ts.map +1 -0
  80. package/package.json +1 -1
  81. package/src/ObservedCall.ts +70 -17
  82. package/src/ObservedCallEventMonitor.ts +17 -14
  83. package/src/ObservedClient.ts +195 -22
  84. package/src/ObservedClientEventMonitor.ts +59 -51
  85. package/src/ObservedTURN.ts +2 -1
  86. package/src/ObservedTurnServer.ts +2 -2
  87. package/src/Observer.ts +22 -27
  88. package/src/ObserverEventMonitor.ts +167 -14
  89. package/src/common/types.ts +20 -1
  90. package/src/index.ts +18 -19
  91. package/src/mediasoup/ObservedMediasoupRouter.ts +12 -0
  92. package/src/schema/ClientEventTypes.ts +187 -83
  93. package/src/schema/ClientSample.ts +1 -1
  94. package/src/updaters/OnAnyCallObserverUpdater.ts +1 -1
  95. package/src/utils/MediasoupRemoteTrackResolver.ts +2 -2
  96. package/src/utils/RemoteTrackResolver.ts +2 -2
  97. package/src/{ObservedCertificate.ts → webrtc/ObservedCertificate.ts} +1 -1
  98. package/src/{ObservedCodec.ts → webrtc/ObservedCodec.ts} +1 -1
  99. package/src/{ObservedDataChannel.ts → webrtc/ObservedDataChannel.ts} +1 -1
  100. package/src/{ObservedIceCandidate.ts → webrtc/ObservedIceCandidate.ts} +1 -1
  101. package/src/{ObservedIceCandidatePair.ts → webrtc/ObservedIceCandidatePair.ts} +19 -3
  102. package/src/{ObservedIceTransport.ts → webrtc/ObservedIceTransport.ts} +1 -1
  103. package/src/{ObservedInboundRtp.ts → webrtc/ObservedInboundRtp.ts} +2 -2
  104. package/src/{ObservedInboundTrack.ts → webrtc/ObservedInboundTrack.ts} +6 -4
  105. package/src/{ObservedMediaPlayout.ts → webrtc/ObservedMediaPlayout.ts} +2 -2
  106. package/src/{ObservedMediaSource.ts → webrtc/ObservedMediaSource.ts} +2 -2
  107. package/src/{ObservedOutboundRtp.ts → webrtc/ObservedOutboundRtp.ts} +2 -2
  108. package/src/{ObservedOutboundTrack.ts → webrtc/ObservedOutboundTrack.ts} +6 -4
  109. package/src/{ObservedPeerConnection.ts → webrtc/ObservedPeerConnection.ts} +122 -37
  110. package/src/{ObservedPeerConnectionTransport.ts → webrtc/ObservedPeerConnectionTransport.ts} +1 -1
  111. package/src/{ObservedRemoteInboundRtp.ts → webrtc/ObservedRemoteInboundRtp.ts} +2 -2
  112. package/src/{ObservedRemoteOutboundRtp.ts → webrtc/ObservedRemoteOutboundRtp.ts} +2 -2
  113. package/lib/ObservedCertificate.d.ts.map +0 -1
  114. package/lib/ObservedCodec.d.ts.map +0 -1
  115. package/lib/ObservedDataChannel.d.ts.map +0 -1
  116. package/lib/ObservedIceCandidate.d.ts.map +0 -1
  117. package/lib/ObservedIceCandidatePair.d.ts.map +0 -1
  118. package/lib/ObservedIceTransport.d.ts.map +0 -1
  119. package/lib/ObservedInboundRtp.d.ts.map +0 -1
  120. package/lib/ObservedInboundTrack.d.ts.map +0 -1
  121. package/lib/ObservedMediaPlayout.d.ts.map +0 -1
  122. package/lib/ObservedMediaSource.d.ts.map +0 -1
  123. package/lib/ObservedOutboundRtp.d.ts.map +0 -1
  124. package/lib/ObservedOutboundTrack.d.ts.map +0 -1
  125. package/lib/ObservedPeerConnection.d.ts.map +0 -1
  126. package/lib/ObservedPeerConnectionTransport.d.ts.map +0 -1
  127. package/lib/ObservedRemoteInboundRtp.d.ts.map +0 -1
  128. package/lib/ObservedRemoteOutboundRtp.d.ts.map +0 -1
  129. package/lib/ObserverSummary.d.ts.map +0 -1
  130. /package/lib/{ObserverSummary.d.ts → common/ObserverSummary.d.ts} +0 -0
  131. /package/lib/{ObserverSummary.js → common/ObserverSummary.js} +0 -0
  132. /package/lib/{ObservedCertificate.js → webrtc/ObservedCertificate.js} +0 -0
  133. /package/lib/{ObservedCodec.js → webrtc/ObservedCodec.js} +0 -0
  134. /package/lib/{ObservedDataChannel.js → webrtc/ObservedDataChannel.js} +0 -0
  135. /package/lib/{ObservedIceCandidate.js → webrtc/ObservedIceCandidate.js} +0 -0
  136. /package/lib/{ObservedIceTransport.js → webrtc/ObservedIceTransport.js} +0 -0
  137. /package/lib/{ObservedInboundRtp.js → webrtc/ObservedInboundRtp.js} +0 -0
  138. /package/lib/{ObservedMediaPlayout.js → webrtc/ObservedMediaPlayout.js} +0 -0
  139. /package/lib/{ObservedMediaSource.js → webrtc/ObservedMediaSource.js} +0 -0
  140. /package/lib/{ObservedOutboundRtp.js → webrtc/ObservedOutboundRtp.js} +0 -0
  141. /package/lib/{ObservedPeerConnectionTransport.js → webrtc/ObservedPeerConnectionTransport.js} +0 -0
  142. /package/lib/{ObservedRemoteInboundRtp.js → webrtc/ObservedRemoteInboundRtp.js} +0 -0
  143. /package/lib/{ObservedRemoteOutboundRtp.js → webrtc/ObservedRemoteOutboundRtp.js} +0 -0
  144. /package/src/{ObserverSummary.ts → common/ObserverSummary.ts} +0 -0
@@ -11,6 +11,9 @@ import { Updater } from './updaters/Updater';
11
11
  import { OnIntervalUpdater } from './updaters/OnIntervalUpdater';
12
12
  import { OnAnyClientCallUpdater } from './updaters/OnAnyClientCallUpdater';
13
13
  import { ObservedCallEventMonitor } from './ObservedCallEventMonitor';
14
+ import { createLogger } from './common/logger';
15
+
16
+ const logger = createLogger('ObservedCall');
14
17
 
15
18
  export type ObservedCallSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
16
19
  callId: string;
@@ -18,6 +21,7 @@ export type ObservedCallSettings<AppData extends Record<string, unknown> = Recor
18
21
  remoteTrackResolvePolicy?: 'p2p' | 'mediasoup-sfu',
19
22
  updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated' | 'update-on-interval',
20
23
  updateIntervalInMs?: number,
24
+ maxIdleIfEmptyMs?: number,
21
25
  };
22
26
 
23
27
  export type ObservedCallEvents = {
@@ -75,6 +79,20 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
75
79
  public maxNumberOfClients = 0;
76
80
  public deltaNumberOfIssues = 0;
77
81
 
82
+ public deltaDataChannelBytesReceived = 0;
83
+ public deltaDataChannelBytesSent = 0;
84
+ public deltaDataChannelMessagesReceived = 0;
85
+ public deltaDataChannelMessagesSent = 0;
86
+ public deltaInboundPacketsLost = 0;
87
+ public deltaInboundPacketsReceived = 0;
88
+ public deltaOutboundPacketsSent = 0;
89
+ public deltaReceivedAudioBytes = 0;
90
+ public deltaReceivedVideoBytes = 0;
91
+ public deltaSentAudioBytes = 0;
92
+ public deltaSentVideoBytes = 0;
93
+ public deltaTransportReceivedBytes = 0;
94
+ public deltaTransportSentBytes = 0;
95
+
78
96
  public deltaRttLt50Measurements = 0;
79
97
  public deltaRttLt150Measurements = 0;
80
98
  public deltaRttLt300Measurements = 0;
@@ -85,14 +103,10 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
85
103
  public startedAt?: number;
86
104
  public endedAt?: number;
87
105
  public closedAt?: number;
106
+ public lastClientTimestamp = 0;
88
107
 
89
- private _callStartedEvent: {
90
- emitted: boolean,
91
- timer?: ReturnType<typeof setTimeout>,
92
- };
93
- private _callEndedEvent: {
94
- emitted: boolean
95
- };
108
+ public maxIdleIfEmptyMs?: number;
109
+ private _autoCloseTimer?: ReturnType<typeof setTimeout>;
96
110
 
97
111
  public constructor(
98
112
  settings: ObservedCallSettings<AppData>,
@@ -104,6 +118,7 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
104
118
  this.callId = settings.callId;
105
119
  this.appData = settings.appData ?? {} as AppData;
106
120
  this.scoreCalculator = new DefaultCallScoreCalculator(this);
121
+ this.maxIdleIfEmptyMs = settings.maxIdleIfEmptyMs;
107
122
  this.detectors = new Detectors();
108
123
 
109
124
  if (settings.updateIntervalInMs) {
@@ -113,10 +128,10 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
113
128
  }
114
129
  switch (settings.updatePolicy) {
115
130
  case 'update-on-any-client-updated':
116
- this.updater = new OnAllClientCallUpdater(this);
131
+ this.updater = new OnAnyClientCallUpdater(this);
117
132
  break;
118
133
  case 'update-when-all-client-updated':
119
- this.updater = new OnAnyClientCallUpdater(this);
134
+ this.updater = new OnAllClientCallUpdater(this);
120
135
  break;
121
136
  case 'update-on-interval':
122
137
  if (!settings.updateIntervalInMs) {
@@ -133,13 +148,6 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
133
148
  case 'mediasoup-sfu':
134
149
  break;
135
150
  }
136
-
137
- this._callStartedEvent = {
138
- emitted: false,
139
- };
140
- this._callEndedEvent = {
141
- emitted: false,
142
- };
143
151
  }
144
152
 
145
153
  public get numberOfClients() {
@@ -157,7 +165,18 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
157
165
 
158
166
  this.updater?.close();
159
167
 
160
- [ ...this.observedClients.values() ].forEach((client) => client.close());
168
+ let minSampleTimestamps: number | undefined;
169
+ let maxSampleTimestamps: number | undefined;
170
+
171
+ for (const client of this.observedClients.values()) {
172
+ client.close();
173
+
174
+ if (client.joinedAt) minSampleTimestamps = Math.min(minSampleTimestamps ?? client.joinedAt, client.joinedAt);
175
+ if (client.leftAt) maxSampleTimestamps = Math.max(maxSampleTimestamps ?? client.leftAt, client.leftAt);
176
+ }
177
+
178
+ if (this.startedAt === undefined) this.startedAt = minSampleTimestamps;
179
+ if (this.endedAt === undefined) this.endedAt = maxSampleTimestamps;
161
180
 
162
181
  this.closedAt = Date.now();
163
182
  this.emit('close');
@@ -187,6 +206,8 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
187
206
 
188
207
  if (this.observedClients.size === 0) {
189
208
  this.emit('empty');
209
+
210
+ this._createAutoClose();
190
211
  }
191
212
  ++this.totalRemovedClients;
192
213
  });
@@ -202,6 +223,9 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
202
223
 
203
224
  if (wasEmpty) {
204
225
  this.emit('not-empty');
226
+
227
+ clearTimeout(this._autoCloseTimer);
228
+ this._autoCloseTimer = undefined;
205
229
  }
206
230
 
207
231
  return result;
@@ -229,6 +253,8 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
229
253
  this.detectors.update();
230
254
  this.scoreCalculator.update();
231
255
 
256
+ this.lastClientTimestamp = Math.max(...Array.from(this.observedClients.values()).map((c) => c.lastSampleTimestamp ?? 0));
257
+
232
258
  this.emit('update');
233
259
 
234
260
  this.deltaNumberOfIssues = 0;
@@ -236,6 +262,20 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
236
262
  this.deltaRttLt150Measurements = 0;
237
263
  this.deltaRttLt300Measurements = 0;
238
264
  this.deltaRttGtOrEq300Measurements = 0;
265
+
266
+ this.deltaDataChannelBytesReceived = 0;
267
+ this.deltaDataChannelBytesSent = 0;
268
+ this.deltaDataChannelMessagesReceived = 0;
269
+ this.deltaDataChannelMessagesSent = 0;
270
+ this.deltaInboundPacketsLost = 0;
271
+ this.deltaInboundPacketsReceived = 0;
272
+ this.deltaOutboundPacketsSent = 0;
273
+ this.deltaReceivedAudioBytes = 0;
274
+ this.deltaReceivedVideoBytes = 0;
275
+ this.deltaSentAudioBytes = 0;
276
+ this.deltaSentVideoBytes = 0;
277
+ this.deltaTransportReceivedBytes = 0;
278
+ this.deltaTransportSentBytes = 0;
239
279
  }
240
280
 
241
281
  private _onClientUpdate(client: ObservedClient) {
@@ -278,6 +318,19 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
278
318
  this.endedAt = Math.max(this.endedAt ?? client.leftAt, client.leftAt);
279
319
  }
280
320
 
321
+ private _createAutoClose() {
322
+ if (this.closed) return;
323
+ if (this.maxIdleIfEmptyMs === undefined || this.maxIdleIfEmptyMs < 1) return;
324
+
325
+ this._autoCloseTimer = setTimeout(() => {
326
+ if (this.closed) return;
327
+ if (this.observedClients.size > 0) return;
328
+
329
+ logger.debug(`Call ${this.callId} is empty for ${this.maxIdleIfEmptyMs}ms, closing...`);
330
+ this.close();
331
+ }, this.maxIdleIfEmptyMs);
332
+ }
333
+
281
334
  // public resetSummaryMetrics() {
282
335
  // this.totalAddedClients = 0;
283
336
  // this.totalRemovedClients = 0;
@@ -1,18 +1,18 @@
1
1
  import { ObservedCall } from './ObservedCall';
2
- import { ObservedCertificate } from './ObservedCertificate';
2
+ import { ObservedCertificate } from './webrtc/ObservedCertificate';
3
3
  import { ObservedClient, ObservedClientEvents } from './ObservedClient';
4
- import { ObservedCodec } from './ObservedCodec';
5
- import { ObservedDataChannel } from './ObservedDataChannel';
6
- import { ObservedIceCandidate } from './ObservedIceCandidate';
7
- import { ObservedIceCandidatePair } from './ObservedIceCandidatePair';
8
- import { ObservedIceTransport } from './ObservedIceTransport';
9
- import { ObservedInboundRtp } from './ObservedInboundRtp';
10
- import { ObservedInboundTrack } from './ObservedInboundTrack';
11
- import { ObservedMediaPlayout } from './ObservedMediaPlayout';
12
- import { ObservedMediaSource } from './ObservedMediaSource';
13
- import { ObservedOutboundRtp } from './ObservedOutboundRtp';
14
- import { ObservedOutboundTrack } from './ObservedOutboundTrack';
15
- import { ObservedPeerConnection } from './ObservedPeerConnection';
4
+ import { ObservedCodec } from './webrtc/ObservedCodec';
5
+ import { ObservedDataChannel } from './webrtc/ObservedDataChannel';
6
+ import { ObservedIceCandidate } from './webrtc/ObservedIceCandidate';
7
+ import { ObservedIceCandidatePair } from './webrtc/ObservedIceCandidatePair';
8
+ import { ObservedIceTransport } from './webrtc/ObservedIceTransport';
9
+ import { ObservedInboundRtp } from './webrtc/ObservedInboundRtp';
10
+ import { ObservedInboundTrack } from './webrtc/ObservedInboundTrack';
11
+ import { ObservedMediaPlayout } from './webrtc/ObservedMediaPlayout';
12
+ import { ObservedMediaSource } from './webrtc/ObservedMediaSource';
13
+ import { ObservedOutboundRtp } from './webrtc/ObservedOutboundRtp';
14
+ import { ObservedOutboundTrack } from './webrtc/ObservedOutboundTrack';
15
+ import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection';
16
16
  import { ClientEvent, ClientIssue, ClientMetaData, ClientSample, ExtensionStat } from './schema/ClientSample';
17
17
 
18
18
  export class ObservedCallEventMonitor<Context> {
@@ -61,6 +61,7 @@ export class ObservedCallEventMonitor<Context> {
61
61
  }
62
62
 
63
63
  // Public event handlers
64
+ public onCallClosed?: (call: ObservedCall, ctx: Context) => void;
64
65
  public onCallEmpty?: (call: ObservedCall, ctx: Context) => void;
65
66
  public onCallNotEmpty?: (call: ObservedCall, ctx: Context) => void;
66
67
  public onCallUpdated?: (call: ObservedCall, ctx: Context) => void;
@@ -132,6 +133,8 @@ export class ObservedCallEventMonitor<Context> {
132
133
  call.off('not-empty', onCallNotEmpty);
133
134
  call.off('update', onCallUpdated);
134
135
 
136
+ this.onCallClosed?.(call, this.context);
137
+
135
138
  });
136
139
  call.on('newclient', this._onClientAdded);
137
140
  call.on('empty', onCallEmpty);
@@ -149,7 +152,7 @@ export class ObservedCallEventMonitor<Context> {
149
152
  const onClientExtensionStats = (extensionStats: ExtensionStat) => this._onClientExtensionStats(observedClient, extensionStats);
150
153
  const onUsingTurn = (usingTurn: boolean) => this._onUsingTurn(observedClient, usingTurn);
151
154
  const onUserMediaError = (error: string) => this._onUserMediaError(observedClient, error);
152
- const onClientUpdated = (...args: ObservedClientEvents['update']) => this.onClientUpdated?.(observedClient, args[0].sample, this.context);
155
+ const onClientUpdated = (...args: ObservedClientEvents['update']) => this.onClientUpdated?.(observedClient, args[0], this.context);
153
156
  const onClientEvent = (event: ClientEvent) => this.onClientEvent?.(observedClient, event, this.context);
154
157
 
155
158
  observedClient.once('close', () => {
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
- import { ObservedPeerConnection } from './ObservedPeerConnection';
2
+ import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection';
3
3
  import { createLogger } from './common/logger';
4
4
  // eslint-disable-next-line camelcase
5
5
  import { ClientEvent, ClientMetaData, ClientSample, PeerConnectionSample, ClientIssue, ExtensionStat } from './schema/ClientSample';
@@ -10,21 +10,19 @@ import { ClientMetaTypes } from './schema/ClientMetaTypes';
10
10
  import { parseJsonAs } from './common/utils';
11
11
  import { CalculatedScore } from './scores/CalculatedScore';
12
12
  import { Detectors } from './detectors/Detectors';
13
+ import { clear } from 'console';
14
+ import { ParsedUserAgent } from './common/types';
13
15
 
14
16
  const logger = createLogger('ObservedClient');
15
17
 
16
18
  export type ObservedClientSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
17
19
  clientId: string;
20
+ maxIdleTimeMs?: number;
18
21
  appData?: AppData;
19
22
  };
20
23
 
21
24
  export type ObservedClientEvents = {
22
- update: [
23
- {
24
- sample: ClientSample;
25
- elapsedTimeInMs: number;
26
- }
27
- ];
25
+ update: [sample: ClientSample, elapsedTimeInMs: number];
28
26
  close: [];
29
27
  joined: [];
30
28
  issue: [ClientIssue];
@@ -49,14 +47,12 @@ export declare interface ObservedClient {
49
47
  export class ObservedClient<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter {
50
48
  public readonly detectors: Detectors;
51
49
 
52
- public readonly clientId: string;
53
50
  public readonly observedPeerConnections = new Map<string, ObservedPeerConnection>();
54
51
  public readonly calculatedScore: CalculatedScore = {
55
52
  weight: 1,
56
53
  value: undefined,
57
54
  };
58
55
 
59
- public appData: AppData;
60
56
  public attachments?: Record<string, unknown>;
61
57
 
62
58
  public updated = Date.now();
@@ -66,6 +62,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
66
62
  public joinedAt?: number;
67
63
  public leftAt?: number;
68
64
  public closedAt?: number;
65
+ public lastSampleTimestamp?: number;
69
66
  // the timestamp of the CLIENT_JOINED event
70
67
  public operationSystem?: MetaData.OperationSystem;
71
68
  public engine?: MetaData.Engine;
@@ -139,16 +136,26 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
139
136
  public issues: ClientIssue[] = [];
140
137
 
141
138
  private _injections: Pick<ClientSample, 'clientEvents' | 'clientIssues' | 'extensionStats' | 'attachments' | 'clientMetaItems'> = {};
139
+ private _autoCloseTimer?: ReturnType<typeof setTimeout>;
142
140
 
143
- public constructor(settings: ObservedClientSettings<AppData>, public readonly call: ObservedCall) {
141
+ public constructor(
142
+ public readonly settings: ObservedClientSettings<AppData>,
143
+ public readonly call: ObservedCall
144
+ ) {
144
145
  super();
145
146
  this.setMaxListeners(Infinity);
147
+ this._createAutoClose();
146
148
 
147
- this.clientId = settings.clientId;
148
- this.appData = settings.appData ?? {} as AppData;
149
-
150
149
  this.detectors = new Detectors();
151
150
  }
151
+
152
+ public get clientId() {
153
+ return this.settings.clientId;
154
+ }
155
+
156
+ public get appData(): AppData {
157
+ return this.settings.appData ?? {} as AppData;
158
+ }
152
159
 
153
160
  public get numberOfPeerConnections() {
154
161
  return this.observedPeerConnections.size;
@@ -162,15 +169,42 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
162
169
  if (this.closed) return;
163
170
  this.closed = true;
164
171
 
172
+ this._injections.clientEvents?.forEach((clientEvent) => this._processClientEvent(clientEvent));
173
+ this._injections.clientIssues?.forEach((clientIssue) => this.addIssue(clientIssue));
174
+ this._injections.extensionStats?.forEach((extensionStat) => this.addExtensionStats(extensionStat));
175
+ this._injections.clientMetaItems?.forEach((clientMetaItem) => this.addMetadata(clientMetaItem));
176
+
165
177
  Array.from(this.observedPeerConnections.values()).forEach((peerConnection) => peerConnection.close());
178
+ if (!this.leftAt) {
179
+ this.leftAt = this.lastSampleTimestamp;
166
180
 
181
+ if (this.leftAt) {
182
+ this.emit('left');
183
+ }
184
+ }
167
185
  this.closedAt = Date.now();
168
186
 
169
187
  this.emit('close');
170
188
  }
171
189
 
190
+ private _createAutoClose() {
191
+ if (this.closed) return;
192
+ if (this.settings.maxIdleTimeMs === undefined || this.settings.maxIdleTimeMs < 1) return;
193
+
194
+ clearTimeout(this._autoCloseTimer);
195
+
196
+ this._autoCloseTimer = setTimeout(() => {
197
+ if (this.closed) return;
198
+
199
+ logger.debug(`Client ${this.clientId} is idle for ${this.settings.maxIdleTimeMs}ms, closing...`);
200
+ this.close();
201
+ }, this.settings.maxIdleTimeMs);
202
+ }
203
+
172
204
  public accept(sample: ClientSample): void {
173
205
  if (this.closed) throw new Error(`Client ${this.clientId} is closed`);
206
+
207
+ this._createAutoClose();
174
208
 
175
209
  const now = Date.now();
176
210
  const elapsedInMs = now - this.updated;
@@ -327,11 +361,12 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
327
361
  this.calculatedScore.value = sample.score;
328
362
  this.detectors.update();
329
363
 
364
+ this.lastSampleTimestamp = sample.timestamp;
330
365
  // emit update
331
- this.emit('update', {
366
+ this.emit('update',
332
367
  sample,
333
- elapsedTimeInMs: now - this.updated,
334
- });
368
+ now - this.updated,
369
+ );
335
370
  this.updated = now;
336
371
 
337
372
  // if result changed after update
@@ -357,6 +392,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
357
392
  logger.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);
358
393
  }
359
394
  }
395
+ logger.debug('Client %s joined at %o', this.clientId, event);
360
396
  break;
361
397
  }
362
398
  case ClientEventTypes.CLIENT_LEFT: {
@@ -367,7 +403,9 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
367
403
  } else {
368
404
  logger.warn(`Client ${this.clientId} leftAt timestamp was already set`);
369
405
  }
406
+
370
407
  }
408
+ logger.debug('Client %s left at %o', this.clientId, event);
371
409
  break;
372
410
  }
373
411
  case ClientEventTypes.PEER_CONNECTION_OPENED: {
@@ -381,7 +419,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
381
419
  } else if (postBuffer) {
382
420
  postBuffer.push(event);
383
421
  } else {
384
- logger.warn('Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection', event);
422
+ logger.warn('Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o', event);
385
423
  }
386
424
 
387
425
  }
@@ -398,7 +436,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
398
436
  } else if (postBuffer) {
399
437
  postBuffer.push(event);
400
438
  } else {
401
- logger.warn('Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection', event);
439
+ logger.warn('Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o', event);
402
440
  }
403
441
  }
404
442
  break;
@@ -415,7 +453,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
415
453
  } else if (postBuffer) {
416
454
  postBuffer.push(event);
417
455
  } else {
418
- logger.warn('Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack', event);
456
+ logger.warn('Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o', event);
419
457
  }
420
458
  }
421
459
  break;
@@ -432,7 +470,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
432
470
  } else if (postBuffer) {
433
471
  postBuffer.push(event);
434
472
  } else {
435
- logger.warn('Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack', event);
473
+ logger.warn('Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o', event);
436
474
  }
437
475
  }
438
476
  break;
@@ -449,7 +487,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
449
487
  } else if (postBuffer) {
450
488
  postBuffer.push(event);
451
489
  } else {
452
- logger.warn('Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel', event);
490
+ logger.warn('Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o', event);
453
491
  }
454
492
  }
455
493
  break;
@@ -466,12 +504,119 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
466
504
  } else if (postBuffer) {
467
505
  postBuffer.push(event);
468
506
  } else {
469
- logger.warn('Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel', event);
507
+ logger.warn('Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o', event);
508
+ }
509
+ }
510
+ break;
511
+ }
512
+ case ClientEventTypes.MEDIA_TRACK_MUTED: {
513
+ const payload = parseJsonAs<Record<string, unknown>>(event.payload);
514
+
515
+ if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') {
516
+ const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);
517
+ const observedInboundTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId);
518
+ const observedOutboundTrack = observedPeerConnection?.observedOutboundTracks.get(payload.trackId);
519
+
520
+ if (observedPeerConnection) {
521
+ if (observedInboundTrack) {
522
+ observedInboundTrack.muted = true;
523
+ observedPeerConnection?.emit('muted-inbound-track', observedInboundTrack);
524
+ } else if (observedOutboundTrack) {
525
+ observedOutboundTrack.muted = true;
526
+ observedPeerConnection?.emit('muted-outbound-track', observedOutboundTrack);
527
+ }
528
+ } else if (postBuffer) {
529
+ postBuffer.push(event);
530
+ } else {
531
+ logger.warn('Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o', event);
532
+ }
533
+ }
534
+
535
+ break;
536
+ }
537
+ case ClientEventTypes.MEDIA_TRACK_UNMUTED: {
538
+ const payload = parseJsonAs<Record<string, unknown>>(event.payload);
539
+
540
+ if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') {
541
+ const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);
542
+ const observedInboundTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId);
543
+ const observedOutboundTrack = observedPeerConnection?.observedOutboundTracks.get(payload.trackId);
544
+
545
+ if (observedPeerConnection) {
546
+ if (observedInboundTrack) {
547
+ observedInboundTrack.muted = false;
548
+ observedPeerConnection?.emit('unmuted-inbound-track', observedInboundTrack);
549
+ } else if (observedOutboundTrack) {
550
+ observedOutboundTrack.muted = false;
551
+ observedPeerConnection?.emit('unmuted-outbound-track', observedOutboundTrack);
552
+ }
553
+ } else if (postBuffer) {
554
+ postBuffer.push(event);
555
+ } else {
556
+ logger.warn('Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o', event);
557
+ }
558
+ }
559
+ break;
560
+ }
561
+ case ClientEventTypes.ICE_CONNECTION_STATE_CHANGED: {
562
+ const payload = parseJsonAs<Record<string, unknown>>(event.payload);
563
+
564
+ if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.iceConnectionState && typeof payload.iceConnectionState === 'string') {
565
+ const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);
566
+
567
+ if (observedPeerConnection) {
568
+ observedPeerConnection.iceConnectionState = payload.iceConnectionState;
569
+ observedPeerConnection.emit('iceconnectionstatechange', {
570
+ state: payload.iceConnectionState,
571
+ });
572
+ } else if (postBuffer) {
573
+ postBuffer.push(event);
574
+ } else {
575
+ logger.warn('Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event);
576
+ }
577
+ }
578
+ break;
579
+ }
580
+ case ClientEventTypes.ICE_GATHERING_STATE_CHANGED: {
581
+ const payload = parseJsonAs<Record<string, unknown>>(event.payload);
582
+
583
+ if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.iceGatheringState && typeof payload.iceGatheringState === 'string') {
584
+ const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);
585
+
586
+ if (observedPeerConnection) {
587
+ observedPeerConnection.iceGatheringState = payload.iceGatheringState;
588
+ observedPeerConnection.emit('icegatheringstatechange', {
589
+ state: payload.iceGatheringState,
590
+ });
591
+ } else if (postBuffer) {
592
+ postBuffer.push(event);
593
+ } else {
594
+ logger.warn('Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event);
595
+ }
596
+ }
597
+ break;
598
+ }
599
+ case ClientEventTypes.PEER_CONNECTION_STATE_CHANGED: {
600
+ const payload = parseJsonAs<Record<string, unknown>>(event.payload);
601
+
602
+ if (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.peerConnectionState && typeof payload.peerConnectionState === 'string') {
603
+ const observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);
604
+
605
+ if (observedPeerConnection) {
606
+ observedPeerConnection.connectionState = payload.peerConnectionState;
607
+ observedPeerConnection.emit('connectionstatechange', {
608
+ state: payload.peerConnectionState,
609
+ });
610
+ } else if (postBuffer) {
611
+ postBuffer.push(event);
612
+ } else {
613
+ logger.warn('Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event);
470
614
  }
471
615
  }
472
616
  break;
473
617
  }
474
618
  }
619
+
475
620
  this.emit('clientEvent', event);
476
621
  }
477
622
 
@@ -536,6 +681,34 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
536
681
  this.operationSystem = parseJsonAs(metadata.payload);
537
682
  break;
538
683
  }
684
+ case 'USER_AGENT_DATA': {
685
+ try {
686
+ const userAgentData = parseJsonAs<ParsedUserAgent>(metadata.payload);
687
+ if (userAgentData) {
688
+ if (this.operationSystem === undefined) {
689
+ this.operationSystem = {
690
+ name: userAgentData.os.name.toLowerCase(),
691
+ version: userAgentData.os.version,
692
+ };
693
+ }
694
+ if (this.engine === undefined) {
695
+ this.engine = {
696
+ name: userAgentData.engine.name.toLowerCase(),
697
+ version: userAgentData.engine.version,
698
+ };
699
+ }
700
+ if (this.browser === undefined) {
701
+ this.browser = {
702
+ name: userAgentData.browser.name.toLowerCase(),
703
+ version: userAgentData.browser.version,
704
+ };
705
+ }
706
+ }
707
+
708
+ } catch (error) {
709
+ logger.warn('Failed to parse USER_AGENT_DATA metadata: %o', error);
710
+ }
711
+ }
539
712
  }
540
713
 
541
714
  this.call.observer.emit('client-metadata', this, metadata);