@pexip/peer-connection-stats 17.2.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.
- package/CHANGELOG.md +253 -0
- package/LICENSE +188 -0
- package/README.md +9 -0
- package/dist/index.d.ts +325 -0
- package/dist/index.mjs +478 -0
- package/package.json +43 -0
- package/src/index.ts +3 -0
- package/src/statsCollector.ts +665 -0
- package/src/statsCollector.types.ts +233 -0
- package/src/tests/__snapshots__/statsCollector.test.ts.snap +208 -0
- package/src/tests/statsCollector.chrome-inbound-presentation.fixtures.ts +990 -0
- package/src/tests/statsCollector.chrome-multiple.fixtures.ts +2692 -0
- package/src/tests/statsCollector.chrome-outbound-presentation.fixtures.ts +925 -0
- package/src/tests/statsCollector.firefox.fixtures.ts +473 -0
- package/src/tests/statsCollector.test.ts +480 -0
- package/src/utils.ts +41 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import {setImmediate as timersSetImmediate} from 'timers';
|
|
2
|
+
|
|
3
|
+
import type {Signal} from '@pexip/signal';
|
|
4
|
+
import {isDefined} from '@pexip/utils';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
inboundAudio,
|
|
8
|
+
inboundVideo,
|
|
9
|
+
outboundAudio,
|
|
10
|
+
outboundVideo,
|
|
11
|
+
statsFrom,
|
|
12
|
+
createStatsCollector,
|
|
13
|
+
createResolver,
|
|
14
|
+
addDeltaStats,
|
|
15
|
+
} from '../statsCollector';
|
|
16
|
+
import type {
|
|
17
|
+
AnyStats,
|
|
18
|
+
CacheStats,
|
|
19
|
+
CallQualityStats,
|
|
20
|
+
Codec,
|
|
21
|
+
InboundAudio,
|
|
22
|
+
InboundVideo,
|
|
23
|
+
NormalizedRTCStats,
|
|
24
|
+
OutboundAudio,
|
|
25
|
+
OutboundVideo,
|
|
26
|
+
} from '../statsCollector.types';
|
|
27
|
+
import {Quality} from '../statsCollector.types';
|
|
28
|
+
|
|
29
|
+
import {multiStats as chromeMulti} from './statsCollector.chrome-multiple.fixtures';
|
|
30
|
+
import {statsData as chromeInboundStats} from './statsCollector.chrome-inbound-presentation.fixtures';
|
|
31
|
+
import {statsData as chromeOutboundStats} from './statsCollector.chrome-outbound-presentation.fixtures';
|
|
32
|
+
import {statsData as firefoxStats} from './statsCollector.firefox.fixtures';
|
|
33
|
+
|
|
34
|
+
jest.useFakeTimers();
|
|
35
|
+
|
|
36
|
+
describe('StatsCollector', () => {
|
|
37
|
+
describe('inboundAudio', () => {
|
|
38
|
+
it('should exist', () => {
|
|
39
|
+
expect(typeof inboundAudio).toEqual('function');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return valid data for all fields', () => {
|
|
43
|
+
const lookup = chromeInboundStats;
|
|
44
|
+
|
|
45
|
+
const audioEntry = lookup.find(
|
|
46
|
+
entry => entry.type === 'inbound-rtp' && entry.kind === 'audio',
|
|
47
|
+
) as InboundAudio;
|
|
48
|
+
|
|
49
|
+
const resolver = createResolver(lookup);
|
|
50
|
+
const metrics = inboundAudio(resolver.expand(audioEntry));
|
|
51
|
+
|
|
52
|
+
expect(metrics).toBeDefined();
|
|
53
|
+
expect(metrics).toMatchSnapshot();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('outboundAudio', () => {
|
|
58
|
+
it('should exist', () => {
|
|
59
|
+
expect(typeof outboundAudio).toEqual('function');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should work', () => {
|
|
63
|
+
const lookup = chromeOutboundStats;
|
|
64
|
+
|
|
65
|
+
const audioEntry = lookup.find(
|
|
66
|
+
entry =>
|
|
67
|
+
entry.type === 'outbound-rtp' && entry.kind === 'audio',
|
|
68
|
+
) as OutboundAudio;
|
|
69
|
+
|
|
70
|
+
const resolver = createResolver(lookup);
|
|
71
|
+
const metrics = outboundAudio(resolver.expand(audioEntry));
|
|
72
|
+
|
|
73
|
+
expect(metrics).toBeDefined();
|
|
74
|
+
expect(metrics).toMatchSnapshot();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('inboundVideo', () => {
|
|
79
|
+
it('should exist', () => {
|
|
80
|
+
expect(typeof inboundVideo).toEqual('function');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should work', () => {
|
|
84
|
+
const lookup = chromeInboundStats;
|
|
85
|
+
|
|
86
|
+
const videoEntry = lookup.find(
|
|
87
|
+
entry => entry.type === 'inbound-rtp' && entry.kind === 'video',
|
|
88
|
+
) as InboundVideo;
|
|
89
|
+
|
|
90
|
+
const resolver = createResolver(lookup);
|
|
91
|
+
const metrics = inboundVideo(resolver.expand(videoEntry));
|
|
92
|
+
|
|
93
|
+
expect(metrics).toBeDefined();
|
|
94
|
+
expect(metrics).toMatchSnapshot();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('outboundVideo', () => {
|
|
99
|
+
it('should exist', () => {
|
|
100
|
+
expect(typeof outboundVideo).toEqual('function');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should work', () => {
|
|
104
|
+
const lookup = chromeOutboundStats;
|
|
105
|
+
|
|
106
|
+
const videoEntry = lookup.find(
|
|
107
|
+
entry =>
|
|
108
|
+
entry.type === 'outbound-rtp' && entry.kind === 'video',
|
|
109
|
+
) as OutboundVideo;
|
|
110
|
+
|
|
111
|
+
const resolver = createResolver(lookup);
|
|
112
|
+
const metrics = outboundVideo(resolver.expand(videoEntry));
|
|
113
|
+
|
|
114
|
+
expect(metrics).toBeDefined();
|
|
115
|
+
expect(metrics).toMatchSnapshot();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('statsFrom', () => {
|
|
120
|
+
it('should exist', () => {
|
|
121
|
+
expect(typeof statsFrom).toEqual('function');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return complete data for chromeInboundStats', () => {
|
|
125
|
+
expect(statsFrom(chromeInboundStats)).toMatchSnapshot();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return complete data for chromeOutboundStats', () => {
|
|
129
|
+
expect(statsFrom(chromeOutboundStats)).toMatchSnapshot();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return complete data for firefoxStats', () => {
|
|
133
|
+
expect(statsFrom(firefoxStats)).toMatchSnapshot();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('createResolver', () => {
|
|
138
|
+
it('should exist', () => {
|
|
139
|
+
expect(typeof createResolver).toEqual('function');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should work for flat data with strings & numbers', () => {
|
|
143
|
+
const inboundRtp: InboundAudio = {
|
|
144
|
+
type: 'inbound-rtp',
|
|
145
|
+
kind: 'audio',
|
|
146
|
+
id: '123',
|
|
147
|
+
packetsLost: 0,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const statsData = [inboundRtp];
|
|
151
|
+
|
|
152
|
+
const resolver = createResolver(statsData);
|
|
153
|
+
const expanded = resolver.expand(inboundRtp);
|
|
154
|
+
|
|
155
|
+
expect(Object.keys(expanded).length).toEqual(4);
|
|
156
|
+
expect(expanded).toMatchSnapshot();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should work for data with only string values', () => {
|
|
160
|
+
const inboundRtp: InboundAudio = {
|
|
161
|
+
type: 'inbound-rtp',
|
|
162
|
+
kind: 'audio',
|
|
163
|
+
id: '123',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const statsData = [inboundRtp];
|
|
167
|
+
|
|
168
|
+
const resolver = createResolver(statsData);
|
|
169
|
+
const expanded = resolver.expand(inboundRtp);
|
|
170
|
+
|
|
171
|
+
expect(Object.keys(expanded).length).toEqual(3);
|
|
172
|
+
expect(expanded).toMatchSnapshot();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should work for nested data', () => {
|
|
176
|
+
const inboundRtp: InboundAudio = {
|
|
177
|
+
type: 'inbound-rtp',
|
|
178
|
+
kind: 'audio',
|
|
179
|
+
id: 'abc',
|
|
180
|
+
codecId: 'codec1',
|
|
181
|
+
packetsLost: 0,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const codec: Codec = {
|
|
185
|
+
type: 'codec',
|
|
186
|
+
id: 'codec1',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const statsData: AnyStats[] = [inboundRtp, codec];
|
|
190
|
+
|
|
191
|
+
const resolver = createResolver(statsData);
|
|
192
|
+
const expanded = resolver.expand(inboundRtp);
|
|
193
|
+
|
|
194
|
+
expect(Object.keys(expanded).length).toEqual(6);
|
|
195
|
+
expect(expanded).toMatchSnapshot();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should not modify input', () => {
|
|
199
|
+
const inboundRtp: InboundAudio = {
|
|
200
|
+
type: 'inbound-rtp',
|
|
201
|
+
kind: 'audio',
|
|
202
|
+
id: 'abc',
|
|
203
|
+
packetsLost: 0,
|
|
204
|
+
codecId: 'codec1',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const codec: Codec = {
|
|
208
|
+
type: 'codec',
|
|
209
|
+
id: 'codec1',
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const statsData: AnyStats[] = [{...inboundRtp}, {...codec}];
|
|
213
|
+
const stat = isDefined(statsData[0]);
|
|
214
|
+
expect(stat).toEqual(inboundRtp);
|
|
215
|
+
|
|
216
|
+
const resolver = createResolver(statsData);
|
|
217
|
+
const expanded = resolver.expand(stat as InboundAudio);
|
|
218
|
+
|
|
219
|
+
expect(stat).toEqual(inboundRtp);
|
|
220
|
+
expect(expanded).not.toEqual(inboundRtp);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('addDeltaStats', () => {
|
|
225
|
+
it('should exist', () => {
|
|
226
|
+
expect(typeof addDeltaStats).toEqual('function');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should calculate bitrate when no "previous" arg', () => {
|
|
230
|
+
const [lookupNew] = chromeMulti;
|
|
231
|
+
|
|
232
|
+
const [outboundNew] = [lookupNew].map(l => {
|
|
233
|
+
const lookup = isDefined(l);
|
|
234
|
+
const videoEntry = lookup.find(
|
|
235
|
+
entry =>
|
|
236
|
+
entry.type === 'outbound-rtp' && entry.kind === 'video',
|
|
237
|
+
) as OutboundVideo;
|
|
238
|
+
|
|
239
|
+
const resolver = createResolver(lookup);
|
|
240
|
+
const metrics = outboundVideo(resolver.expand(videoEntry));
|
|
241
|
+
|
|
242
|
+
expect(metrics).toBeDefined();
|
|
243
|
+
expect(metrics.bitrate).toEqual(undefined);
|
|
244
|
+
return metrics;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const statsNew = outboundNew as NormalizedRTCStats;
|
|
248
|
+
const statsCache: CacheStats = {
|
|
249
|
+
callQuality: Quality.GOOD,
|
|
250
|
+
callPacketsStats: [],
|
|
251
|
+
callQualityStats: [],
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const [stats, callQuality] = addDeltaStats(statsNew, statsCache);
|
|
255
|
+
|
|
256
|
+
expect(stats?.bitrate).toEqual(undefined);
|
|
257
|
+
expect(callQuality).not.toEqual(Quality.TERRIBLE);
|
|
258
|
+
expect(stats).toMatchSnapshot();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should calculate bitrate based on "previous" arg', () => {
|
|
262
|
+
const [lookupOld, lookupNew] = chromeMulti;
|
|
263
|
+
|
|
264
|
+
const [outboundOld, outboundNew] = [lookupOld, lookupNew].map(l => {
|
|
265
|
+
const lookup = isDefined(l);
|
|
266
|
+
const videoEntry = lookup.find(
|
|
267
|
+
entry =>
|
|
268
|
+
entry.type === 'outbound-rtp' && entry.kind === 'video',
|
|
269
|
+
) as OutboundVideo;
|
|
270
|
+
|
|
271
|
+
const resolver = createResolver(lookup);
|
|
272
|
+
const metrics = outboundVideo(resolver.expand(videoEntry));
|
|
273
|
+
|
|
274
|
+
expect(metrics).toBeDefined();
|
|
275
|
+
expect(metrics.bitrate).toEqual(undefined);
|
|
276
|
+
return metrics;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const statsNew = outboundNew as NormalizedRTCStats;
|
|
280
|
+
const statsOld: CacheStats = {
|
|
281
|
+
previous: outboundOld,
|
|
282
|
+
callQuality: Quality.GOOD,
|
|
283
|
+
callPacketsStats: [],
|
|
284
|
+
callQualityStats: [],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const [results] = addDeltaStats(statsNew, statsOld);
|
|
288
|
+
|
|
289
|
+
expect(typeof results?.bitrate).toEqual('number');
|
|
290
|
+
expect(results).toMatchSnapshot();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should calculate bitrate based on "previous" arg for audio and video', () => {
|
|
294
|
+
const [statsOld, statsLatest] = chromeMulti.map(statsFrom);
|
|
295
|
+
const statsNew = isDefined(statsLatest);
|
|
296
|
+
|
|
297
|
+
expect(typeof statsNew?.bitrate).toEqual('undefined');
|
|
298
|
+
expect(typeof statsNew?.bitrate).toEqual('undefined');
|
|
299
|
+
|
|
300
|
+
const [results] = addDeltaStats(statsNew, {
|
|
301
|
+
previous: statsOld,
|
|
302
|
+
callQuality: Quality.GOOD,
|
|
303
|
+
callPacketsStats: [],
|
|
304
|
+
callQualityStats: [],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(typeof results?.bitrate).toEqual('number');
|
|
308
|
+
expect(isNaN(results?.bitrate as number)).toEqual(false);
|
|
309
|
+
|
|
310
|
+
expect(typeof results?.bitrate).toEqual('number');
|
|
311
|
+
expect(isNaN(results?.bitrate as number)).toEqual(false);
|
|
312
|
+
|
|
313
|
+
expect(typeof results?.bitrate).toEqual('number');
|
|
314
|
+
expect(isNaN(results.bitrate as number)).toEqual(false);
|
|
315
|
+
|
|
316
|
+
expect(typeof results?.bitrate).toEqual('number');
|
|
317
|
+
expect(isNaN(results?.bitrate as number)).toEqual(false);
|
|
318
|
+
|
|
319
|
+
expect(results).toMatchSnapshot();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('createStatsCollector', () => {
|
|
324
|
+
// Use setImmediate from the timers module instead of the global
|
|
325
|
+
// to avoid it being faked by jest
|
|
326
|
+
const flushPromises = () => new Promise(timersSetImmediate);
|
|
327
|
+
|
|
328
|
+
const getStatsCollector = ({
|
|
329
|
+
statsEmitMock = jest.fn(),
|
|
330
|
+
qualityEmitMock = jest.fn(),
|
|
331
|
+
qualityStatsEmitMock = jest.fn(),
|
|
332
|
+
mockedStats = {},
|
|
333
|
+
interval = 1000,
|
|
334
|
+
}) =>
|
|
335
|
+
createStatsCollector({
|
|
336
|
+
input: {
|
|
337
|
+
getStats: () =>
|
|
338
|
+
Promise.resolve({
|
|
339
|
+
forEach: (
|
|
340
|
+
callbackfn: (
|
|
341
|
+
value: unknown,
|
|
342
|
+
key: string,
|
|
343
|
+
parent: RTCStatsReport,
|
|
344
|
+
) => void,
|
|
345
|
+
) => {
|
|
346
|
+
callbackfn(mockedStats, '', {
|
|
347
|
+
forEach: jest.fn(),
|
|
348
|
+
});
|
|
349
|
+
/* nothing */
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
signals: {
|
|
354
|
+
onRtcStats: {
|
|
355
|
+
emit: statsEmitMock,
|
|
356
|
+
} as unknown as Signal<NormalizedRTCStats>,
|
|
357
|
+
onCallQuality: {
|
|
358
|
+
emit: qualityEmitMock,
|
|
359
|
+
} as unknown as Signal<Quality>,
|
|
360
|
+
onCallQualityStats: {
|
|
361
|
+
emit: qualityStatsEmitMock,
|
|
362
|
+
} as unknown as Signal<CallQualityStats>,
|
|
363
|
+
},
|
|
364
|
+
interval,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it.each`
|
|
368
|
+
interval | emitted
|
|
369
|
+
${undefined} | ${2}
|
|
370
|
+
${100} | ${20}
|
|
371
|
+
`(
|
|
372
|
+
'given interval = $interval stats should be emitted $emitted times',
|
|
373
|
+
async ({
|
|
374
|
+
interval,
|
|
375
|
+
emitted,
|
|
376
|
+
}: {
|
|
377
|
+
interval?: number;
|
|
378
|
+
emitted: number;
|
|
379
|
+
}) => {
|
|
380
|
+
const emit = jest.fn();
|
|
381
|
+
const {cleanup} = getStatsCollector({
|
|
382
|
+
statsEmitMock: emit,
|
|
383
|
+
interval,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
jest.advanceTimersByTime(2000);
|
|
387
|
+
await flushPromises();
|
|
388
|
+
cleanup();
|
|
389
|
+
|
|
390
|
+
expect(emit).toHaveBeenCalledWith({
|
|
391
|
+
kind: 'audio',
|
|
392
|
+
packetsLost: 0,
|
|
393
|
+
packetsTransmitted: 0,
|
|
394
|
+
recentPercentageLost: 0,
|
|
395
|
+
type: 'inbound-rtp',
|
|
396
|
+
});
|
|
397
|
+
expect(emit).toHaveBeenCalledTimes(emitted);
|
|
398
|
+
jest.clearAllMocks();
|
|
399
|
+
|
|
400
|
+
jest.advanceTimersByTime(2000);
|
|
401
|
+
await flushPromises();
|
|
402
|
+
|
|
403
|
+
expect(emit).not.toHaveBeenCalled();
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
it('halts stats updates upon stats reset until manually resumed', async () => {
|
|
408
|
+
const statsEmit = jest.fn();
|
|
409
|
+
const qualityEmit = jest.fn();
|
|
410
|
+
const {cleanup, resetStats} = getStatsCollector({
|
|
411
|
+
statsEmitMock: statsEmit,
|
|
412
|
+
qualityEmitMock: qualityEmit,
|
|
413
|
+
mockedStats: {
|
|
414
|
+
audioLevel: 0.0017700735496078372,
|
|
415
|
+
bytesReceived: 10689,
|
|
416
|
+
codecId: 'RTCCodec_0_Inbound_111',
|
|
417
|
+
concealedSamples: 38287,
|
|
418
|
+
concealmentEvents: 2,
|
|
419
|
+
fecPacketsDiscarded: 0,
|
|
420
|
+
fecPacketsReceived: 0,
|
|
421
|
+
headerBytesReceived: 1780,
|
|
422
|
+
id: 'RTCInboundRTPAudioStream_325581171',
|
|
423
|
+
insertedSamplesForDeceleration: 3250,
|
|
424
|
+
jitter: 0.001,
|
|
425
|
+
jitterBufferDelay: 3628.8,
|
|
426
|
+
jitterBufferEmittedCount: 84480,
|
|
427
|
+
kind: 'audio',
|
|
428
|
+
lastPacketReceivedTimestamp: 1660660870114,
|
|
429
|
+
mediaType: 'audio',
|
|
430
|
+
packetsDiscarded: 0,
|
|
431
|
+
packetsLost: 89,
|
|
432
|
+
packetsReceived: 89,
|
|
433
|
+
removedSamplesForAcceleration: 2254,
|
|
434
|
+
silentConcealedSamples: 37240,
|
|
435
|
+
ssrc: 325581171,
|
|
436
|
+
timestamp: 1660660870129.574,
|
|
437
|
+
totalAudioEnergy: 0.0000037897456073932108,
|
|
438
|
+
totalSamplesDuration: 4.139999999999956,
|
|
439
|
+
totalSamplesReceived: 123200,
|
|
440
|
+
trackId: 'RTCMediaStreamTrack_receiver_1',
|
|
441
|
+
transportId: 'RTCTransport_0_1',
|
|
442
|
+
type: 'inbound-rtp',
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// interval works as usual
|
|
447
|
+
jest.advanceTimersByTime(1000);
|
|
448
|
+
await flushPromises();
|
|
449
|
+
expect(statsEmit).toHaveBeenCalledTimes(1);
|
|
450
|
+
jest.clearAllMocks();
|
|
451
|
+
|
|
452
|
+
// interval disabled with stats reset
|
|
453
|
+
const resumeStatsUpdates = resetStats();
|
|
454
|
+
jest.advanceTimersByTime(1000);
|
|
455
|
+
await flushPromises();
|
|
456
|
+
expect(statsEmit).not.toHaveBeenCalled();
|
|
457
|
+
expect(qualityEmit).not.toHaveBeenCalled();
|
|
458
|
+
jest.clearAllMocks();
|
|
459
|
+
|
|
460
|
+
// resume stats updates
|
|
461
|
+
resumeStatsUpdates();
|
|
462
|
+
await flushPromises();
|
|
463
|
+
// manual emit upon reset to clear a potential different old state
|
|
464
|
+
// because the callQuality has init value of quality:GOOD, which will likely be the same after an ice restart.
|
|
465
|
+
// This results in not emitting the signal as there is no delta
|
|
466
|
+
expect(qualityEmit).toHaveBeenCalledTimes(1);
|
|
467
|
+
jest.clearAllMocks();
|
|
468
|
+
|
|
469
|
+
// resuming already flowing stats should be ignored
|
|
470
|
+
resumeStatsUpdates();
|
|
471
|
+
|
|
472
|
+
jest.advanceTimersByTime(1000);
|
|
473
|
+
await flushPromises();
|
|
474
|
+
expect(statsEmit).toHaveBeenCalledTimes(1);
|
|
475
|
+
expect(qualityEmit).not.toHaveBeenCalled(); // because the cache is init with quality:GOOD already
|
|
476
|
+
|
|
477
|
+
cleanup();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {Signal} from '@pexip/signal';
|
|
2
|
+
import {createSignal} from '@pexip/signal';
|
|
3
|
+
|
|
4
|
+
import type {StatsSignals} from './statsCollector.types';
|
|
5
|
+
|
|
6
|
+
const SIGNAL_PREFIX = 'call:peerConnection:stats';
|
|
7
|
+
|
|
8
|
+
const REQUIRED_STATS_SIGNAL_KEYS = [
|
|
9
|
+
'onRtcStats',
|
|
10
|
+
'onCallQualityStats',
|
|
11
|
+
'onCallQuality',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create and return all required and optional (if specified with `more`),
|
|
16
|
+
* signals for infinity client to work
|
|
17
|
+
*
|
|
18
|
+
* @param scope - any scope prefix for the generated signal name, @see Signal
|
|
19
|
+
* @param more - Keys from `InfinitySignalsOptional`, @see InfinitySignalsOptional
|
|
20
|
+
*
|
|
21
|
+
* The following signals created by default
|
|
22
|
+
* - 'onConnected',
|
|
23
|
+
* - 'onAnswer',
|
|
24
|
+
*
|
|
25
|
+
* @see REQUIRED_STATS_SIGNAL_KEYS
|
|
26
|
+
*/
|
|
27
|
+
export const createStatsSignals = (scope = '') => {
|
|
28
|
+
const signalScope = scope && [scope, ':'].join('');
|
|
29
|
+
return REQUIRED_STATS_SIGNAL_KEYS.reduce(
|
|
30
|
+
(signals, key) => ({
|
|
31
|
+
...signals,
|
|
32
|
+
[key]: createSignal<
|
|
33
|
+
StatsSignals[typeof key] extends Signal<infer S> ? S : never
|
|
34
|
+
>({
|
|
35
|
+
name: `${SIGNAL_PREFIX}:${signalScope}:${key}`,
|
|
36
|
+
allowEmittingWithoutObserver: true,
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
{} as StatsSignals,
|
|
40
|
+
);
|
|
41
|
+
};
|