@stream-io/video-client 2.0.0-beta.0 → 2.0.0-beta.1
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 +18 -0
- package/dist/index.browser.es.js +1126 -791
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1126 -791
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1126 -791
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +37 -11
- package/dist/src/StreamVideoClient.d.ts +15 -21
- package/dist/src/coordinator/connection/types.d.ts +16 -0
- package/dist/src/events/call.d.ts +0 -11
- package/dist/src/gen/coordinator/index.d.ts +73 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
- package/dist/src/ringing/RingStatePoller.d.ts +46 -0
- package/dist/src/ringing/RingTimeout.d.ts +27 -0
- package/dist/src/ringing/index.d.ts +4 -0
- package/dist/src/ringing/reconcileRingState.d.ts +18 -0
- package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
- package/dist/src/rtc/Publisher.d.ts +18 -0
- package/dist/src/rtc/TransceiverCache.d.ts +10 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
- package/dist/src/stats/utils.d.ts +6 -0
- package/dist/src/store/CallState.d.ts +118 -267
- package/dist/src/store/ClientState.d.ts +62 -0
- package/dist/src/store/index.d.ts +1 -1
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/types.d.ts +22 -0
- package/dist/src/types.d.ts +33 -4
- package/index.ts +1 -0
- package/package.json +2 -2
- package/src/Call.ts +285 -125
- package/src/StreamSfuClient.ts +6 -2
- package/src/StreamVideoClient.ts +40 -47
- package/src/__tests__/Call.autodrop.test.ts +118 -66
- package/src/__tests__/Call.getRingState.test.ts +87 -0
- package/src/__tests__/Call.lifecycle.test.ts +499 -6
- package/src/__tests__/Call.publishing.test.ts +2 -2
- package/src/__tests__/Call.ringSettled.test.ts +71 -0
- package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
- package/src/__tests__/StreamSfuClient.test.ts +48 -0
- package/src/__tests__/StreamVideoClient.disconnect.test.ts +62 -0
- package/src/coordinator/connection/types.ts +19 -0
- package/src/devices/__tests__/CameraManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -2
- package/src/devices/__tests__/ScreenShareManager.test.ts +3 -3
- package/src/devices/__tests__/SpeakerManager.test.ts +4 -4
- package/src/events/__tests__/call.test.ts +5 -220
- package/src/events/call.ts +1 -73
- package/src/events/callEventHandlers.ts +12 -23
- package/src/gen/coordinator/index.ts +67 -0
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +2 -2
- package/src/helpers/__tests__/DynascaleManager.test.ts +2 -2
- package/src/helpers/__tests__/ViewportTracker.test.ts +2 -2
- package/src/reporting/ClientEventReporter.ts +30 -4
- package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
- package/src/ringing/RingStatePoller.ts +177 -0
- package/src/ringing/RingTimeout.ts +90 -0
- package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
- package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
- package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
- package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
- package/src/ringing/index.ts +4 -0
- package/src/ringing/reconcileRingState.ts +108 -0
- package/src/ringing/resolveOwnRingOutcome.ts +58 -0
- package/src/rtc/Publisher.ts +66 -12
- package/src/rtc/TransceiverCache.ts +17 -0
- package/src/rtc/__tests__/Call.reconnect.test.ts +5 -5
- package/src/rtc/__tests__/Publisher.test.ts +209 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
- package/src/rtc/e2ee/E2EEManager.ts +10 -1
- package/src/rtc/e2ee/EncryptionManager.ts +1 -1
- package/src/rtc/e2ee/SPEC.md +1 -1
- package/src/stats/__tests__/utils.test.ts +33 -0
- package/src/stats/utils.ts +15 -0
- package/src/store/CallState.ts +305 -550
- package/src/store/ClientState.ts +101 -0
- package/src/store/__tests__/CallState.test.ts +60 -1
- package/src/store/__tests__/ClientState.test.ts +35 -0
- package/src/store/__tests__/rxUtils.test.ts +67 -1
- package/src/store/index.ts +1 -1
- package/src/store/rxUtils.ts +8 -4
- package/src/store/subjects.ts +30 -0
- package/src/store/types.ts +45 -0
- package/src/types.ts +40 -4
- package/dist/src/store/stateStore.d.ts +0 -84
- package/src/store/stateStore.ts +0 -156
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { Call } from '../Call';
|
|
9
|
+
import { CallingState } from '../store';
|
|
10
|
+
import { StreamClient } from '../coordinator/connection/client';
|
|
11
|
+
import { ClientEventReporter } from '../reporting';
|
|
12
|
+
import { generateUUIDv4 } from '../coordinator/connection/utils';
|
|
13
|
+
import { ClientState } from '../store';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The core's whole part in the React Native ringing lifecycle: await RN's
|
|
17
|
+
* preparation inside the join, tell RN when a ringing join failed terminally or
|
|
18
|
+
* the call ended, and stop a join that a `leave()` overtook. What preparation
|
|
19
|
+
* and release actually do is RN's, and is tested there.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const createCall = (ringing: boolean) => {
|
|
23
|
+
const streamClient = new StreamClient('abc');
|
|
24
|
+
return new Call({
|
|
25
|
+
type: 'test',
|
|
26
|
+
id: generateUUIDv4(),
|
|
27
|
+
ringing,
|
|
28
|
+
streamClient,
|
|
29
|
+
clientEventReporter: new ClientEventReporter({
|
|
30
|
+
streamClient,
|
|
31
|
+
enabled: false,
|
|
32
|
+
}),
|
|
33
|
+
clientState: new ClientState(),
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const install = (overrides: Record<string, unknown> = {}) => {
|
|
38
|
+
const beforeJoin = vi.fn(() => Promise.resolve());
|
|
39
|
+
const onJoinFailed = vi.fn(() => Promise.resolve());
|
|
40
|
+
const onLeave = vi.fn();
|
|
41
|
+
const callingX = { joinCall: vi.fn(), endCall: vi.fn() };
|
|
42
|
+
globalThis.streamRNVideoSDK = {
|
|
43
|
+
ringingCallLifecycle: { beforeJoin, onJoinFailed, onLeave },
|
|
44
|
+
callingX,
|
|
45
|
+
// `leave()` reaches this unconditionally; the globals object is all-or-nothing
|
|
46
|
+
callManager: { setup: vi.fn(), start: vi.fn(), stop: vi.fn() },
|
|
47
|
+
...overrides,
|
|
48
|
+
} as any;
|
|
49
|
+
return { beforeJoin, onJoinFailed, onLeave, callingX };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe('ringing call lifecycle integration', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
globalThis.streamRNVideoSDK = undefined;
|
|
55
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
|
56
|
+
headers: { get: () => 'AMS1-P2' },
|
|
57
|
+
} as Response);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
globalThis.streamRNVideoSDK = undefined;
|
|
62
|
+
vi.restoreAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('prepares a ringing call before it joins', async () => {
|
|
66
|
+
const { beforeJoin, callingX } = install();
|
|
67
|
+
const call = createCall(true);
|
|
68
|
+
const doJoin = vi.spyOn(call as any, 'doJoin').mockResolvedValue(undefined);
|
|
69
|
+
|
|
70
|
+
await call.join();
|
|
71
|
+
|
|
72
|
+
expect(beforeJoin).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(beforeJoin).toHaveBeenCalledBefore(callingX.joinCall);
|
|
74
|
+
expect(doJoin).toHaveBeenCalledTimes(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('leaves ordinary calls to join themselves', async () => {
|
|
78
|
+
const { beforeJoin, onJoinFailed } = install();
|
|
79
|
+
const call = createCall(false);
|
|
80
|
+
vi.spyOn(call as any, 'doJoin').mockResolvedValue(undefined);
|
|
81
|
+
|
|
82
|
+
await call.join();
|
|
83
|
+
|
|
84
|
+
expect(beforeJoin).not.toHaveBeenCalled();
|
|
85
|
+
expect(onJoinFailed).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('does not join when preparation fails, and reports the failure', async () => {
|
|
89
|
+
const { beforeJoin, onJoinFailed } = install();
|
|
90
|
+
beforeJoin.mockRejectedValue(new Error('no key'));
|
|
91
|
+
const call = createCall(true);
|
|
92
|
+
const doJoin = vi.spyOn(call as any, 'doJoin').mockResolvedValue(undefined);
|
|
93
|
+
|
|
94
|
+
await expect(call.join()).rejects.toThrow('no key');
|
|
95
|
+
|
|
96
|
+
expect(doJoin).not.toHaveBeenCalled();
|
|
97
|
+
expect(onJoinFailed).toHaveBeenCalledWith(call);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('reports a terminal join failure, keeping the original error', async () => {
|
|
101
|
+
const { onJoinFailed } = install();
|
|
102
|
+
const call = createCall(true);
|
|
103
|
+
vi.spyOn(call as any, 'doJoin').mockRejectedValue(new Error('sfu down'));
|
|
104
|
+
|
|
105
|
+
await expect(call.join({ maxJoinRetries: 1 })).rejects.toThrow('sfu down');
|
|
106
|
+
|
|
107
|
+
expect(onJoinFailed).toHaveBeenCalledWith(call);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('refuses a duplicate join on a live call without tearing it down', async () => {
|
|
111
|
+
const { beforeJoin, onJoinFailed, onLeave } = install();
|
|
112
|
+
const call = createCall(true);
|
|
113
|
+
vi.spyOn(call as any, 'doJoin').mockImplementation(async () => {
|
|
114
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await call.join();
|
|
118
|
+
await expect(call.join()).rejects.toThrow('Illegal State');
|
|
119
|
+
|
|
120
|
+
expect(beforeJoin).toHaveBeenCalledTimes(1);
|
|
121
|
+
// the guard sits ahead of the failure boundary, so nothing is released
|
|
122
|
+
expect(onJoinFailed).not.toHaveBeenCalled();
|
|
123
|
+
expect(onLeave).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('tells the owner when a ringing call ends', async () => {
|
|
127
|
+
const { onLeave } = install();
|
|
128
|
+
const call = createCall(true);
|
|
129
|
+
|
|
130
|
+
await call.leave();
|
|
131
|
+
|
|
132
|
+
expect(onLeave).toHaveBeenCalledWith(call);
|
|
133
|
+
expect(call.state.callingState).toBe(CallingState.LEFT);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not tell the owner about an ordinary call', async () => {
|
|
137
|
+
const { onLeave } = install();
|
|
138
|
+
|
|
139
|
+
await createCall(false).leave();
|
|
140
|
+
|
|
141
|
+
expect(onLeave).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('keeps an app-owned manager across leave, as setE2EEManager promises', async () => {
|
|
145
|
+
install();
|
|
146
|
+
const call = createCall(true);
|
|
147
|
+
const manager = { encrypt: vi.fn(), decrypt: vi.fn() } as any;
|
|
148
|
+
call.setE2EEManager(manager);
|
|
149
|
+
|
|
150
|
+
await call.leave();
|
|
151
|
+
|
|
152
|
+
expect(call.e2eeManager).toBe(manager);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('abandons a join that a leave overtook during native registration', async () => {
|
|
156
|
+
let finishRegistration: () => void = () => {};
|
|
157
|
+
const { callingX, onLeave, onJoinFailed } = install();
|
|
158
|
+
callingX.joinCall.mockImplementation(
|
|
159
|
+
() => new Promise<void>((resolve) => (finishRegistration = resolve)),
|
|
160
|
+
);
|
|
161
|
+
const call = createCall(true);
|
|
162
|
+
const doJoin = vi.spyOn(call as any, 'doJoin').mockResolvedValue(undefined);
|
|
163
|
+
|
|
164
|
+
const joining = call.join();
|
|
165
|
+
await vi.waitFor(() => expect(callingX.joinCall).toHaveBeenCalled());
|
|
166
|
+
await call.leave({ reject: false });
|
|
167
|
+
finishRegistration();
|
|
168
|
+
|
|
169
|
+
// A superseding leave is not a join failure: the join settles quietly rather
|
|
170
|
+
// than rejecting, and the error path stays shut.
|
|
171
|
+
await expect(joining).resolves.toBeUndefined();
|
|
172
|
+
expect(onJoinFailed).not.toHaveBeenCalled();
|
|
173
|
+
expect(callingX.endCall).not.toHaveBeenCalledWith(call, 'error');
|
|
174
|
+
// Release still happens - `leave()` itself fires it, which is why the join
|
|
175
|
+
// does not need to.
|
|
176
|
+
expect(onLeave).toHaveBeenCalledTimes(1);
|
|
177
|
+
// `doJoin` captures the generation itself, so this check has to be here
|
|
178
|
+
expect(doJoin).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('does not resume the retry loop after a leave during backoff', async () => {
|
|
182
|
+
vi.useFakeTimers();
|
|
183
|
+
try {
|
|
184
|
+
install();
|
|
185
|
+
const call = createCall(true);
|
|
186
|
+
const doJoin = vi
|
|
187
|
+
.spyOn(call as any, 'doJoin')
|
|
188
|
+
.mockRejectedValue(new Error('recoverable'));
|
|
189
|
+
|
|
190
|
+
const joining = call.join().catch(() => {});
|
|
191
|
+
await vi.waitFor(() => expect(doJoin).toHaveBeenCalledTimes(1));
|
|
192
|
+
await call.leave({ reject: false });
|
|
193
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
194
|
+
await joining;
|
|
195
|
+
|
|
196
|
+
expect(doJoin).toHaveBeenCalledTimes(1);
|
|
197
|
+
} finally {
|
|
198
|
+
vi.useRealTimers();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('prepares an outgoing call that this join makes ringing', async () => {
|
|
203
|
+
const { beforeJoin } = install();
|
|
204
|
+
// a fresh instance, not ringing until its first join says so
|
|
205
|
+
const call = createCall(false);
|
|
206
|
+
vi.spyOn(call as any, 'doJoin').mockResolvedValue(undefined);
|
|
207
|
+
|
|
208
|
+
await call.join({ ring: true });
|
|
209
|
+
|
|
210
|
+
// reading `call.ringing` before applying `options.ring` would skip
|
|
211
|
+
// preparation entirely on this path
|
|
212
|
+
expect(beforeJoin).toHaveBeenCalledTimes(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('stops a join that a leave overtook during preparation', async () => {
|
|
216
|
+
let releaseHook: () => void = () => {};
|
|
217
|
+
const { beforeJoin, onLeave, onJoinFailed } = install();
|
|
218
|
+
beforeJoin.mockImplementation(
|
|
219
|
+
() => new Promise<void>((resolve) => (releaseHook = resolve)),
|
|
220
|
+
);
|
|
221
|
+
const call = createCall(true);
|
|
222
|
+
const doJoin = vi.spyOn(call as any, 'doJoin').mockResolvedValue(undefined);
|
|
223
|
+
|
|
224
|
+
const joining = call.join();
|
|
225
|
+
await vi.waitFor(() => expect(beforeJoin).toHaveBeenCalled());
|
|
226
|
+
await call.leave({ reject: false });
|
|
227
|
+
releaseHook();
|
|
228
|
+
|
|
229
|
+
await expect(joining).resolves.toBeUndefined();
|
|
230
|
+
expect(onJoinFailed).not.toHaveBeenCalled();
|
|
231
|
+
// Whatever `beforeJoin` installed is released exactly once, by the `leave()`
|
|
232
|
+
// that overtook the join rather than by the join itself.
|
|
233
|
+
expect(onLeave).toHaveBeenCalledTimes(1);
|
|
234
|
+
expect(doJoin).not.toHaveBeenCalled();
|
|
235
|
+
expect(call.state.callingState).toBe(CallingState.LEFT);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('is inert when no React Native globals are registered', async () => {
|
|
239
|
+
const call = createCall(true);
|
|
240
|
+
await expect(call.leave()).resolves.not.toThrow();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -3,6 +3,8 @@ import { StreamSfuClient } from '../StreamSfuClient';
|
|
|
3
3
|
import { Dispatcher } from '../rtc';
|
|
4
4
|
import { StreamClient } from '../coordinator/connection/client';
|
|
5
5
|
import { getTimers } from '../timers';
|
|
6
|
+
import { getSdkInfo, setSdkInfo } from '../helpers/client-details';
|
|
7
|
+
import { SdkType } from '../gen/video/sfu/models/models';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Minimal `WebSocket` stub used to drive `StreamSfuClient.close()` while the
|
|
@@ -431,3 +433,49 @@ describe('StreamSfuClient.leaveAndClose()', () => {
|
|
|
431
433
|
expect(notifyLeaveSpy).not.toHaveBeenCalled();
|
|
432
434
|
});
|
|
433
435
|
});
|
|
436
|
+
|
|
437
|
+
describe('StreamSfuClient RPC headers', () => {
|
|
438
|
+
beforeEach(() => {
|
|
439
|
+
CapturingWebSocket.instances = [];
|
|
440
|
+
vi.stubGlobal('WebSocket', CapturingWebSocket);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
afterEach(() => {
|
|
444
|
+
vi.unstubAllGlobals();
|
|
445
|
+
vi.restoreAllMocks();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('identifies the SDK and its version in every RPC request', async () => {
|
|
449
|
+
const sdkInfo = getSdkInfo();
|
|
450
|
+
setSdkInfo({
|
|
451
|
+
type: SdkType.REACT,
|
|
452
|
+
major: '1',
|
|
453
|
+
minor: '2',
|
|
454
|
+
patch: '3',
|
|
455
|
+
});
|
|
456
|
+
const fetchMock = vi
|
|
457
|
+
.fn()
|
|
458
|
+
.mockResolvedValue(
|
|
459
|
+
new Response('{}', { headers: { 'content-type': 'application/json' } }),
|
|
460
|
+
);
|
|
461
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const sfuClient = buildSfuClient();
|
|
465
|
+
(
|
|
466
|
+
sfuClient as unknown as {
|
|
467
|
+
joinResponseTask: { resolve: (v: unknown) => void };
|
|
468
|
+
}
|
|
469
|
+
).joinResponseTask.resolve({});
|
|
470
|
+
|
|
471
|
+
await sfuClient.startNoiseCancellation();
|
|
472
|
+
|
|
473
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
474
|
+
expect(new Headers(init.headers).get('x-stream-client')).toBe(
|
|
475
|
+
'stream-video-react-v1.2.3',
|
|
476
|
+
);
|
|
477
|
+
} finally {
|
|
478
|
+
setSdkInfo(sdkInfo!);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { StreamVideoClient } from '../StreamVideoClient';
|
|
4
|
+
import { CallingState } from '../store';
|
|
5
|
+
|
|
6
|
+
const fakeCall = (cid: string, callingState: CallingState) => ({
|
|
7
|
+
cid,
|
|
8
|
+
state: { callingState },
|
|
9
|
+
leave: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
watching: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('disconnectUser leaves calls', () => {
|
|
14
|
+
it('leaves non-LEFT calls, before closing the connection', async () => {
|
|
15
|
+
const client = new StreamVideoClient('abc');
|
|
16
|
+
const order: string[] = [];
|
|
17
|
+
|
|
18
|
+
const active = fakeCall('default:active', CallingState.JOINED);
|
|
19
|
+
const left = fakeCall('default:left', CallingState.LEFT);
|
|
20
|
+
active.leave.mockImplementation(async () => {
|
|
21
|
+
order.push('leave:active');
|
|
22
|
+
});
|
|
23
|
+
left.leave.mockImplementation(async () => {
|
|
24
|
+
order.push('leave:left');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
client.state.setCalls([active, left] as never);
|
|
28
|
+
|
|
29
|
+
// @ts-expect-error faking the minimum the guard needs
|
|
30
|
+
client.streamClient.user = { id: 'jane' };
|
|
31
|
+
vi.spyOn(client.streamClient, 'disconnectUser').mockImplementation(
|
|
32
|
+
async () => {
|
|
33
|
+
order.push('ws:close');
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await client.disconnectUser();
|
|
38
|
+
|
|
39
|
+
expect(active.leave).toHaveBeenCalledWith({
|
|
40
|
+
message: 'client.disconnectUser() called',
|
|
41
|
+
});
|
|
42
|
+
expect(left.leave).not.toHaveBeenCalled();
|
|
43
|
+
expect(order).toEqual(['leave:active', 'ws:close']);
|
|
44
|
+
expect(client.state.connectedUser).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('a failing leave does not block the disconnect', async () => {
|
|
48
|
+
const client = new StreamVideoClient('abc');
|
|
49
|
+
const boom = fakeCall('default:boom', CallingState.JOINED);
|
|
50
|
+
boom.leave.mockRejectedValue(new Error('nope'));
|
|
51
|
+
client.state.setCalls([boom] as never);
|
|
52
|
+
|
|
53
|
+
// @ts-expect-error faking the minimum the guard needs
|
|
54
|
+
client.streamClient.user = { id: 'jane' };
|
|
55
|
+
vi.spyOn(client.streamClient, 'disconnectUser').mockResolvedValue(
|
|
56
|
+
undefined as never,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await expect(client.disconnectUser()).resolves.toBeUndefined();
|
|
60
|
+
expect(boom.leave).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -200,6 +200,18 @@ export type Logger = (
|
|
|
200
200
|
...args: unknown[]
|
|
201
201
|
) => void;
|
|
202
202
|
|
|
203
|
+
export type RingStatePollingOptions = {
|
|
204
|
+
/**
|
|
205
|
+
* Quiet time after the ring starts, before the first poll. Defaults to 15_000.
|
|
206
|
+
*/
|
|
207
|
+
startAfterMs?: number;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The interval between polls. Defaults to 5_000.
|
|
211
|
+
*/
|
|
212
|
+
intervalMs?: number;
|
|
213
|
+
};
|
|
214
|
+
|
|
203
215
|
export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
204
216
|
/**
|
|
205
217
|
* Used to disable warnings that are triggered by using connectUser or connectAnonymousUser server-side.
|
|
@@ -305,6 +317,13 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
305
317
|
*/
|
|
306
318
|
rejectCallWhenBusy?: boolean;
|
|
307
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Caller-side polling for the ring outcome, used when the `call.accepted`,
|
|
322
|
+
* `call.rejected` or `call.missed` event never arrives. Enabled by default;
|
|
323
|
+
* set to `false` to disable, or pass an object to tune the timings.
|
|
324
|
+
*/
|
|
325
|
+
ringStatePolling?: false | RingStatePollingOptions;
|
|
326
|
+
|
|
308
327
|
/**
|
|
309
328
|
* Device persistence preference options (web only).
|
|
310
329
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Call } from '../../Call';
|
|
2
2
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
3
3
|
import { ClientEventReporter } from '../../reporting';
|
|
4
|
-
import { CallingState,
|
|
4
|
+
import { CallingState, ClientState } from '../../store';
|
|
5
5
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
|
7
7
|
import { fromPartial } from '@total-typescript/shoehorn';
|
|
@@ -89,7 +89,7 @@ describe('CameraManager', () => {
|
|
|
89
89
|
type: '',
|
|
90
90
|
streamClient,
|
|
91
91
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
92
|
-
|
|
92
|
+
clientState: new ClientState(),
|
|
93
93
|
});
|
|
94
94
|
manager = new CameraManager(call, devicePersistence);
|
|
95
95
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Call } from '../../Call';
|
|
3
3
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
4
4
|
import { ClientEventReporter } from '../../reporting';
|
|
5
|
-
import { CallingState,
|
|
5
|
+
import { CallingState, ClientState } from '../../store';
|
|
6
6
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
8
|
import {
|
|
@@ -99,7 +99,7 @@ describe('Device Manager', () => {
|
|
|
99
99
|
type: '',
|
|
100
100
|
streamClient,
|
|
101
101
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
102
|
-
|
|
102
|
+
clientState: new ClientState(),
|
|
103
103
|
}),
|
|
104
104
|
{ enabled: false, storageKey },
|
|
105
105
|
);
|
|
@@ -3,7 +3,7 @@ import { of } from 'rxjs';
|
|
|
3
3
|
import { Call } from '../../Call';
|
|
4
4
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
5
5
|
import { ClientEventReporter } from '../../reporting';
|
|
6
|
-
import {
|
|
6
|
+
import { ClientState } from '../../store';
|
|
7
7
|
import { DeviceManagerState } from '../DeviceManagerState';
|
|
8
8
|
import { DeviceManager } from '../DeviceManager';
|
|
9
9
|
import {
|
|
@@ -53,7 +53,7 @@ describe('MediaStream Filters', () => {
|
|
|
53
53
|
type: '',
|
|
54
54
|
streamClient,
|
|
55
55
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
56
|
-
|
|
56
|
+
clientState: new ClientState(),
|
|
57
57
|
}),
|
|
58
58
|
);
|
|
59
59
|
});
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
AudioBitrateProfile,
|
|
13
13
|
TrackType,
|
|
14
14
|
} from '../../gen/video/sfu/models/models';
|
|
15
|
-
import { CallingState,
|
|
15
|
+
import { CallingState, ClientState } from '../../store';
|
|
16
16
|
import {
|
|
17
17
|
createLocalStorageMock,
|
|
18
18
|
emitDeviceIds,
|
|
@@ -105,7 +105,7 @@ describe('MicrophoneManager', () => {
|
|
|
105
105
|
type: '',
|
|
106
106
|
streamClient,
|
|
107
107
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
108
|
-
|
|
108
|
+
clientState: new ClientState(),
|
|
109
109
|
});
|
|
110
110
|
const devicePersistence = { enabled: false, storageKey: '' };
|
|
111
111
|
manager = new MicrophoneManager(call, devicePersistence, 'disable-tracks');
|
|
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { MicrophoneManager } from '../MicrophoneManager';
|
|
3
3
|
import { Call } from '../../Call';
|
|
4
4
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
5
|
-
import {
|
|
5
|
+
import { ClientState } from '../../store';
|
|
6
6
|
import {
|
|
7
7
|
mockAudioDevices,
|
|
8
8
|
mockAudioStream,
|
|
@@ -91,7 +91,7 @@ describe('MicrophoneManager React Native', () => {
|
|
|
91
91
|
id: '',
|
|
92
92
|
type: '',
|
|
93
93
|
streamClient,
|
|
94
|
-
|
|
94
|
+
clientState: new ClientState(),
|
|
95
95
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
96
96
|
}),
|
|
97
97
|
devicePersistence,
|
|
@@ -3,7 +3,7 @@ import { ScreenShareManager } from '../ScreenShareManager';
|
|
|
3
3
|
import { Call } from '../../Call';
|
|
4
4
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
5
5
|
import { ClientEventReporter } from '../../reporting';
|
|
6
|
-
import { CallingState,
|
|
6
|
+
import { CallingState, ClientState } from '../../store';
|
|
7
7
|
import * as RxUtils from '../../store/rxUtils';
|
|
8
8
|
import { mockCall, mockDeviceIds$, mockScreenShareStream } from './mocks';
|
|
9
9
|
import { getScreenShareStream } from '../devices';
|
|
@@ -44,7 +44,7 @@ describe('ScreenShareManager', () => {
|
|
|
44
44
|
type: '',
|
|
45
45
|
streamClient,
|
|
46
46
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
47
|
-
|
|
47
|
+
clientState: new ClientState(),
|
|
48
48
|
}),
|
|
49
49
|
);
|
|
50
50
|
});
|
|
@@ -99,7 +99,7 @@ describe('ScreenShareManager', () => {
|
|
|
99
99
|
|
|
100
100
|
it('should use call settings to set up constraints', async () => {
|
|
101
101
|
const call = manager['call'];
|
|
102
|
-
|
|
102
|
+
RxUtils.setCurrentValue(call.state['settingsSubject'], {
|
|
103
103
|
// @ts-expect-error partial data
|
|
104
104
|
screensharing: {
|
|
105
105
|
target_resolution: {
|
|
@@ -17,7 +17,7 @@ import { checkIfAudioOutputChangeSupported } from '../devices';
|
|
|
17
17
|
import { Call } from '../../Call';
|
|
18
18
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
19
19
|
import { ClientEventReporter } from '../../reporting';
|
|
20
|
-
import {
|
|
20
|
+
import { ClientState } from '../../store';
|
|
21
21
|
import { defaultDeviceId } from '../devicePersistence';
|
|
22
22
|
|
|
23
23
|
vi.mock('../devices.ts', () => {
|
|
@@ -55,7 +55,7 @@ describe('SpeakerManager.test', () => {
|
|
|
55
55
|
type: '',
|
|
56
56
|
streamClient,
|
|
57
57
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
58
|
-
|
|
58
|
+
clientState: new ClientState(),
|
|
59
59
|
}),
|
|
60
60
|
devicePersistence,
|
|
61
61
|
);
|
|
@@ -162,7 +162,7 @@ describe('SpeakerManager.test', () => {
|
|
|
162
162
|
type: '',
|
|
163
163
|
streamClient,
|
|
164
164
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
165
|
-
|
|
165
|
+
clientState: new ClientState(),
|
|
166
166
|
}),
|
|
167
167
|
{ enabled: true, storageKey },
|
|
168
168
|
);
|
|
@@ -190,7 +190,7 @@ describe('SpeakerManager.test', () => {
|
|
|
190
190
|
type: '',
|
|
191
191
|
streamClient,
|
|
192
192
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
193
|
-
|
|
193
|
+
clientState: new ClientState(),
|
|
194
194
|
}),
|
|
195
195
|
{ enabled: true, storageKey },
|
|
196
196
|
);
|