@observertc/observer-js 0.41.7 → 0.42.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 (53) hide show
  1. package/lib/ObservedCall.d.ts +10 -3
  2. package/lib/ObservedCall.d.ts.map +1 -1
  3. package/lib/ObservedCall.js +28 -0
  4. package/lib/ObservedClient.d.ts +4 -0
  5. package/lib/ObservedClient.d.ts.map +1 -1
  6. package/lib/ObservedClient.js +81 -32
  7. package/lib/ObservedInboundAudioTrack.d.ts +94 -0
  8. package/lib/ObservedInboundAudioTrack.d.ts.map +1 -0
  9. package/lib/ObservedInboundAudioTrack.js +215 -0
  10. package/lib/ObservedInboundVideoTrack.d.ts +98 -0
  11. package/lib/ObservedInboundVideoTrack.d.ts.map +1 -0
  12. package/lib/{ObservedInboundTrack.js → ObservedInboundVideoTrack.js} +53 -44
  13. package/lib/ObservedOutboundAudioTrack.d.ts +85 -0
  14. package/lib/ObservedOutboundAudioTrack.d.ts.map +1 -0
  15. package/lib/{ObservedOutboundTrack.js → ObservedOutboundAudioTrack.js} +35 -58
  16. package/lib/ObservedOutboundVideoTrack.d.ts +88 -0
  17. package/lib/ObservedOutboundVideoTrack.d.ts.map +1 -0
  18. package/lib/ObservedOutboundVideoTrack.js +204 -0
  19. package/lib/ObservedPeerConnection.d.ts +26 -16
  20. package/lib/ObservedPeerConnection.d.ts.map +1 -1
  21. package/lib/ObservedPeerConnection.js +157 -86
  22. package/lib/RemoteTrackAssigner.d.ts +14 -0
  23. package/lib/RemoteTrackAssigner.d.ts.map +1 -0
  24. package/lib/RemoteTrackAssigner.js +52 -0
  25. package/lib/common/CalculatedScore.d.ts +79 -0
  26. package/lib/common/CalculatedScore.d.ts.map +1 -0
  27. package/lib/common/CalculatedScore.js +155 -0
  28. package/lib/common/QualityScoreUtils.d.ts +1 -0
  29. package/lib/common/QualityScoreUtils.d.ts.map +1 -0
  30. package/lib/common/QualityScoreUtils.js +1 -0
  31. package/lib/common/utils.d.ts +1 -0
  32. package/lib/common/utils.d.ts.map +1 -1
  33. package/lib/common/utils.js +14 -1
  34. package/lib/index.d.ts +4 -2
  35. package/lib/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/ObservedCall.ts +40 -4
  38. package/src/ObservedClient.ts +77 -17
  39. package/src/ObservedInboundAudioTrack.ts +336 -0
  40. package/src/{ObservedInboundTrack.ts → ObservedInboundVideoTrack.ts} +85 -66
  41. package/src/{ObservedOutboundTrack.ts → ObservedOutboundAudioTrack.ts} +59 -74
  42. package/src/ObservedOutboundVideoTrack.ts +326 -0
  43. package/src/ObservedPeerConnection.ts +205 -131
  44. package/src/RemoteTrackAssigner.ts +68 -0
  45. package/src/common/CalculatedScore.ts +180 -0
  46. package/src/common/QualityScoreUtils.ts +0 -0
  47. package/src/common/utils.ts +17 -1
  48. package/src/index.ts +4 -2
  49. package/src/monitors/CallSummaryMonitor.ts +3 -3
  50. package/lib/ObservedInboundTrack.d.ts +0 -96
  51. package/lib/ObservedInboundTrack.d.ts.map +0 -1
  52. package/lib/ObservedOutboundTrack.d.ts +0 -84
  53. package/lib/ObservedOutboundTrack.d.ts.map +0 -1
