@stream-io/video-client 1.55.2 → 1.57.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 +22 -0
- package/dist/index.browser.es.js +216 -119
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +216 -119
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +216 -119
- package/dist/index.es.js.map +1 -1
- package/dist/src/helpers/BlockedAudioTracker.d.ts +16 -1
- package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +4 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +9 -2
- package/dist/src/store/rxUtils.d.ts +5 -0
- package/package.json +11 -11
- package/src/Call.ts +1 -0
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -2
- package/src/devices/devicePersistence.ts +1 -2
- package/src/helpers/BlockedAudioTracker.ts +71 -13
- package/src/helpers/DynascaleManager.ts +14 -3
- package/src/helpers/MediaPlaybackWatchdog.ts +35 -16
- package/src/helpers/__tests__/BlockedAudioTracker.test.ts +54 -6
- package/src/helpers/__tests__/DynascaleManager.test.ts +45 -0
- package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +137 -8
- package/src/helpers/__tests__/ViewportTracker.test.ts +1 -2
- package/src/reporting/ClientEventReporter.ts +39 -39
- package/src/reporting/__tests__/ClientEventReporter.test.ts +159 -44
- package/src/rtc/BasePeerConnection.ts +1 -0
- package/src/rtc/Publisher.ts +78 -58
- package/src/rtc/__tests__/Publisher.test.ts +297 -0
- package/src/rtc/types.ts +14 -13
- package/src/stats/rtc/types.ts +8 -2
- package/src/store/CallState.ts +6 -19
- package/src/store/rxUtils.ts +15 -0
- package/src/types.ts +1 -4
|
@@ -28,20 +28,33 @@ const createMediaElement = (
|
|
|
28
28
|
state: FakeMediaState = {},
|
|
29
29
|
) => {
|
|
30
30
|
const el = document.createElement(kind) as HTMLMediaElement;
|
|
31
|
-
Object.defineProperty(el, 'srcObject', {
|
|
31
|
+
Object.defineProperty(el, 'srcObject', {
|
|
32
|
+
value: 'srcObject' in state ? state.srcObject : new MediaStream(),
|
|
33
|
+
writable: true,
|
|
34
|
+
});
|
|
32
35
|
Object.defineProperty(el, 'paused', { writable: true, configurable: true });
|
|
33
36
|
Object.defineProperty(el, 'readyState', {
|
|
37
|
+
value: state.readyState ?? 4,
|
|
38
|
+
writable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(el, 'ended', {
|
|
42
|
+
value: state.ended ?? false,
|
|
34
43
|
writable: true,
|
|
35
44
|
configurable: true,
|
|
36
45
|
});
|
|
37
|
-
|
|
38
|
-
el.srcObject = 'srcObject' in state ? state.srcObject : new MediaStream();
|
|
39
|
-
el.paused = state.paused ?? true;
|
|
40
|
-
el.readyState = state.readyState ?? 4;
|
|
41
|
-
el.ended = state.ended ?? false;
|
|
46
|
+
setPaused(el, state.paused ?? true);
|
|
42
47
|
return el;
|
|
43
48
|
};
|
|
44
49
|
|
|
50
|
+
const setPaused = (el: HTMLMediaElement, paused: boolean) => {
|
|
51
|
+
Object.defineProperty(el, 'paused', {
|
|
52
|
+
value: paused,
|
|
53
|
+
writable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
45
58
|
type SetupOpts = {
|
|
46
59
|
kind?: 'audio' | 'video';
|
|
47
60
|
state?: FakeMediaState;
|
|
@@ -111,15 +124,131 @@ describe('MediaPlaybackWatchdog', () => {
|
|
|
111
124
|
await vi.advanceTimersByTimeAsync(6000);
|
|
112
125
|
expect(play).toHaveBeenCalledTimes(2);
|
|
113
126
|
|
|
114
|
-
// Simulate the element actually starting to play
|
|
127
|
+
// Simulate the element actually starting to play, and staying up long
|
|
128
|
+
// enough for the recovery to count as settled.
|
|
129
|
+
setPaused(el, false);
|
|
115
130
|
el.dispatchEvent(new Event('playing'));
|
|
131
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
116
132
|
|
|
117
|
-
// A subsequent pause should
|
|
133
|
+
// A subsequent pause should trigger a fresh, immediate recovery attempt.
|
|
134
|
+
setPaused(el, true);
|
|
118
135
|
el.dispatchEvent(new Event('pause'));
|
|
119
136
|
await vi.advanceTimersByTimeAsync(0);
|
|
120
137
|
expect(play).toHaveBeenCalledTimes(3);
|
|
121
138
|
});
|
|
122
139
|
|
|
140
|
+
it('backs off instead of retrying immediately when playback has not settled', async () => {
|
|
141
|
+
el.dispatchEvent(new Event('pause'));
|
|
142
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
143
|
+
expect(play).toHaveBeenCalledTimes(1);
|
|
144
|
+
|
|
145
|
+
// resumed, but re-paused before the settle delay elapsed
|
|
146
|
+
setPaused(el, false);
|
|
147
|
+
el.dispatchEvent(new Event('playing'));
|
|
148
|
+
expect(tracer.trace).not.toHaveBeenCalledWith(
|
|
149
|
+
'mediaPlayback.recover.success',
|
|
150
|
+
{
|
|
151
|
+
kind: 'audio',
|
|
152
|
+
attempts: 1,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
156
|
+
setPaused(el, true);
|
|
157
|
+
el.dispatchEvent(new Event('pause'));
|
|
158
|
+
|
|
159
|
+
// the next attempt is subject to backoff, not scheduled on the next tick
|
|
160
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
161
|
+
expect(play).toHaveBeenCalledTimes(1);
|
|
162
|
+
|
|
163
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
164
|
+
expect(play).toHaveBeenCalledTimes(2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('stops fighting an element that re-pauses after every successful play', async () => {
|
|
168
|
+
setup({ kind: 'video' });
|
|
169
|
+
// Safari re-pauses the element right after play() resolves. This used to
|
|
170
|
+
// reset the attempt counter, so the loop spun with a zero delay forever.
|
|
171
|
+
play.mockImplementation(async () => {
|
|
172
|
+
setPaused(el, false);
|
|
173
|
+
el.dispatchEvent(new Event('playing'));
|
|
174
|
+
setPaused(el, true);
|
|
175
|
+
el.dispatchEvent(new Event('pause'));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
el.dispatchEvent(new Event('pause'));
|
|
179
|
+
for (let i = 0; i < 20; i++) {
|
|
180
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
expect(play).toHaveBeenCalledTimes(10);
|
|
184
|
+
expect(tracer.trace).toHaveBeenCalledWith('mediaPlayback.recover.giveUp', {
|
|
185
|
+
kind: 'video',
|
|
186
|
+
attempts: 10,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// External playback can still emit `playing` after the watchdog gave up
|
|
190
|
+
// (for example from a user gesture or another playback helper). If it
|
|
191
|
+
// re-pauses before settling, it must not revive the watchdog or trace a
|
|
192
|
+
// false recovery success.
|
|
193
|
+
setPaused(el, false);
|
|
194
|
+
el.dispatchEvent(new Event('playing'));
|
|
195
|
+
setPaused(el, true);
|
|
196
|
+
el.dispatchEvent(new Event('pause'));
|
|
197
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
198
|
+
|
|
199
|
+
expect(tracer.trace).not.toHaveBeenCalledWith(
|
|
200
|
+
'mediaPlayback.recover.success',
|
|
201
|
+
{
|
|
202
|
+
kind: 'video',
|
|
203
|
+
attempts: 10,
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// and it stays stopped, without tracing the pause storm
|
|
208
|
+
const traceMock = tracer.trace as unknown as MockInstance;
|
|
209
|
+
const tracesBefore = traceMock.mock.calls.length;
|
|
210
|
+
for (let i = 0; i < 100; i++) {
|
|
211
|
+
el.dispatchEvent(new Event('pause'));
|
|
212
|
+
}
|
|
213
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
214
|
+
|
|
215
|
+
expect(play).toHaveBeenCalledTimes(10);
|
|
216
|
+
expect(traceMock.mock.calls.length).toBe(tracesBefore);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('revives once playback genuinely holds for the settle window', async () => {
|
|
220
|
+
setup({ kind: 'video' });
|
|
221
|
+
play.mockImplementation(async () => {
|
|
222
|
+
setPaused(el, false);
|
|
223
|
+
el.dispatchEvent(new Event('playing'));
|
|
224
|
+
setPaused(el, true);
|
|
225
|
+
el.dispatchEvent(new Event('pause'));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
el.dispatchEvent(new Event('pause'));
|
|
229
|
+
for (let i = 0; i < 20; i++) {
|
|
230
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
231
|
+
}
|
|
232
|
+
expect(play).toHaveBeenCalledTimes(10); // gave up
|
|
233
|
+
|
|
234
|
+
play.mockResolvedValue(undefined);
|
|
235
|
+
setPaused(el, false);
|
|
236
|
+
el.dispatchEvent(new Event('playing'));
|
|
237
|
+
await vi.advanceTimersByTimeAsync(1500);
|
|
238
|
+
|
|
239
|
+
expect(tracer.trace).toHaveBeenCalledWith('mediaPlayback.recover.success', {
|
|
240
|
+
kind: 'video',
|
|
241
|
+
attempts: 10,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// recovery is live again: the attempt counter was reset, so a fresh pause
|
|
245
|
+
// gets an immediate attempt rather than being ignored
|
|
246
|
+
setPaused(el, true);
|
|
247
|
+
el.dispatchEvent(new Event('pause'));
|
|
248
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
249
|
+
expect(play).toHaveBeenCalledTimes(11);
|
|
250
|
+
});
|
|
251
|
+
|
|
123
252
|
it('does not attempt recovery when srcObject is null', async () => {
|
|
124
253
|
setup({ state: { srcObject: null } });
|
|
125
254
|
|
|
@@ -38,8 +38,7 @@ describe('ViewportTracker', () => {
|
|
|
38
38
|
|
|
39
39
|
it('updates participant viewportVisibilityState as visibility changes', () => {
|
|
40
40
|
let visibilityHandler:
|
|
41
|
-
|
|
42
|
-
| undefined;
|
|
41
|
+
((entry: IntersectionObserverEntry) => void) | undefined;
|
|
43
42
|
vi.spyOn(viewportTracker, 'observe').mockImplementation((_el, handler) => {
|
|
44
43
|
visibilityHandler = handler;
|
|
45
44
|
return vi.fn();
|
|
@@ -33,16 +33,12 @@ export type ClientEventStage =
|
|
|
33
33
|
| 'FirstAudioFrame';
|
|
34
34
|
|
|
35
35
|
export type MediaPermissionState =
|
|
36
|
-
| '
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
| 'NOT_INITIATED';
|
|
36
|
+
'INITIATED' | 'FAILED' | 'GRANTED' | 'NOT_INITIATED';
|
|
37
|
+
|
|
38
|
+
export type ReportedIceState = 'CONNECTED' | 'FAILED' | 'NOT_CONNECTED';
|
|
40
39
|
|
|
41
40
|
export type JoinReason =
|
|
42
|
-
|
|
43
|
-
| 'network-available'
|
|
44
|
-
| 'migration'
|
|
45
|
-
| 'full-rejoin';
|
|
41
|
+
'first-attempt' | 'network-available' | 'migration' | 'full-rejoin';
|
|
46
42
|
|
|
47
43
|
export type ClientEventStandardCode =
|
|
48
44
|
| 'CLIENT_ABORTED'
|
|
@@ -86,6 +82,7 @@ type PeerConnectionPairState = StagePairState & {
|
|
|
86
82
|
sfuId: string;
|
|
87
83
|
userSessionId: string;
|
|
88
84
|
wasPreviouslyConnected: boolean;
|
|
85
|
+
lastIceState?: RTCIceConnectionState;
|
|
89
86
|
};
|
|
90
87
|
|
|
91
88
|
const pcKey = (cid: string, role: ClientEventPeerConnection): string =>
|
|
@@ -356,20 +353,8 @@ export class ClientEventReporter {
|
|
|
356
353
|
this.failCoordinator(cid);
|
|
357
354
|
this.failWs(cid);
|
|
358
355
|
|
|
359
|
-
this.emitPeerConnectionFailure(
|
|
360
|
-
|
|
361
|
-
'publish',
|
|
362
|
-
code,
|
|
363
|
-
reason,
|
|
364
|
-
'NOT_CONNECTED',
|
|
365
|
-
);
|
|
366
|
-
this.emitPeerConnectionFailure(
|
|
367
|
-
cid,
|
|
368
|
-
'subscribe',
|
|
369
|
-
code,
|
|
370
|
-
reason,
|
|
371
|
-
'NOT_CONNECTED',
|
|
372
|
-
);
|
|
356
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason);
|
|
357
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
|
|
373
358
|
} catch (err) {
|
|
374
359
|
this.logger.warn('Failed to report abort', err);
|
|
375
360
|
}
|
|
@@ -384,7 +369,6 @@ export class ClientEventReporter {
|
|
|
384
369
|
role,
|
|
385
370
|
'CLIENT_ABORTED',
|
|
386
371
|
'superseded by a new join attempt',
|
|
387
|
-
'NOT_CONNECTED',
|
|
388
372
|
);
|
|
389
373
|
}
|
|
390
374
|
};
|
|
@@ -573,34 +557,35 @@ export class ClientEventReporter {
|
|
|
573
557
|
const role: ClientEventPeerConnection =
|
|
574
558
|
event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
575
559
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
560
|
+
const pair = this.peerConnectionPairs.get(pcKey(cid, role));
|
|
561
|
+
if (pair) pair.lastIceState = event.iceConnectionState;
|
|
562
|
+
|
|
563
|
+
if (event.stateType === 'ice') {
|
|
564
|
+
if (event.state === 'failed') {
|
|
565
|
+
this.emitPeerConnectionFailure(
|
|
566
|
+
cid,
|
|
567
|
+
role,
|
|
568
|
+
'ICE_CONNECTIVITY_FAILED',
|
|
569
|
+
'ICE connectivity checks failed',
|
|
570
|
+
);
|
|
571
|
+
}
|
|
584
572
|
return;
|
|
585
573
|
}
|
|
586
574
|
|
|
587
|
-
if (event.
|
|
575
|
+
if (event.state === 'failed') {
|
|
588
576
|
this.emitPeerConnectionFailure(
|
|
589
577
|
cid,
|
|
590
578
|
role,
|
|
591
579
|
'DTLS_CONNECTIVITY_FAILED',
|
|
592
580
|
'DTLS connectivity checks failed',
|
|
593
|
-
'CONNECTED',
|
|
594
581
|
);
|
|
595
582
|
return;
|
|
596
583
|
}
|
|
597
584
|
|
|
598
|
-
if (event.stateType !== 'peerConnection') return;
|
|
599
|
-
|
|
600
585
|
switch (event.state) {
|
|
601
586
|
case 'connecting':
|
|
602
|
-
if (
|
|
603
|
-
this.openPeerConnectionPair(cid, role);
|
|
587
|
+
if (pair) return;
|
|
588
|
+
this.openPeerConnectionPair(cid, role, event.iceConnectionState);
|
|
604
589
|
break;
|
|
605
590
|
case 'connected':
|
|
606
591
|
this.emitPeerConnectionSuccess(cid, role);
|
|
@@ -614,6 +599,7 @@ export class ClientEventReporter {
|
|
|
614
599
|
private openPeerConnectionPair = (
|
|
615
600
|
cid: string,
|
|
616
601
|
role: ClientEventPeerConnection,
|
|
602
|
+
iceConnectionState: RTCIceConnectionState,
|
|
617
603
|
) => {
|
|
618
604
|
const key = pcKey(cid, role);
|
|
619
605
|
const pair: PeerConnectionPairState = {
|
|
@@ -624,6 +610,7 @@ export class ClientEventReporter {
|
|
|
624
610
|
sfuId: this.getSfuId(cid),
|
|
625
611
|
userSessionId: this.getUserSessionId(cid),
|
|
626
612
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
613
|
+
lastIceState: iceConnectionState,
|
|
627
614
|
};
|
|
628
615
|
this.peerConnectionPairs.set(key, pair);
|
|
629
616
|
|
|
@@ -670,7 +657,6 @@ export class ClientEventReporter {
|
|
|
670
657
|
role: ClientEventPeerConnection,
|
|
671
658
|
code: ClientEventStandardCode,
|
|
672
659
|
reason: string,
|
|
673
|
-
iceState: 'CONNECTED' | 'FAILED' | 'NOT_CONNECTED',
|
|
674
660
|
) => {
|
|
675
661
|
const key = pcKey(cid, role);
|
|
676
662
|
const pair = this.peerConnectionPairs.get(key);
|
|
@@ -689,7 +675,7 @@ export class ClientEventReporter {
|
|
|
689
675
|
outcome: 'failure',
|
|
690
676
|
retry_count_attempt: 0,
|
|
691
677
|
elapsed_time: Date.now() - pair.startedAt,
|
|
692
|
-
ice_state:
|
|
678
|
+
ice_state: toReportedIceState(pair.lastIceState),
|
|
693
679
|
retry_failure_reason: reason,
|
|
694
680
|
retry_failure_code: code,
|
|
695
681
|
});
|
|
@@ -782,6 +768,20 @@ export class ClientEventReporter {
|
|
|
782
768
|
};
|
|
783
769
|
}
|
|
784
770
|
|
|
771
|
+
const toReportedIceState = (
|
|
772
|
+
state: RTCIceConnectionState | undefined,
|
|
773
|
+
): ReportedIceState => {
|
|
774
|
+
switch (state) {
|
|
775
|
+
case 'connected':
|
|
776
|
+
case 'completed':
|
|
777
|
+
return 'CONNECTED';
|
|
778
|
+
case 'failed':
|
|
779
|
+
return 'FAILED';
|
|
780
|
+
default:
|
|
781
|
+
return 'NOT_CONNECTED';
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
785
|
const readPermissionStatus = (
|
|
786
786
|
permission: BrowserPermission,
|
|
787
787
|
): MediaPermissionState => {
|
|
@@ -7,6 +7,7 @@ import { ErrorFromResponse } from '../../coordinator/connection/types';
|
|
|
7
7
|
import { SfuTimeoutError } from '../../errors';
|
|
8
8
|
import type { AxiosResponse } from 'axios';
|
|
9
9
|
import { PeerType, TrackType } from '../../gen/video/sfu/models/models';
|
|
10
|
+
import type { PeerConnectionStateChangeEvent } from '../../rtc';
|
|
10
11
|
|
|
11
12
|
vi.mock('../../devices', () => ({
|
|
12
13
|
getAudioBrowserPermission: () => ({ asStateObservable: () => of('granted') }),
|
|
@@ -15,6 +16,27 @@ vi.mock('../../devices', () => ({
|
|
|
15
16
|
|
|
16
17
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
17
18
|
|
|
19
|
+
const peerConnectionEvent = (
|
|
20
|
+
peerType: PeerType,
|
|
21
|
+
state: RTCPeerConnectionState,
|
|
22
|
+
iceConnectionState: RTCIceConnectionState,
|
|
23
|
+
): PeerConnectionStateChangeEvent => ({
|
|
24
|
+
peerType,
|
|
25
|
+
stateType: 'peerConnection',
|
|
26
|
+
state,
|
|
27
|
+
iceConnectionState,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const iceEvent = (
|
|
31
|
+
peerType: PeerType,
|
|
32
|
+
state: RTCIceConnectionState,
|
|
33
|
+
): PeerConnectionStateChangeEvent => ({
|
|
34
|
+
peerType,
|
|
35
|
+
stateType: 'ice',
|
|
36
|
+
state,
|
|
37
|
+
iceConnectionState: state,
|
|
38
|
+
});
|
|
39
|
+
|
|
18
40
|
describe('ClientEventReporter', () => {
|
|
19
41
|
const cid = 'default:call-1';
|
|
20
42
|
let doAxiosRequest: ReturnType<typeof vi.fn>;
|
|
@@ -24,6 +46,21 @@ describe('ClientEventReporter', () => {
|
|
|
24
46
|
const postedEvents = (): Array<Record<string, any>> =>
|
|
25
47
|
doAxiosRequest.mock.calls.map((call) => call[2].events[0]);
|
|
26
48
|
|
|
49
|
+
const reportPeerConnectionState = (
|
|
50
|
+
peerType: PeerType,
|
|
51
|
+
state: RTCPeerConnectionState,
|
|
52
|
+
iceConnectionState: RTCIceConnectionState,
|
|
53
|
+
) => {
|
|
54
|
+
reporter.onPeerConnectionStateChange(
|
|
55
|
+
cid,
|
|
56
|
+
peerConnectionEvent(peerType, state, iceConnectionState),
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const reportIceState = (peerType: PeerType, state: RTCIceConnectionState) => {
|
|
61
|
+
reporter.onPeerConnectionStateChange(cid, iceEvent(peerType, state));
|
|
62
|
+
};
|
|
63
|
+
|
|
27
64
|
beforeEach(() => {
|
|
28
65
|
doAxiosRequest = vi.fn().mockResolvedValue({});
|
|
29
66
|
const streamClient = fromPartial<StreamClient>({
|
|
@@ -465,16 +502,16 @@ describe('ClientEventReporter', () => {
|
|
|
465
502
|
|
|
466
503
|
it('tracks a publisher peer connection connect', async () => {
|
|
467
504
|
reporter.startCorrelation(cid, 'first-attempt');
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
505
|
+
reportPeerConnectionState(
|
|
506
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
507
|
+
'connecting',
|
|
508
|
+
'checking',
|
|
509
|
+
);
|
|
510
|
+
reportPeerConnectionState(
|
|
511
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
512
|
+
'connected',
|
|
513
|
+
'connected',
|
|
514
|
+
);
|
|
478
515
|
await flush();
|
|
479
516
|
|
|
480
517
|
const events = postedEvents().filter(
|
|
@@ -497,16 +534,8 @@ describe('ClientEventReporter', () => {
|
|
|
497
534
|
|
|
498
535
|
it('reports an ICE failure on the peer connection', async () => {
|
|
499
536
|
reporter.startCorrelation(cid, 'first-attempt');
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
stateType: 'peerConnection',
|
|
503
|
-
state: 'connecting',
|
|
504
|
-
});
|
|
505
|
-
reporter.onPeerConnectionStateChange(cid, {
|
|
506
|
-
peerType: PeerType.SUBSCRIBER,
|
|
507
|
-
stateType: 'ice',
|
|
508
|
-
state: 'failed',
|
|
509
|
-
});
|
|
537
|
+
reportPeerConnectionState(PeerType.SUBSCRIBER, 'connecting', 'checking');
|
|
538
|
+
reportIceState(PeerType.SUBSCRIBER, 'failed');
|
|
510
539
|
await flush();
|
|
511
540
|
|
|
512
541
|
const completed = postedEvents().filter(
|
|
@@ -522,25 +551,111 @@ describe('ClientEventReporter', () => {
|
|
|
522
551
|
});
|
|
523
552
|
});
|
|
524
553
|
|
|
525
|
-
it('
|
|
554
|
+
it('reports a DTLS failure with the real ICE state (connected)', async () => {
|
|
526
555
|
reporter.startCorrelation(cid, 'first-attempt');
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
556
|
+
reportPeerConnectionState(
|
|
557
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
558
|
+
'connecting',
|
|
559
|
+
'checking',
|
|
560
|
+
);
|
|
561
|
+
reportIceState(PeerType.PUBLISHER_UNSPECIFIED, 'connected');
|
|
562
|
+
// Aggregate connection fails while ICE stays connected -> genuine DTLS.
|
|
563
|
+
reportPeerConnectionState(
|
|
564
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
565
|
+
'failed',
|
|
566
|
+
'connected',
|
|
567
|
+
);
|
|
568
|
+
await flush();
|
|
569
|
+
|
|
570
|
+
const completed = postedEvents().filter(
|
|
571
|
+
(e) =>
|
|
572
|
+
e.stage === 'PeerConnectionConnect' && e.event_type === 'completed',
|
|
573
|
+
);
|
|
574
|
+
expect(completed).toHaveLength(1);
|
|
575
|
+
expect(completed[0]).toMatchObject({
|
|
576
|
+
outcome: 'failure',
|
|
577
|
+
peer_connection: 'publish',
|
|
578
|
+
retry_failure_code: 'DTLS_CONNECTIVITY_FAILED',
|
|
579
|
+
ice_state: 'CONNECTED',
|
|
531
580
|
});
|
|
581
|
+
});
|
|
532
582
|
|
|
533
|
-
|
|
534
|
-
reporter.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
583
|
+
it('reports a DTLS failure with the real ICE state (never connected)', async () => {
|
|
584
|
+
reporter.startCorrelation(cid, 'first-attempt');
|
|
585
|
+
reportPeerConnectionState(
|
|
586
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
587
|
+
'connecting',
|
|
588
|
+
'checking',
|
|
589
|
+
);
|
|
590
|
+
// ICE is still `checking` when the aggregate peer-connection state fails:
|
|
591
|
+
// the reported ice_state must be NOT_CONNECTED, not an assumed CONNECTED.
|
|
592
|
+
reportPeerConnectionState(
|
|
593
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
594
|
+
'failed',
|
|
595
|
+
'checking',
|
|
596
|
+
);
|
|
597
|
+
await flush();
|
|
598
|
+
|
|
599
|
+
const completed = postedEvents().filter(
|
|
600
|
+
(e) =>
|
|
601
|
+
e.stage === 'PeerConnectionConnect' && e.event_type === 'completed',
|
|
602
|
+
);
|
|
603
|
+
expect(completed).toHaveLength(1);
|
|
604
|
+
expect(completed[0]).toMatchObject({
|
|
605
|
+
outcome: 'failure',
|
|
606
|
+
peer_connection: 'publish',
|
|
607
|
+
retry_failure_code: 'DTLS_CONNECTIVITY_FAILED',
|
|
608
|
+
ice_state: 'NOT_CONNECTED',
|
|
538
609
|
});
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('reports the ICE state captured when the pair opened, with no ICE event', async () => {
|
|
613
|
+
reporter.startCorrelation(cid, 'first-attempt');
|
|
614
|
+
// The pair opens with ICE already connected (carried on the connecting
|
|
615
|
+
// event); no separate ICE event is ever delivered.
|
|
616
|
+
reportPeerConnectionState(
|
|
617
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
618
|
+
'connecting',
|
|
619
|
+
'connected',
|
|
620
|
+
);
|
|
621
|
+
reportPeerConnectionState(
|
|
622
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
623
|
+
'failed',
|
|
624
|
+
'connected',
|
|
625
|
+
);
|
|
626
|
+
await flush();
|
|
627
|
+
|
|
628
|
+
const completed = postedEvents().filter(
|
|
629
|
+
(e) =>
|
|
630
|
+
e.stage === 'PeerConnectionConnect' && e.event_type === 'completed',
|
|
631
|
+
);
|
|
632
|
+
expect(completed).toHaveLength(1);
|
|
633
|
+
expect(completed[0]).toMatchObject({
|
|
634
|
+
outcome: 'failure',
|
|
635
|
+
retry_failure_code: 'DTLS_CONNECTIVITY_FAILED',
|
|
636
|
+
ice_state: 'CONNECTED',
|
|
543
637
|
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('opens a fresh peer connection pair after a new correlation', async () => {
|
|
641
|
+
reporter.startCorrelation(cid, 'first-attempt');
|
|
642
|
+
reportPeerConnectionState(
|
|
643
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
644
|
+
'connecting',
|
|
645
|
+
'checking',
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
reporter.startCorrelation(cid, 'full-rejoin');
|
|
649
|
+
reportPeerConnectionState(
|
|
650
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
651
|
+
'connecting',
|
|
652
|
+
'checking',
|
|
653
|
+
);
|
|
654
|
+
reportPeerConnectionState(
|
|
655
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
656
|
+
'connected',
|
|
657
|
+
'connected',
|
|
658
|
+
);
|
|
544
659
|
await flush();
|
|
545
660
|
|
|
546
661
|
const pc = postedEvents().filter(
|
|
@@ -566,16 +681,16 @@ describe('ClientEventReporter', () => {
|
|
|
566
681
|
it('marks a peer connection as previously connected on reconnect', async () => {
|
|
567
682
|
reporter.startCorrelation(cid, 'first-attempt');
|
|
568
683
|
const connect = () => {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
684
|
+
reportPeerConnectionState(
|
|
685
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
686
|
+
'connecting',
|
|
687
|
+
'checking',
|
|
688
|
+
);
|
|
689
|
+
reportPeerConnectionState(
|
|
690
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
691
|
+
'connected',
|
|
692
|
+
'connected',
|
|
693
|
+
);
|
|
579
694
|
};
|
|
580
695
|
connect();
|
|
581
696
|
connect();
|