@observertc/observer-js 0.30.1-beta → 0.30.3-beta

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 (39) hide show
  1. package/lib/ObservedClient.d.ts +15 -6
  2. package/lib/ObservedClient.d.ts.map +1 -1
  3. package/lib/ObservedClient.js +146 -73
  4. package/lib/ObservedICE.d.ts +47 -0
  5. package/lib/ObservedICE.d.ts.map +1 -0
  6. package/lib/ObservedICE.js +105 -0
  7. package/lib/ObservedInboundTrack.d.ts +29 -1
  8. package/lib/ObservedInboundTrack.d.ts.map +1 -1
  9. package/lib/ObservedInboundTrack.js +78 -8
  10. package/lib/ObservedOutboundTrack.d.ts +20 -3
  11. package/lib/ObservedOutboundTrack.d.ts.map +1 -1
  12. package/lib/ObservedOutboundTrack.js +53 -9
  13. package/lib/ObservedPeerConnection.d.ts +47 -1
  14. package/lib/ObservedPeerConnection.d.ts.map +1 -1
  15. package/lib/ObservedPeerConnection.js +155 -0
  16. package/lib/common/CallEventType.d.ts +22 -0
  17. package/lib/common/CallEventType.d.ts.map +1 -0
  18. package/lib/common/CallEventType.js +26 -0
  19. package/lib/common/callEventReports.d.ts +0 -10
  20. package/lib/common/callEventReports.d.ts.map +1 -1
  21. package/lib/common/callEventReports.js +10 -21
  22. package/lib/models/Models.d.ts +3 -70
  23. package/lib/models/Models.d.ts.map +1 -1
  24. package/lib/models/Models.js +2 -72
  25. package/lib/models/samples_pb.js +1 -1
  26. package/package.json +1 -1
  27. package/src/ObservedClient.ts +146 -89
  28. package/src/ObservedICE.ts +176 -0
  29. package/src/ObservedInboundTrack.ts +97 -3
  30. package/src/ObservedOutboundTrack.ts +70 -9
  31. package/src/ObservedPeerConnection.ts +205 -1
  32. package/src/common/CallEventType.ts +23 -0
  33. package/src/common/callEventReports.ts +1 -11
  34. package/src/models/Models.ts +4 -113
  35. package/src/models/samples_pb.ts +1 -1
  36. package/lib/common/ReportsCollector.d.ts +0 -23
  37. package/lib/common/ReportsCollector.d.ts.map +0 -1
  38. package/lib/common/ReportsCollector.js +0 -26
  39. package/src/common/ReportsCollector.ts +0 -71
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
- // @generated by protoc-gen-es v1.1.1 with parameter "target=ts"
2
+ // @generated by protoc-gen-es v1.7.2 with parameter "target=ts"
3
3
  // @generated from file models.proto (package org.observertc.observer.models, syntax proto2)
4
4
  /* eslint-disable */
5
5
  // @ts-nocheck
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.SfuSctpChannel = exports.SfuOutboundRtpPad = exports.SfuInboundRtpPad = exports.SfuTransport = exports.Sfu = exports.OutboundTrack = exports.InboundTrack = exports.InboundTrackStats = exports.InboundTrackVideoStats = exports.PeerConnection = exports.Client = exports.ClientCoordinate = exports.Call = void 0;
7
+ exports.SfuSctpChannel = exports.SfuOutboundRtpPad = exports.SfuInboundRtpPad = exports.SfuTransport = exports.Sfu = exports.OutboundTrack = exports.InboundTrack = exports.InboundTrackStats = exports.InboundTrackVideoStats = exports.PeerConnection = exports.Client = exports.Call = void 0;
8
8
  const protobuf_1 = require("@bufbuild/protobuf");
9
9
  const samples_pb_1 = require("./samples_pb");
10
10
  /**
@@ -42,34 +42,6 @@ Call.fields = protobuf_1.proto2.util.newFieldList(() => [
42
42
  { no: 4, name: "started", kind: "scalar", T: 4 /* ScalarType.UINT64 */ },
43
43
  { no: 5, name: "clientIds", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
44
44
  ]);