package/lib/index.d.ts CHANGED
@@ -9,8 +9,10 @@ export type { ClientIssueMonitorConfig, ClientIssueMonitorEmittedIssueEvent, Cli
9
9
  export type { ObservedCall, ObservedCallModel } from './ObservedCall';
10
10
  export type { ObservedClient, ObservedClientModel } from './ObservedClient';
11
11
  export type { ObservedPeerConnection, ObservedPeerConnectionModel, ObservedPeerConnectionEvents } from './ObservedPeerConnection';
12
- export type { ObservedInboundTrack, ObservedInboundTrackModel, ObservedInboundTrackEvents } from './ObservedInboundTrack';
13
- export type { ObservedOutboundTrack, ObservedOutboundTrackModel, ObservedOutboundTrackEvents } from './ObservedOutboundTrack';
12
+ export type { ObservedInboundAudioTrack, ObservedInboundAudioTrackModel, ObservedInboundAudioTrackEvents } from './ObservedInboundAudioTrack';
13
+ export type { ObservedOutboundAudioTrack, ObservedOutboundAudioTrackModel, ObservedOutboundAudioTrackEvents } from './ObservedOutboundAudioTrack';
14
+ export type { ObservedInboundVideoTrack, ObservedInboundVideoTrackModel, ObservedInboundVideoTrackEvents } from './ObservedInboundVideoTrack';
15
+ export type { ObservedOutboundVideoTrack, ObservedOutboundVideoTrackModel, ObservedOutboundVideoTrackEvents } from './ObservedOutboundVideoTrack';
14
16
  export type { CallSummary, ClientSummary, ClientIssue } from './monitors/CallSummary';
15
17
  export type { ObserverSinkContext } from './common/types';
16
18
  export type { SfuServerMonitorMetricsRecord } from './monitors/SfuServerMonitor';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAE9D,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAExD,YAAY,EACX,OAAO,EACP,YAAY,EACZ,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,WAAW,GACX,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACX,eAAe,EACf,cAAc,EACd,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,6BAA6B,EAC7B,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACX,wBAAwB,EACxB,mCAAmC,EACnC,kBAAkB,EAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,YAAY,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAClI,YAAY,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAC1H,YAAY,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAC9H,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACtF,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,YAAY,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACjF,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAClH,OAAO,EACN,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mCAAmC,EACnC,kCAAkC,EAClC,oCAAoC,GACpC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACtD,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,YAE9D;AAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAA8B,MAAM,iBAAiB,CAAC;AAC/E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAG3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAE9D,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAExD,YAAY,EACX,OAAO,EACP,YAAY,EACZ,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,WAAW,GACX,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACX,eAAe,EACf,cAAc,EACd,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,6BAA6B,EAC7B,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACX,wBAAwB,EACxB,mCAAmC,EACnC,kBAAkB,EAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,YAAY,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAClI,YAAY,EAAE,yBAAyB,EAAE,8BAA8B,EAAE,+BAA+B,EAAE,MAAM,6BAA6B,CAAC;AAC9I,YAAY,EAAE,0BAA0B,EAAE,+BAA+B,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAClJ,YAAY,EAAE,yBAAyB,EAAE,8BAA8B,EAAE,+BAA+B,EAAE,MAAM,6BAA6B,CAAC;AAC9I,YAAY,EAAE,0BAA0B,EAAE,+BAA+B,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAClJ,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACtF,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,YAAY,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACjF,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAClH,OAAO,EACN,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mCAAmC,EACnC,kCAAkC,EAClC,oCAAoC,GACpC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACtD,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,YAE9D;AAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAA8B,MAAM,iBAAiB,CAAC;AAC/E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAG3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@observertc/observer-js",
3
- "version": "0.41.7",
3
+ "version": "0.42.0",
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,13 +1,15 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { ObservedClient, ObservedClientEvents, ObservedClientModel } from './ObservedClient';
3
- import { ObservedOutboundTrack } from './ObservedOutboundTrack';
4
3
  import { Observer } from './Observer';
5
4
  import { createClientJoinedEventReport } from './common/callEventReports';
6
- import { PartialBy } from './common/utils';
5
+ import { getMedian, PartialBy } from './common/utils';
7
6
  import { CallEventReport } from '@observertc/report-schemas-js';
8
7
  import { createProcessor } from './common/Middleware';
9
8
  import { ClientSample } from '@observertc/sample-schemas-js';
10
9
  import { ClientIssue } from './monitors/CallSummary';
10
+ import { ObservedOutboundAudioTrack } from './ObservedOutboundAudioTrack';
11
+ import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack';
12
+ import { CalculatedScore } from './common/CalculatedScore';
11
13
 
12
14
  type ClientIssueDetectionConfig = Pick<ClientIssue, 'severity'>;
13
15
 
@@ -38,8 +40,13 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
38
40
 
39
41
  private readonly _clients = new Map<string, ObservedClient>();
40
42
 
41
- public readonly sfuStreamIdToOutboundAudioTrack = new Map<string | number, ObservedOutboundTrack<'audio'>>();
42
- public readonly sfuStreamIdToOutboundVideoTrack = new Map<string | number, ObservedOutboundTrack<'video'>>();
43
+ public _scoreData?: {
44
+ score: CalculatedScore,
45
+ calculated: number,
46
+ };
47
+
48
+ public readonly sfuStreamIdToOutboundAudioTrack = new Map<string | number, ObservedOutboundAudioTrack>();
49
+ public readonly sfuStreamIdToOutboundVideoTrack = new Map<string | number, ObservedOutboundVideoTrack>();
43
50
  private _closed = false;
44
51
  private _updated = Date.now();
45
52
  private _ended?: number;
@@ -84,6 +91,35 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
84
91
  return this._clients;
85
92
  }
