@pexip/peer-connection-stats 17.8.0 → 17.10.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.
@@ -0,0 +1,497 @@
1
+ import { Quality } from './statsCollector.types';
2
+ export const STATS_SIZE = 60;
3
+ /**
4
+ * The WebRTC stats are given to us as a flat structure (an array of objects
5
+ * that contain id-fields pointing to other objects in the same array).
6
+ *
7
+ * This class takes such an array as input and can expand an entry so that
8
+ * references become nested objects.
9
+ *
10
+ * https://www.w3.org/TR/webrtc-stats/#summary
11
+ * https://www.w3.org/TR/webrtc-stats/#rtctatstype-*
12
+ */
13
+ export const createResolver = (statsReport) => {
14
+ const idLookup = statsReport.reduce((map, entry) => map.set(entry.id, entry), new Map());
15
+ const expandTail = (stats, seenIds = []) => Object.keys(stats).reduce((expanded, key) => {
16
+ const id = stats[key];
17
+ expanded[key] = id;
18
+ if (!id || typeof id !== 'string' || seenIds.includes(id)) {
19
+ return expanded;
20
+ }
21
+ switch (key) {
22
+ case 'codecId': {
23
+ const codec = idLookup.get(id);
24
+ if (codec) {
25
+ expanded.codec = expandTail(codec, [...seenIds, id]);
26
+ }
27
+ break;
28
+ }
29
+ case 'remoteCandidateId': {
30
+ const remoteCandidate = idLookup.get(id);
31
+ if (remoteCandidate) {
32
+ expanded.remoteCandidate = expandTail(remoteCandidate, [
33
+ ...seenIds,
34
+ id,
35
+ ]);
36
+ }
37
+ break;
38
+ }
39
+ case 'remoteId': {
40
+ const remote = idLookup.get(id);
41
+ if (remote) {
42
+ expanded.remote = expandTail(remote, [...seenIds, id]);
43
+ }
44
+ break;
45
+ }
46
+ case 'selectedCandidatePairId': {
47
+ const selectedCandidatePair = idLookup.get(id);
48
+ if (selectedCandidatePair) {
49
+ expanded.selectedCandidatePair = expandTail(selectedCandidatePair, [...seenIds, id]);
50
+ }
51
+ break;
52
+ }
53
+ case 'trackId': {
54
+ const track = idLookup.get(id);
55
+ if (track) {
56
+ expanded.track = expandTail(track, [...seenIds, id]);
57
+ }
58
+ break;
59
+ }
60
+ case 'trackIds': {
61
+ if (Array.isArray(id)) {
62
+ const ids = id;
63
+ const tracks = ids.flatMap(id => {
64
+ const track = idLookup.get(id);
65
+ return !track
66
+ ? []
67
+ : [expandTail(track, [...seenIds, ...ids])];
68
+ });
69
+ expanded.tracks = tracks;
70
+ }
71
+ break;
72
+ }
73
+ case 'transportId': {
74
+ const transport = idLookup.get(id);
75
+ if (transport) {
76
+ expanded.transport = expandTail(transport, [
77
+ ...seenIds,
78
+ id,
79
+ ]);
80
+ }
81
+ break;
82
+ }
83
+ case 'localCandidateId':
84
+ case 'localCertificateId':
85
+ case 'localId':
86
+ case 'mediaSourceId':
87
+ case 'remoteCertificateId':
88
+ default:
89
+ break;
90
+ }
91
+ return expanded;
92
+ }, {});
93
+ function expand(rawStats) {
94
+ return expandTail(rawStats);
95
+ }
96
+ return {
97
+ expand,
98
+ };
99
+ };
100
+ /**
101
+ * Normalize inbound audio stats
102
+ *
103
+ * @param statsData - Raw inbound audio stats @see InboundAudio
104
+ *
105
+ * @returns normalized stats for inbound audio from PeerConnection
106
+ */
107
+ export const inboundAudio = (statsData) => {
108
+ const packetsReceived = statsData.packetsReceived ?? 0;
109
+ const packetsLost = statsData.packetsLost ?? 0;
110
+ const totalPackets = packetsReceived + packetsLost;
111
+ return {
112
+ type: statsData.type,
113
+ kind: statsData.kind,
114
+ jitter: statsData.jitter ?? 0,
115
+ timestamp: statsData.timestamp,
116
+ packetsTransmitted: packetsReceived,
117
+ packetsLost,
118
+ bytesTransmitted: statsData.bytesReceived,
119
+ codec: statsData.codec?.mimeType,
120
+ roundTripTime: statsData.transport?.selectedCandidatePair?.currentRoundTripTime,
121
+ totalPercentageLost: totalPackets && packetsLost / totalPackets,
122
+ };
123
+ };
124
+ /**
125
+ * Normalize outbound audio stats
126
+ *
127
+ * @param statsData - Raw outbound audio stats @see OutboundAudio
128
+ *
129
+ * @returns normalized stats for outbound audio from PeerConnection
130
+ */
131
+ export const outboundAudio = (statsData) => {
132
+ const packetsSent = statsData.packetsSent ?? 0;
133
+ const packetsLost = statsData.remote?.packetsLost ?? 0;
134
+ const totalPackets = packetsSent + packetsLost;
135
+ // firefox typically uses remote.roundTripTime and chrome uses currentRoundTripTime
136
+ const roundTripTime = statsData.remote?.roundTripTime ??
137
+ statsData.transport?.selectedCandidatePair?.currentRoundTripTime;
138
+ return {
139
+ type: statsData.type,
140
+ kind: statsData.kind,
141
+ jitter: statsData?.remote?.jitter ?? 0,
142
+ timestamp: statsData.timestamp,
143
+ packetsTransmitted: packetsSent,
144
+ packetsLost,
145
+ bytesTransmitted: statsData.bytesSent,
146
+ codec: statsData.codec?.mimeType,
147
+ roundTripTime,
148
+ totalPercentageLost: totalPackets && packetsLost / totalPackets,
149
+ };
150
+ };
151
+ /**
152
+ * Normalize inbound video stats
153
+ *
154
+ * @param statsData - Raw inbound video stats @see InboundVideo
155
+ *
156
+ * @returns normalized stats for inbound video from PeerConnection
157
+ */
158
+ export const inboundVideo = (statsData) => {
159
+ const packetsReceived = statsData.packetsReceived ?? 0;
160
+ const packetsLost = statsData.packetsLost ?? 0;
161
+ const totalPackets = packetsReceived + packetsLost;
162
+ return {
163
+ type: statsData.type,
164
+ kind: statsData.kind,
165
+ timestamp: statsData.timestamp,
166
+ packetsTransmitted: packetsReceived,
167
+ packetsLost,
168
+ bytesTransmitted: statsData.bytesReceived,
169
+ // firefox typically uses bitrateMean while chrome does not
170
+ bitrate: statsData.bitrateMean,
171
+ codec: statsData.codec?.mimeType,
172
+ resolutionWidth: statsData.frameWidth,
173
+ resolutionHeight: statsData.frameHeight,
174
+ resolution: statsData.frameWidth && statsData.frameHeight
175
+ ? `${statsData.frameWidth}x${statsData.frameHeight}`
176
+ : undefined,
177
+ // firefox typically uses framerateMean while chrome does not
178
+ framesPerSecond: statsData.framerateMean ?? statsData.framesPerSecond,
179
+ roundTripTime: statsData.transport?.selectedCandidatePair?.currentRoundTripTime,
180
+ totalPercentageLost: totalPackets && packetsLost / totalPackets,
181
+ };
182
+ };
183
+ /**
184
+ * Normalize outbound video stats
185
+ *
186
+ * @param statsData - Raw outbound video stats @see OutboundVideo
187
+ *
188
+ * @returns normalized stats for outbound video from PeerConnection
189
+ */
190
+ export const outboundVideo = (statsData) => {
191
+ const totalPacketSendDelay = statsData?.totalPacketSendDelay ?? 0;
192
+ const packetsSent = statsData.packetsSent ?? 0;
193
+ const packetsLost = statsData.remote?.packetsLost ?? 0;
194
+ const totalPackets = packetsSent + packetsLost;
195
+ // firefox typically uses remote.roundTripTime and chrome uses currentRoundTripTime
196
+ const roundTripTime = statsData.remote?.roundTripTime ??
197
+ statsData.transport?.selectedCandidatePair?.currentRoundTripTime;
198
+ return {
199
+ type: statsData.type,
200
+ kind: statsData.kind,
201
+ timestamp: statsData.timestamp,
202
+ packetsTransmitted: packetsSent,
203
+ packetsLost,
204
+ bytesTransmitted: statsData.bytesSent,
205
+ totalPacketSendDelay: statsData.totalPacketSendDelay,
206
+ averagePacketSendDelay: totalPacketSendDelay && totalPacketSendDelay / packetsSent,
207
+ // firefox typically uses bitrateMean while chrome does not
208
+ bitrate: statsData.bitrateMean,
209
+ codec: statsData.codec?.mimeType,
210
+ resolutionWidth: statsData?.frameWidth,
211
+ resolutionHeight: statsData?.frameHeight,
212
+ resolution: statsData.frameWidth && statsData.frameHeight
213
+ ? `${statsData.frameWidth}x${statsData.frameHeight}`
214
+ : undefined,
215
+ // firefox typically uses framerateMean while chrome does not
216
+ framesPerSecond: statsData.framerateMean ?? statsData.framesPerSecond,
217
+ roundTripTime,
218
+ totalPercentageLost: totalPackets && packetsLost / totalPackets,
219
+ };
220
+ };
221
+ const isInbound = (entry) => entry.type === 'inbound-rtp';
222
+ const isOutbound = (entry) => entry.type === 'outbound-rtp';
223
+ const isAudio = (entry) => entry.kind === 'audio';
224
+ const isVideo = (entry) => entry.kind === 'video';
225
+ const isAudioInbound = (entry) => isInbound(entry) && isAudio(entry);
226
+ const isAudioOutbound = (entry) => isOutbound(entry) && isAudio(entry);
227
+ const isVideoInbound = (entry) => isInbound(entry) && isVideo(entry);
228
+ const isVideoOutbound = (entry) => isOutbound(entry) && isVideo(entry);
229
+ /**
230
+ * Normalize stats into inbound and outbound video and audio stats
231
+ *
232
+ * @param statsReports - reports to for mapping
233
+ *
234
+ * @returns normalized stats for inbound, outbound audio and video
235
+ */
236
+ export const statsFrom = (statsReports) => {
237
+ const resolver = createResolver(statsReports);
238
+ // TODO: support them combined if we pass peer-connection obj instead of transceiver
239
+ // as now we return first found
240
+ const audioIn = statsReports.find(isAudioInbound);
241
+ if (audioIn) {
242
+ return inboundAudio(resolver.expand(audioIn));
243
+ }
244
+ const audioOut = statsReports.find(isAudioOutbound);
245
+ if (audioOut) {
246
+ return outboundAudio(resolver.expand(audioOut));
247
+ }
248
+ const videoIn = statsReports.find(isVideoInbound);
249
+ if (videoIn) {
250
+ return inboundVideo(resolver.expand(videoIn));
251
+ }
252
+ const videoOut = statsReports.find(isVideoOutbound);
253
+ if (videoOut) {
254
+ return outboundVideo(resolver.expand(videoOut));
255
+ }
256
+ };
257
+ /**
258
+ * Gets raw stats from peerConnection and normalized them
259
+ *
260
+ * https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_Statistics_API
261
+ *
262
+ * A RTCPeerConnection has getStats()
263
+ * - getStats() returns promise which resolve to a RTCStatsReport
264
+ * - RTCStatsReport behaves like an array of RTCStats objects, or more
265
+ * specific RTCRtpStreamStats objects
266
+ *
267
+ * https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsReport
268
+ * https://developer.mozilla.org/en-US/docs/Web/API/RTCStats
269
+ * https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpStreamStats
270
+ *
271
+ */
272
+ export const statsFromRTCPeer = async (rtcPeer) => {
273
+ const stats = await rtcPeer?.getStats(null);
274
+ const reports = [];
275
+ stats.forEach(report => reports.push(report));
276
+ return (statsFrom(reports) ?? {
277
+ type: 'inbound-rtp',
278
+ kind: 'audio',
279
+ packetsLost: 0,
280
+ packetsTransmitted: 0,
281
+ });
282
+ };
283
+ const getRecentPacketsLost = (oldMetrics, newMetrics) => Math.max(newMetrics.packetsLost - oldMetrics.packetsLost, 0);
284
+ const getRecentTotalPackets = (oldMetrics, newMetrics) => {
285
+ const totalPacketsDelta = newMetrics.packetsTransmitted +
286
+ newMetrics.packetsLost -
287
+ (oldMetrics.packetsTransmitted + oldMetrics.packetsLost);
288
+ return Math.max(totalPacketsDelta, 1);
289
+ };
290
+ const getPacketStats = (oldMetrics, newMetrics) => [getRecentPacketsLost, getRecentTotalPackets].map(fn => fn(oldMetrics, newMetrics));
291
+ const removeObsolete = (metric, qualityHistorySize = STATS_SIZE) => {
292
+ if (metric.length === qualityHistorySize) {
293
+ metric.pop();
294
+ }
295
+ };
296
+ const addPacketsStats = (metric, data) => {
297
+ removeObsolete(metric);
298
+ metric.unshift(data);
299
+ };
300
+ const recordCallPacketsStats = (oldStats, newStats, callPacketsStats) => {
301
+ if (oldStats && newStats) {
302
+ if (!callPacketsStats) {
303
+ callPacketsStats = [];
304
+ }
305
+ addPacketsStats(callPacketsStats, getPacketStats(oldStats, newStats));
306
+ }
307
+ return callPacketsStats;
308
+ };
309
+ const addAudioQualityMetric = (metric, data) => {
310
+ removeObsolete(metric);
311
+ metric.unshift(data);
312
+ };
313
+ const addVideoQualityMetric = (metric, data) => {
314
+ removeObsolete(metric);
315
+ metric.unshift(data);
316
+ };
317
+ const recordCallQualityStats = (prevStats, stats, callQualityStats) => {
318
+ const calcRecentPacketLoss = ({ pT = 0, prevPT = 0, pL = 0, prevPL = 0 }) => {
319
+ if (pT <= 0) {
320
+ return 0;
321
+ }
322
+ if (pT - prevPT <= 0) {
323
+ return pL / pT;
324
+ }
325
+ return (pL - prevPL) / (pT - prevPT);
326
+ };
327
+ if (stats) {
328
+ if (!callQualityStats) {
329
+ callQualityStats = [];
330
+ }
331
+ if (stats.kind === 'audio') {
332
+ addAudioQualityMetric((callQualityStats || []), [
333
+ calcRecentPacketLoss({
334
+ pT: stats.packetsTransmitted,
335
+ prevPT: prevStats?.packetsTransmitted,
336
+ pL: stats.packetsLost,
337
+ prevPL: prevStats?.packetsLost,
338
+ }),
339
+ stats.jitter ?? 0,
340
+ ]);
341
+ }
342
+ if (stats.kind === 'video') {
343
+ addVideoQualityMetric((callQualityStats || []), calcRecentPacketLoss({
344
+ pT: stats.packetsTransmitted,
345
+ prevPT: prevStats?.packetsTransmitted,
346
+ pL: stats.packetsLost,
347
+ prevPL: prevStats?.packetsLost,
348
+ }));
349
+ }
350
+ }
351
+ return callQualityStats;
352
+ };
353
+ export const getQuality = (stats) => {
354
+ const qualityOverTime = stats.map(stats => calculateQuality(stats));
355
+ const goodOrOk = qualityOverTime.filter(stat => stat === Quality.GOOD || stat === Quality.OK);
356
+ const qualitySum = qualityOverTime.reduce((acc, val) => acc + val, 0);
357
+ return {
358
+ quality: Math.round(qualitySum / qualityOverTime.length),
359
+ goodOrOkQuality: goodOrOk.length / qualityOverTime.length,
360
+ qualityOverTime,
361
+ };
362
+ };
363
+ export const addDeltaStats = (newStats, cache) => {
364
+ const oldStats = cache.previous ??
365
+ Object.keys(newStats).reduce((stats, key) => {
366
+ const statKey = key;
367
+ if (typeof newStats[statKey] === 'number' &&
368
+ statKey !== 'timestamp') {
369
+ return { ...stats, [statKey]: 0 };
370
+ }
371
+ return { ...stats, [statKey]: newStats[statKey] };
372
+ }, {});
373
+ const deltaStats = {
374
+ ...newStats,
375
+ };
376
+ const callPacketsStats = recordCallPacketsStats(oldStats, newStats, cache.callPacketsStats);
377
+ const metrics = [
378
+ [newStats, oldStats, deltaStats, callPacketsStats ?? []],
379
+ ];
380
+ metrics.forEach(([newMetrics, oldMetrics, deltaMetrics, packets]) => {
381
+ if (newMetrics?.bytesTransmitted &&
382
+ newMetrics?.timestamp &&
383
+ oldMetrics?.bytesTransmitted &&
384
+ oldMetrics?.timestamp &&
385
+ deltaMetrics // && deltaMetrics.bitrate == null
386
+ ) {
387
+ const dMs = newMetrics.timestamp - oldMetrics.timestamp;
388
+ const dBytes = newMetrics.bytesTransmitted - oldMetrics.bytesTransmitted;
389
+ if (dMs !== 0) {
390
+ deltaMetrics.bitrate = Math.round((dBytes * 8) / (dMs / 1000));
391
+ }
392
+ }
393
+ if (deltaMetrics) {
394
+ const [recentPacketsLost, recentTotalPackets] = packets.reduce((acc, [lost, total]) => {
395
+ acc[0] += lost;
396
+ acc[1] += total;
397
+ return acc;
398
+ }, [0, 0]);
399
+ deltaMetrics.recentPercentageLost =
400
+ recentTotalPackets === 0
401
+ ? 0
402
+ : recentPacketsLost / recentTotalPackets;
403
+ }
404
+ });
405
+ const callQualityStats = recordCallQualityStats(oldStats, deltaStats, cache.callQualityStats);
406
+ return [
407
+ deltaStats,
408
+ getQuality(callQualityStats ?? []).quality,
409
+ callQualityStats,
410
+ ];
411
+ };
412
+ // https://docs.pexip.com/admin/media_statistics.htm
413
+ export const calculateQuality = (stats) => {
414
+ let packetLoss;
415
+ let jitter;
416
+ if (typeof stats === 'number') {
417
+ packetLoss == stats;
418
+ }
419
+ else {
420
+ [packetLoss, jitter] = stats;
421
+ }
422
+ let callQuality = Quality.OK;
423
+ if (typeof packetLoss === 'number') {
424
+ if (packetLoss < 0.01) {
425
+ callQuality = Quality.GOOD;
426
+ }
427
+ else if (packetLoss < 0.03) {
428
+ callQuality = Quality.OK;
429
+ }
430
+ else if (packetLoss < 0.1) {
431
+ callQuality = Quality.BAD;
432
+ }
433
+ else {
434
+ callQuality = Quality.TERRIBLE;
435
+ }
436
+ }
437
+ if (jitter && jitter > 0.04) {
438
+ if (callQuality === Quality.GOOD) {
439
+ callQuality = Quality.OK;
440
+ }
441
+ else if (callQuality === Quality.OK) {
442
+ callQuality = Quality.BAD;
443
+ }
444
+ else if (callQuality === Quality.BAD) {
445
+ callQuality = Quality.TERRIBLE;
446
+ }
447
+ }
448
+ return callQuality;
449
+ };
450
+ /**
451
+ * Creates stats collector
452
+ *
453
+ * @param input - input to get stats from
454
+ * @param signal - Signal which distributes stats
455
+ * @param interval - how often signal with fire with new stats
456
+ *
457
+ * @returns \{function to reset stats window, cleanup func\}
458
+ */
459
+ export const createStatsCollector = ({ input, signals: { onCallQuality, onCallQualityStats, onRtcStats }, interval = 1000, }) => {
460
+ const newCache = () => ({
461
+ callQuality: Quality.GOOD,
462
+ callPacketsStats: [],
463
+ callQualityStats: [],
464
+ });
465
+ let cache = newCache();
466
+ const pushStats = () => {
467
+ void statsFromRTCPeer(input).then(newStats => {
468
+ const [stats, callQuality, callQualityStats] = addDeltaStats(newStats, cache);
469
+ if (callQuality != cache.callQuality) {
470
+ onCallQuality.emit(callQuality);
471
+ cache.callQuality = callQuality;
472
+ }
473
+ cache.previous = newStats;
474
+ onRtcStats.emit(stats);
475
+ onCallQualityStats.emit(callQualityStats);
476
+ });
477
+ };
478
+ let it = window.setInterval(pushStats, interval);
479
+ const clearInterval = () => {
480
+ window.clearInterval(it);
481
+ it = 0;
482
+ };
483
+ const resumeStats = () => {
484
+ if (it === 0) {
485
+ cache = newCache();
486
+ pushStats();
487
+ it = window.setInterval(pushStats, interval);
488
+ }
489
+ };
490
+ return {
491
+ resetStats: () => {
492
+ clearInterval();
493
+ return resumeStats;
494
+ },
495
+ cleanup: clearInterval,
496
+ };
497
+ };
@@ -0,0 +1,160 @@
1
+ import type { Signal } from '@pexip/signal';
2
+ export interface RTCStats {
3
+ id: string;
4
+ type: string;
5
+ kind?: 'audio' | 'video';
6
+ timestamp?: number;
7
+ [key: string]: unknown;
8
+ }
9
+ export interface Remote extends RTCStats {
10
+ type: 'remote-inbound-rtp';
11
+ jitter?: number;
12
+ packetsLost?: number;
13
+ roundTripTime?: number;
14
+ codec?: Codec;
15
+ transport?: Transport;
16
+ }
17
+ export interface CandidatePair extends RTCStats {
18
+ type: 'candidate-pair';
19
+ availableIncomingBitrate?: number;
20
+ availableOutgoingBitrate?: number;
21
+ currentRoundTripTime?: number;
22
+ transport?: Transport;
23
+ localCandidate?: CandidatePair;
24
+ remoteCandidate?: CandidatePair;
25
+ }
26
+ export interface Transport extends RTCStats {
27
+ type: 'transport';
28
+ selectedCandidatePair?: CandidatePair;
29
+ }
30
+ export interface Codec extends RTCStats {
31
+ type: 'codec';
32
+ mimeType?: string;
33
+ }
34
+ export interface InboundAudio extends RTCStats {
35
+ type: 'inbound-rtp';
36
+ kind: 'audio';
37
+ jitter?: number;
38
+ packetsReceived?: number;
39
+ packetsLost?: number;
40
+ bytesReceived?: number;
41
+ track?: Track;
42
+ transport?: Transport;
43
+ codec?: Codec;
44
+ }
45
+ export interface OutboundAudio extends RTCStats {
46
+ type: 'outbound-rtp';
47
+ kind: 'audio';
48
+ packetsSent?: number;
49
+ bytesSent?: number;
50
+ remote?: Remote;
51
+ transport?: Transport;
52
+ codec?: Codec;
53
+ }
54
+ export interface InboundVideo extends RTCStats {
55
+ type: 'inbound-rtp';
56
+ kind: 'video';
57
+ packetsReceived?: number;
58
+ packetsLost?: number;
59
+ bytesReceived?: number;
60
+ transport?: Transport;
61
+ codec?: Codec;
62
+ frameWidth?: number;
63
+ frameHeight?: number;
64
+ framesPerSecond?: number;
65
+ bitrateMean?: number;
66
+ framerateMean?: number;
67
+ }
68
+ export interface OutboundVideo extends RTCStats {
69
+ type: 'outbound-rtp';
70
+ kind: 'video';
71
+ packetsSent?: number;
72
+ packetsLost?: number;
73
+ bytesSent?: number;
74
+ totalPacketSendDelay?: number;
75
+ frameWidth?: number;
76
+ frameHeight?: number;
77
+ framesPerSecond?: number;
78
+ codec?: Codec;
79
+ mediaSource?: MediaSource;
80
+ remote?: Remote;
81
+ track?: Track;
82
+ transport?: Transport;
83
+ bitrateMean?: number;
84
+ framerateMean?: number;
85
+ }
86
+ export interface MediaSource extends RTCStats {
87
+ type: 'media-source';
88
+ kind: 'audio' | 'video';
89
+ }
90
+ export interface Track extends RTCStats {
91
+ type: 'track';
92
+ kind: 'audio' | 'video';
93
+ mediaSource?: MediaSource;
94
+ }
95
+ export interface Stream extends RTCStats {
96
+ type: 'stream';
97
+ tracks?: Track[];
98
+ }
99
+ export type AnyStats = CandidatePair | Codec | InboundAudio | InboundVideo | OutboundAudio | OutboundVideo | RTCStats | Remote | Stream | Track | Transport;
100
+ export interface Metrics {
101
+ type: 'inbound-rtp' | 'outbound-rtp';
102
+ kind: 'audio' | 'video';
103
+ bitrate?: number;
104
+ bytesTransmitted?: number;
105
+ codec?: string;
106
+ jitter?: number;
107
+ packetsLost: number;
108
+ packetsTransmitted: number;
109
+ recentPercentageLost?: number;
110
+ roundTripTime?: number;
111
+ timestamp?: number;
112
+ totalPercentageLost?: number;
113
+ }
114
+ export interface VideoMetrics extends Metrics {
115
+ framesPerSecond?: number;
116
+ resolution?: string;
117
+ resolutionHeight?: number;
118
+ resolutionWidth?: number;
119
+ }
120
+ export type InboundAudioMetrics = Metrics;
121
+ export type InboundVideoMetrics = VideoMetrics;
122
+ export type OutboundAudioMetrics = Metrics;
123
+ export type OutboundVideoMetrics = VideoMetrics & {
124
+ totalPacketSendDelay?: number;
125
+ averagePacketSendDelay?: number;
126
+ };
127
+ export type NormalizedRTCStats = InboundAudioMetrics | InboundVideoMetrics | OutboundAudioMetrics | OutboundVideoMetrics;
128
+ export type PacketsStats = Array<[number, number]>;
129
+ export type AudioQualityStats = Array<[number, number]>;
130
+ export type VideoQualityStats = number[];
131
+ export type CallQualityStats = AudioQualityStats | VideoQualityStats;
132
+ export type CallPacketsStats = PacketsStats;
133
+ export interface CacheStats {
134
+ previous?: NormalizedRTCStats;
135
+ callPacketsStats: CallPacketsStats;
136
+ callQuality: Quality;
137
+ callQualityStats: CallQualityStats;
138
+ }
139
+ export declare enum Quality {
140
+ GOOD = 0,
141
+ OK = 1,
142
+ BAD = 2,
143
+ TERRIBLE = 3
144
+ }
145
+ export interface StatsSignals {
146
+ onRtcStats: Signal<NormalizedRTCStats>;
147
+ onCallQuality: Signal<Quality>;
148
+ onCallQualityStats: Signal<CallQualityStats>;
149
+ }
150
+ export interface StatsCollectorOptions {
151
+ input: {
152
+ getStats: (selector?: MediaStreamTrack | null) => Promise<RTCStatsReport>;
153
+ };
154
+ signals: StatsSignals;
155
+ interval?: number;
156
+ }
157
+ export interface StatsCollector {
158
+ resetStats: () => () => void;
159
+ cleanup: () => void;
160
+ }
@@ -0,0 +1,7 @@
1
+ export var Quality;
2
+ (function (Quality) {
3
+ Quality[Quality["GOOD"] = 0] = "GOOD";
4
+ Quality[Quality["OK"] = 1] = "OK";
5
+ Quality[Quality["BAD"] = 2] = "BAD";
6
+ Quality[Quality["TERRIBLE"] = 3] = "TERRIBLE";
7
+ })(Quality || (Quality = {}));
@@ -0,0 +1,2 @@
1
+ import type { AnyStats } from '../statsCollector.types';
2
+ export declare const statsData: AnyStats[];