45
- /**
46
- * @generated from message org.observertc.observer.models.ClientCoordinate
47
- */
48
- class ClientCoordinate extends protobuf_1.Message {
49
- constructor(data) {
50
- super();
51
- protobuf_1.proto2.util.initPartial(data, this);
52
- }
53
- static fromBinary(bytes, options) {
54
- return new ClientCoordinate().fromBinary(bytes, options);
55
- }
56
- static fromJson(jsonValue, options) {
57
- return new ClientCoordinate().fromJson(jsonValue, options);
58
- }
59
- static fromJsonString(jsonString, options) {
60
- return new ClientCoordinate().fromJsonString(jsonString, options);
61
- }
62
- static equals(a, b) {
63
- return protobuf_1.proto2.util.equals(ClientCoordinate, a, b);
64
- }
65
- }
66
- exports.ClientCoordinate = ClientCoordinate;
67
- ClientCoordinate.runtime = protobuf_1.proto2;
68
- ClientCoordinate.typeName = "org.observertc.observer.models.ClientCoordinate";
69
- ClientCoordinate.fields = protobuf_1.proto2.util.newFieldList(() => [
70
- { no: 1, name: "latitude", kind: "scalar", T: 2 /* ScalarType.FLOAT */ },
71
- { no: 2, name: "longitude", kind: "scalar", T: 2 /* ScalarType.FLOAT */ },
72
- ]);
73
45
  /**
74
46
  * @generated from message org.observertc.observer.models.Client
75
47
  */
@@ -114,7 +86,6 @@ Client.fields = protobuf_1.proto2.util.newFieldList(() => [
114
86
  { no: 13, name: "operationSystem", kind: "message", T: samples_pb_1.Samples_ClientSample_OperationSystem, opt: true },
115
87
  { no: 14, name: "platform", kind: "message", T: samples_pb_1.Samples_ClientSample_Platform, opt: true },
116
88
  { no: 15, name: "engine", kind: "message", T: samples_pb_1.Samples_ClientSample_Engine, opt: true },
117
- { no: 16, name: "coordinates", kind: "message", T: ClientCoordinate, opt: true },
118
89
  ]);
119
90
  /**
120
91
  * @generated from message org.observertc.observer.models.PeerConnection
@@ -130,22 +101,6 @@ class PeerConnection extends protobuf_1.Message {
130
101
  * @generated from field: repeated string outboundTrackIds = 12;
131
102
  */
132
103
  this.outboundTrackIds = [];
133
- /**
134
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.IceLocalCandidate icelocalCandidates = 13;
135
- */
136
- this.icelocalCandidates = [];
137
- /**
138
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.IceRemoteCandidate iceRemoteCandidates = 14;
139
- */
140
- this.iceRemoteCandidates = [];
141
- /**
142
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.IceCandidatePair iceCandidatePairs = 15;
143
- */
144
- this.iceCandidatePairs = [];
145
- /**
146
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.PeerConnectionTransport transports = 16;
147
- */
148
- this.transports = [];
149
104
  protobuf_1.proto2.util.initPartial(data, this);
150
105
  }
151
106
  static fromBinary(bytes, options) {
@@ -177,10 +132,6 @@ PeerConnection.fields = protobuf_1.proto2.util.newFieldList(() => [
177
132
  { no: 10, name: "label", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
178
133
  { no: 11, name: "inboundTrackIds", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
179
134
  { no: 12, name: "outboundTrackIds", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
180
- { no: 13, name: "icelocalCandidates", kind: "message", T: samples_pb_1.Samples_ClientSample_IceLocalCandidate, repeated: true },
181
- { no: 14, name: "iceRemoteCandidates", kind: "message", T: samples_pb_1.Samples_ClientSample_IceRemoteCandidate, repeated: true },
182
- { no: 15, name: "iceCandidatePairs", kind: "message", T: samples_pb_1.Samples_ClientSample_IceCandidatePair, repeated: true },
183
- { no: 16, name: "transports", kind: "message", T: samples_pb_1.Samples_ClientSample_PeerConnectionTransport, repeated: true },
184
135
  { no: 17, name: "touched", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true },
185
136
  ]);
186
137
  /**
@@ -210,7 +161,6 @@ InboundTrackVideoStats.typeName = "org.observertc.observer.models.InboundTrackVi
210
161
  InboundTrackVideoStats.fields = protobuf_1.proto2.util.newFieldList(() => [
211
162
  { no: 1, name: "ssrc", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
212
163
  { no: 2, name: "fps", kind: "scalar", T: 2 /* ScalarType.FLOAT */, opt: true },
213
- { no: 4, name: "sample", kind: "message", T: samples_pb_1.Samples_ClientSample_InboundVideoTrack, opt: true },
214
164
  ]);
215
165
  /**
216
166
  * @generated from message org.observertc.observer.models.InboundTrackStats
@@ -251,14 +201,6 @@ class InboundTrack extends protobuf_1.Message {
251
201
  * @generated from field: repeated int64 ssrc = 14;
252
202
  */
253
203
  this.ssrc = [];
254
- /**
255
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.InboundAudioTrack audioStats = 15;
256
- */
257
- this.audioStats = [];
258
- /**
259
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.InboundVideoTrack videoStats = 16;
260
- */
261
- this.videoStats = [];
262
204
  protobuf_1.proto2.util.initPartial(data, this);
263
205
  }
