@stream-io/video-client 1.51.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 +13 -0
- package/dist/index.browser.es.js +311 -14
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +311 -13
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +311 -14
- package/dist/index.es.js.map +1 -1
- 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 +1 -1
- package/dist/src/rtc/types.d.ts +1 -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 +5 -1
- package/src/devices/__tests__/CameraManager.test.ts +3 -1
- package/src/devices/__tests__/DeviceManager.test.ts +3 -1
- 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/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 +7 -1
- package/src/rtc/Subscriber.ts +1 -0
- package/src/rtc/__tests__/Subscriber.test.ts +5 -1
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +16 -15
- package/src/rtc/types.ts +1 -0
- package/src/stats/rtc/StatsTracer.ts +25 -4
- package/src/stats/rtc/__tests__/StatsTracer.test.ts +155 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
hasAudio,
|
|
4
|
+
hasInterruptedTrack,
|
|
4
5
|
hasPausedTrack,
|
|
5
6
|
hasScreenShare,
|
|
6
7
|
hasScreenShareAudio,
|
|
@@ -120,6 +121,52 @@ describe('participantUtils', () => {
|
|
|
120
121
|
});
|
|
121
122
|
});
|
|
122
123
|
|
|
124
|
+
describe('hasInterruptedTrack', () => {
|
|
125
|
+
it('returns true when the track is both interrupted and published', () => {
|
|
126
|
+
const participant = createMockParticipant({
|
|
127
|
+
publishedTracks: [TrackType.AUDIO],
|
|
128
|
+
interruptedTracks: [TrackType.AUDIO],
|
|
129
|
+
});
|
|
130
|
+
expect(hasInterruptedTrack(participant, TrackType.AUDIO)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns false when the track is interrupted but no longer published', () => {
|
|
134
|
+
const participant = createMockParticipant({
|
|
135
|
+
publishedTracks: [],
|
|
136
|
+
interruptedTracks: [TrackType.AUDIO],
|
|
137
|
+
});
|
|
138
|
+
expect(hasInterruptedTrack(participant, TrackType.AUDIO)).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns false when the track is published but not interrupted', () => {
|
|
142
|
+
const participant = createMockParticipant({
|
|
143
|
+
publishedTracks: [TrackType.AUDIO],
|
|
144
|
+
interruptedTracks: [],
|
|
145
|
+
});
|
|
146
|
+
expect(hasInterruptedTrack(participant, TrackType.AUDIO)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns false when interruptedTracks is undefined', () => {
|
|
150
|
+
const participant = createMockParticipant({
|
|
151
|
+
publishedTracks: [TrackType.AUDIO],
|
|
152
|
+
interruptedTracks: undefined,
|
|
153
|
+
});
|
|
154
|
+
expect(hasInterruptedTrack(participant, TrackType.AUDIO)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('checks each track type independently', () => {
|
|
158
|
+
const participant = createMockParticipant({
|
|
159
|
+
publishedTracks: [TrackType.AUDIO, TrackType.VIDEO],
|
|
160
|
+
interruptedTracks: [TrackType.VIDEO],
|
|
161
|
+
});
|
|
162
|
+
expect(hasInterruptedTrack(participant, TrackType.AUDIO)).toBe(false);
|
|
163
|
+
expect(hasInterruptedTrack(participant, TrackType.VIDEO)).toBe(true);
|
|
164
|
+
expect(hasInterruptedTrack(participant, TrackType.SCREEN_SHARE)).toBe(
|
|
165
|
+
false,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
123
170
|
describe('hasPausedTrack', () => {
|
|
124
171
|
it('should return true when participant has paused VIDEO track', () => {
|
|
125
172
|
const participant = createMockParticipant({
|
|
@@ -137,6 +137,7 @@ export const getClientDetails = async (): Promise<ClientDetails> => {
|
|
|
137
137
|
sdk: sdkInfo,
|
|
138
138
|
os: osInfo,
|
|
139
139
|
device: deviceInfo,
|
|
140
|
+
webrtcVersion: webRtcInfo?.version || '',
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -173,11 +174,12 @@ export const getClientDetails = async (): Promise<ClientDetails> => {
|
|
|
173
174
|
const uaBrowser = userAgentData?.fullVersionList?.find(
|
|
174
175
|
(v) => !v.brand.includes('Chromium') && !v.brand.match(/[()\-./:;=?_]/g),
|
|
175
176
|
);
|
|
177
|
+
const browserVersion = uaBrowser?.version || browser.version || '';
|
|
176
178
|
return {
|
|
177
179
|
sdk: sdkInfo,
|
|
178
180
|
browser: {
|
|
179
181
|
name: uaBrowser?.brand || browser.name || navigator.userAgent,
|
|
180
|
-
version:
|
|
182
|
+
version: browserVersion,
|
|
181
183
|
},
|
|
182
184
|
os: {
|
|
183
185
|
name: userAgentData?.platform || os.name || '',
|
|
@@ -190,5 +192,6 @@ export const getClientDetails = async (): Promise<ClientDetails> => {
|
|
|
190
192
|
.join(' '),
|
|
191
193
|
version: '',
|
|
192
194
|
},
|
|
195
|
+
webrtcVersion: browserVersion,
|
|
193
196
|
};
|
|
194
197
|
};
|
|
@@ -41,6 +41,21 @@ export const hasScreenShareAudio = (p: StreamVideoParticipant): boolean =>
|
|
|
41
41
|
export const isPinned = (p: StreamVideoParticipant): boolean =>
|
|
42
42
|
!!p.pin && (p.pin.isLocalPin || p.pin.pinnedAt > 0);
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Check if a participant has a track that is currently interrupted: the
|
|
46
|
+
* participant intends to publish it (it is in `publishedTracks`) but no
|
|
47
|
+
* media is flowing right now (it is in `interruptedTracks`).
|
|
48
|
+
*
|
|
49
|
+
* @param p the participant to check.
|
|
50
|
+
* @param trackType the track type to check.
|
|
51
|
+
*/
|
|
52
|
+
export const hasInterruptedTrack = (
|
|
53
|
+
p: StreamVideoParticipant,
|
|
54
|
+
trackType: TrackType,
|
|
55
|
+
): boolean =>
|
|
56
|
+
!!p.interruptedTracks?.includes(trackType) &&
|
|
57
|
+
p.publishedTracks.includes(trackType);
|
|
58
|
+
|
|
44
59
|
/**
|
|
45
60
|
* Check if a participant has a paused track of the specified type.
|
|
46
61
|
*
|
|
@@ -69,6 +69,7 @@ export abstract class BasePeerConnection {
|
|
|
69
69
|
enableTracing,
|
|
70
70
|
clientPublishOptions,
|
|
71
71
|
iceRestartDelay = 2500,
|
|
72
|
+
statsTimestampDriftThresholdMs = 0,
|
|
72
73
|
}: BasePeerConnectionOpts,
|
|
73
74
|
) {
|
|
74
75
|
this.peerType = peerType;
|
|
@@ -85,7 +86,12 @@ export abstract class BasePeerConnection {
|
|
|
85
86
|
{ tags: [tag] },
|
|
86
87
|
);
|
|
87
88
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
88
|
-
this.stats = new StatsTracer(
|
|
89
|
+
this.stats = new StatsTracer(
|
|
90
|
+
this.pc,
|
|
91
|
+
peerType,
|
|
92
|
+
this.trackIdToTrackType,
|
|
93
|
+
statsTimestampDriftThresholdMs,
|
|
94
|
+
);
|
|
89
95
|
if (enableTracing) {
|
|
90
96
|
this.tracer = new Tracer(
|
|
91
97
|
`${tag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`,
|
package/src/rtc/Subscriber.ts
CHANGED
|
@@ -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: '',
|
package/src/rtc/types.ts
CHANGED
|
@@ -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
|
+
});
|