@stream-io/video-client 1.59.1 → 1.61.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.browser.es.js +744 -156
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +744 -155
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.es.js +744 -156
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/Call.d.ts +34 -8
  10. package/dist/src/coordinator/connection/types.d.ts +16 -0
  11. package/dist/src/events/call.d.ts +0 -11
  12. package/dist/src/gen/coordinator/index.d.ts +73 -0
  13. package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
  14. package/dist/src/ringing/RingStatePoller.d.ts +46 -0
  15. package/dist/src/ringing/RingTimeout.d.ts +27 -0
  16. package/dist/src/ringing/index.d.ts +4 -0
  17. package/dist/src/ringing/reconcileRingState.d.ts +18 -0
  18. package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
  19. package/dist/src/rtc/Publisher.d.ts +18 -0
  20. package/dist/src/rtc/TransceiverCache.d.ts +10 -0
  21. package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
  22. package/dist/src/stats/utils.d.ts +6 -0
  23. package/dist/src/store/CallState.d.ts +10 -1
  24. package/dist/src/types.d.ts +30 -1
  25. package/index.ts +1 -0
  26. package/package.json +1 -1
  27. package/src/Call.ts +272 -108
  28. package/src/StreamSfuClient.ts +6 -2
  29. package/src/__tests__/Call.autodrop.test.ts +116 -64
  30. package/src/__tests__/Call.getRingState.test.ts +87 -0
  31. package/src/__tests__/Call.lifecycle.test.ts +498 -5
  32. package/src/__tests__/Call.ringSettled.test.ts +71 -0
  33. package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
  34. package/src/__tests__/StreamSfuClient.test.ts +48 -0
  35. package/src/coordinator/connection/types.ts +19 -0
  36. package/src/events/__tests__/call.test.ts +2 -217
  37. package/src/events/call.ts +1 -73
  38. package/src/events/callEventHandlers.ts +12 -23
  39. package/src/gen/coordinator/index.ts +67 -0
  40. package/src/reporting/ClientEventReporter.ts +30 -4
  41. package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
  42. package/src/ringing/RingStatePoller.ts +177 -0
  43. package/src/ringing/RingTimeout.ts +90 -0
  44. package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
  45. package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
  46. package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
  47. package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
  48. package/src/ringing/index.ts +4 -0
  49. package/src/ringing/reconcileRingState.ts +108 -0
  50. package/src/ringing/resolveOwnRingOutcome.ts +58 -0
  51. package/src/rtc/Publisher.ts +66 -12
  52. package/src/rtc/TransceiverCache.ts +17 -0
  53. package/src/rtc/__tests__/Publisher.test.ts +209 -0
  54. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
  55. package/src/rtc/e2ee/E2EEManager.ts +10 -1
  56. package/src/rtc/e2ee/EncryptionManager.ts +1 -1
  57. package/src/rtc/e2ee/SPEC.md +1 -1
  58. package/src/stats/__tests__/utils.test.ts +33 -0
  59. package/src/stats/utils.ts +15 -0
  60. package/src/store/CallState.ts +25 -0
  61. package/src/store/__tests__/CallState.test.ts +59 -0
  62. package/src/types.ts +37 -1
@@ -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 { StreamVideoWriteableStateStore } 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
+ clientStore: new StreamVideoWriteableStateStore(),
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
+ });
@@ -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,16 +1,8 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { fromPartial } from '@total-typescript/shoehorn';
3
2
  import { CallingState, StreamVideoWriteableStateStore } from '../../store';
3
+ import { watchCallEnded, watchSfuCallEnded } from '../call';
4
4
  import {
5
- watchCallAccepted,
6
- watchCallEnded,
7
- watchCallRejected,
8
- watchSfuCallEnded,
9
- } from '../call';
10
- import {
11
- CallAcceptedEvent,
12
5
  CallEndedEvent,
13
- CallResponse,
14
6
  OwnCapability,
15
7
  RejectCallResponse,
16
8
  } from '../../gen/coordinator';
@@ -20,189 +12,7 @@ import { ClientEventReporter } from '../../reporting';
20
12
  import { SfuEvent } from '../../gen/video/sfu/event/events';
21
13
  import { CallEndedReason } from '../../gen/video/sfu/models/models';
22
14
 