264
206
  static fromBinary(bytes, options) {
@@ -292,8 +234,6 @@ InboundTrack.fields = protobuf_1.proto2.util.newFieldList(() => [
292
234
  { no: 12, name: "sfuSinkId", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
293
235
  { no: 13, name: "sfuStreamId", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
294
236
  { no: 14, name: "ssrc", kind: "scalar", T: 3 /* ScalarType.INT64 */, repeated: true },
295
- { no: 15, name: "audioStats", kind: "message", T: samples_pb_1.Samples_ClientSample_InboundAudioTrack, repeated: true },
296
- { no: 16, name: "videoStats", kind: "message", T: samples_pb_1.Samples_ClientSample_InboundVideoTrack, repeated: true },
297
237
  { no: 17, name: "touched", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true },
298
238
  ]);
299
239
  /**
@@ -306,14 +246,6 @@ class OutboundTrack extends protobuf_1.Message {
306
246
  * @generated from field: repeated int64 ssrc = 13;
307
247
  */
308
248
  this.ssrc = [];
309
- /**
310
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.OutboundAudioTrack audioStats = 14;
311
- */
312
- this.audioStats = [];
313
- /**
314
- * @generated from field: repeated org.observertc.schemas.protobuf.Samples.ClientSample.OutboundVideoTrack videoStats = 15;
315
- */
316
- this.videoStats = [];
317
249
  protobuf_1.proto2.util.initPartial(data, this);
318
250
  }
319
251
  static fromBinary(bytes, options) {
@@ -346,8 +278,6 @@ OutboundTrack.fields = protobuf_1.proto2.util.newFieldList(() => [
346
278
  { no: 11, name: "marker", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
347
279
  { no: 12, name: "sfuStreamId", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
348
280
  { no: 13, name: "ssrc", kind: "scalar", T: 3 /* ScalarType.INT64 */, repeated: true },
349
- { no: 14, name: "audioStats", kind: "message", T: samples_pb_1.Samples_ClientSample_OutboundAudioTrack, repeated: true },
350
- { no: 15, name: "videoStats", kind: "message", T: samples_pb_1.Samples_ClientSample_OutboundVideoTrack, repeated: true },
351
281
  { no: 16, name: "touched", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true },
352
282
  ]);
353
283
  /**
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- // @generated by protoc-gen-es v1.1.1 with parameter "target=ts"
2
+ // @generated by protoc-gen-es v1.7.2 with parameter "target=ts"
3
3
  // @generated from file samples.proto (package org.observertc.schemas.protobuf, syntax proto2)
4
4
  /* eslint-disable */
5
5
  // @ts-nocheck
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@observertc/observer-js",
3
- "version": "0.30.1-beta",
3
+ "version": "0.30.3-beta",
4
4
  "description": "Server Side NodeJS Library for processing ObserveRTC Samples",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { ClientSample, IceLocalCandidate } from '@observertc/sample-schemas-js';
1
+ import { ClientSample } from '@observertc/sample-schemas-js';
2
2
  import { ObservedCall } from './ObservedCall';
3
3
  import * as Models from './models/Models';
4
4
  import { StorageProvider } from './storages/StorageProvider';
@@ -9,9 +9,9 @@ import { createLogger } from './common/logger';
9
9
  import { CallMetaType, createCallMetaReport } from './common/callMetaReports';
10
10
  // eslint-disable-next-line camelcase
11
11
  import { Samples_ClientSample_Browser, Samples_ClientSample_Engine, Samples_ClientSample_OperationSystem, Samples_ClientSample_Platform } from './models/samples_pb';
12
- import { IceCandidatePairReport } from '@observertc/report-schemas-js';
13
12
  import { isValidUuid } from './common/utils';
14
13
  import { createClientJoinedEventReport, createClientLeftEventReport } from './common/callEventReports';
14
+ import { CallEventType } from './common/CallEventType';
15
15
 
16
16
  const logger = createLogger('ObservedClient');
17
17
 
@@ -33,7 +33,6 @@ export type ObservedClientEvents = {
33
33
  update: [],
34
34
  close: [],
35
35
  'processing-sample-overflow': [],
36
- 'candidate-pair-change': [],
37
36
  newpeerconnection: [ObservedPeerConnection],
38
37
  };
39
38
 
@@ -89,6 +88,8 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
89
88
  return result;
90
89
  }
91
90
 
91
+ public created = Date.now();
92
+
92
93
  private readonly _peerConnections = new Map<string, ObservedPeerConnection>();
93
94
  private readonly _execute = createSingleExecutor();
94
95
 
@@ -97,15 +98,28 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
97
98
  private _updated = Date.now();
98
99
  private _acceptedSample = 0;
99
100
  private _processingSample = 0;
100
- private _selectedLocalIceCandidate?: IceLocalCandidate;
101
- private _selectedRemoteIceCandidate?: IceLocalCandidate;
102
101
  private _timeZoneOffsetInHours?: number;
103
102
  private _leaveReported = false;
104
103
 
104
+ public totalInboundPacketsLost = 0;
105
+ public totalInboundPacketsReceived = 0;
106
+ public totalOutboundPacketsSent = 0;
107
+ public totalDataChannelBytesSent = 0;
108
+ public totalDataChannelBytesReceived = 0;
109
+ public totalSentBytes = 0;
110
+ public totalReceivedBytes = 0;
111
+
112
+ public avgRttInS?: number;
113
+ public sendingBitrate?: number;
114
+ public receivingBitrate?: number;
115
+
105
116
  public readonly mediaDevices: string[] = [];
106
117
  public readonly codecs: string[] = [];
107
118
  public readonly userMediaErrors: string[] = [];
108
119
 
120
+ public readonly ωpendingCreatedTracksTimestamp = new Map<string, number>();
121
+ public readonly ωpendingCreatedPeerConnectionTimestamp = new Map<string, number>();
122
+
109
123
  private constructor(
110
124
  private readonly _model: Models.Client,
111
125
  public readonly call: ObservedCall,
@@ -153,14 +167,6 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
153
167
  return this._model.userId;
154
168
  }
155
169
 
156
- public get selectedLocalIceCandidate() {
157
- return this._selectedLocalIceCandidate;
158
- }
159
-
160
- public get selectedRemoteIceCandidate() {
161
- return this._selectedRemoteIceCandidate;
162
- }
163
-
164
170
  public get timeZoneOffsetInHours() {
165
171
  return this._timeZoneOffsetInHours;
166
172
  }
@@ -193,6 +199,10 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
193
199
  return this._updated;
194
200
  }
195
201
 
202
+ public get uptimeInMs() {
203
+ return this._updated - this.created;
204
+ }
205
+
196
206
  public get closed() {
197
207
  return this._closed;
198
208
  }
@@ -225,6 +235,11 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
225
235
  }
226
236
 
227
237
  return this._execute(async () => {
238
+ for (const peerConnection of this._peerConnections.values()) {
239
+ if (peerConnection.closed) continue;
240
+ peerConnection.resetMetrics();
241
+ }
242
+
228
243
  let executeSave = false;
229
244
 
230
245
  if (this.userId !== sample.userId) {
@@ -379,9 +394,47 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
379
394
  });
380
395
  }
381
396
 
382
- for (const { timestamp = Date.now(), ...callEvent } of sample.customCallEvents ?? []) {
383
- if (callEvent.name === 'CLIENT_LEFT') this._leaveReported = true;
397
+ for (const { timestamp, ...callEvent } of sample.customCallEvents ?? []) {
398
+ const peerConnection = this._peerConnections.get(callEvent.peerConnectionId ?? '');
384
399
 
400
+ switch (callEvent.name) {
401
+ case CallEventType.CLIENT_JOINED:
402
+ this.created = timestamp ?? this.created;
403
+ break;
404
+ case CallEventType.CLIENT_LEFT:
405
+ this._leaveReported = true;
406
+ break;
407
+ case CallEventType.ICE_CONNECTION_STATE_CHANGED:
408
+ peerConnection && callEvent.value && peerConnection.emit('iceconnectionstatechange', callEvent.value);
409
+ break;
410
+ case CallEventType.ICE_GATHERING_STATE_CHANGED:
411
+ peerConnection && callEvent.value && peerConnection.emit('icegatheringstatechange', callEvent.value);
412
+ break;
413
+ case CallEventType.PEER_CONNECTION_STATE_CHANGED:
414
+ peerConnection && callEvent.value && peerConnection.emit('connectionstatechange', callEvent.value);
415
+ break;
416
+ case CallEventType.PEER_CONNECTION_OPENED: {
417
+ if (!callEvent.peerConnectionId) break;
418
+ if (peerConnection) {
419
+ peerConnection.created = timestamp ?? peerConnection.created;
420
+ } else {
421
+ this.ωpendingCreatedPeerConnectionTimestamp.set(callEvent.peerConnectionId, timestamp ?? Date.now());
422
+ }
423
+ break;
424
+ }
425
+ case CallEventType.MEDIA_TRACK_ADDED: {
426
+ if (!callEvent.mediaTrackId) break;
427
+ const track = peerConnection?.getTrack(callEvent.mediaTrackId);
428
+
429
+ if (track) {
430
+ track.created = timestamp ?? track.created;
431
+ } else {
432
+ this.ωpendingCreatedTracksTimestamp.set(callEvent.mediaTrackId, timestamp ?? Date.now());
433
+ }
434
+ }
435
+
436
+ }
437
+
385
438
  this.reports.addCallEventReport({
386
439
  serviceId: this.serviceId,
387
440
  mediaUnitId: this.mediaUnitId,
@@ -389,7 +442,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
389
442
  callId: this.callId,
390
443
  clientId: this.clientId,
391
444
  userId: this.userId,
392
- timestamp,
445
+ timestamp: timestamp ?? Date.now(),
393
446
  ...callEvent,
394
447
  });
395
448
  }
@@ -480,76 +533,6 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
480
533
 
481
534
  this.reports.addCallMetaReport(callMetaReport);
482
535
  }
483
- let selectedLocalCandidateId: string | undefined;
484
- let selectedRemoteCandidateId: string | undefined;
485
-
486
- for (const candidatePair of sample.iceCandidatePairs ?? []) {
487
- if (candidatePair.nominated) {
488
- if (!selectedLocalCandidateId) selectedLocalCandidateId = candidatePair.localCandidateId;
489
- if (!selectedRemoteCandidateId) selectedRemoteCandidateId = candidatePair.remoteCandidateId;
490
- }
491
-
492
- const report: IceCandidatePairReport = {
493
- serviceId: this.serviceId,
494
- mediaUnitId: this.mediaUnitId,
495
- roomId: this.roomId,
496
- callId: this.callId,
497
- clientId: this.clientId,
498
- userId: this.userId,
499
- timestamp: sample.timestamp,
500
- ...candidatePair,
501
- sampleSeq: -1, // deprecated
502
- };
503
-
504
- this.reports.addIceCandidatePairReport(report);
505
-
506
- }
507
-
508
- let changeIceCandidatePair = false;
509
-
510
- for (const iceLocalCandidate of sample.iceLocalCandidates ?? []) {
511
- if (
512
- iceLocalCandidate.id && selectedLocalCandidateId &&
513
- iceLocalCandidate.id === selectedLocalCandidateId &&
514
- this._selectedLocalIceCandidate?.id !== iceLocalCandidate.id
515
- ) {
516
- this._selectedLocalIceCandidate = iceLocalCandidate;
517
- changeIceCandidatePair = true;
518
- }
519
- const callMetaReport = createCallMetaReport(
520
- this.serviceId,
521
- this.mediaUnitId,
522
- this.roomId,
523
- this.callId,
524
- this.clientId, {
525
- type: CallMetaType.ICE_LOCAL_CANDIDATE,
526
- payload: iceLocalCandidate,
527
- }, this.userId);
528
-
529
- this.reports.addCallMetaReport(callMetaReport);
530
- }
531
-
532
- for (const iceRemoteCandidate of sample.iceRemoteCandidates ?? []) {
533
- if (
534
- iceRemoteCandidate.id && selectedRemoteCandidateId &&
535
- iceRemoteCandidate.id === selectedRemoteCandidateId &&
536
- this._selectedRemoteIceCandidate?.id !== iceRemoteCandidate.id
537
- ) {
538
- this._selectedRemoteIceCandidate = iceRemoteCandidate;
539
- changeIceCandidatePair = true;
540
- }
541
- const callMetaReport = createCallMetaReport(
542
- this.serviceId,
543
- this.mediaUnitId,
544
- this.roomId,
545
- this.callId,
546
- this.clientId, {
547
- type: CallMetaType.ICE_REMOTE_CANDIDATE,
548
- payload: iceRemoteCandidate,
549
- }, this.userId);
550
-
551
- this.reports.addCallMetaReport(callMetaReport);
552
- }
553
536
 
554
537
  for (const transport of sample.pcTransports ?? []) {
555
538
  try {
@@ -676,6 +659,48 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
676
659
  }
677
660
  }
678
661
 
662
+ for (const iceLocalCandidate of sample.iceLocalCandidates ?? []) {
663
+ if (!iceLocalCandidate.peerConnectionId) {
664
+ logger.warn(`Local ice candidate without peerConnectionId: ${JSON.stringify(iceLocalCandidate)}`);
665
+ continue;
666
+ }
667
+ const peerConnection = this._peerConnections.get(iceLocalCandidate.peerConnectionId);
668
+
669
+ if (!peerConnection) {
670
+ logger.debug(`Peer connection ${iceLocalCandidate.peerConnectionId} not found for ice local candidate ${iceLocalCandidate.id}`);
671
+ continue;
672
+ }
673
+
674
+ peerConnection.ICE.addLocalCandidate(iceLocalCandidate);
675
+ }
676
+
677
+ for (const iceRemoteCandidate of sample.iceRemoteCandidates ?? []) {
678
+ if (!iceRemoteCandidate.peerConnectionId) {
679
+ logger.warn(`Remote ice candidate without peerConnectionId: ${JSON.stringify(iceRemoteCandidate)}`);
680
+ continue;
681
+ }
682
+ const peerConnection = this._peerConnections.get(iceRemoteCandidate.peerConnectionId);
683
+
684
+ if (!peerConnection) {
685
+ logger.debug(`Peer connection ${iceRemoteCandidate.peerConnectionId} not found for ice remote candidate ${iceRemoteCandidate.id}`);
686
+ continue;
687
+ }
688
+
689
+ peerConnection.ICE.addRemoteCandidate(iceRemoteCandidate);
690
+ }
691
+
692
+ for (const candidatePair of sample.iceCandidatePairs ?? []) {
693
+ const peerConnection = this._peerConnections.get(candidatePair.peerConnectionId);
694
+
695
+ if (!peerConnection) {
696
+ logger.debug(`Peer connection ${candidatePair.peerConnectionId} not found for ice candidate pair ${candidatePair.localCandidateId}, ${candidatePair.remoteCandidateId}`);
697
+
698
+ continue;
699
+ }
700
+
701
+ peerConnection.ICE.update(candidatePair, sample.timestamp);
702
+ }
703
+
679
704
  // close entries that are idle for too long
680
705
  if (this.call.observer.config.maxEntryIdleTimeInMs && this._updated < sample.timestamp) {
681
706
 
@@ -706,10 +731,35 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
706
731
  }
707
732
  }
708
733
  }