86
93
 
94
+ public get score(): CalculatedScore | undefined {
95
+ const now = Date.now();
96
+
97
+ if (now - 15000 < (this._scoreData?.calculated ?? 0)) {
98
+ return this._scoreData?.score;
99
+ } else if (this.clients.size < 1) {
100
+ return {
101
+ remarks: [ {
102
+ severity: 'none',
103
+ text: 'No clients',
104
+ } ],
105
+ score: 5.0,
106
+ timestamp: now,
107
+ };
108
+ }
109
+ const scores = [ ...this.clients.values() ].map((client) => client.score?.score ?? 5.0);
110
+
111
+ this._scoreData = {
112
+ score: {
113
+ remarks: [],
114
+ score: getMedian(scores),
115
+ timestamp: now,
116
+ },
117
+ calculated: now,
118
+ };
119
+
120
+ return this._scoreData.score;
121
+ }
122
+
87
123
  public get closed() {
88
124
  return this._closed;
89
125
  }
@@ -11,6 +11,7 @@ import { CallEventType } from './common/CallEventType';
11
11
  import { ObservedSfu } from './ObservedSfu';
12
12
  import { ClientIssue } from './monitors/CallSummary';
13
13
  import { CallEventReport } from '@observertc/report-schemas-js';
14
+ import { CalculatedScore } from './common/CalculatedScore';
14
15
 
15
16
  const logger = createLogger('ObservedClient');
16
17
 
@@ -59,6 +60,7 @@ export type ObservedClientEvents = {
59
60
  rejoined: [{
60
61
  lastJoined: number,
61
62
  }],
63
+ score: [CalculatedScore],
62
64
  };
63
65
 
64
66
  export declare interface ObservedClient<AppData extends Record<string, unknown> = Record<string, unknown>> {
@@ -84,6 +86,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
84
86
  private _timeZoneOffsetInHours?: number;
85
87
  private _left?: number;
86
88
 
89
+ public score?: CalculatedScore;
87
90
  public usingTURN = false;
88
91
  public availableOutgoingBitrate = 0;
89
92
  public availableIncomingBitrate = 0;
@@ -238,7 +241,6 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
238
241
  if (!this._left) {
239
242
  this._addClientLeftReport();
240
243
  }
241
-
242
244
  Array.from(this._peerConnections.values()).forEach((peerConnection) => peerConnection.close());
243
245
 
244
246
  this.emit('close');
@@ -302,8 +304,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
302
304
  logger.warn(`Error adding client issue: ${(err as Error)?.message}`);
303
305
  }
304
306
 
305
- this.issues.push(issue);
306
- this.emit('issue', issue);
307
+ this._addAndEmitIssue(issue);
307
308
  }
308
309
 
309
310
  public accept(sample: ClientSample): void {
@@ -515,9 +516,8 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
515
516
  trackId: callEvent.mediaTrackId,
516
517
  attachments: callEvent.attachments ? JSON.parse(callEvent.attachments) : undefined,
517
518
  };
518
-
519
- this.issues.push(issue);
520
- this.emit('issue', issue);
519
+
520
+ this._addAndEmitIssue(issue);
521
521
  } catch (err) {
522
522
  logger.warn(`Error parsing client issue: ${(err as Error)?.message}`);
523
523
  }
@@ -619,7 +619,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
619
619
  type: CallMetaType.MEDIA_DEVICE,
620
620
  payload: mediaDevice,
621
621
  }, this.userId);
622
-
622
+
623
623
  this.reports.addCallMetaReport(callMetaReport);
