@observertc/observer-js 0.41.6 → 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 (61) 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/CallEventType.d.ts +13 -1
  29. package/lib/common/CallEventType.d.ts.map +1 -1
  30. package/lib/common/CallEventType.js +12 -0
  31. package/lib/common/QualityScoreUtils.d.ts +1 -0
  32. package/lib/common/QualityScoreUtils.d.ts.map +1 -0
  33. package/lib/common/QualityScoreUtils.js +1 -0
  34. package/lib/common/utils.d.ts +1 -0
  35. package/lib/common/utils.d.ts.map +1 -1
  36. package/lib/common/utils.js +14 -1
  37. package/lib/index.d.ts +4 -2
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/monitors/ClientQualityMonitor.d.ts +1 -0
  40. package/lib/monitors/ClientQualityMonitor.d.ts.map +1 -0
  41. package/lib/monitors/ClientQualityMonitor.js +1 -0
  42. package/package.json +1 -1
  43. package/src/ObservedCall.ts +40 -4
  44. package/src/ObservedClient.ts +77 -17
  45. package/src/ObservedInboundAudioTrack.ts +336 -0
  46. package/src/{ObservedInboundTrack.ts → ObservedInboundVideoTrack.ts} +85 -66
  47. package/src/{ObservedOutboundTrack.ts → ObservedOutboundAudioTrack.ts} +59 -74
  48. package/src/ObservedOutboundVideoTrack.ts +326 -0
  49. package/src/ObservedPeerConnection.ts +205 -131
  50. package/src/RemoteTrackAssigner.ts +68 -0
  51. package/src/common/CalculatedScore.ts +180 -0
  52. package/src/common/CallEventType.ts +14 -1
  53. package/src/common/QualityScoreUtils.ts +0 -0
  54. package/src/common/utils.ts +17 -1
  55. package/src/index.ts +4 -2
  56. package/src/monitors/CallSummaryMonitor.ts +3 -3
  57. package/src/monitors/ClientQualityMonitor.ts +0 -0
  58. package/lib/ObservedInboundTrack.d.ts +0 -96
  59. package/lib/ObservedInboundTrack.d.ts.map +0 -1
  60. package/lib/ObservedOutboundTrack.d.ts +0 -84
  61. package/lib/ObservedOutboundTrack.d.ts.map +0 -1
@@ -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
+ }
@@ -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
  }