709
-
710
- if (changeIceCandidatePair) {
711
- this.emit('candidate-pair-change');
734
+
735
+ // update metrics
736
+ this.totalSentBytes = 0;
737
+ this.totalReceivedBytes = 0;
738
+ this.totalOutboundPacketsSent = 0;
739
+ this.totalInboundPacketsReceived = 0;
740
+ this.totalInboundPacketsLost = 0;
741
+ this.totalDataChannelBytesSent = 0;
742
+ this.totalDataChannelBytesReceived = 0;
743
+ this.sendingBitrate = 0;
744
+ this.receivingBitrate = 0;
745
+ let sumRttInS = 0;
746
+
747
+ for (const peerConnection of this._peerConnections.values()) {
748
+ if (peerConnection.closed) continue;
749
+ peerConnection.updateMetrics();
750
+ this.totalSentBytes += peerConnection.totalSentAudioBytes + peerConnection.totalSentVideoBytes;
751
+ this.totalReceivedBytes += peerConnection.totalReceivedAudioBytes + peerConnection.totalReceivedVideoBytes;
752
+ this.totalOutboundPacketsSent += peerConnection.totalOutboundPacketsSent;
753
+ this.totalInboundPacketsReceived += peerConnection.totalInboundPacketsReceived;
754
+ this.totalInboundPacketsLost += peerConnection.totalInboundPacketsLost;
755
+ this.totalDataChannelBytesSent += peerConnection.totalDataChannelBytesSent;
756
+ this.totalDataChannelBytesReceived += peerConnection.totalDataChannelBytesReceived;
757
+ this.sendingBitrate += peerConnection.sendingAudioBitrate + peerConnection.sendingVideoBitrate;
758
+ this.receivingBitrate += peerConnection.receivingAudioBitrate + peerConnection.receivingVideoBitrate;
759
+ sumRttInS += peerConnection.avgRttInS ?? 0;
712
760
  }
761
+
762
+ this.avgRttInS = this._peerConnections.size ? sumRttInS / this._peerConnections.size : -1;
713
763
 
714
764
  if (executeSave) await this._save();
715
765
 
@@ -725,6 +775,13 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
725
775
  peerConnectionId,
726
776
  }, this, this._storageProvider);