624
624
  mediaDevice.label && this.mediaDevices.push(mediaDevice.label);
625
625
  }
@@ -634,7 +634,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
634
634
  type: CallMetaType.MEDIA_SOURCE,
635
635
  payload: mediaSource,
636
636
  }, this.userId);
637
-
637
+
638
638
  this.reports.addCallMetaReport(callMetaReport);
639
639
  }
640
640
 
@@ -670,10 +670,17 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
670
670
 
671
671
  inboundAudioTrack.update(track, sample.timestamp);
672
672
 
673
- const remoteOutboundTrack = this.call.sfuStreamIdToOutboundAudioTrack.get(track.sfuStreamId ?? '');
674
-
675
- if (remoteOutboundTrack && inboundAudioTrack.sfuStreamId && !remoteOutboundTrack.remoteInboundTracks.has(inboundAudioTrack.sfuStreamId)) {
676
- remoteOutboundTrack.connectInboundTrack(inboundAudioTrack);
673
+ if (!inboundAudioTrack.remoteOutboundTrack) {
674
+ const remoteOutboundTrack = this.call.sfuStreamIdToOutboundAudioTrack.get(inboundAudioTrack.sfuStreamId ?? '');
675
+
676
+ if (remoteOutboundTrack) {
677
+ inboundAudioTrack.remoteOutboundTrack = remoteOutboundTrack;
678
+
679
+ inboundAudioTrack.once('close', () => {
680
+ remoteOutboundTrack?.remoteInboundTracks.delete(inboundAudioTrack.trackId ?? '');
681
+ });
682
+ remoteOutboundTrack.remoteInboundTracks.set(inboundAudioTrack.trackId ?? '', inboundAudioTrack);
683
+ }
677
684
  }
678
685
  } catch (err) {
679
686
  logger.error(`Error creating inbound audio track: ${(err as Error)?.message}`);
@@ -705,11 +712,18 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
705
712
  });
706
713
 
707
714
  inboundVideoTrack.update(track, sample.timestamp);
708
-
709
- const remoteOutboundTrack = this.call.sfuStreamIdToOutboundVideoTrack.get(inboundVideoTrack.sfuStreamId ?? '');
710
-
711
- if (remoteOutboundTrack && inboundVideoTrack.sfuStreamId && !remoteOutboundTrack.remoteInboundTracks.has(inboundVideoTrack.sfuStreamId)) {
712
- remoteOutboundTrack.connectInboundTrack(inboundVideoTrack);
715
+
716
+ if (!inboundVideoTrack.remoteOutboundTrack) {
717
+ const remoteOutboundTrack = this.call.sfuStreamIdToOutboundVideoTrack.get(inboundVideoTrack.sfuStreamId ?? '');
718
+
719
+ if (remoteOutboundTrack) {
720
+ inboundVideoTrack.remoteOutboundTrack = remoteOutboundTrack;
721
+
722
+ inboundVideoTrack.once('close', () => {
723
+ remoteOutboundTrack?.remoteInboundTracks.delete(inboundVideoTrack.trackId ?? '');
724
+ });
725
+ remoteOutboundTrack.remoteInboundTracks.set(inboundVideoTrack.trackId ?? '', inboundVideoTrack);
726
+ }
713
727
  }
714
728
  } catch (err) {
715
729
  logger.error(`Error creating inbound video track: ${(err as Error)?.message}`);
@@ -734,6 +748,11 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
734
748
  });
735
749
 
736
750
  outboundAudioTrack.update(track, sample.timestamp);
751
+
752
+ if (outboundAudioTrack.sfuStreamId && !this.call.sfuStreamIdToOutboundAudioTrack.has(outboundAudioTrack.sfuStreamId)) {
753
+ this.call.sfuStreamIdToOutboundAudioTrack.set(outboundAudioTrack.sfuStreamId, outboundAudioTrack);
754
+ }
755
+
737
756
  } catch (err) {
738
757
  logger.error(`Error creating outbound audio track: ${(err as Error)?.message}`);
739
758
  }
@@ -759,6 +778,10 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
759
778
 
760
779
  outboundVideoTrack.update(track, sample.timestamp);
761
780
 
781
+ if (outboundVideoTrack.sfuStreamId && !this.call.sfuStreamIdToOutboundVideoTrack.has(outboundVideoTrack.sfuStreamId)) {
782
+ this.call.sfuStreamIdToOutboundVideoTrack.set(outboundVideoTrack.sfuStreamId, outboundVideoTrack);
783
+ }
784
+
762
785
  } catch (err) {
763
786
  logger.error(`Error creating outbound video track: ${(err as Error)?.message}`);
764
787
  }