23
- describe('Call ringing events', () => {
24
- describe(`call.accepted`, () => {
25
- it(`will ignore events from the current user`, async () => {
26
- const call = fakeCall();
27
- vi.spyOn(call, 'join');
28
- const handler = watchCallAccepted(call);
29
- const event: CallAcceptedEvent = {
30
- type: 'call.accepted',
31
- // @ts-expect-error incomplete data
32
- user: { id: 'test-user-id' },
33
- };
34
- await handler(event);
35
-
36
- expect(call.join).not.toHaveBeenCalled();
37
- });
38
-
39
- it(`will join the call for the caller if atleast one callee has accepted`, async () => {
40
- const call = fakeCall({ currentUserId: 'test-user' });
41
- vi.spyOn(call, 'join').mockImplementation(async () => {
42
- console.log(`TEST: join() called`);
43
- });
44
- const handler = watchCallAccepted(call);
45
- const event: CallAcceptedEvent = {
46
- type: 'call.accepted',
47
- // @ts-expect-error incomplete data
48
- user: { id: 'test-user-id-callee' },
49
- // @ts-expect-error incomplete data
50
- call: { created_by: { id: 'test-user' } },
51
- };
52
- await handler(event);
53
-
54
- expect(call.join).toHaveBeenCalled();
55
- });
56
- });
57
-
58
- it('will not join the call for the other callee automatically when someone accepts', async () => {
59
- const call = fakeCall({ currentUserId: 'test-user-id-callee-2' });
60
- vi.spyOn(call, 'join').mockImplementation(async () => {
61
- console.log(`TEST: join() called`);
62
- });
63
- const handler = watchCallAccepted(call);
64
- const event: CallAcceptedEvent = {
65
- type: 'call.accepted',
66
- // @ts-expect-error incomplete data
67
- user: { id: 'test-user-id-callee-1' },
68
- // @ts-expect-error incomplete data
69
- call: { created_by: { id: 'test-user-id-caller' } },
70
- };
71
-
72
- await handler(event);
73
-
74
- expect(call.join).not.toHaveBeenCalled();
75
- });
76
-
77
- describe(`call.rejected`, () => {
78
- it(`caller will leave the call if all callees have rejected`, async () => {
79
- const call = fakeCall({ currentUserId: 'm1' });
80
- call.state.updateFromCallResponse({
81
- ...fakeMetadata(),
82
- // @ts-expect-error type issue
83
- created_by: { id: 'm1' },
84
- });
85
- call.state.setMembers([
86
- // @ts-expect-error incomplete data
87
- { user_id: 'm1' },
88
- // @ts-expect-error incomplete data
89
- { user_id: 'm2' },
90
- // @ts-expect-error incomplete data
91
- { user_id: 'm3' },
92
- ]);
93
- call.state.setCallingState(CallingState.RINGING);
94
- vi.spyOn(call, 'leave').mockImplementation(async () => {
95
- console.log(`TEST: leave() called`);
96
- });
97
-
98
- const handler = watchCallRejected(call);
99
- // all members reject the call
100
- await handler({
101
- type: 'call.rejected',
102
- // @ts-expect-error type issue
103
- user: {
104
- id: 'm2',
105
- },
106
- call: {
107
- // @ts-expect-error type issue
108
- created_by: {
109
- id: 'm1',
110
- },
111
- // @ts-expect-error type issue
112
- session: {
113
- rejected_by: {
114
- m2: new Date().toISOString(),
115
- m3: new Date().toISOString(),
116
- },
117
- },
118
- },
119
- });
120
- expect(call.leave).toHaveBeenCalledWith({
121
- reject: true,
122
- reason: 'cancel',
123
- message: 'ring: everyone rejected',
124
- });
125
- });
126
-
127
- it(`caller will not leave the call if only one callee rejects`, async () => {
128
- const call = fakeCall();
129
- call.state.updateFromCallResponse({
130
- ...fakeMetadata(),
131
- // @ts-expect-error type issue
132
- created_by: { id: 'm0' },
133
- });
134
- // @ts-expect-error incomplete data
135
- call.state.setMembers([{ user_id: 'm1' }, { user_id: 'm2' }]);
136
- vi.spyOn(call, 'leave').mockImplementation(async () => {
137
- console.log(`TEST: leave() called`);
138
- });
139
- const handler = watchCallRejected(call);
140
-
141
- // only one member rejects the call
142
- const event: CallAcceptedEvent = {
143
- type: 'call.rejected',
144
- // @ts-expect-error type issue
145
- user: {
146
- id: 'm2',
147
- },
148
- call: {
149
- // @ts-expect-error type issue
150
- created_by: {
151
- id: 'm0',
152
- },
153
- // @ts-expect-error type issue
154
- session: {
155
- rejected_by: {
156
- m2: new Date().toISOString(),
157
- },
158
- },
159
- },
160
- };
161
- await handler(event);
162
-
163
- expect(call.leave).not.toHaveBeenCalled();
164
- });
165
-
166
- it('callee will leave the call if caller rejects', async () => {
167
- const call = fakeCall({ currentUserId: 'm1' });
168
- call.state.updateFromCallResponse({
169
- ...fakeMetadata(),
170
- // @ts-expect-error type issue
171
- created_by: { id: 'm0' },
172
- });
173
- // @ts-expect-error incomplete data
174
- call.state.setMembers([{ user_id: 'm1' }, { user_id: 'm2' }]);
175
- vi.spyOn(call, 'leave').mockImplementation(async () => {
176
- console.log(`TEST: leave() called`);
177
- });
178
- const handler = watchCallRejected(call);
179
-
180
- // only one member rejects the call
181
- const event: CallAcceptedEvent = {
182
- type: 'call.rejected',
183
- // @ts-expect-error type issue
184
- user: {
185
- id: 'm0',
186
- },
187
- call: {
188
- // @ts-expect-error type issue
189
- created_by: {
190
- id: 'm0',
191
- },
192
- // @ts-expect-error type issue
193
- session: {
194
- rejected_by: {
195
- m0: new Date().toISOString(),
196
- },
197
- },
198
- },
199
- };
200
- await handler(event);
201
-
202
- expect(call.leave).toHaveBeenCalled();
203
- });
204
- });
205
-
15
+ describe('Call lifecycle events', () => {
206
16
  describe(`call.ended`, () => {
207
17
  it(`will leave the call unless joined`, async () => {
208
18
  const call = fakeCall();
@@ -377,28 +187,3 @@ const fakeCall = ({ ring = true, currentUserId = 'test-user-id' } = {}) => {
377
187
  ringing: ring,
378
188
  });
379
189
  };
380
-
381
- const fakeMetadata = (): CallResponse => {
382
- return fromPartial({
383
- id: '12345',
384
- type: 'development',
385
- cid: 'development:12345',
386
-
387
- created_by: {
388
- id: 'test-user-id',
389
- },
390
- blocked_user_ids: [],
391
- egress: {},
392
-
393
- settings: {
394
- ring: {
395
- auto_cancel_timeout_ms: 30000,
396
- incoming_call_timeout_ms: 30000,
397
- missed_call_timeout_ms: 30000,
398
- },
399
- screensharing: {
400
- target_resolution: undefined,
401
- },
402
- },
403
- });
404
- };
@@ -1,81 +1,9 @@
1
1
  import { CallingState } from '../store';
2
2
  import { Call } from '../Call';
3
- import {
4
- CallAcceptedEvent,
5
- CallRejectedEvent,
6
- OwnCapability,
7
- } from '../gen/coordinator';
3
+ import { OwnCapability } from '../gen/coordinator';
8
4
  import { CallEnded } from '../gen/video/sfu/event/events';
9
5
  import { CallEndedReason } from '../gen/video/sfu/models/models';
10
6
 
11
- /**
12
- * Event handler that watched the delivery of `call.accepted`.
13
- * Once the event is received, the call is joined.
14
- */
15
- export const watchCallAccepted = (call: Call) => {
16
- return async function onCallAccepted(event: CallAcceptedEvent) {
17
- // We want to discard the event if it's from the current user
18
- if (event.user.id === call.currentUserId) return;
19
- const { state } = call;
20
- if (
21
- event.call.created_by.id === call.currentUserId &&
22
- state.callingState === CallingState.RINGING
23
- ) {
24
- await call.join();
25
- }
26
- };
27
- };
28
-
29
- /**
30
- * Event handler that watches delivery of `call.rejected` Websocket event.
31
- * Once the event is received, the call is left.
32
- */
33
- export const watchCallRejected = (call: Call) => {
34
- return async function onCallRejected(event: CallRejectedEvent) {
35
- // We want to discard the event if it's from the current user
36
- if (event.user.id === call.currentUserId) return;
37
- const { call: eventCall } = event;
38
- const { session: callSession } = eventCall;
39
-
40
- if (!callSession) {
41
- call.logger.warn(
42
- 'No call session provided. Ignoring call.rejected event.',
43
- event,
44
- );
45
- return;
46
- }
47
-
48
- const rejectedBy = callSession.rejected_by;
49
- const { members, callingState } = call.state;
50
- if (callingState !== CallingState.RINGING) {
51
- call.logger.info(
52
- 'Call is not in ringing mode (it is either accepted or rejected already). Ignoring call.rejected event.',
53
- event,
54
- );
55
- return;
56
- }
57
- if (call.isCreatedByMe) {
58
- const everyoneElseRejected = members
59
- .filter((m) => m.user_id !== call.currentUserId)
60
- .every((m) => rejectedBy[m.user_id]);
61
- if (everyoneElseRejected) {
62
- call.logger.info('everyone rejected, leaving the call');
63
- await call.leave({
64
- reject: true,
65
- reason: 'cancel',
66
- message: 'ring: everyone rejected',
67
- });
68
- }
69
- } else {
70
- if (rejectedBy[eventCall.created_by.id]) {
71
- call.logger.info('call creator rejected, leaving call');
72
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
73
- await call.leave({ message: 'ring: creator rejected' });
74
- }
75
- }
76
- };
77
- };
78
-
79
7
  /**
80
8
  * Event handler that watches the delivery of `call.ended` Websocket event.
81
9
  */