@observertc/observer-js 0.41.7 → 0.42.1
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.
- package/lib/ObservedCall.d.ts +10 -3
- package/lib/ObservedCall.d.ts.map +1 -1
- package/lib/ObservedCall.js +28 -0
- package/lib/ObservedClient.d.ts +5 -0
- package/lib/ObservedClient.d.ts.map +1 -1
- package/lib/ObservedClient.js +83 -32
- package/lib/ObservedInboundAudioTrack.d.ts +94 -0
- package/lib/ObservedInboundAudioTrack.d.ts.map +1 -0
- package/lib/ObservedInboundAudioTrack.js +215 -0
- package/lib/ObservedInboundVideoTrack.d.ts +98 -0
- package/lib/ObservedInboundVideoTrack.d.ts.map +1 -0
- package/lib/{ObservedInboundTrack.js → ObservedInboundVideoTrack.js} +53 -44
- package/lib/ObservedOutboundAudioTrack.d.ts +85 -0
- package/lib/ObservedOutboundAudioTrack.d.ts.map +1 -0
- package/lib/{ObservedOutboundTrack.js → ObservedOutboundAudioTrack.js} +35 -58
- package/lib/ObservedOutboundVideoTrack.d.ts +88 -0
- package/lib/ObservedOutboundVideoTrack.d.ts.map +1 -0
- package/lib/ObservedOutboundVideoTrack.js +204 -0
- package/lib/ObservedPeerConnection.d.ts +26 -16
- package/lib/ObservedPeerConnection.d.ts.map +1 -1
- package/lib/ObservedPeerConnection.js +157 -86
- package/lib/Observer.d.ts +1 -0
- package/lib/Observer.d.ts.map +1 -1
- package/lib/Observer.js +2 -2
- package/lib/RemoteTrackAssigner.d.ts +14 -0
- package/lib/RemoteTrackAssigner.d.ts.map +1 -0
- package/lib/RemoteTrackAssigner.js +52 -0
- package/lib/common/CalculatedScore.d.ts +79 -0
- package/lib/common/CalculatedScore.d.ts.map +1 -0
- package/lib/common/CalculatedScore.js +155 -0
- package/lib/common/QualityScoreUtils.d.ts +1 -0
- package/lib/common/QualityScoreUtils.d.ts.map +1 -0
- package/lib/common/QualityScoreUtils.js +1 -0
- package/lib/common/utils.d.ts +1 -0
- package/lib/common/utils.d.ts.map +1 -1
- package/lib/common/utils.js +14 -1
- package/lib/index.d.ts +4 -2
- package/lib/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ObservedCall.ts +40 -4
- package/src/ObservedClient.ts +79 -17
- package/src/ObservedInboundAudioTrack.ts +336 -0
- package/src/{ObservedInboundTrack.ts → ObservedInboundVideoTrack.ts} +85 -66
- package/src/{ObservedOutboundTrack.ts → ObservedOutboundAudioTrack.ts} +59 -74
- package/src/ObservedOutboundVideoTrack.ts +326 -0
- package/src/ObservedPeerConnection.ts +205 -131
- package/src/Observer.ts +3 -2
- package/src/RemoteTrackAssigner.ts +68 -0
- package/src/common/CalculatedScore.ts +180 -0
- package/src/common/QualityScoreUtils.ts +0 -0
- package/src/common/utils.ts +17 -1
- package/src/index.ts +4 -2
- package/src/monitors/CallSummaryMonitor.ts +3 -3
- package/lib/ObservedInboundTrack.d.ts +0 -96
- package/lib/ObservedInboundTrack.d.ts.map +0 -1
- package/lib/ObservedOutboundTrack.d.ts +0 -84
- 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 {
|
|
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 {
|
|
5
|
+
import { CalculatedScore, calculateBaseVideoScore } from './common/CalculatedScore';
|
|
6
|
+
import { ClientIssue } from './monitors/CallSummary';
|
|
7
|
+
import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack';
|
|
7
8
|
|
|
8
|
-
export type
|
|
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
|
|
15
|
+
export type ObservedInboundVideoTrackEvents = {
|
|
16
16
|
update: [{
|
|
17
17
|
elapsedTimeInMs: number;
|
|
18
18
|
}],
|
|
19
|
+
score: [CalculatedScore],
|
|
19
20
|
close: [],
|
|
20
|
-
remoteoutboundtrack: [
|
|
21
|
+
remoteoutboundtrack: [ObservedOutboundVideoTrack | undefined],
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
export type
|
|
24
|
+
export type ObservedInboundVideoTrackStatsUpdate = InboundVideoTrack;
|
|
24
25
|
|
|
25
|
-
export type
|
|
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
|
|
47
|
-
on<U extends keyof
|
|
48
|
-
off<U extends keyof
|
|
49
|
-
once<U extends keyof
|
|
50
|
-
emit<U extends keyof
|
|
51
|
-
update(sample:
|
|
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
|
|
53
|
+
export class ObservedInboundVideoTrack extends EventEmitter {
|
|
55
54
|
public readonly created = Date.now();
|
|
56
55
|
public visited = false;
|
|
57
56
|
|
|
58
|
-
|
|
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?:
|
|
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
|
|
86
|
-
public
|
|
86
|
+
public highestLayer?: ObservedInboundVideoTrackStats;
|
|
87
|
+
public score?: CalculatedScore;
|
|
87
88
|
|
|
89
|
+
public ωpendingIssuesForScores: ClientIssue[] = [];
|
|
90
|
+
|
|
88
91
|
public constructor(
|
|
89
|
-
private readonly _model:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
const videoSample = sample as InboundVideoTrack;
|
|
240
|
+
const lastVideoStat = lastStat as InboundVideoTrack | undefined;
|
|
236
241
|
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|