@stream-io/video-client 1.60.0 → 1.61.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 +17 -0
- package/dist/index.browser.es.js +264 -57
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +264 -56
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +264 -57
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +11 -5
- 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/types.d.ts +30 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/Call.ts +180 -48
- package/src/__tests__/Call.lifecycle.test.ts +479 -9
- package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
- package/src/reporting/ClientEventReporter.ts +7 -2
- package/src/reporting/__tests__/ClientEventReporter.test.ts +2 -2
- package/src/rtc/BasePeerConnection.ts +6 -1
- package/src/rtc/Publisher.ts +66 -12
- package/src/rtc/TransceiverCache.ts +17 -0
- package/src/rtc/__tests__/Publisher.test.ts +259 -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/types.ts +37 -1
|
@@ -26,6 +26,7 @@ import { TransceiverCache } from '../TransceiverCache';
|
|
|
26
26
|
import { promiseWithResolvers } from '../../helpers/promise';
|
|
27
27
|
import { settled } from '../../helpers/concurrency';
|
|
28
28
|
import { isFirefox } from '../../helpers/browsers';
|
|
29
|
+
import { isReactNative } from '../../helpers/platforms';
|
|
29
30
|
|
|
30
31
|
vi.mock('../../StreamSfuClient', () => {
|
|
31
32
|
console.log('MOCKING StreamSfuClient');
|
|
@@ -40,6 +41,12 @@ vi.mock('../../helpers/browsers', async (importOriginal) => {
|
|
|
40
41
|
return { ...actual, isFirefox: vi.fn().mockReturnValue(false) };
|
|
41
42
|
});
|
|
42
43
|
|
|
44
|
+
vi.mock('../../helpers/platforms', async (importOriginal) => {
|
|
45
|
+
const actual =
|
|
46
|
+
await importOriginal<typeof import('../../helpers/platforms')>();
|
|
47
|
+
return { ...actual, isReactNative: vi.fn().mockReturnValue(false) };
|
|
48
|
+
});
|
|
49
|
+
|
|
43
50
|
describe('Publisher', () => {
|
|
44
51
|
const sessionId = 'session-id-test';
|
|
45
52
|
let publisher: Publisher;
|
|
@@ -106,6 +113,208 @@ describe('Publisher', () => {
|
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
describe('Publishing', () => {
|
|
116
|
+
describe('E2EE initialization failures', () => {
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
vi.mocked(isReactNative).mockReturnValue(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const setup = async () => {
|
|
122
|
+
await publisher.dispose();
|
|
123
|
+
const e2ee = { encrypt: vi.fn(), decrypt: vi.fn() };
|
|
124
|
+
publisher = new Publisher(
|
|
125
|
+
{
|
|
126
|
+
sfuClient,
|
|
127
|
+
dispatcher,
|
|
128
|
+
state,
|
|
129
|
+
tag: 'test',
|
|
130
|
+
enableTracing: false,
|
|
131
|
+
e2ee,
|
|
132
|
+
},
|
|
133
|
+
[
|
|
134
|
+
fromPartial<PublishOption>({
|
|
135
|
+
id: 1,
|
|
136
|
+
trackType: TrackType.VIDEO,
|
|
137
|
+
codec: { name: 'vp9' },
|
|
138
|
+
}),
|
|
139
|
+
],
|
|
140
|
+
);
|
|
141
|
+
const transceivers: RTCRtpTransceiver[] = [];
|
|
142
|
+
vi.mocked(publisher['pc'].addTransceiver).mockImplementation((kind) => {
|
|
143
|
+
expect(kind).toBe('video');
|
|
144
|
+
const transceiver = new RTCRtpTransceiver();
|
|
145
|
+
vi.mocked(transceiver.sender.replaceTrack).mockImplementation(
|
|
146
|
+
async (track) => {
|
|
147
|
+
expect(e2ee.encrypt).toHaveBeenCalledWith(
|
|
148
|
+
transceiver.sender,
|
|
149
|
+
'vp9',
|
|
150
|
+
'VIDEO',
|
|
151
|
+
);
|
|
152
|
+
Object.assign(transceiver.sender, { track });
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
transceivers.push(transceiver);
|
|
156
|
+
return transceiver;
|
|
157
|
+
});
|
|
158
|
+
const track = new MediaStreamTrack();
|
|
159
|
+
vi.mocked(track.clone).mockImplementation(() => new MediaStreamTrack());
|
|
160
|
+
const realNegotiate = publisher['negotiate'];
|
|
161
|
+
const negotiate = vi.fn<Publisher['negotiate']>().mockResolvedValue();
|
|
162
|
+
publisher['negotiate'] = negotiate;
|
|
163
|
+
return { e2ee, transceivers, track, negotiate, realNegotiate };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
it.each([
|
|
167
|
+
'encrypt',
|
|
168
|
+
'getParameters',
|
|
169
|
+
'setParameters',
|
|
170
|
+
'replaceTrack',
|
|
171
|
+
] as const)(
|
|
172
|
+
'retires the sender after %s fails and attaches encryption on retry',
|
|
173
|
+
async (failurePoint) => {
|
|
174
|
+
const { e2ee, transceivers, track, negotiate } = await setup();
|
|
175
|
+
const error = new Error('initialization failed');
|
|
176
|
+
e2ee.encrypt.mockImplementationOnce((sender: RTCRtpSender) => {
|
|
177
|
+
expect(sender.track).toBeNull();
|
|
178
|
+
if (failurePoint === 'encrypt') throw error;
|
|
179
|
+
if (failurePoint === 'getParameters') {
|
|
180
|
+
vi.mocked(sender.getParameters).mockImplementationOnce(() => {
|
|
181
|
+
throw error;
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
vi.mocked(sender[failurePoint]).mockRejectedValueOnce(error);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
await expect(publisher.publish(track, TrackType.VIDEO)).rejects.toBe(
|
|
188
|
+
error,
|
|
189
|
+
);
|
|
190
|
+
const failed = transceivers[0];
|
|
191
|
+
expect(failed.sender.track).toBeNull();
|
|
192
|
+
expect(failed.stop).toHaveBeenCalledOnce();
|
|
193
|
+
expect(publisher['transceiverCache'].items()).toHaveLength(0);
|
|
194
|
+
expect(publisher['clonedTracks'].size).toBe(0);
|
|
195
|
+
expect(negotiate).not.toHaveBeenCalled();
|
|
196
|
+
|
|
197
|
+
await publisher.publish(track, TrackType.VIDEO);
|
|
198
|
+
expect(transceivers).toHaveLength(2);
|
|
199
|
+
expect(e2ee.encrypt).toHaveBeenLastCalledWith(
|
|
200
|
+
transceivers[1].sender,
|
|
201
|
+
'vp9',
|
|
202
|
+
'VIDEO',
|
|
203
|
+
);
|
|
204
|
+
expect(transceivers[1].sender.track).not.toBeNull();
|
|
205
|
+
expect(publisher['transceiverCache'].indexOf(failed)).toBe(-1);
|
|
206
|
+
expect(publisher['transceiverCache'].indexOf(transceivers[1])).toBe(
|
|
207
|
+
0,
|
|
208
|
+
);
|
|
209
|
+
expect(negotiate).toHaveBeenCalledOnce();
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
it('keeps the mid index of a retired sender that reached an m-section', async () => {
|
|
214
|
+
const { e2ee, transceivers, track } = await setup();
|
|
215
|
+
const error = new Error('initialization failed');
|
|
216
|
+
e2ee.encrypt.mockImplementationOnce((sender: RTCRtpSender) => {
|
|
217
|
+
Object.assign(transceivers[0], { mid: '0' });
|
|
218
|
+
vi.mocked(sender.replaceTrack).mockRejectedValueOnce(error);
|
|
219
|
+
});
|
|
220
|
+
await expect(publisher.publish(track, TrackType.VIDEO)).rejects.toBe(
|
|
221
|
+
error,
|
|
222
|
+
);
|
|
223
|
+
expect(publisher['transceiverCache'].items()).toHaveLength(0);
|
|
224
|
+
expect(publisher['transceiverCache'].indexOf(transceivers[0])).toBe(0);
|
|
225
|
+
|
|
226
|
+
await publisher.publish(track, TrackType.VIDEO);
|
|
227
|
+
expect(publisher['transceiverCache'].indexOf(transceivers[1])).toBe(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('reports the initialization failure even when cleanup throws', async () => {
|
|
231
|
+
const { e2ee, transceivers, track } = await setup();
|
|
232
|
+
const error = new Error('initialization failed');
|
|
233
|
+
e2ee.encrypt.mockImplementationOnce(() => {
|
|
234
|
+
vi.mocked(transceivers[0].stop).mockImplementationOnce(() => {
|
|
235
|
+
throw new Error('peer connection is closed');
|
|
236
|
+
});
|
|
237
|
+
throw error;
|
|
238
|
+
});
|
|
239
|
+
await expect(publisher.publish(track, TrackType.VIDEO)).rejects.toBe(
|
|
240
|
+
error,
|
|
241
|
+
);
|
|
242
|
+
expect(publisher['transceiverCache'].items()).toHaveLength(0);
|
|
243
|
+
expect(publisher['clonedTracks'].size).toBe(0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('leaves the cloned track alone on React Native', async () => {
|
|
247
|
+
vi.mocked(isReactNative).mockReturnValue(true);
|
|
248
|
+
const { e2ee, transceivers, track } = await setup();
|
|
249
|
+
const error = new Error('initialization failed');
|
|
250
|
+
e2ee.encrypt.mockImplementationOnce(() => {
|
|
251
|
+
throw error;
|
|
252
|
+
});
|
|
253
|
+
await expect(publisher.publish(track, TrackType.VIDEO)).rejects.toBe(
|
|
254
|
+
error,
|
|
255
|
+
);
|
|
256
|
+
const [clone] = publisher['clonedTracks'];
|
|
257
|
+
expect(clone.stop).not.toHaveBeenCalled();
|
|
258
|
+
expect(transceivers[0].stop).toHaveBeenCalledOnce();
|
|
259
|
+
expect(publisher['transceiverCache'].items()).toHaveLength(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('reuses an encrypted sender when only negotiation failed', async () => {
|
|
263
|
+
const { e2ee, transceivers, track, negotiate } = await setup();
|
|
264
|
+
const error = new Error('negotiation failed');
|
|
265
|
+
negotiate.mockRejectedValueOnce(error);
|
|
266
|
+
await expect(publisher.publish(track, TrackType.VIDEO)).rejects.toBe(
|
|
267
|
+
error,
|
|
268
|
+
);
|
|
269
|
+
expect(transceivers[0].sender.track).not.toBeNull();
|
|
270
|
+
expect(transceivers[0].stop).not.toHaveBeenCalled();
|
|
271
|
+
await publisher.publish(track, TrackType.VIDEO);
|
|
272
|
+
expect(transceivers).toHaveLength(1);
|
|
273
|
+
expect(e2ee.encrypt).toHaveBeenCalledOnce();
|
|
274
|
+
expect(negotiate).toHaveBeenCalledTimes(2);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('has no media to expose if ICE restart negotiates during parameter setup', async () => {
|
|
278
|
+
const { e2ee, transceivers, track, negotiate, realNegotiate } =
|
|
279
|
+
await setup();
|
|
280
|
+
negotiate.mockImplementation(realNegotiate);
|
|
281
|
+
publisher['publishOptions'].push(
|
|
282
|
+
fromPartial<PublishOption>({
|
|
283
|
+
id: 2,
|
|
284
|
+
trackType: TrackType.VIDEO,
|
|
285
|
+
codec: { name: 'vp9' },
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
sfuClient.setPublisher = vi
|
|
289
|
+
.fn()
|
|
290
|
+
.mockResolvedValue({ response: { sdp: 'answer-sdp' } });
|
|
291
|
+
const parameters = promiseWithResolvers<void>();
|
|
292
|
+
e2ee.encrypt
|
|
293
|
+
.mockImplementationOnce(() => {})
|
|
294
|
+
.mockImplementationOnce((sender: RTCRtpSender) => {
|
|
295
|
+
vi.mocked(sender.setParameters).mockReturnValueOnce(
|
|
296
|
+
parameters.promise,
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
const publish = publisher.publish(track, TrackType.VIDEO);
|
|
300
|
+
await vi.waitFor(() => expect(e2ee.encrypt).toHaveBeenCalledTimes(2));
|
|
301
|
+
const [published, pending] = transceivers;
|
|
302
|
+
expect(pending.sender.track).toBeNull();
|
|
303
|
+
expect(pending.sender.replaceTrack).not.toHaveBeenCalled();
|
|
304
|
+
|
|
305
|
+
await publisher.restartIce();
|
|
306
|
+
const restart = vi.mocked(sfuClient.setPublisher).mock.calls[1][0];
|
|
307
|
+
expect(restart.tracks.map((t) => t.trackId)).toEqual([
|
|
308
|
+
published.sender.track!.id,
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
parameters.resolve();
|
|
312
|
+
await publish;
|
|
313
|
+
expect(pending.sender.track).not.toBeNull();
|
|
314
|
+
expect(sfuClient.setPublisher).toHaveBeenCalledTimes(3);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
109
318
|
it('should throw when publishing ended tracks', async () => {
|
|
110
319
|
const track = new MediaStreamTrack();
|
|
111
320
|
// @ts-expect-error readonly field
|
|
@@ -722,6 +931,56 @@ describe('Publisher', () => {
|
|
|
722
931
|
);
|
|
723
932
|
});
|
|
724
933
|
|
|
934
|
+
it.each(['checking', 'failed'] as const)(
|
|
935
|
+
`connection-state 'failed' before ICE connects emits ICE_NEVER_CONNECTED when ICE is '%s'`,
|
|
936
|
+
async (iceState) => {
|
|
937
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
938
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
939
|
+
// @ts-expect-error readonly field
|
|
940
|
+
publisher['pc'].iceConnectionState = iceState;
|
|
941
|
+
// @ts-expect-error readonly field
|
|
942
|
+
publisher['pc'].connectionState = 'failed';
|
|
943
|
+
|
|
944
|
+
// No ICE state-change event precedes the aggregate failure.
|
|
945
|
+
await publisher['onConnectionStateChange']();
|
|
946
|
+
|
|
947
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
948
|
+
expect(
|
|
949
|
+
publisher['onReconnectionNeeded'],
|
|
950
|
+
).toHaveBeenCalledExactlyOnceWith(
|
|
951
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
952
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
953
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
954
|
+
);
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
it(`connection-state 'failed' cancels the pre-connect watchdog`, async () => {
|
|
959
|
+
vi.useFakeTimers();
|
|
960
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
961
|
+
const watchdogMs = publisher['iceRestartDelay'] * 2;
|
|
962
|
+
|
|
963
|
+
// @ts-expect-error readonly field
|
|
964
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
965
|
+
publisher['onIceConnectionStateChange']();
|
|
966
|
+
expect(publisher['preConnectStuckTimeout']).toBeDefined();
|
|
967
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
968
|
+
|
|
969
|
+
// Keep ICE disconnected so an uncanceled watchdog would reconnect again.
|
|
970
|
+
// @ts-expect-error readonly field
|
|
971
|
+
publisher['pc'].connectionState = 'failed';
|
|
972
|
+
await publisher['onConnectionStateChange']();
|
|
973
|
+
|
|
974
|
+
expect(publisher['preConnectStuckTimeout']).toBeUndefined();
|
|
975
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledExactlyOnceWith(
|
|
976
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
977
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
978
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
979
|
+
);
|
|
980
|
+
await vi.advanceTimersByTimeAsync(watchdogMs + 100);
|
|
981
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledTimes(1);
|
|
982
|
+
});
|
|
983
|
+
|
|
725
984
|
it(`connection-state 'failed' (distinct from ICE state) still fires REJOIN even after ICE was connected`, () => {
|
|
726
985
|
// mark ICE as connected first
|
|
727
986
|
// @ts-expect-error private api
|
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
* attaching their own encoded transform in these two methods.
|
|
6
6
|
*/
|
|
7
7
|
export interface E2EEManager {
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Synchronously attach a transform that encrypts the sender's outgoing frames.
|
|
10
|
+
* Must support a sender without a track, and throw if attachment fails.
|
|
11
|
+
*
|
|
12
|
+
* `sender.track` is always null here: the track is attached only after this
|
|
13
|
+
* returns, so implementations must not read it. Use `codec` and `trackType`
|
|
14
|
+
* instead.
|
|
15
|
+
*
|
|
16
|
+
* `trackType` only groups perf stats, keeping a camera and screen share apart.
|
|
17
|
+
*/
|
|
9
18
|
encrypt(sender: RTCRtpSender, codec?: string, trackType?: string): void;
|
|
10
19
|
/** `trackType` only groups perf stats, keeping a peer's audio and video apart. */
|
|
11
20
|
decrypt(receiver: RTCRtpReceiver, userId: string, trackType?: string): void;
|
|
@@ -263,9 +263,9 @@ export class EncryptionManager
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
if ((this.piped ??= new WeakSet()).has(target)) return;
|
|
266
|
-
this.piped.add(target);
|
|
267
266
|
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
268
267
|
const { readable, writable } = target.createEncodedStreams();
|
|
268
|
+
this.piped.add(target);
|
|
269
269
|
this.worker.postMessage(
|
|
270
270
|
{ type: 'cmd.setup_transform', ...options, readable, writable },
|
|
271
271
|
[readable, writable],
|
package/src/rtc/e2ee/SPEC.md
CHANGED
|
@@ -79,7 +79,7 @@ await call.join();
|
|
|
79
79
|
- **Removal comes in per-epoch and per-user forms, and the names must stay distinct.** `removeKey(userId, keyIndex)` retires one epoch; `removeAllKeys(userId)` revokes the participant entirely. Do not render these as overloads of one name: dropping the index argument would then silently mean "revoke everything", which is the opposite of the caller's intent and impossible to catch in review.
|
|
80
80
|
- **Every key operation's outcome is tabulated in Appendix B.** Implement the edge cases (removing the latest or active epoch, a failed import, an absent index) from that table rather than from prose.
|
|
81
81
|
- **Join request carries `e2ee: true`** so the backend knows the call is encrypted.
|
|
82
|
-
- The internal attach points (`encrypt(sender, codec, trackType)` / `decrypt(receiver, userId, trackType)`) are called by the RTC layer, not by apps. Keeping
|
|
82
|
+
- The internal attach points (`encrypt(sender, codec, trackType)` / `decrypt(receiver, userId, trackType)`) are called by the RTC layer, not by apps. The publisher creates an E2EE sender without a track and attaches media only after `encrypt` returns and sender parameters succeed. `encrypt` must synchronously attach the transform or throw, and must support a sender whose `track` is null. A failed initialization retires that transceiver so retries cannot bypass attachment. Keeping these points behind a small interface lets an integrator plug in a different scheme (e.g. SFrame).
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
package/src/types.ts
CHANGED
|
@@ -458,7 +458,17 @@ type StreamRNVideoSDKEndCallReason =
|
|
|
458
458
|
| 'unknown';
|
|
459
459
|
|
|
460
460
|
type StreamRNVideoSDKCallingX = {
|
|
461
|
-
|
|
461
|
+
/**
|
|
462
|
+
* @param isCancelled - polled around the bridge's waits: registration is
|
|
463
|
+
* skipped, or undone, when it returns true. Supplied by the join attempt
|
|
464
|
+
* because the call's own state cannot distinguish a fresh join starting
|
|
465
|
+
* from `LEFT` from an abandoned one that reached `LEFT` while waiting.
|
|
466
|
+
*/
|
|
467
|
+
joinCall: (
|
|
468
|
+
call: Call,
|
|
469
|
+
activeCalls: Call[],
|
|
470
|
+
isCancelled?: () => boolean,
|
|
471
|
+
) => Promise<void>;
|
|
462
472
|
endCall: (
|
|
463
473
|
call: Call,
|
|
464
474
|
reason?: StreamRNVideoSDKEndCallReason,
|
|
@@ -468,8 +478,34 @@ type StreamRNVideoSDKCallingX = {
|
|
|
468
478
|
unwireAudioEngineSubscription: () => void;
|
|
469
479
|
};
|
|
470
480
|
|
|
481
|
+
/**
|
|
482
|
+
* React Native's preparation and cleanup for a ringing call's join.
|
|
483
|
+
*
|
|
484
|
+
* A ringing call is joined by the SDK rather than by app code, so there is no
|
|
485
|
+
* point at which the app holds the call and can still set it up. These are that
|
|
486
|
+
* point, and the matching release.
|
|
487
|
+
*/
|
|
488
|
+
type StreamRNVideoSDKRingingCallLifecycle = {
|
|
489
|
+
/**
|
|
490
|
+
* Runs the app's pre-join setup and resolves once it is done, bounded by a
|
|
491
|
+
* deadline. Rejecting fails the join closed rather than joining with nothing
|
|
492
|
+
* installed.
|
|
493
|
+
*/
|
|
494
|
+
beforeJoin: (call: Call) => Promise<void>;
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The join failed terminally. Ends the ringing flow and releases what the
|
|
498
|
+
* setup installed. Never rejects, so the original join error survives.
|
|
499
|
+
*/
|
|
500
|
+
onJoinFailed: (call: Call) => Promise<void>;
|
|
501
|
+
|
|
502
|
+
/** The call has ended; release whatever the pre-join hook installed. */
|
|
503
|
+
onLeave: (call: Call) => void;
|
|
504
|
+
};
|
|
505
|
+
|
|
471
506
|
export type StreamRNVideoSDKGlobals = {
|
|
472
507
|
callingX: StreamRNVideoSDKCallingX;
|
|
508
|
+
ringingCallLifecycle: StreamRNVideoSDKRingingCallLifecycle;
|
|
473
509
|
callManager: {
|
|
474
510
|
/**
|
|
475
511
|
* Sets up the in call manager.
|