@@ -897,6 +920,9 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
897
920
  let sumRttInMs = 0;
898
921
  let anyPeerConnectionUsingTurn = false;
899
922
 
923
+ let minPcScore = Number.MAX_SAFE_INTEGER;
924
+ let maxPcScore = Number.MIN_SAFE_INTEGER;
925
+
900
926
  for (const peerConnection of this._peerConnections.values()) {
901
927
  if (peerConnection.closed) continue;
902
928
  peerConnection.updateMetrics();
@@ -916,8 +942,21 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
916
942
  sumRttInMs += peerConnection.avgRttInMs ?? 0;
917
943
 
918
944
  anyPeerConnectionUsingTurn ||= peerConnection.usingTURN;
945
+
946
+ minPcScore = Math.min(minPcScore, peerConnection.score?.score ?? Number.MAX_SAFE_INTEGER);
947
+ maxPcScore = Math.max(maxPcScore, peerConnection.score?.score ?? Number.MIN_SAFE_INTEGER);
919
948
  }
920
949
 
950
+ this.score = {
951
+ score: minPcScore,
952
+ remarks: [ {
953
+ severity: 'none',
954
+ text: `Min and max score of all peer connections: ${minPcScore}, ${maxPcScore}`,
955
+ } ],
956
+ timestamp: sample.timestamp,
957
+ };
958
+ this.emit('score', this.score);
959
+
921
960
  this.usingTURN = anyPeerConnectionUsingTurn === true;
922
961
 
923
962
  if (wasUsingTURN !== this.usingTURN) this.emit('usingturn', this.usingTURN);
@@ -951,6 +990,27 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
951
990
  });
952
991
  }
953
992
 