727
777
 
778
+ const pendingTracksTimestamp = this.ωpendingCreatedTracksTimestamp.get(peerConnectionId);
779
+
780
+ if (pendingTracksTimestamp) {
781
+ result.created = pendingTracksTimestamp;
782
+ this.ωpendingCreatedTracksTimestamp.delete(peerConnectionId);
783
+ }
784
+
728
785
  result.once('close', () => {
729
786
  this._peerConnections.delete(peerConnectionId);
730
787
  this._model.peerConnectionIds = this._model.peerConnectionIds.filter((id) => id !== peerConnectionId);
@@ -0,0 +1,176 @@
1
+ import { IceCandidatePair, IceLocalCandidate, IceRemoteCandidate } from '@observertc/sample-schemas-js';
2
+ import { EventEmitter } from 'events';
3
+ import { ObservedPeerConnection } from './ObservedPeerConnection';
4
+ import { IceCandidatePairReport } from '@observertc/report-schemas-js';
5
+ import { CallMetaType, createCallMetaReport } from './common/callMetaReports';
6
+
7
+ export type ObservedICEEvents = {
8
+ update: [],
9
+ 'new-local-candidate': [IceLocalCandidate],
10
+ 'new-remote-candidate': [IceRemoteCandidate],
11
+ 'new-selected-candidate-pair': [{
12
+ localCandidate: IceLocalCandidate,
13
+ remoteCandidate: IceRemoteCandidate,
14
+ }],
15
+ close: [],
16
+ };
17
+
18
+ export declare interface ObservedICE {
19
+ on<U extends keyof ObservedICEEvents>(event: U, listener: (...args: ObservedICEEvents[U]) => void): this;
20
+ off<U extends keyof ObservedICEEvents>(event: U, listener: (...args: ObservedICEEvents[U]) => void): this;
21
+ once<U extends keyof ObservedICEEvents>(event: U, listener: (...args: ObservedICEEvents[U]) => void): this;
22
+ emit<U extends keyof ObservedICEEvents>(event: U, ...args: ObservedICEEvents[U]): boolean;
23
+ }
24
+
25
+ export class ObservedICE extends EventEmitter {
26
+ public static create(peerConnection: ObservedPeerConnection,) {
27
+ return new ObservedICE(peerConnection);
28
+ }
29
+
30
+ private readonly _localCandidates = new Map<string, IceLocalCandidate>();
31
+ private readonly _remoteCandidates = new Map<string, IceLocalCandidate>();
32
+
33
+ private _selectedLocalCandidateId?: string;
34
+ private _selectedRemoteCandidateId?: string;
35
+ private _updated = Date.now();
36
+ private _stats?: IceCandidatePair;
37
+ private _closed = false;
38
+
39
+ private constructor(
40
+ public readonly peerConnection: ObservedPeerConnection,
41
+ ) {
42
+ super();
43
+ }
44
+
45
+ public get reports() {
46
+ return this.peerConnection.reports;
47
+ }
48
+
49
+ public get localCandidates(): ReadonlyMap<string, IceLocalCandidate> {
50
+ return this._localCandidates;
51
+ }
52
+
53
+ public get remoteCandidates(): ReadonlyMap<string, IceRemoteCandidate> {
54
+ return this._remoteCandidates;
55
+ }
56
+
57
+ public get selectedLocalCandidate() {
58
+ return this._localCandidates.get(this._selectedLocalCandidateId ?? '');
59
+ }
60
+
61
+ public get selectedRemoteCandidate() {
62
+ return this._remoteCandidates.get(this._selectedRemoteCandidateId ?? '');
63
+ }
64
+
65
+ public get stats() {
66
+ return this._stats;
67
+ }
68
+
69
+ public get updated() {
70
+ return this._updated;
71
+ }
72
+
73
+ public get closed() {
74
+ return this._closed;
75
+ }
76
+
77
+ public close() {
78
+ if (this._closed) return;
79
+ this._closed = true;
80
+ this.emit('close');
81
+ }
82
+
83
+ public addLocalCandidate(candidate: IceLocalCandidate) {
84
+ if (!candidate.id) return;
85
+
86
+ const newCandidate = !this._localCandidates.has(candidate.id);
87
+
88
+ this._localCandidates.set(candidate.id, candidate);
89
+
90
+ if (newCandidate) {
91
+ const callMetaReport = createCallMetaReport(
92
+ this.peerConnection.client.serviceId,
93
+ this.peerConnection.client.mediaUnitId,
94
+ this.peerConnection.client.roomId,
95
+ this.peerConnection.client.callId,
96
+ this.peerConnection.client.clientId, {
97
+ type: CallMetaType.ICE_LOCAL_CANDIDATE,
98
+ payload: candidate,
99
+ }, this.peerConnection.client.userId);
100
+
101
+ this.reports.addCallMetaReport(callMetaReport);
102
+
103
+ this.emit('new-local-candidate', candidate);
104
+ }
105
+ }
106
+
107
+ public addRemoteCandidate(candidate: IceRemoteCandidate) {
108
+ if (!candidate.id) return;
109
+
110
+ const newCandidate = !this._remoteCandidates.has(candidate.id);
111
+
112
+ this._remoteCandidates.set(candidate.id, candidate);
113
+
114
+ if (newCandidate) {
115
+ const callMetaReport = createCallMetaReport(
116
+ this.peerConnection.client.serviceId,
117
+ this.peerConnection.client.mediaUnitId,
118
+ this.peerConnection.client.roomId,
119
+ this.peerConnection.client.callId,
120
+ this.peerConnection.client.clientId, {
121
+ type: CallMetaType.ICE_REMOTE_CANDIDATE,
122
+ payload: candidate,
123
+ }, this.peerConnection.client.userId);
124
+
125
+ this.reports.addCallMetaReport(callMetaReport);
126
+
127
+ this.emit('new-remote-candidate', candidate);
128
+ }
129
+ }
130
+
131
+ public update(candidatePair: IceCandidatePair, timestamp: number) {
132
+
133
+ const report: IceCandidatePairReport = {
134
+ serviceId: this.peerConnection.client.serviceId,
135
+ mediaUnitId: this.peerConnection.client.mediaUnitId,
136
+ roomId: this.peerConnection.client.roomId,
137
+ callId: this.peerConnection.client.callId,
138
+ clientId: this.peerConnection.client.clientId,
139
+ userId: this.peerConnection.client.userId,
140
+ timestamp,
141
+ ...candidatePair,
142
+ sampleSeq: -1, // deprecated
143
+ };
144
+
145
+ this.reports.addIceCandidatePairReport(report);
146
+
147
+ if (!candidatePair.nominated) return;
148
+
149
+ this._stats = candidatePair;
150
+
151
+ if (
152
+ candidatePair.localCandidateId &&
153
+ candidatePair.localCandidateId !== this._selectedLocalCandidateId &&
154
+ this._localCandidates.has(candidatePair.localCandidateId) &&
155
+ candidatePair.remoteCandidateId &&
156
+ candidatePair.remoteCandidateId !== this._selectedRemoteCandidateId &&
157
+ this._remoteCandidates.has(candidatePair.remoteCandidateId)
158
+ ) {
159
+ this._selectedLocalCandidateId = candidatePair.localCandidateId;
160
+ this._selectedRemoteCandidateId = candidatePair.remoteCandidateId;
161
+
162
+ const localCandidate = this._localCandidates.get(candidatePair.localCandidateId);
163
+ const remoteCandidate = this._remoteCandidates.get(candidatePair.remoteCandidateId);
164
+
165
+ if (localCandidate && remoteCandidate) {
166
+ this.emit('new-selected-candidate-pair', {
167
+ localCandidate,
168
+ remoteCandidate,
169
+ });
170
+ }
171
+ }
172
+
173
+ this._updated = timestamp;
174
+ this.emit('update');
175
+ }
176
+ }