@stream-io/video-client 1.50.0 → 1.52.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 +24 -0
- package/dist/index.browser.es.js +597 -70
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +597 -69
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +597 -70
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +1 -0
- package/dist/src/devices/CameraManager.d.ts +1 -0
- package/dist/src/devices/DeviceManager.d.ts +20 -0
- package/dist/src/devices/VirtualDevice.d.ts +59 -0
- package/dist/src/devices/devicePersistence.d.ts +1 -1
- package/dist/src/devices/index.d.ts +1 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +204 -2
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +9 -1
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +67 -1
- package/dist/src/helpers/participantUtils.d.ts +10 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +8 -3
- package/dist/src/rtc/Publisher.d.ts +21 -3
- package/dist/src/rtc/TransceiverCache.d.ts +5 -1
- package/dist/src/rtc/helpers/degradationPreference.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +3 -0
- package/dist/src/stats/rtc/StatsTracer.d.ts +2 -1
- package/dist/src/stats/utils.d.ts +1 -0
- package/package.json +14 -14
- package/src/Call.ts +27 -12
- package/src/devices/CameraManager.ts +9 -2
- package/src/devices/DeviceManager.ts +148 -8
- package/src/devices/DeviceManagerState.ts +4 -1
- package/src/devices/VirtualDevice.ts +69 -0
- package/src/devices/__tests__/CameraManager.test.ts +22 -1
- package/src/devices/__tests__/DeviceManager.test.ts +124 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +3 -1
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +3 -1
- package/src/devices/__tests__/ScreenShareManager.test.ts +3 -1
- package/src/devices/__tests__/web-audio.mocks.ts +3 -1
- package/src/devices/devicePersistence.ts +2 -1
- package/src/devices/index.ts +1 -0
- package/src/gen/video/sfu/event/events.ts +10 -0
- package/src/gen/video/sfu/models/models.ts +338 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +28 -2
- package/src/gen/video/sfu/signal_rpc/signal.ts +121 -15
- package/src/helpers/__tests__/DynascaleManager.test.ts +8 -7
- package/src/helpers/__tests__/browsers.test.ts +4 -4
- package/src/helpers/__tests__/participantUtils.test.ts +47 -0
- package/src/helpers/client-details.ts +4 -1
- package/src/helpers/participantUtils.ts +15 -0
- package/src/rtc/BasePeerConnection.ts +22 -4
- package/src/rtc/Publisher.ts +140 -41
- package/src/rtc/Subscriber.ts +1 -0
- package/src/rtc/TransceiverCache.ts +10 -3
- package/src/rtc/__tests__/Call.reconnect.test.ts +121 -2
- package/src/rtc/__tests__/Publisher.test.ts +659 -112
- package/src/rtc/__tests__/Subscriber.test.ts +7 -3
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +16 -15
- package/src/rtc/helpers/__tests__/degradationPreference.test.ts +33 -1
- package/src/rtc/helpers/degradationPreference.ts +18 -0
- package/src/rtc/types.ts +3 -0
- package/src/stats/rtc/StatsTracer.ts +25 -4
- package/src/stats/rtc/__tests__/StatsTracer.test.ts +155 -0
|
@@ -64,11 +64,11 @@ describe('Subscriber', () => {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
afterEach(() => {
|
|
67
|
+
afterEach(async () => {
|
|
68
68
|
vi.useRealTimers();
|
|
69
69
|
vi.clearAllMocks();
|
|
70
70
|
vi.resetModules();
|
|
71
|
-
subscriber.dispose();
|
|
71
|
+
await subscriber.dispose();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
describe('Subscriber ICE restart', () => {
|
|
@@ -425,7 +425,10 @@ describe('Subscriber', () => {
|
|
|
425
425
|
.mockResolvedValue({ sdp: 'answer-sdp' });
|
|
426
426
|
vi.spyOn(subscriber['pc'], 'setRemoteDescription').mockResolvedValue();
|
|
427
427
|
|
|
428
|
-
const offer = SubscriberOffer.create({
|
|
428
|
+
const offer = SubscriberOffer.create({
|
|
429
|
+
sdp: 'offer-sdp',
|
|
430
|
+
negotiationId: 42,
|
|
431
|
+
});
|
|
429
432
|
// @ts-expect-error - private method
|
|
430
433
|
await subscriber.negotiate(offer);
|
|
431
434
|
expect(subscriber['pc'].setRemoteDescription).toHaveBeenCalledWith({
|
|
@@ -437,6 +440,7 @@ describe('Subscriber', () => {
|
|
|
437
440
|
expect(sfuClient.sendAnswer).toHaveBeenCalledWith({
|
|
438
441
|
peerType: PeerType.SUBSCRIBER,
|
|
439
442
|
sdp: 'answer-sdp',
|
|
443
|
+
negotiationId: 42,
|
|
440
444
|
});
|
|
441
445
|
});
|
|
442
446
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
const RTCPeerConnectionMock = vi.fn((): Partial<RTCPeerConnection>
|
|
3
|
+
const RTCPeerConnectionMock = vi.fn(function (): Partial<RTCPeerConnection> {
|
|
4
4
|
return {
|
|
5
5
|
addEventListener: vi.fn(),
|
|
6
6
|
addIceCandidate: vi.fn(),
|
|
@@ -23,7 +23,7 @@ const RTCPeerConnectionMock = vi.fn((): Partial<RTCPeerConnection> => {
|
|
|
23
23
|
});
|
|
24
24
|
vi.stubGlobal('RTCPeerConnection', RTCPeerConnectionMock);
|
|
25
25
|
|
|
26
|
-
const MediaStreamMock = vi.fn((): Partial<MediaStream>
|
|
26
|
+
const MediaStreamMock = vi.fn(function (): Partial<MediaStream> {
|
|
27
27
|
return {
|
|
28
28
|
getTracks: vi.fn().mockReturnValue([]),
|
|
29
29
|
addTrack: vi.fn(),
|
|
@@ -34,7 +34,7 @@ const MediaStreamMock = vi.fn((): Partial<MediaStream> => {
|
|
|
34
34
|
});
|
|
35
35
|
vi.stubGlobal('MediaStream', MediaStreamMock);
|
|
36
36
|
|
|
37
|
-
const MediaStreamTrackMock = vi.fn((): Partial<MediaStreamTrack>
|
|
37
|
+
const MediaStreamTrackMock = vi.fn(function (): Partial<MediaStreamTrack> {
|
|
38
38
|
return {
|
|
39
39
|
addEventListener: vi.fn(),
|
|
40
40
|
removeEventListener: vi.fn(),
|
|
@@ -49,7 +49,7 @@ const MediaStreamTrackMock = vi.fn((): Partial<MediaStreamTrack> => {
|
|
|
49
49
|
});
|
|
50
50
|
vi.stubGlobal('MediaStreamTrack', MediaStreamTrackMock);
|
|
51
51
|
|
|
52
|
-
const RTCRtpTransceiverMock = vi.fn((): Partial<RTCRtpTransceiver>
|
|
52
|
+
const RTCRtpTransceiverMock = vi.fn(function (): Partial<RTCRtpTransceiver> {
|
|
53
53
|
return {
|
|
54
54
|
// @ts-expect-error - incomplete mock
|
|
55
55
|
sender: {
|
|
@@ -64,24 +64,25 @@ const RTCRtpTransceiverMock = vi.fn((): Partial<RTCRtpTransceiver> => {
|
|
|
64
64
|
});
|
|
65
65
|
vi.stubGlobal('RTCRtpTransceiver', RTCRtpTransceiverMock);
|
|
66
66
|
|
|
67
|
-
const RTCTrackEvent = vi.fn(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
const RTCTrackEvent = vi.fn(function (
|
|
68
|
+
type: string,
|
|
69
|
+
eventInitDict: RTCTrackEventInit,
|
|
70
|
+
): Partial<RTCTrackEvent> {
|
|
71
|
+
return {
|
|
72
|
+
type,
|
|
73
|
+
...eventInitDict,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
75
76
|
vi.stubGlobal('RTCTrackEvent', RTCTrackEvent);
|
|
76
77
|
|
|
77
|
-
const RTCRtpReceiverMock = vi.fn((): Partial<typeof RTCRtpReceiver>
|
|
78
|
+
const RTCRtpReceiverMock = vi.fn(function (): Partial<typeof RTCRtpReceiver> {
|
|
78
79
|
return {
|
|
79
80
|
getCapabilities: vi.fn(),
|
|
80
81
|
};
|
|
81
82
|
});
|
|
82
83
|
vi.stubGlobal('RTCRtpReceiver', RTCRtpReceiverMock);
|
|
83
84
|
|
|
84
|
-
const RTCRtpSenderMock = vi.fn((): Partial<typeof RTCRtpSender>
|
|
85
|
+
const RTCRtpSenderMock = vi.fn(function (): Partial<typeof RTCRtpSender> {
|
|
85
86
|
return {
|
|
86
87
|
getCapabilities: vi.fn(),
|
|
87
88
|
// @ts-expect-error - incomplete mock
|
|
@@ -90,7 +91,7 @@ const RTCRtpSenderMock = vi.fn((): Partial<typeof RTCRtpSender> => {
|
|
|
90
91
|
});
|
|
91
92
|
vi.stubGlobal('RTCRtpSender', RTCRtpSenderMock);
|
|
92
93
|
|
|
93
|
-
const AudioContextMock = vi.fn((): Partial<AudioContext>
|
|
94
|
+
const AudioContextMock = vi.fn(function (): Partial<AudioContext> {
|
|
94
95
|
return {
|
|
95
96
|
state: 'suspended',
|
|
96
97
|
sinkId: '',
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { DegradationPreference } from '../../../gen/video/sfu/models/models';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fromRTCDegradationPreference,
|
|
5
|
+
toRTCDegradationPreference,
|
|
6
|
+
} from '../degradationPreference';
|
|
4
7
|
|
|
5
8
|
describe('toRTCDegradationPreference', () => {
|
|
6
9
|
it.each([
|
|
@@ -21,3 +24,32 @@ describe('toRTCDegradationPreference', () => {
|
|
|
21
24
|
).toBeUndefined();
|
|
22
25
|
});
|
|
23
26
|
});
|
|
27
|
+
|
|
28
|
+
describe('fromRTCDegradationPreference', () => {
|
|
29
|
+
it.each([
|
|
30
|
+
['balanced', DegradationPreference.BALANCED],
|
|
31
|
+
['maintain-framerate', DegradationPreference.MAINTAIN_FRAMERATE],
|
|
32
|
+
['maintain-resolution', DegradationPreference.MAINTAIN_RESOLUTION],
|
|
33
|
+
[
|
|
34
|
+
'maintain-framerate-and-resolution',
|
|
35
|
+
DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION,
|
|
36
|
+
],
|
|
37
|
+
] as const)('maps "%s" to %s', (preference, expected) => {
|
|
38
|
+
// @ts-expect-error not in the lib types yet
|
|
39
|
+
expect(fromRTCDegradationPreference(preference)).toBe(expected);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns UNSPECIFIED for undefined', () => {
|
|
43
|
+
expect(fromRTCDegradationPreference(undefined)).toBe(
|
|
44
|
+
DegradationPreference.UNSPECIFIED,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns UNSPECIFIED for an unknown value', () => {
|
|
49
|
+
expect(
|
|
50
|
+
fromRTCDegradationPreference(
|
|
51
|
+
'something-else' as unknown as RTCDegradationPreference,
|
|
52
|
+
),
|
|
53
|
+
).toBe(DegradationPreference.UNSPECIFIED);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -20,3 +20,21 @@ export const toRTCDegradationPreference = (
|
|
|
20
20
|
ensureExhausted(preference, 'Unknown degradation preference');
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
+
|
|
24
|
+
export const fromRTCDegradationPreference = (
|
|
25
|
+
preference: RTCDegradationPreference | undefined,
|
|
26
|
+
): DegradationPreference => {
|
|
27
|
+
switch (preference) {
|
|
28
|
+
case 'balanced':
|
|
29
|
+
return DegradationPreference.BALANCED;
|
|
30
|
+
case 'maintain-framerate':
|
|
31
|
+
return DegradationPreference.MAINTAIN_FRAMERATE;
|
|
32
|
+
case 'maintain-resolution':
|
|
33
|
+
return DegradationPreference.MAINTAIN_RESOLUTION;
|
|
34
|
+
// @ts-expect-error not in the typedefs yet
|
|
35
|
+
case 'maintain-framerate-and-resolution':
|
|
36
|
+
return DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION;
|
|
37
|
+
default:
|
|
38
|
+
return DegradationPreference.UNSPECIFIED;
|
|
39
|
+
}
|
|
40
|
+
};
|
package/src/rtc/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { CallState } from '../store';
|
|
|
9
9
|
import { Dispatcher } from './Dispatcher';
|
|
10
10
|
import type { OptimalVideoLayer } from './layers';
|
|
11
11
|
import type { ClientPublishOptions } from '../types';
|
|
12
|
+
import type { VideoSender } from '../gen/video/sfu/event/events';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Canonical reasons the SDK uses to trigger a reconnection. Free-form strings
|
|
@@ -63,6 +64,7 @@ export type BasePeerConnectionOpts = {
|
|
|
63
64
|
enableTracing: boolean;
|
|
64
65
|
iceRestartDelay?: number;
|
|
65
66
|
clientPublishOptions?: ClientPublishOptions;
|
|
67
|
+
statsTimestampDriftThresholdMs?: number;
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
export type TrackPublishOptions = {
|
|
@@ -73,6 +75,7 @@ export type PublishBundle = {
|
|
|
73
75
|
publishOption: PublishOption;
|
|
74
76
|
transceiver: RTCRtpTransceiver;
|
|
75
77
|
options: TrackPublishOptions;
|
|
78
|
+
videoSender?: VideoSender;
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
export type TrackLayersCache = {
|
|
@@ -19,6 +19,7 @@ export class StatsTracer {
|
|
|
19
19
|
private readonly pc: RTCPeerConnection;
|
|
20
20
|
private readonly peerType: PeerType;
|
|
21
21
|
private readonly trackIdToTrackType: Map<string, TrackType>;
|
|
22
|
+
private readonly driftThresholdMs: number;
|
|
22
23
|
|
|
23
24
|
private costOverrides?: Map<TrackType, number>;
|
|
24
25
|
|
|
@@ -33,10 +34,12 @@ export class StatsTracer {
|
|
|
33
34
|
pc: RTCPeerConnection,
|
|
34
35
|
peerType: PeerType,
|
|
35
36
|
trackIdToTrackType: Map<string, TrackType>,
|
|
37
|
+
statsTimestampDriftThresholdMs: number = 0,
|
|
36
38
|
) {
|
|
37
39
|
this.pc = pc;
|
|
38
40
|
this.peerType = peerType;
|
|
39
41
|
this.trackIdToTrackType = trackIdToTrackType;
|
|
42
|
+
this.driftThresholdMs = statsTimestampDriftThresholdMs;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -49,7 +52,11 @@ export class StatsTracer {
|
|
|
49
52
|
*/
|
|
50
53
|
get = async (): Promise<ComputedStats> => {
|
|
51
54
|
const stats = await this.pc.getStats();
|
|
52
|
-
const currentStats =
|
|
55
|
+
const currentStats = toObjectWithCorrectedTimestamp(
|
|
56
|
+
stats,
|
|
57
|
+
Date.now(),
|
|
58
|
+
this.driftThresholdMs,
|
|
59
|
+
);
|
|
53
60
|
|
|
54
61
|
const performanceStats = this.withOverrides(
|
|
55
62
|
this.peerType === PeerType.SUBSCRIBER
|
|
@@ -213,14 +220,28 @@ export class StatsTracer {
|
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
/**
|
|
216
|
-
* Convert the stat report to an object.
|
|
223
|
+
* Convert the stat report to an object, correcting clock drift along the way.
|
|
224
|
+
* Entries whose `timestamp` differs from `wallNow` by more than `thresholdMs`
|
|
225
|
+
* are replaced with a clone whose `timestamp` is set to `wallNow`. The platform
|
|
226
|
+
* clock backing `DOMHighResTimeStamp` can desynchronise from `Date.now()` after
|
|
227
|
+
* system sleep or clock-jump events (notably on Electron/Chromium), which
|
|
228
|
+
* corrupts the delta-compressed stats payload. A non-positive `thresholdMs`
|
|
229
|
+
* disables correction.
|
|
217
230
|
*
|
|
218
231
|
* @param report the stat report to convert.
|
|
232
|
+
* @param wallNow current wall-clock time used as the drift reference.
|
|
233
|
+
* @param thresholdMs maximum tolerated drift in milliseconds.
|
|
219
234
|
*/
|
|
220
|
-
const
|
|
235
|
+
const toObjectWithCorrectedTimestamp = (
|
|
236
|
+
report: RTCStatsReport,
|
|
237
|
+
wallNow: number,
|
|
238
|
+
thresholdMs: number,
|
|
239
|
+
): Record<string, RTCStats> => {
|
|
221
240
|
const obj: Record<string, RTCStats> = {};
|
|
241
|
+
const correct = thresholdMs > 0;
|
|
222
242
|
report.forEach((v, k) => {
|
|
223
|
-
|
|
243
|
+
const drift = Math.abs(v.timestamp - wallNow);
|
|
244
|
+
obj[k] = correct && drift > thresholdMs ? { ...v, timestamp: wallNow } : v;
|
|
224
245
|
});
|
|
225
246
|
return obj;
|
|
226
247
|
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { StatsTracer } from '../StatsTracer';
|
|
3
|
+
import { PeerType } from '../../../gen/video/sfu/models/models';
|
|
4
|
+
|
|
5
|
+
const WALL_NOW = 1_700_000_000_000;
|
|
6
|
+
const THRESHOLD_MS = 5000;
|
|
7
|
+
|
|
8
|
+
const makeReport = (
|
|
9
|
+
entries: Array<{ id: string; timestamp: number }>,
|
|
10
|
+
): RTCStatsReport => {
|
|
11
|
+
const map = new Map<string, RTCStats>();
|
|
12
|
+
for (const e of entries) {
|
|
13
|
+
map.set(e.id, { id: e.id, timestamp: e.timestamp, type: 'codec' });
|
|
14
|
+
}
|
|
15
|
+
return map as unknown as RTCStatsReport;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const makePc = (report: RTCStatsReport) => {
|
|
19
|
+
return {
|
|
20
|
+
getStats: vi.fn().mockResolvedValue(report),
|
|
21
|
+
} as unknown as RTCPeerConnection;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('StatsTracer timestamp drift correction', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
vi.setSystemTime(WALL_NOW);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.useRealTimers();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps original timestamps when no entry drifts past the threshold', async () => {
|
|
35
|
+
const report = makeReport([
|
|
36
|
+
{ id: 'a', timestamp: WALL_NOW - 1000 },
|
|
37
|
+
{ id: 'b', timestamp: WALL_NOW + 2000 },
|
|
38
|
+
]);
|
|
39
|
+
const tracer = new StatsTracer(
|
|
40
|
+
makePc(report),
|
|
41
|
+
PeerType.SUBSCRIBER,
|
|
42
|
+
new Map(),
|
|
43
|
+
THRESHOLD_MS,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const { delta } = await tracer.get();
|
|
47
|
+
|
|
48
|
+
expect(delta.timestamp).toBe(WALL_NOW + 2000);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('replaces drifted timestamps with wall time', async () => {
|
|
52
|
+
const report = makeReport([
|
|
53
|
+
{ id: 'a', timestamp: WALL_NOW },
|
|
54
|
+
{ id: 'b', timestamp: WALL_NOW + 10_000 },
|
|
55
|
+
]);
|
|
56
|
+
const tracer = new StatsTracer(
|
|
57
|
+
makePc(report),
|
|
58
|
+
PeerType.SUBSCRIBER,
|
|
59
|
+
new Map(),
|
|
60
|
+
THRESHOLD_MS,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const { delta } = await tracer.get();
|
|
64
|
+
|
|
65
|
+
expect(delta.timestamp).toBe(WALL_NOW);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('clamps stale timestamps to wall time (past drift)', async () => {
|
|
69
|
+
const report = makeReport([
|
|
70
|
+
{ id: 'a', timestamp: WALL_NOW + 2000 },
|
|
71
|
+
{ id: 'b', timestamp: WALL_NOW - 10_000 },
|
|
72
|
+
]);
|
|
73
|
+
const tracer = new StatsTracer(
|
|
74
|
+
makePc(report),
|
|
75
|
+
PeerType.SUBSCRIBER,
|
|
76
|
+
new Map(),
|
|
77
|
+
THRESHOLD_MS,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const { delta } = await tracer.get();
|
|
81
|
+
|
|
82
|
+
expect(delta['b'].timestamp).toBe(WALL_NOW);
|
|
83
|
+
expect(delta.timestamp).toBe(WALL_NOW + 2000);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps timestamps with drift exactly at the threshold (exclusive boundary)', async () => {
|
|
87
|
+
const report = makeReport([
|
|
88
|
+
{ id: 'a', timestamp: WALL_NOW },
|
|
89
|
+
{ id: 'b', timestamp: WALL_NOW + THRESHOLD_MS },
|
|
90
|
+
]);
|
|
91
|
+
const tracer = new StatsTracer(
|
|
92
|
+
makePc(report),
|
|
93
|
+
PeerType.SUBSCRIBER,
|
|
94
|
+
new Map(),
|
|
95
|
+
THRESHOLD_MS,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const { delta } = await tracer.get();
|
|
99
|
+
|
|
100
|
+
expect(delta.timestamp).toBe(WALL_NOW + THRESHOLD_MS);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('clamps remote-sourced stat types like any other stat (no special handling)', async () => {
|
|
104
|
+
const map = new Map<string, RTCStats>([
|
|
105
|
+
['anchor', { id: 'anchor', timestamp: WALL_NOW + 2000, type: 'codec' }],
|
|
106
|
+
[
|
|
107
|
+
'remote-in',
|
|
108
|
+
{
|
|
109
|
+
id: 'remote-in',
|
|
110
|
+
timestamp: WALL_NOW - 30_000,
|
|
111
|
+
type: 'remote-inbound-rtp',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
'remote-out',
|
|
116
|
+
{
|
|
117
|
+
id: 'remote-out',
|
|
118
|
+
timestamp: WALL_NOW + 30_000,
|
|
119
|
+
type: 'remote-outbound-rtp',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
]);
|
|
123
|
+
const tracer = new StatsTracer(
|
|
124
|
+
makePc(map as unknown as RTCStatsReport),
|
|
125
|
+
PeerType.SUBSCRIBER,
|
|
126
|
+
new Map(),
|
|
127
|
+
THRESHOLD_MS,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const { delta } = await tracer.get();
|
|
131
|
+
|
|
132
|
+
// both stale and future drift get clamped to wall time, while the
|
|
133
|
+
// within-threshold anchor stays the delta's top-level timestamp.
|
|
134
|
+
expect(delta['remote-in'].timestamp).toBe(WALL_NOW);
|
|
135
|
+
expect(delta['remote-out'].timestamp).toBe(WALL_NOW);
|
|
136
|
+
expect(delta.timestamp).toBe(WALL_NOW + 2000);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('does not correct when the threshold is disabled (0)', async () => {
|
|
140
|
+
const report = makeReport([
|
|
141
|
+
{ id: 'a', timestamp: WALL_NOW },
|
|
142
|
+
{ id: 'b', timestamp: WALL_NOW + 99_999 },
|
|
143
|
+
]);
|
|
144
|
+
const tracer = new StatsTracer(
|
|
145
|
+
makePc(report),
|
|
146
|
+
PeerType.SUBSCRIBER,
|
|
147
|
+
new Map(),
|
|
148
|
+
0,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const { delta } = await tracer.get();
|
|
152
|
+
|
|
153
|
+
expect(delta.timestamp).toBe(WALL_NOW + 99_999);
|
|
154
|
+
});
|
|
155
|
+
});
|