993
+ private _addAndEmitIssue(issue: ClientIssue) {
994
+ this.issues.push(issue);
995
+
996
+ if (issue.peerConnectionId) {
997
+ const peerConnection = this._peerConnections.get(issue.peerConnectionId);
998
+
999
+ if (peerConnection) {
1000
+
1001
+ if (issue.trackId) {
1002
+ const track = peerConnection.inboundAudioTracks.get(issue.trackId) ?? peerConnection.inboundVideoTracks.get(issue.trackId);
1003
+
1004
+ if (track) track.ωpendingIssuesForScores.push(issue);
1005
+ } else {
1006
+ peerConnection.ωpendingIssuesForScores.push(issue);
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ this.emit('issue', issue);
1012
+ }
1013
+
954
1014
  private _createPeerConnection(peerConnectionId: string, label?: string): ObservedPeerConnection {
955
1015
  const result = new ObservedPeerConnection({
956
1016
  peerConnectionId,
@@ -0,0 +1,336 @@
1
+ import { EventEmitter } from 'events';
2
+ import { ObservedPeerConnection } from './ObservedPeerConnection';
3
+ import { InboundAudioTrack } from '@observertc/sample-schemas-js';
4
+ import { InboundAudioTrackReport, InboundVideoTrackReport } from '@observertc/report-schemas-js';
5
+ import { calculateBaseAudioScore, CalculatedScore } from './common/CalculatedScore';
6
+ import { ClientIssue } from './monitors/CallSummary';
7
+ import { ObservedOutboundAudioTrack } from './ObservedOutboundAudioTrack';
8
+
9
+ export type ObservedInboundAudioTrackModel = {
10
+ trackId: string;
11
+ sfuStreamId?: string;
12
+ sfuSinkId?: string;
13
+ }
14
+
15
+ export type ObservedInboundAudioTrackEvents = {
16
+ update: [{
17
+ elapsedTimeInMs: number;
18
+ }],
19
+ score: [CalculatedScore],
20
+ close: [],
21
+ remoteoutboundtrack: [ObservedOutboundAudioTrack | undefined],
22
+ };
23
+
24
+ export type ObservedInboundAudioTrackStats = InboundAudioTrack & {
25
+ ssrc: number;
26
+ bitrate: number;
27
+ fractionLost: number;
28
+ rttInMs?: number;
29
+ deltaReceivedBytes?: number;
30
+ deltaLostPackets?: number;
31
+ deltaReceivedPackets?: number;
32
+ deltaReceivedFrames?: number;
33
+ deltaDecodedFrames?: number;
34
+ deltaDroppedFrames?: number;
35
+ deltaReceivedSamples?: number;
36
+ deltaSilentConcealedSamples?: number;
37
+ fractionLoss?: number;
38
+ statsTimestamp: number;
39
+ };
40
+
41
+ // {
42
+ // [Property in keyof ObservedInboundAudioTrackStats<K>]: ObservedInboundAudioTrackStats<K>[Property];
43
+ // }
44
+
45
+ export declare interface ObservedInboundAudioTrack {
46
+ on<U extends keyof ObservedInboundAudioTrackEvents>(event: U, listener: (...args: ObservedInboundAudioTrackEvents[U]) => void): this;
47
+ off<U extends keyof ObservedInboundAudioTrackEvents>(event: U, listener: (...args: ObservedInboundAudioTrackEvents[U]) => void): this;
48
+ once<U extends keyof ObservedInboundAudioTrackEvents>(event: U, listener: (...args: ObservedInboundAudioTrackEvents[U]) => void): this;
49
+ emit<U extends keyof ObservedInboundAudioTrackEvents>(event: U, ...args: ObservedInboundAudioTrackEvents[U]): boolean;
50
+ update(sample: InboundAudioTrack, timestamp: number): void;
51
+ }
52
+
53
+ export class ObservedInboundAudioTrack extends EventEmitter {
54
+ public readonly created = Date.now();
55
+ public visited = false;
56
+
57
+ private readonly _stats = new Map<number, ObservedInboundAudioTrackStats>();
58
+
59
+ private _closed = false;
60
+ private _updated = Date.now();
61
+ private _remoteOutboundTrack?: ObservedOutboundAudioTrack;
62
+ private _lastMaxStatsTimestamp = 0;
63
+
64
+ public bitrate = 0;
65
+ public rttInMs?: number;
66
+ public jitter?: number;
67
+ public fractionLoss = 0;
68
+ public marker?: string;
69
+
70
+ public totalLostPackets = 0;
71
+ public totalReceivedPackets = 0;
72
+ public totalBytesReceived = 0;
73
+ public totalReceivedSamples = 0;
74
+ public totalSilentConcealedSamples = 0;
75
+
76
+ public deltaLostPackets = 0;
77
+ public deltaReceivedPackets = 0;
78
+ public deltaBytesReceived = 0;
79
+ public deltaReceivedSamples = 0;
80
+ public deltaSilentConcealedSamples = 0;
81
+
82
+ public score?: CalculatedScore;
83
+ public ωpendingIssuesForScores: ClientIssue[] = [];
84
+
85
+ public constructor(
86
+ private readonly _model: ObservedInboundAudioTrackModel,
87
+ public readonly peerConnection: ObservedPeerConnection,
88
+ ) {
89
+ super();
90
+ this.setMaxListeners(Infinity);
91
+ }
92
+
93
+ public get serviceId() {
94
+ return this.peerConnection.serviceId;
95
+ }
96
+
97
+ public get roomId() {
98
+ return this.peerConnection.roomId;
99
+ }
100
+
101
+ public get callId() {
102
+ return this.peerConnection.callId;
103
+ }
104
+
105
+ public get clientId() {
106
+ return this.peerConnection.clientId;
107
+ }
108
+
109
+ public get mediaUnitId() {
110
+ return this.peerConnection.mediaUnitId;
111
+ }
112
+
113
+ public get peerConnectionId() {
114
+ return this.peerConnection.peerConnectionId;
115
+ }
116
+
117
+ public get trackId() {
118
+ return this._model.trackId;
119
+ }
120
+
121
+ public get sfuStreamId() {
122
+ return this._model.sfuStreamId;
123
+ }
124
+
125
+ public get sfuSinkId() {
126
+ return this._model.sfuSinkId;
127
+ }
128
+
129
+ public get updated() {
130
+ return this._updated;
131
+ }
132
+
133
+ public get statsTimestamp() {
134
+ return this._lastMaxStatsTimestamp;
135
+ }
136
+
137
+ public get stats(): ReadonlyMap<number, ObservedInboundAudioTrackStats> {
138
+ return this._stats;
139
+ }
140
+
141
+ public get reports() {
142
+ return this.peerConnection.reports;
143
+ }
144
+
145
+ public set remoteOutboundTrack(track: ObservedOutboundAudioTrack | undefined) {
146
+ if (this._closed) return;
147
+ if (this._remoteOutboundTrack) return;
148
+ if (!track) return;
149
+
150
+ track.once('close', () => {
151
+ this._remoteOutboundTrack = undefined;
152
+ this.emit('remoteoutboundtrack', undefined);
153
+ });
154
+ this._remoteOutboundTrack = track;
155
+ this.emit('remoteoutboundtrack', track);
156
+ }
157
+
158
+ public get remoteOutboundTrack() {
159
+ return this._remoteOutboundTrack;
160
+ }
161
+
162
+ public get closed() {
163
+ return this._closed;
164
+ }
165
+
166
+ public close() {
167
+ if (this._closed) return;
168
+
169
+ this._closed = true;
170
+
171
+ this.emit('close');
172
+ }
173
+
174
+ public update(sample: InboundAudioTrack, statsTimestamp: number) {
175
+ if (this._closed) return;
176
+
177
+ const now = Date.now();
178
+ const report: InboundAudioTrackReport | InboundVideoTrackReport = {
179
+ serviceId: this.peerConnection.client.call.serviceId,
180
+ roomId: this.peerConnection.client.call.roomId,
181
+ callId: this.peerConnection.client.call.callId,
182
+ clientId: this.peerConnection.client.clientId,
183
+ userId: this.peerConnection.client.userId,
184
+ mediaUnitId: this.peerConnection.client.mediaUnitId,
185
+ peerConnectionId: this.peerConnection.peerConnectionId,
186
+ ...sample,
187
+ timestamp: statsTimestamp,
188
+ sampleSeq: -1,
189
+
190
+ remoteClientId: sample.remoteClientId ?? this.remoteOutboundTrack?.peerConnection.client.clientId,
191
+ remoteUserId: this.remoteOutboundTrack?.peerConnection.client.userId,
192
+ remoteTrackId: this.remoteOutboundTrack?.trackId,
193
+ remotePeerConnectionId: this.remoteOutboundTrack?.peerConnection.peerConnectionId,
194
+ marker: this.marker,
195
+ };
196
+
197
+ this.reports.addInboundAudioTrackReport(report);
198
+
199
+ if (!this._model.sfuStreamId && sample.sfuStreamId) {
200
+ this._model.sfuStreamId = sample.sfuStreamId;
201
+ }
202
+
203
+ if (!this._model.sfuSinkId && sample.sfuSinkId) {
204
+ this._model.sfuSinkId = sample.sfuSinkId;
205
+ }
206
+
207
+ const elapsedTimeInMs = Math.max(1, now - this._updated);
208
+ const lastStat = this._stats.get(sample.ssrc);
209
+ const rttInMs = sample.roundTripTime ? sample.roundTripTime * 1000 : undefined;
210
+ let bitrate = 0;
211
+ let deltaReceivedBytes = 0;
212
+ let fractionLost = 0;
213
+ let deltaLostPackets = 0;
214
+ let deltaReceivedPackets = 0;
215
+
216
+ if (lastStat?.bytesReceived && sample.bytesReceived && lastStat.bytesReceived < sample.bytesReceived) {
217
+ deltaReceivedBytes = sample.bytesReceived - lastStat.bytesReceived;
218
+ bitrate = deltaReceivedBytes / (elapsedTimeInMs / 1000);
219
+ }
220
+ if (lastStat?.packetsReceived && sample?.packetsReceived && lastStat.packetsReceived < sample.packetsReceived) {
221
+ deltaReceivedPackets = sample.packetsReceived - lastStat.packetsReceived;
222
+ }
223
+ if (lastStat?.packetsLost && sample.packetsLost && lastStat.packetsLost < sample.packetsLost) {
224
+ deltaLostPackets = sample.packetsLost - lastStat.packetsLost;
225
+ if (0 < deltaReceivedPackets) {
226
+ fractionLost = deltaLostPackets / (deltaReceivedPackets + deltaLostPackets);
227
+ }
228
+ }
229
+
230
+ let deltaDecodedFrames: number | undefined;
231
+ let deltaDroppedFrames: number | undefined;
232
+ let deltaReceivedFrames: number | undefined;
233
+ let deltaSilentConcealedSamples: number | undefined;
234
+ const audioSample = sample as InboundAudioTrack;
235
+ const lastAudioStat = lastStat as InboundAudioTrack | undefined;
236
+
237
+ if (audioSample.silentConcealedSamples && lastAudioStat?.silentConcealedSamples && lastAudioStat.silentConcealedSamples < audioSample.silentConcealedSamples) {
238
+ deltaSilentConcealedSamples = audioSample.silentConcealedSamples - lastAudioStat.silentConcealedSamples;
239
+ }
240
+
241
+ const stats: ObservedInboundAudioTrackStats = {
242
+ ...sample,
243
+ fractionLost,
244
+ rttInMs,
245
+ bitrate,
246
+ ssrc: sample.ssrc,
247
+ deltaReceivedBytes,
248
+ deltaLostPackets,
249
+ deltaReceivedPackets,
250
+ deltaReceivedFrames,
251
+ deltaDecodedFrames,
252
+ deltaDroppedFrames,
253
+ deltaSilentConcealedSamples,
254
+ statsTimestamp,
255
+ };
256
+
257
+ this._stats.set(sample.ssrc, stats);
258
+
259
+ this.visited = true;
260
+ this._updated = now;
261
+
262
+ this.emit('update', {
263
+ elapsedTimeInMs,
264
+ });
265
+ }
266
+
267
+ public updateMetrics() {
268
+ let maxStatsTimestamp = 0;
269
+ let rttInMsSum = 0;
270
+ let jitterSum = 0;
271
+ let size = 0;
272
+
273
+ this.bitrate = 0;
274
+ this.rttInMs = undefined;
275
+ this.deltaBytesReceived = 0;
276
+ this.deltaLostPackets = 0;
277
+ this.deltaReceivedSamples = 0;
278
+ this.deltaSilentConcealedSamples = 0;
279
+ this.fractionLoss = 0;
280
+
281
+ for (const [ , stats ] of this._stats) {
282
+ if (stats.statsTimestamp <= this._lastMaxStatsTimestamp) continue;
283
+
284
+ this.deltaBytesReceived += stats.deltaReceivedBytes ?? 0;
285
+ this.deltaLostPackets += stats.deltaLostPackets ?? 0;
286
+ this.deltaReceivedPackets += stats.deltaReceivedFrames ?? 0;
287
+ this.deltaReceivedSamples += stats.deltaReceivedSamples ?? 0;
288
+ this.deltaSilentConcealedSamples += stats.deltaSilentConcealedSamples ?? 0;
289
+ this.bitrate += stats.bitrate;
290
+
291
+ maxStatsTimestamp = Math.max(maxStatsTimestamp, stats.statsTimestamp);
292
+
293
+ rttInMsSum += stats.rttInMs ?? 0;
294
+ jitterSum += stats.jitter ?? 0;
295
+ ++size;
296
+ }
297
+
298
+ this.totalBytesReceived += this.deltaBytesReceived;
299
+ this.totalLostPackets += this.deltaLostPackets;
300
+ this.totalReceivedPackets += this.deltaReceivedPackets;
301
+ this.totalReceivedSamples += this.deltaReceivedSamples;
302
+ this.totalSilentConcealedSamples += this.deltaSilentConcealedSamples;
303
+
304
+ this.rttInMs = rttInMsSum / Math.max(size, 1);
305
+ this.jitter = jitterSum / Math.max(size, 1);
306
+ this.fractionLoss = 0 < this.deltaReceivedPackets && 0 < this.deltaLostPackets ? (this.deltaLostPackets / (this.deltaReceivedPackets + this.deltaLostPackets)) : 0;
307
+
308
+ this._lastMaxStatsTimestamp = maxStatsTimestamp;
309
+
310
+ this._updateQualityScore(maxStatsTimestamp);
311
+ }
312
+
313
+ private _updateQualityScore(timestamp: number) {
314
+ const newIssues = this.ωpendingIssuesForScores;
315
+ const score = calculateBaseAudioScore(this, newIssues);
316
+
317
+ if (0 < newIssues.length) {
318
+ this.ωpendingIssuesForScores = [];
319
+ }
320
+
321
+ if (!score) return (this.score = undefined);
322
+
323
+ if (0.05 < this.fractionLoss) {
324
+ score.score *= 0.5;
325
+ score.remarks.push({
326
+ severity: 'major',
327
+ text: 'Fraction loss is too high',
328
+ });
329
+ }
330
+
331
+ score.timestamp = timestamp;
332
+ this.score = score;
333
+
334
+ this.emit('score', this.score);
335
+ }
336
+ }