@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
@@ -1,28 +1,29 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { ObservedPeerConnection } from './ObservedPeerConnection';
3
- import { InboundAudioTrack, InboundVideoTrack } from '@observertc/sample-schemas-js';
4
- import { ObservedOutboundTrack } from './ObservedOutboundTrack';
3
+ import { InboundVideoTrack } from '@observertc/sample-schemas-js';
5
4
  import { InboundAudioTrackReport, InboundVideoTrackReport } from '@observertc/report-schemas-js';
6
- import { MediaKind } from './common/types';
5
+ import { CalculatedScore, calculateBaseVideoScore } from './common/CalculatedScore';
6
+ import { ClientIssue } from './monitors/CallSummary';
7
+ import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack';
7
8
 
8
- export type ObservedInboundTrackModel<K extends MediaKind> = {
9
+ export type ObservedInboundVideoTrackModel = {
9
10
  trackId: string;
10
- kind: K;
11
11
  sfuStreamId?: string;
12
12
  sfuSinkId?: string;
13
13
  }
14
14
 
15
- export type ObservedInboundTrackEvents<K extends MediaKind> = {
15
+ export type ObservedInboundVideoTrackEvents = {
16
16
  update: [{
17
17
  elapsedTimeInMs: number;
18
18
  }],
19
+ score: [CalculatedScore],
19
20
  close: [],
20
- remoteoutboundtrack: [ObservedOutboundTrack<K>],
21
+ remoteoutboundtrack: [ObservedOutboundVideoTrack | undefined],
21
22
  };
22
23
 
23
- export type ObservedInboundTrackStatsUpdate<K extends MediaKind> = K extends 'audio' ? InboundAudioTrack : InboundVideoTrack;
24
+ export type ObservedInboundVideoTrackStatsUpdate = InboundVideoTrack;
24
25
 
25
- export type ObservedInboundTrackStats<K extends MediaKind> = ObservedInboundTrackStatsUpdate<K> & {
26
+ export type ObservedInboundVideoTrackStats = InboundVideoTrack & {
26
27
  ssrc: number;
27
28
  bitrate: number;
28
29
  fractionLost: number;
@@ -33,8 +34,6 @@ export type ObservedInboundTrackStats<K extends MediaKind> = ObservedInboundTrac
33
34
  deltaReceivedFrames?: number;
34
35
  deltaDecodedFrames?: number;
35
36
  deltaDroppedFrames?: number;
36
- deltaReceivedSamples?: number;
37
- deltaSilentConcealedSamples?: number;
38
37
  fractionLoss?: number;
39
38
  statsTimestamp: number;
40
39
  };
@@ -43,27 +42,31 @@ export type ObservedInboundTrackStats<K extends MediaKind> = ObservedInboundTrac
43
42
  // [Property in keyof ObservedInboundTrackStats<K>]: ObservedInboundTrackStats<K>[Property];
44
43
  // }
45
44
 
46
- export declare interface ObservedInboundTrack<Kind extends MediaKind> {
47
- on<U extends keyof ObservedInboundTrackEvents<Kind>>(event: U, listener: (...args: ObservedInboundTrackEvents<Kind>[U]) => void): this;
48
- off<U extends keyof ObservedInboundTrackEvents<Kind>>(event: U, listener: (...args: ObservedInboundTrackEvents<Kind>[U]) => void): this;
49
- once<U extends keyof ObservedInboundTrackEvents<Kind>>(event: U, listener: (...args: ObservedInboundTrackEvents<Kind>[U]) => void): this;
50
- emit<U extends keyof ObservedInboundTrackEvents<Kind>>(event: U, ...args: ObservedInboundTrackEvents<Kind>[U]): boolean;
51
- update(sample: ObservedInboundTrackStatsUpdate<Kind>, timestamp: number): void;
45
+ export declare interface ObservedInboundVideoTrack {
46
+ on<U extends keyof ObservedInboundVideoTrackEvents>(event: U, listener: (...args: ObservedInboundVideoTrackEvents[U]) => void): this;
47
+ off<U extends keyof ObservedInboundVideoTrackEvents>(event: U, listener: (...args: ObservedInboundVideoTrackEvents[U]) => void): this;
48
+ once<U extends keyof ObservedInboundVideoTrackEvents>(event: U, listener: (...args: ObservedInboundVideoTrackEvents[U]) => void): this;
49
+ emit<U extends keyof ObservedInboundVideoTrackEvents>(event: U, ...args: ObservedInboundVideoTrackEvents[U]): boolean;
50
+ update(sample: InboundVideoTrack, timestamp: number): void;
52
51
  }
53
52
 
54
- export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
53
+ export class ObservedInboundVideoTrack extends EventEmitter {
55
54
  public readonly created = Date.now();
56
55
  public visited = false;
57
56
 
58
- private readonly _stats = new Map<number, ObservedInboundTrackStats<Kind>>();
57
+ public contentType: 'lowmotion' | 'standard' | 'highmotion' = 'standard';
58
+ public codec: 'vp8' | 'vp9' | 'h264' = 'vp8';
59
+
60
+ private readonly _stats = new Map<number, ObservedInboundVideoTrackStats>();
59
61
 
60
62
  private _closed = false;
61
63
  private _updated = Date.now();
62
- private _remoteOutboundTrack?: ObservedOutboundTrack<Kind>;
64
+ private _remoteOutboundTrack?: ObservedOutboundVideoTrack;
63
65
  private _lastMaxStatsTimestamp = 0;
64
66
 
65
67
  public bitrate = 0;
66
68
  public rttInMs?: number;
69
+ public jitter?: number;
67
70
  public fractionLoss = 0;
68
71
  public marker?: string;
69
72
 
@@ -73,8 +76,6 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
73
76
  public totalReceivedFrames = 0;
74
77
  public totalDecodedFrames = 0;
75
78
  public totalDroppedFrames = 0;
76
- public totalReceivedSamples = 0;
77
- public totalSilentConcealedSamples = 0;
78
79
 
79
80
  public deltaLostPackets = 0;
80
81
  public deltaReceivedPackets = 0;
@@ -82,11 +83,13 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
82
83
  public deltaReceivedFrames = 0;
83
84
  public deltaDecodedFrames = 0;
84
85
  public deltaDroppedFrames = 0;
85
- public deltaReceivedSamples = 0;
86
- public deltaSilentConcealedSamples = 0;
86
+ public highestLayer?: ObservedInboundVideoTrackStats;
87
+ public score?: CalculatedScore;
87
88
 
89
+ public ωpendingIssuesForScores: ClientIssue[] = [];
90
+
88
91
  public constructor(
89
- private readonly _model: ObservedInboundTrackModel<Kind>,
92
+ private readonly _model: ObservedInboundVideoTrackModel,
90
93
  public readonly peerConnection: ObservedPeerConnection,
91
94
  ) {
92
95
  super();
@@ -121,10 +124,6 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
121
124
  return this._model.trackId;
122
125
  }
123
126
 
124
- public get kind(): Kind {
125
- return this._model.kind as Kind;
126
- }
127
-
128
127
  public get sfuStreamId() {
129
128
  return this._model.sfuStreamId;
130
129
  }
@@ -137,7 +136,11 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
137
136
  return this._updated;
138
137
  }
139
138
 
140
- public get stats(): ReadonlyMap<number, ObservedInboundTrackStats<Kind>> {
139
+ public get statsTimestamp() {
140
+ return this._lastMaxStatsTimestamp;
141
+ }
142
+
143
+ public get stats(): ReadonlyMap<number, ObservedInboundVideoTrackStats> {
141
144
  return this._stats;
142
145
  }
143
146
 
@@ -145,12 +148,14 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
145
148
  return this.peerConnection.reports;
146
149
  }
147
150
 
148
- public set remoteOutboundTrack(track: ObservedOutboundTrack<Kind> | undefined) {
151
+ public set remoteOutboundTrack(track: ObservedOutboundVideoTrack | undefined) {
149
152
  if (this._closed) return;
150
153
  if (this._remoteOutboundTrack) return;
151
154
  if (!track) return;
152
- track?.once('close', () => {
155
+
156
+ track.once('close', () => {
153
157
  this._remoteOutboundTrack = undefined;
158
+ this.emit('remoteoutboundtrack', undefined);
154
159
  });
155
160
  this._remoteOutboundTrack = track;
156
161
  this.emit('remoteoutboundtrack', track);
@@ -172,7 +177,7 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
172
177
  this.emit('close');
173
178
  }
174
179
 
175
- public update(sample: ObservedInboundTrackStatsUpdate<Kind>, statsTimestamp: number) {
180
+ public update(sample: ObservedInboundVideoTrackStatsUpdate, statsTimestamp: number) {
176
181
  if (this._closed) return;
177
182
 
178
183
  const now = Date.now();
@@ -195,8 +200,7 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
195
200
  marker: this.marker,
196
201
  };
197
202
 
198
- if (this.kind === 'audio') this.reports.addInboundAudioTrackReport(report);
199
- else if (this.kind === 'video') this.reports.addInboundVideoTrackReport(report);
203
+ this.reports.addInboundVideoTrackReport(report);
200
204
 
201
205
  if (!this._model.sfuStreamId && sample.sfuStreamId) {
202
206
  this._model.sfuStreamId = sample.sfuStreamId;
@@ -232,31 +236,20 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
232
236
  let deltaDecodedFrames: number | undefined;
233
237
  let deltaDroppedFrames: number | undefined;
234
238
  let deltaReceivedFrames: number | undefined;
235
- let deltaSilentConcealedSamples: number | undefined;
239
+ const videoSample = sample as InboundVideoTrack;
240
+ const lastVideoStat = lastStat as InboundVideoTrack | undefined;
236
241
 
237
- if (this.kind === 'video') {
238
- const videoSample = sample as InboundVideoTrack;
239
- const lastVideoStat = lastStat as InboundVideoTrack | undefined;
240
-
241
- if (videoSample.framesDecoded && lastVideoStat?.framesDecoded && lastVideoStat.framesDecoded < videoSample.framesDecoded) {
242
- deltaDecodedFrames = videoSample.framesDecoded - lastVideoStat.framesDecoded;
243
- }
244
- if (videoSample.framesDropped && lastVideoStat?.framesDropped && lastVideoStat.framesDropped < videoSample.framesDropped) {
245
- deltaDroppedFrames = videoSample.framesDropped - lastVideoStat.framesDropped;
246
- }
247
- if (videoSample.framesReceived && lastVideoStat?.framesReceived && lastVideoStat.framesReceived < videoSample.framesReceived) {
248
- deltaReceivedFrames = videoSample.framesReceived - lastVideoStat.framesReceived;
249
- }
250
- } else if (this.kind === 'audio') {
251
- const audioSample = sample as InboundAudioTrack;
252
- const lastAudioStat = lastStat as InboundAudioTrack | undefined;
253
-
254
- if (audioSample.silentConcealedSamples && lastAudioStat?.silentConcealedSamples && lastAudioStat.silentConcealedSamples < audioSample.silentConcealedSamples) {
255
- deltaSilentConcealedSamples = audioSample.silentConcealedSamples - lastAudioStat.silentConcealedSamples;
256
- }
242
+ if (videoSample.framesDecoded && lastVideoStat?.framesDecoded && lastVideoStat.framesDecoded < videoSample.framesDecoded) {
243
+ deltaDecodedFrames = videoSample.framesDecoded - lastVideoStat.framesDecoded;
244
+ }
245
+ if (videoSample.framesDropped && lastVideoStat?.framesDropped && lastVideoStat.framesDropped < videoSample.framesDropped) {
246
+ deltaDroppedFrames = videoSample.framesDropped - lastVideoStat.framesDropped;
247
+ }
248
+ if (videoSample.framesReceived && lastVideoStat?.framesReceived && lastVideoStat.framesReceived < videoSample.framesReceived) {
249
+ deltaReceivedFrames = videoSample.framesReceived - lastVideoStat.framesReceived;
257
250
  }
258
251
 
259
- const stats: ObservedInboundTrackStats<Kind> = {
252
+ const stats: ObservedInboundVideoTrackStats = {
260
253
  ...sample,
261
254
  fractionLost,
262
255
  rttInMs,
@@ -268,7 +261,6 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
268
261
  deltaReceivedFrames,
269
262
  deltaDecodedFrames,
270
263
  deltaDroppedFrames,
271
- deltaSilentConcealedSamples,
272
264
  statsTimestamp,
273
265
  };
274
266
 
@@ -284,6 +276,7 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
284
276
  public updateMetrics() {
285
277
  let maxStatsTimestamp = 0;
286
278
  let rttInMsSum = 0;
279
+ let jitterSum = 0;
287
280
  let size = 0;
288
281
 
289
282
  this.bitrate = 0;
@@ -293,11 +286,11 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
293
286
  this.deltaReceivedFrames = 0;
294
287
  this.deltaDecodedFrames = 0;
295
288
  this.deltaDroppedFrames = 0;
296
- this.deltaReceivedSamples = 0;
297
- this.deltaSilentConcealedSamples = 0;
298
289
  this.fractionLoss = 0;
299
290
 
300
- for (const [ , stats ] of this._stats) {
291
+ const highestLayer = { ssrc: -1, bitrate: -1 };
292
+
293
+ for (const [ ssrc, stats ] of this._stats) {
301
294
  if (stats.statsTimestamp <= this._lastMaxStatsTimestamp) continue;
302
295
 
303
296
  this.deltaBytesReceived += stats.deltaReceivedBytes ?? 0;
@@ -306,28 +299,54 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
306
299
  this.deltaReceivedFrames += stats.deltaReceivedFrames ?? 0;
307
300
  this.deltaDecodedFrames += stats.deltaDecodedFrames ?? 0;
308
301
  this.deltaDroppedFrames += stats.deltaDroppedFrames ?? 0;
309
- this.deltaReceivedSamples += stats.deltaReceivedSamples ?? 0;
310
- this.deltaSilentConcealedSamples += stats.deltaSilentConcealedSamples ?? 0;
311
302
  this.bitrate += stats.bitrate;
312
303
 
313
304
  maxStatsTimestamp = Math.max(maxStatsTimestamp, stats.statsTimestamp);
314
305
 
306
+ if (highestLayer.bitrate < stats.bitrate) {
307
+ highestLayer.ssrc = ssrc;
308
+ }
315
309
  rttInMsSum += stats.rttInMs ?? 0;
310
+ jitterSum += stats.jitter ?? 0;
316
311
  ++size;
317
312
  }
318
-
313
+ this.highestLayer = this.stats.get(highestLayer.ssrc);
319
314
  this.totalBytesReceived += this.deltaBytesReceived;
320
315
  this.totalLostPackets += this.deltaLostPackets;
321
316
  this.totalReceivedPackets += this.deltaReceivedPackets;
322
317
  this.totalReceivedFrames += this.deltaReceivedFrames;
323
318
  this.totalDecodedFrames += this.deltaDecodedFrames;
324
319
  this.totalDroppedFrames += this.deltaDroppedFrames;
325
- this.totalReceivedSamples += this.deltaReceivedSamples;
326
- this.totalSilentConcealedSamples += this.deltaSilentConcealedSamples;
327
320
 
328
321
  this.rttInMs = rttInMsSum / Math.max(size, 1);
322
+ this.jitter = jitterSum / Math.max(size, 1);
329
323
  this.fractionLoss = 0 < this.deltaReceivedPackets && 0 < this.deltaLostPackets ? (this.deltaLostPackets / (this.deltaReceivedPackets + this.deltaLostPackets)) : 0;
330
324
 
331
325
  this._lastMaxStatsTimestamp = maxStatsTimestamp;
326
+ this._updateQualityScore(maxStatsTimestamp);
327
+ }
328
+
329
+ private _updateQualityScore(timestamp: number) {
330
+ const newIssues = this.ωpendingIssuesForScores;
331
+ const score = calculateBaseVideoScore(this, newIssues);
332
+
333
+ if (0 < this.ωpendingIssuesForScores.length) {
334
+ this.ωpendingIssuesForScores = [];
335
+ }
336
+
337
+ if (!score) return (this.score = undefined);
338
+
339
+ if (0.05 < this.fractionLoss) {
340
+ score.score *= 0.5;
341
+ score.remarks.push({
342
+ severity: 'major',
343
+ text: 'Fraction loss is too high',
344
+ });
345
+ }
346
+
347
+ score.timestamp = timestamp;
348
+ this.score = score;
349
+
350
+ this.emit('score', this.score);
332
351
  }
333
352
  }
@@ -1,27 +1,26 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { ObservedPeerConnection } from './ObservedPeerConnection';
3
3
  import { OutboundAudioTrack, OutboundVideoTrack } from '@observertc/sample-schemas-js';
4
- import { ObservedInboundTrack } from './ObservedInboundTrack';
5
4
  import { OutboundAudioTrackReport, OutboundVideoTrackReport } from '@observertc/report-schemas-js';
6
- import { MediaKind } from './common/types';
5
+ import { ObservedInboundAudioTrack } from './ObservedInboundAudioTrack';
6
+ import { calculateBaseAudioScore, CalculatedScore } from './common/CalculatedScore';
7
+ import { ClientIssue } from './monitors/CallSummary';
7
8
 
8
- export type ObservedOutboundTrackModel<K extends MediaKind> = {
9
+ export type ObservedOutboundAudioTrackModel = {
9
10
  trackId: string;
10
- kind: K;
11
11
  sfuStreamId?: string;
12
12
  }
13
13
 
14
- export type ObservedOutboundTrackEvents = {
14
+ export type ObservedOutboundAudioTrackEvents = {
15
15
  qualitylimitationchanged: [string];
16
16
  update: [{
17
17
  elapsedTimeInMs: number;
18
18
  }],
19
+ score: [CalculatedScore],
19
20
  close: [],
20
21
  };
21
22
 
22
- export type ObservedOutboundTrackStatsUpdate<K extends MediaKind> = K extends 'audio' ? OutboundAudioTrack : OutboundVideoTrack;
23
-
24
- export type ObservedOutboundTrackStats<K extends MediaKind> = ObservedOutboundTrackStatsUpdate<K> & {
23
+ export type ObservedOutboundAudioTrackStats = OutboundAudioTrack & {
25
24
  ssrc: number;
26
25
  bitrate: number;
27
26
  rttInMs?: number;
@@ -35,24 +34,25 @@ export type ObservedOutboundTrackStats<K extends MediaKind> = ObservedOutboundTr
35
34
  statsTimestamp: number;
36
35
  };
37
36
 
38
- // export type ObservedOutboundTrackStatsUpdate<K extends MediaKind> = {
37
+ // export type ObservedOutboundTrackStatsUpdate = {
39
38
  // [Property in keyof ObservedOutboundTrackStats<K>]: ObservedOutboundTrackStats<K>[Property];
40
39
  // }
41
40
 
42
- export declare interface ObservedOutboundTrack<Kind extends MediaKind> {
43
- on<U extends keyof ObservedOutboundTrackEvents>(event: U, listener: (...args: ObservedOutboundTrackEvents[U]) => void): this;
44
- off<U extends keyof ObservedOutboundTrackEvents>(event: U, listener: (...args: ObservedOutboundTrackEvents[U]) => void): this;
45
- once<U extends keyof ObservedOutboundTrackEvents>(event: U, listener: (...args: ObservedOutboundTrackEvents[U]) => void): this;
46
- emit<U extends keyof ObservedOutboundTrackEvents>(event: U, ...args: ObservedOutboundTrackEvents[U]): boolean;
47
- update(sample: ObservedOutboundTrackStatsUpdate<Kind>, timestamp: number): void;
41
+ export declare interface ObservedOutboundAudioTrack {
42
+ on<U extends keyof ObservedOutboundAudioTrackEvents>(event: U, listener: (...args: ObservedOutboundAudioTrackEvents[U]) => void): this;
43
+ off<U extends keyof ObservedOutboundAudioTrackEvents>(event: U, listener: (...args: ObservedOutboundAudioTrackEvents[U]) => void): this;
44
+ once<U extends keyof ObservedOutboundAudioTrackEvents>(event: U, listener: (...args: ObservedOutboundAudioTrackEvents[U]) => void): this;
45
+ emit<U extends keyof ObservedOutboundAudioTrackEvents>(event: U, ...args: ObservedOutboundAudioTrackEvents[U]): boolean;
46
+ update(sample: OutboundAudioTrack, timestamp: number): void;
48
47
  }
49
48
 
50
- export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter {
49
+ export class ObservedOutboundAudioTrack extends EventEmitter {
51
50
  public readonly created = Date.now();
52
51
  public visited = false;
53
52
 
54
53
  public bitrate = 0;
55
54
  public rttInMs?: number;
55
+ public jitter?: number;
56
56
  public marker?: string;
57
57
 
58
58
  public totalLostPackets = 0;
@@ -68,21 +68,24 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
68
68
 
69
69
  public sendingBitrate = 0;
70
70
 
71
- private readonly _stats = new Map<number, ObservedOutboundTrackStats<Kind>>();
71
+ private readonly _stats = new Map<number, ObservedOutboundAudioTrackStats>();
72
72
  private _lastMaxStatsTimestamp = 0;
73
73
 
74
74
  private _closed = false;
75
75
  private _updated = Date.now();
76
- private _remoteInboundTracks = new Map<string, ObservedInboundTrack<Kind>>();
76
+ private _lastUpdateMetrics?: number;
77
+
78
+ public score?: CalculatedScore;
79
+ public ωpendingIssuesForScores: ClientIssue[] = [];
80
+
81
+ public readonly remoteInboundTracks = new Map<string, ObservedInboundAudioTrack>();
77
82
 
78
83
  public constructor(
79
- private readonly _model: ObservedOutboundTrackModel<Kind>,
84
+ private readonly _model: ObservedOutboundAudioTrackModel,
80
85
  public readonly peerConnection: ObservedPeerConnection,
81
86
  ) {
82
87
  super();
83
88
  this.setMaxListeners(Infinity);
84
-
85
- if (this._model.sfuStreamId) this._assignSfuStreamId(this._model.sfuStreamId);
86
89
  }
87
90
 
88
91
  public get serviceId() {
@@ -109,10 +112,6 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
109
112
  return this.peerConnection.peerConnectionId;
110
113
  }
111
114
 
112
- public get kind(): Kind {
113
- return this._model.kind as Kind;
114
- }
115
-
116
115
  public get trackId() {
117
116
  return this._model.trackId;
118
117
  }
@@ -121,19 +120,19 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
121
120
  return this._model.sfuStreamId;
122
121
  }
123
122
 
124
- public get remoteInboundTracks(): ReadonlyMap<string, ObservedInboundTrack<Kind>> {
125
- return this._remoteInboundTracks;
126
- }
127
-
128
123
  public get reports() {
129
124
  return this.peerConnection.reports;
130
125
  }
131
126
 
127
+ public get statsTimestamp() {
128
+ return this._lastMaxStatsTimestamp;
129
+ }
130
+
132
131
  public get updated() {
133
132
  return this._updated;
134
133
  }
135
134
 
136
- public get stats(): ReadonlyMap<number, ObservedOutboundTrackStats<Kind>> {
135
+ public get stats(): ReadonlyMap<number, ObservedOutboundAudioTrackStats> {
137
136
  return this._stats;
138
137
  }
139
138
 
@@ -148,7 +147,7 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
148
147
  this.emit('close');
149
148
  }
150
149
 
151
- public update(sample: ObservedOutboundTrackStatsUpdate<Kind>, statsTimestamp: number): void {
150
+ public update(sample: OutboundAudioTrack, statsTimestamp: number): void {
152
151
  if (this._closed) return;
153
152
 
154
153
  const now = Date.now();
@@ -166,8 +165,7 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
166
165
  marker: this.marker,
167
166
  };
168
167
 
169
- if (this.kind === 'audio') this.reports.addOutboundAudioTrackReport(report);
170
- else if (this.kind === 'video') this.reports.addOutboundVideoTrackReport(report);
168
+ this.reports.addOutboundAudioTrackReport(report);
171
169
 
172
170
  const elapsedTimeInMs = Math.max(1, now - this._updated);
173
171
  const lastStat = this._stats.get(sample.ssrc);
@@ -193,26 +191,21 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
193
191
  let deltaEncodedFrames: number | undefined;
194
192
  let deltaSentFrames: number | undefined;
195
193
 
196
- if (this.kind === 'video') {
197
- const videoSample = sample as OutboundVideoTrack;
198
- const lastVideoStats = lastStat as OutboundVideoTrack | undefined;
194
+ const videoSample = sample as OutboundVideoTrack;
195
+ const lastVideoStats = lastStat as OutboundVideoTrack | undefined;
199
196
 
200
- if (videoSample?.framesEncoded && lastVideoStats?.framesEncoded && lastVideoStats.framesEncoded < videoSample.framesEncoded) {
201
- deltaEncodedFrames = videoSample.framesEncoded - lastVideoStats.framesEncoded;
202
- }
203
- if (videoSample?.framesSent && lastVideoStats?.framesSent && lastVideoStats.framesSent < videoSample.framesSent) {
204
- deltaSentFrames = videoSample.framesSent - lastVideoStats.framesSent;
205
- }
206
-
207
- if (videoSample.qualityLimitationReason && lastVideoStats?.qualityLimitationReason !== videoSample.qualityLimitationReason) {
208
- this.emit('qualitylimitationchanged', videoSample.qualityLimitationReason);
209
- }
210
- } else if (this.kind === 'audio') {
211
- // const audioSample = sample as OutboundAudioTrack;
212
- // const lastAudioStats = lastStat as OutboundAudioTrack | undefined;
197
+ if (videoSample?.framesEncoded && lastVideoStats?.framesEncoded && lastVideoStats.framesEncoded < videoSample.framesEncoded) {
198
+ deltaEncodedFrames = videoSample.framesEncoded - lastVideoStats.framesEncoded;
199
+ }
200
+ if (videoSample?.framesSent && lastVideoStats?.framesSent && lastVideoStats.framesSent < videoSample.framesSent) {
201
+ deltaSentFrames = videoSample.framesSent - lastVideoStats.framesSent;
202
+ }
213
203
 
204
+ if (videoSample.qualityLimitationReason && lastVideoStats?.qualityLimitationReason !== videoSample.qualityLimitationReason) {
205
+ this.emit('qualitylimitationchanged', videoSample.qualityLimitationReason);
214
206
  }
215
- const stats: ObservedOutboundTrackStats<Kind> = {
207
+
208
+ const stats: ObservedOutboundAudioTrackStats = {
216
209
  ...sample,
217
210
  rttInMs,
218
211
  bitrate,
@@ -244,9 +237,6 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
244
237
  // this.deltaSentBytes = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.deltaSentBytes, 0);
245
238
 
246
239
  // setting up sfu connection as it is not always available at the first sample
247
- if (sample.sfuStreamId && !this._model.sfuStreamId) {
248
- this._assignSfuStreamId(sample.sfuStreamId);
249
- }
250
240
 
251
241
  this.visited = true;
252
242
  this._updated = now;
@@ -255,21 +245,10 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
255
245
  });
256
246
  }
257
247
 
258
- public connectInboundTrack(track: ObservedInboundTrack<Kind>) {
259
- if (this._closed) return;
260
-
261
- track.remoteOutboundTrack = this;
262
- track.once('close', () => {
263
- this._remoteInboundTracks.delete(track.trackId);
264
- });
265
-
266
- this._remoteInboundTracks.set(track.trackId, track);
267
- }
268
- private _lastUpdateMetrics?: number;
269
-
270
248
  public updateMetrics() {
271
249
  let maxStatsTimestamp = 0;
272
250
  let rttInMsSum = 0;
251
+ let jitterSum = 0;
273
252
  let size = 0;
274
253
 
275
254
  this.bitrate = 0;
@@ -295,6 +274,7 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
295
274
  maxStatsTimestamp = Math.max(maxStatsTimestamp, stats.statsTimestamp);
296
275
 
297
276
  rttInMsSum += stats.rttInMs ?? 0;
277
+ jitterSum += stats.jitter ?? 0;
298
278
  ++size;
299
279
  }
300
280
 
@@ -311,20 +291,25 @@ export class ObservedOutboundTrack<Kind extends MediaKind> extends EventEmitter
311
291
  this.totalSentFrames += this.deltaSentFrames;
312
292
 
313
293
  this.rttInMs = rttInMsSum / Math.max(size, 1);
294
+ this.jitter = jitterSum / Math.max(size, 1);
314
295
 
315
296
  this._lastMaxStatsTimestamp = maxStatsTimestamp;
297
+ this._updateQualityScore(maxStatsTimestamp);
316
298
  }
317
299
 
318
- private _assignSfuStreamId(sfuStreamId: string) {
319
- this._model.sfuStreamId = sfuStreamId;
300
+ private _updateQualityScore(timestamp: number) {
301
+ const newIssues = this.ωpendingIssuesForScores;
302
+ const score = calculateBaseAudioScore(this, newIssues);
303
+
304
+ if (0 < newIssues.length) {
305
+ this.ωpendingIssuesForScores = [];
306
+ }
307
+
308
+ if (!score) return (this.score = undefined);
320
309
 
321
- this.once('close', () => {
322
- if (!this._model.sfuStreamId) return;
323
- if (this.kind === 'audio') this.peerConnection.client.call.sfuStreamIdToOutboundAudioTrack.delete(this._model.sfuStreamId);
324
- else if (this.kind === 'video') this.peerConnection.client.call.sfuStreamIdToOutboundVideoTrack.delete(this._model.sfuStreamId);
325
- });
310
+ score.timestamp = timestamp;
311
+ this.score = score;
326
312
 
327
- if (this.kind === 'audio') this.peerConnection.client.call.sfuStreamIdToOutboundAudioTrack.set(this._model.sfuStreamId, this as ObservedOutboundTrack<'audio'>);
328
- else if (this.kind === 'video') this.peerConnection.client.call.sfuStreamIdToOutboundVideoTrack.set(this._model.sfuStreamId, this as ObservedOutboundTrack<'video'>);
313
+ this.emit('score', this.score);
329
314
  }
330
315
  }