@stream-io/video-client 1.5.0-0 → 1.5.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 +6 -230
- package/dist/index.browser.es.js +1498 -1963
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1495 -1961
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1498 -1963
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +9 -93
- package/dist/src/StreamSfuClient.d.ts +56 -72
- package/dist/src/StreamVideoClient.d.ts +10 -2
- package/dist/src/coordinator/connection/client.d.ts +4 -3
- package/dist/src/coordinator/connection/types.d.ts +1 -5
- package/dist/src/devices/InputMediaDeviceManager.d.ts +0 -4
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +3 -1
- package/dist/src/events/internal.d.ts +0 -4
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -106
- package/dist/src/gen/video/sfu/models/models.d.ts +65 -64
- package/dist/src/logger.d.ts +0 -1
- package/dist/src/rpc/createClient.d.ts +0 -2
- package/dist/src/rpc/index.d.ts +0 -1
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +25 -24
- package/dist/src/rtc/Subscriber.d.ts +11 -12
- package/dist/src/rtc/flows/join.d.ts +20 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -3
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +2 -46
- package/package.json +3 -3
- package/src/Call.ts +562 -615
- package/src/StreamSfuClient.ts +246 -277
- package/src/StreamVideoClient.ts +65 -15
- package/src/coordinator/connection/client.ts +8 -25
- package/src/coordinator/connection/connection.ts +0 -1
- package/src/coordinator/connection/token_manager.ts +1 -1
- package/src/coordinator/connection/types.ts +0 -6
- package/src/devices/BrowserPermission.ts +1 -5
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +3 -12
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +13 -10
- package/src/events/__tests__/participant.test.ts +0 -75
- package/src/events/callEventHandlers.ts +7 -4
- package/src/events/internal.ts +3 -20
- package/src/events/mutes.ts +3 -5
- package/src/events/participant.ts +15 -48
- package/src/gen/video/sfu/event/events.ts +8 -451
- package/src/gen/video/sfu/models/models.ts +204 -211
- package/src/logger.ts +1 -3
- package/src/rpc/createClient.ts +0 -21
- package/src/rpc/index.ts +0 -1
- package/src/rtc/Dispatcher.ts +2 -6
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +163 -127
- package/src/rtc/Subscriber.ts +155 -94
- package/src/rtc/__tests__/Publisher.test.ts +95 -18
- package/src/rtc/__tests__/Subscriber.test.ts +99 -63
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/flows/join.ts +65 -0
- package/src/rtc/helpers/tracks.ts +7 -27
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +10 -1
- package/src/stats/SfuStatsReporter.ts +0 -1
- package/src/store/CallState.ts +2 -109
- package/src/store/__tests__/CallState.test.ts +37 -48
- package/dist/src/helpers/ensureExhausted.d.ts +0 -1
- package/dist/src/helpers/withResolvers.d.ts +0 -14
- package/dist/src/rpc/retryable.d.ts +0 -23
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +0 -2
- package/src/helpers/ensureExhausted.ts +0 -5
- package/src/helpers/withResolvers.ts +0 -43
- package/src/rpc/__tests__/retryable.test.ts +0 -72
- package/src/rpc/retryable.ts +0 -57
- package/src/rtc/helpers/rtcConfiguration.ts +0 -11
package/src/Call.ts
CHANGED
|
@@ -7,12 +7,12 @@ import {
|
|
|
7
7
|
Subscriber,
|
|
8
8
|
} from './rtc';
|
|
9
9
|
import { muteTypeToTrackType } from './rtc/helpers/tracks';
|
|
10
|
-
import { toRtcConfiguration } from './rtc/helpers/rtcConfiguration';
|
|
11
10
|
import {
|
|
12
11
|
hasScreenShare,
|
|
13
12
|
hasScreenShareAudio,
|
|
14
13
|
hasVideo,
|
|
15
14
|
} from './helpers/participantUtils';
|
|
15
|
+
import { GoAwayReason, TrackType } from './gen/video/sfu/models/models';
|
|
16
16
|
import {
|
|
17
17
|
registerEventHandlers,
|
|
18
18
|
registerRingingCallEventHandlers,
|
|
@@ -22,18 +22,13 @@ import {
|
|
|
22
22
|
CallState,
|
|
23
23
|
StreamVideoWriteableStateStore,
|
|
24
24
|
} from './store';
|
|
25
|
+
import { createSubscription, getCurrentValue } from './store/rxUtils';
|
|
25
26
|
import {
|
|
26
|
-
createSafeAsyncSubscription,
|
|
27
|
-
createSubscription,
|
|
28
|
-
getCurrentValue,
|
|
29
|
-
} from './store/rxUtils';
|
|
30
|
-
import type {
|
|
31
27
|
AcceptCallResponse,
|
|
32
28
|
BlockUserRequest,
|
|
33
29
|
BlockUserResponse,
|
|
34
30
|
CollectUserFeedbackRequest,
|
|
35
31
|
CollectUserFeedbackResponse,
|
|
36
|
-
Credentials,
|
|
37
32
|
EndCallResponse,
|
|
38
33
|
GetCallResponse,
|
|
39
34
|
GetCallStatsResponse,
|
|
@@ -41,12 +36,11 @@ import type {
|
|
|
41
36
|
GetOrCreateCallResponse,
|
|
42
37
|
GoLiveRequest,
|
|
43
38
|
GoLiveResponse,
|
|
44
|
-
JoinCallRequest,
|
|
45
|
-
JoinCallResponse,
|
|
46
39
|
ListRecordingsResponse,
|
|
47
40
|
ListTranscriptionsResponse,
|
|
48
41
|
MuteUsersRequest,
|
|
49
42
|
MuteUsersResponse,
|
|
43
|
+
OwnCapability,
|
|
50
44
|
PinRequest,
|
|
51
45
|
PinResponse,
|
|
52
46
|
QueryCallMembersRequest,
|
|
@@ -59,6 +53,7 @@ import type {
|
|
|
59
53
|
SendCallEventResponse,
|
|
60
54
|
SendReactionRequest,
|
|
61
55
|
SendReactionResponse,
|
|
56
|
+
SFUResponse,
|
|
62
57
|
StartHLSBroadcastingResponse,
|
|
63
58
|
StartRecordingRequest,
|
|
64
59
|
StartRecordingResponse,
|
|
@@ -80,7 +75,7 @@ import type {
|
|
|
80
75
|
UpdateUserPermissionsRequest,
|
|
81
76
|
UpdateUserPermissionsResponse,
|
|
82
77
|
} from './gen/coordinator';
|
|
83
|
-
import {
|
|
78
|
+
import { join } from './rtc/flows/join';
|
|
84
79
|
import {
|
|
85
80
|
AudioTrackType,
|
|
86
81
|
CallConstructor,
|
|
@@ -93,6 +88,7 @@ import {
|
|
|
93
88
|
SubscriptionChanges,
|
|
94
89
|
TrackMuteType,
|
|
95
90
|
VideoTrackType,
|
|
91
|
+
VisibilityState,
|
|
96
92
|
} from './types';
|
|
97
93
|
import {
|
|
98
94
|
BehaviorSubject,
|
|
@@ -105,21 +101,22 @@ import {
|
|
|
105
101
|
} from 'rxjs';
|
|
106
102
|
import { TrackSubscriptionDetails } from './gen/video/sfu/signal_rpc/signal';
|
|
107
103
|
import {
|
|
108
|
-
|
|
104
|
+
JoinResponse,
|
|
105
|
+
Migration,
|
|
109
106
|
VideoLayerSetting,
|
|
110
107
|
} from './gen/video/sfu/event/events';
|
|
111
|
-
import {
|
|
112
|
-
ClientDetails,
|
|
113
|
-
TrackType,
|
|
114
|
-
WebsocketReconnectStrategy,
|
|
115
|
-
} from './gen/video/sfu/models/models';
|
|
108
|
+
import { Timestamp } from './gen/google/protobuf/timestamp';
|
|
116
109
|
import { createStatsReporter, SfuStatsReporter, StatsReporter } from './stats';
|
|
117
110
|
import { DynascaleManager } from './helpers/DynascaleManager';
|
|
118
111
|
import { PermissionsContext } from './permissions';
|
|
119
112
|
import { CallTypes } from './CallType';
|
|
120
113
|
import { StreamClient } from './coordinator/connection/client';
|
|
121
|
-
import {
|
|
122
|
-
|
|
114
|
+
import {
|
|
115
|
+
KnownCodes,
|
|
116
|
+
retryInterval,
|
|
117
|
+
sleep,
|
|
118
|
+
} from './coordinator/connection/utils';
|
|
119
|
+
import {
|
|
123
120
|
AllCallEvents,
|
|
124
121
|
CallEventListener,
|
|
125
122
|
Logger,
|
|
@@ -137,11 +134,6 @@ import {
|
|
|
137
134
|
} from './devices';
|
|
138
135
|
import { getSdkSignature } from './stats/utils';
|
|
139
136
|
import { withoutConcurrency } from './helpers/concurrency';
|
|
140
|
-
import { ensureExhausted } from './helpers/ensureExhausted';
|
|
141
|
-
import {
|
|
142
|
-
PromiseWithResolvers,
|
|
143
|
-
promiseWithResolvers,
|
|
144
|
-
} from './helpers/withResolvers';
|
|
145
137
|
|
|
146
138
|
/**
|
|
147
139
|
* An object representation of a `Call`.
|
|
@@ -230,22 +222,9 @@ export class Call {
|
|
|
230
222
|
private readonly clientStore: StreamVideoWriteableStateStore;
|
|
231
223
|
public readonly streamClient: StreamClient;
|
|
232
224
|
private sfuClient?: StreamSfuClient;
|
|
233
|
-
private sfuClientTag = 0;
|
|
234
|
-
|
|
235
|
-
private readonly reconnectConcurrencyTag = Symbol('reconnectConcurrencyTag');
|
|
236
225
|
private reconnectAttempts = 0;
|
|
237
|
-
private
|
|
238
|
-
private
|
|
239
|
-
private lastOfflineTimestamp: number = 0;
|
|
240
|
-
private networkAvailableTask: PromiseWithResolvers<void> | undefined;
|
|
241
|
-
// maintain the order of publishing tracks to restore them after a reconnection
|
|
242
|
-
// it shouldn't contain duplicates
|
|
243
|
-
private trackPublishOrder: TrackType[] = [];
|
|
244
|
-
private joinCallData?: JoinCallData;
|
|
245
|
-
private hasJoinedOnce = false;
|
|
246
|
-
private deviceSettingsAppliedOnce = false;
|
|
247
|
-
private credentials?: Credentials;
|
|
248
|
-
|
|
226
|
+
private maxReconnectAttempts = 10;
|
|
227
|
+
private isLeaving = false;
|
|
249
228
|
private initialized = false;
|
|
250
229
|
private readonly joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
|
|
251
230
|
|
|
@@ -308,7 +287,9 @@ export class Call {
|
|
|
308
287
|
|
|
309
288
|
private async setup() {
|
|
310
289
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
311
|
-
if (this.initialized)
|
|
290
|
+
if (this.initialized) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
312
293
|
|
|
313
294
|
this.leaveCallHooks.add(
|
|
314
295
|
this.on('all', (event) => {
|
|
@@ -317,9 +298,10 @@ export class Call {
|
|
|
317
298
|
}),
|
|
318
299
|
);
|
|
319
300
|
|
|
320
|
-
this.leaveCallHooks.add(
|
|
301
|
+
this.leaveCallHooks.add(
|
|
302
|
+
registerEventHandlers(this, this.state, this.dispatcher),
|
|
303
|
+
);
|
|
321
304
|
this.registerEffects();
|
|
322
|
-
this.registerReconnectHandlers();
|
|
323
305
|
|
|
324
306
|
this.leaveCallHooks.add(
|
|
325
307
|
createSubscription(
|
|
@@ -353,10 +335,71 @@ export class Call {
|
|
|
353
335
|
|
|
354
336
|
this.leaveCallHooks.add(
|
|
355
337
|
// handle the case when the user permissions are modified.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.
|
|
359
|
-
|
|
338
|
+
createSubscription(this.state.ownCapabilities$, (ownCapabilities) => {
|
|
339
|
+
// update the permission context.
|
|
340
|
+
this.permissionsContext.setPermissions(ownCapabilities);
|
|
341
|
+
|
|
342
|
+
if (!this.publisher) return;
|
|
343
|
+
|
|
344
|
+
// check if the user still has publishing permissions and stop publishing if not.
|
|
345
|
+
const permissionToTrackType = {
|
|
346
|
+
[OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
|
|
347
|
+
[OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
|
|
348
|
+
[OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
|
|
349
|
+
};
|
|
350
|
+
for (const [permission, trackType] of Object.entries(
|
|
351
|
+
permissionToTrackType,
|
|
352
|
+
)) {
|
|
353
|
+
const hasPermission = this.permissionsContext.hasPermission(
|
|
354
|
+
permission as OwnCapability,
|
|
355
|
+
);
|
|
356
|
+
if (
|
|
357
|
+
!hasPermission &&
|
|
358
|
+
(this.publisher.isPublishing(trackType) ||
|
|
359
|
+
this.publisher.isLive(trackType))
|
|
360
|
+
) {
|
|
361
|
+
// Stop tracks, then notify device manager
|
|
362
|
+
this.stopPublish(trackType)
|
|
363
|
+
.catch((err) => {
|
|
364
|
+
this.logger(
|
|
365
|
+
'error',
|
|
366
|
+
`Error stopping publish ${trackType}`,
|
|
367
|
+
err,
|
|
368
|
+
);
|
|
369
|
+
})
|
|
370
|
+
.then(() => {
|
|
371
|
+
if (
|
|
372
|
+
trackType === TrackType.VIDEO &&
|
|
373
|
+
this.camera.state.status === 'enabled'
|
|
374
|
+
) {
|
|
375
|
+
this.camera
|
|
376
|
+
.disable()
|
|
377
|
+
.catch((err) =>
|
|
378
|
+
this.logger(
|
|
379
|
+
'error',
|
|
380
|
+
`Error disabling camera after permission revoked`,
|
|
381
|
+
err,
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (
|
|
386
|
+
trackType === TrackType.AUDIO &&
|
|
387
|
+
this.microphone.state.status === 'enabled'
|
|
388
|
+
) {
|
|
389
|
+
this.microphone
|
|
390
|
+
.disable()
|
|
391
|
+
.catch((err) =>
|
|
392
|
+
this.logger(
|
|
393
|
+
'error',
|
|
394
|
+
`Error disabling microphone after permission revoked`,
|
|
395
|
+
err,
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}),
|
|
360
403
|
);
|
|
361
404
|
|
|
362
405
|
this.leaveCallHooks.add(
|
|
@@ -433,49 +476,6 @@ export class Call {
|
|
|
433
476
|
);
|
|
434
477
|
}
|
|
435
478
|
|
|
436
|
-
private handleOwnCapabilitiesUpdated = async (
|
|
437
|
-
ownCapabilities: OwnCapability[],
|
|
438
|
-
) => {
|
|
439
|
-
// update the permission context.
|
|
440
|
-
this.permissionsContext.setPermissions(ownCapabilities);
|
|
441
|
-
|
|
442
|
-
if (!this.publisher) return;
|
|
443
|
-
|
|
444
|
-
// check if the user still has publishing permissions and stop publishing if not.
|
|
445
|
-
const permissionToTrackType = {
|
|
446
|
-
[OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
|
|
447
|
-
[OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
|
|
448
|
-
[OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
|
|
449
|
-
};
|
|
450
|
-
for (const [permission, trackType] of Object.entries(
|
|
451
|
-
permissionToTrackType,
|
|
452
|
-
)) {
|
|
453
|
-
const hasPermission = this.permissionsContext.hasPermission(
|
|
454
|
-
permission as OwnCapability,
|
|
455
|
-
);
|
|
456
|
-
if (hasPermission) continue;
|
|
457
|
-
try {
|
|
458
|
-
switch (trackType) {
|
|
459
|
-
case TrackType.AUDIO:
|
|
460
|
-
if (this.microphone.enabled) await this.microphone.disable();
|
|
461
|
-
break;
|
|
462
|
-
case TrackType.VIDEO:
|
|
463
|
-
if (this.camera.enabled) await this.camera.disable();
|
|
464
|
-
break;
|
|
465
|
-
case TrackType.SCREEN_SHARE:
|
|
466
|
-
if (this.screenShare.enabled) await this.screenShare.disable();
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
} catch (err) {
|
|
470
|
-
this.logger(
|
|
471
|
-
'error',
|
|
472
|
-
`Can't disable mic/camera/screenshare after revoked permissions`,
|
|
473
|
-
err,
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
|
|
479
479
|
/**
|
|
480
480
|
* You can subscribe to WebSocket events provided by the API. To remove a subscription, call the `off` method.
|
|
481
481
|
* Please note that subscribing to WebSocket events is an advanced use-case.
|
|
@@ -541,9 +541,11 @@ export class Call {
|
|
|
541
541
|
}
|
|
542
542
|
|
|
543
543
|
if (callingState === CallingState.JOINING) {
|
|
544
|
-
await this.
|
|
544
|
+
await this.assertCallJoined();
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
this.isLeaving = true;
|
|
548
|
+
|
|
547
549
|
if (this.ringing) {
|
|
548
550
|
// I'm the one who started the call, so I should cancel it.
|
|
549
551
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
@@ -570,10 +572,10 @@ export class Call {
|
|
|
570
572
|
this.subscriber?.close();
|
|
571
573
|
this.subscriber = undefined;
|
|
572
574
|
|
|
573
|
-
this.publisher?.close(
|
|
575
|
+
this.publisher?.close();
|
|
574
576
|
this.publisher = undefined;
|
|
575
577
|
|
|
576
|
-
|
|
578
|
+
this.sfuClient?.close(StreamSfuClient.NORMAL_CLOSURE, reason);
|
|
577
579
|
this.sfuClient = undefined;
|
|
578
580
|
|
|
579
581
|
this.state.setCallingState(CallingState.LEFT);
|
|
@@ -581,7 +583,6 @@ export class Call {
|
|
|
581
583
|
// Call all leave call hooks, e.g. to clean up global event handlers
|
|
582
584
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
583
585
|
this.initialized = false;
|
|
584
|
-
this.hasJoinedOnce = false;
|
|
585
586
|
this.clientStore.unregisterCall(this);
|
|
586
587
|
|
|
587
588
|
this.camera.dispose();
|
|
@@ -656,7 +657,7 @@ export class Call {
|
|
|
656
657
|
this.clientStore.registerCall(this);
|
|
657
658
|
}
|
|
658
659
|
|
|
659
|
-
await this.applyDeviceConfig(
|
|
660
|
+
await this.applyDeviceConfig();
|
|
660
661
|
|
|
661
662
|
return response;
|
|
662
663
|
};
|
|
@@ -687,7 +688,7 @@ export class Call {
|
|
|
687
688
|
this.clientStore.registerCall(this);
|
|
688
689
|
}
|
|
689
690
|
|
|
690
|
-
await this.applyDeviceConfig(
|
|
691
|
+
await this.applyDeviceConfig();
|
|
691
692
|
|
|
692
693
|
return response;
|
|
693
694
|
};
|
|
@@ -755,218 +756,326 @@ export class Call {
|
|
|
755
756
|
await this.setup();
|
|
756
757
|
const callingState = this.state.callingState;
|
|
757
758
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
758
|
-
|
|
759
|
+
this.logger(
|
|
760
|
+
'warn',
|
|
761
|
+
'Join method called twice, you should only call this once',
|
|
762
|
+
);
|
|
763
|
+
throw new Error(`Illegal State: Already joined.`);
|
|
759
764
|
}
|
|
760
765
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
this.logger('debug', 'Starting join flow');
|
|
766
|
+
const isMigrating = callingState === CallingState.MIGRATING;
|
|
767
|
+
const isReconnecting = callingState === CallingState.RECONNECTING;
|
|
764
768
|
this.state.setCallingState(CallingState.JOINING);
|
|
769
|
+
this.logger('debug', 'Starting join flow');
|
|
765
770
|
|
|
766
|
-
|
|
767
|
-
this.
|
|
768
|
-
|
|
769
|
-
this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
770
|
-
const performingFastReconnect =
|
|
771
|
-
this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
771
|
+
if (data?.ring && !this.ringing) {
|
|
772
|
+
this.ringingSubject.next(true);
|
|
773
|
+
}
|
|
772
774
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
this.
|
|
787
|
-
|
|
775
|
+
if (this.ringing && !this.isCreatedByMe) {
|
|
776
|
+
// signals other users that I have accepted the incoming call.
|
|
777
|
+
await this.accept();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
let sfuServer: SFUResponse;
|
|
781
|
+
let sfuToken: string;
|
|
782
|
+
let connectionConfig: RTCConfiguration | undefined;
|
|
783
|
+
let statsOptions: StatsOptions | undefined;
|
|
784
|
+
try {
|
|
785
|
+
if (this.sfuClient?.isFastReconnecting) {
|
|
786
|
+
// use previous SFU configuration and values
|
|
787
|
+
connectionConfig = this.publisher?.connectionConfiguration;
|
|
788
|
+
sfuServer = this.sfuClient.sfuServer;
|
|
789
|
+
sfuToken = this.sfuClient.token;
|
|
790
|
+
statsOptions = this.sfuStatsReporter?.options;
|
|
791
|
+
} else {
|
|
792
|
+
// full join flow - let the Coordinator pick a new SFU for us
|
|
793
|
+
const call = await join(this.streamClient, this.type, this.id, data);
|
|
794
|
+
this.state.updateFromCallResponse(call.metadata);
|
|
795
|
+
this.state.setMembers(call.members);
|
|
796
|
+
this.state.setOwnCapabilities(call.ownCapabilities);
|
|
797
|
+
connectionConfig = call.connectionConfig;
|
|
798
|
+
sfuServer = call.sfuServer;
|
|
799
|
+
sfuToken = call.token;
|
|
800
|
+
statsOptions = call.statsOptions;
|
|
788
801
|
}
|
|
802
|
+
|
|
803
|
+
if (this.streamClient._hasConnectionID()) {
|
|
804
|
+
this.watching = true;
|
|
805
|
+
this.clientStore.registerCall(this);
|
|
806
|
+
}
|
|
807
|
+
} catch (error) {
|
|
808
|
+
// restore the previous call state if the join-flow fails
|
|
809
|
+
this.state.setCallingState(callingState);
|
|
810
|
+
throw error;
|
|
789
811
|
}
|
|
790
812
|
|
|
791
813
|
const previousSfuClient = this.sfuClient;
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
?
|
|
818
|
-
:
|
|
819
|
-
|
|
820
|
-
subscriberSdp: receivingCapabilitiesSdp,
|
|
821
|
-
clientDetails,
|
|
822
|
-
fastReconnect: performingFastReconnect,
|
|
823
|
-
reconnectDetails,
|
|
824
|
-
});
|
|
814
|
+
const sfuClient = (this.sfuClient = new StreamSfuClient({
|
|
815
|
+
dispatcher: this.dispatcher,
|
|
816
|
+
sfuServer,
|
|
817
|
+
token: sfuToken,
|
|
818
|
+
sessionId: previousSfuClient?.sessionId,
|
|
819
|
+
}));
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* A closure which hides away the re-connection logic.
|
|
823
|
+
*/
|
|
824
|
+
const reconnect = async (
|
|
825
|
+
strategy: 'full' | 'fast' | 'migrate',
|
|
826
|
+
reason: string,
|
|
827
|
+
): Promise<void> => {
|
|
828
|
+
const currentState = this.state.callingState;
|
|
829
|
+
if (
|
|
830
|
+
currentState === CallingState.MIGRATING ||
|
|
831
|
+
currentState === CallingState.RECONNECTING
|
|
832
|
+
) {
|
|
833
|
+
// prevent parallel reconnection attempts
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
this.reconnectAttempts++;
|
|
837
|
+
this.state.setCallingState(
|
|
838
|
+
strategy === 'migrate'
|
|
839
|
+
? CallingState.MIGRATING
|
|
840
|
+
: CallingState.RECONNECTING,
|
|
841
|
+
);
|
|
825
842
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
843
|
+
if (strategy === 'migrate') {
|
|
844
|
+
this.logger(
|
|
845
|
+
'debug',
|
|
846
|
+
`[Migration]: migrating call ${this.cid} away from ${sfuServer.edge_name}`,
|
|
847
|
+
);
|
|
848
|
+
sfuClient.isMigratingAway = true;
|
|
849
|
+
} else {
|
|
850
|
+
this.logger(
|
|
851
|
+
'debug',
|
|
852
|
+
`[Rejoin]: ${strategy} rejoin call ${this.cid} (${this.reconnectAttempts})...`,
|
|
832
853
|
);
|
|
833
854
|
}
|
|
834
|
-
}
|
|
835
855
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
856
|
+
// take a snapshot of the current "local participant" state
|
|
857
|
+
// we'll need it for restoring the previous publishing state later
|
|
858
|
+
const localParticipant = this.state.localParticipant;
|
|
859
|
+
|
|
860
|
+
if (strategy === 'fast') {
|
|
861
|
+
sfuClient.close(
|
|
862
|
+
StreamSfuClient.ERROR_CONNECTION_BROKEN,
|
|
863
|
+
`attempting fast reconnect: ${reason}`,
|
|
864
|
+
);
|
|
865
|
+
} else if (strategy === 'full') {
|
|
866
|
+
// in migration or recovery scenarios, we don't want to
|
|
867
|
+
// wait before attempting to reconnect to an SFU server
|
|
868
|
+
await sleep(retryInterval(this.reconnectAttempts));
|
|
869
|
+
|
|
870
|
+
// in full-reconnect, we need to dispose all Peer Connections
|
|
871
|
+
this.subscriber?.close();
|
|
872
|
+
this.subscriber = undefined;
|
|
873
|
+
this.publisher?.close({ stopTracks: false });
|
|
874
|
+
this.publisher = undefined;
|
|
875
|
+
this.statsReporter?.stop();
|
|
876
|
+
this.statsReporter = undefined;
|
|
877
|
+
this.sfuStatsReporter?.stop();
|
|
878
|
+
this.sfuStatsReporter = undefined;
|
|
879
|
+
|
|
880
|
+
// clean up current connection
|
|
881
|
+
sfuClient.close(
|
|
882
|
+
StreamSfuClient.NORMAL_CLOSURE,
|
|
883
|
+
`attempting full reconnect: ${reason}`,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
await this.join({
|
|
887
|
+
...data,
|
|
888
|
+
...(strategy === 'migrate' && { migrating_from: sfuServer.edge_name }),
|
|
853
889
|
});
|
|
854
|
-
}
|
|
855
890
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
);
|
|
861
|
-
} else if (!isWsHealthy) {
|
|
862
|
-
previousSfuClient?.close(4002, 'Closing unhealthy WS after reconnect');
|
|
863
|
-
}
|
|
891
|
+
// clean up previous connection
|
|
892
|
+
if (strategy === 'migrate') {
|
|
893
|
+
sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'attempting migration');
|
|
894
|
+
}
|
|
864
895
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
896
|
+
this.logger(
|
|
897
|
+
'info',
|
|
898
|
+
`[Rejoin]: Attempt ${this.reconnectAttempts} successful!`,
|
|
899
|
+
);
|
|
900
|
+
// we shouldn't be republishing the streams if we're migrating
|
|
901
|
+
// as the underlying peer connection will take care of it as part
|
|
902
|
+
// of the ice-restart process
|
|
903
|
+
if (localParticipant && strategy === 'full') {
|
|
904
|
+
const {
|
|
905
|
+
audioStream,
|
|
906
|
+
videoStream,
|
|
907
|
+
screenShareStream,
|
|
908
|
+
screenShareAudioStream,
|
|
909
|
+
} = localParticipant;
|
|
910
|
+
|
|
911
|
+
let screenShare: MediaStream | undefined;
|
|
912
|
+
if (screenShareStream || screenShareAudioStream) {
|
|
913
|
+
screenShare = new MediaStream();
|
|
914
|
+
screenShareStream?.getVideoTracks().forEach((track) => {
|
|
915
|
+
screenShare?.addTrack(track);
|
|
916
|
+
});
|
|
917
|
+
screenShareAudioStream?.getAudioTracks().forEach((track) => {
|
|
918
|
+
screenShare?.addTrack(track);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
873
921
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const strategy = this.reconnectStrategy;
|
|
883
|
-
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
884
|
-
const announcedTracks = this.publisher?.getAnnouncedTracks() || [];
|
|
885
|
-
const subscribedTracks = getCurrentValue(this.trackSubscriptionsSubject);
|
|
886
|
-
return {
|
|
887
|
-
strategy,
|
|
888
|
-
announcedTracks,
|
|
889
|
-
subscriptions: subscribedTracks.data || [],
|
|
890
|
-
reconnectAttempt: this.reconnectAttempts,
|
|
891
|
-
fromSfuId: migratingFromSfuId || '',
|
|
892
|
-
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
893
|
-
};
|
|
894
|
-
};
|
|
922
|
+
// restore previous publishing state
|
|
923
|
+
if (audioStream) await this.publishAudioStream(audioStream);
|
|
924
|
+
if (videoStream) {
|
|
925
|
+
await this.publishVideoStream(videoStream, {
|
|
926
|
+
preferredCodec: this.camera.preferredCodec,
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
if (screenShare) await this.publishScreenShareStream(screenShare);
|
|
895
930
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
* This method can throw an error if the ICE restart fails.
|
|
901
|
-
* This error should be handled by the reconnect loop,
|
|
902
|
-
* and a new reconnection shall be attempted.
|
|
903
|
-
*
|
|
904
|
-
* @internal
|
|
905
|
-
*/
|
|
906
|
-
private restoreICE = async (
|
|
907
|
-
nextSfuClient: StreamSfuClient,
|
|
908
|
-
opts: { includeSubscriber?: boolean; includePublisher?: boolean } = {},
|
|
909
|
-
) => {
|
|
910
|
-
const { includeSubscriber = true, includePublisher = true } = opts;
|
|
911
|
-
if (this.subscriber) {
|
|
912
|
-
this.subscriber.setSfuClient(nextSfuClient);
|
|
913
|
-
if (includeSubscriber) {
|
|
914
|
-
await this.subscriber.restartIce();
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
if (this.publisher) {
|
|
918
|
-
this.publisher.setSfuClient(nextSfuClient);
|
|
919
|
-
if (includePublisher) {
|
|
920
|
-
await this.publisher.restartIce();
|
|
931
|
+
this.logger(
|
|
932
|
+
'info',
|
|
933
|
+
`[Rejoin]: State restored. Attempt: ${this.reconnectAttempts}`,
|
|
934
|
+
);
|
|
921
935
|
}
|
|
922
|
-
}
|
|
923
|
-
};
|
|
936
|
+
};
|
|
924
937
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
sfuClient,
|
|
938
|
-
connectionConfig,
|
|
939
|
-
clientDetails,
|
|
940
|
-
statsOptions,
|
|
941
|
-
closePreviousInstances,
|
|
942
|
-
} = opts;
|
|
943
|
-
if (closePreviousInstances && this.subscriber) {
|
|
944
|
-
this.subscriber.close();
|
|
945
|
-
}
|
|
946
|
-
this.subscriber = new Subscriber({
|
|
947
|
-
sfuClient,
|
|
948
|
-
dispatcher: this.dispatcher,
|
|
949
|
-
state: this.state,
|
|
950
|
-
connectionConfig,
|
|
951
|
-
logTag: String(this.reconnectAttempts),
|
|
952
|
-
onUnrecoverableError: () => {
|
|
953
|
-
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
938
|
+
// reconnect if the connection was closed unexpectedly. example:
|
|
939
|
+
// - SFU crash or restart
|
|
940
|
+
// - network change
|
|
941
|
+
sfuClient.signalReady.then(() => {
|
|
942
|
+
// register a handler for the "goAway" event
|
|
943
|
+
const unregisterGoAway = this.dispatcher.on('goAway', (event) => {
|
|
944
|
+
const { reason } = event;
|
|
945
|
+
this.logger(
|
|
946
|
+
'info',
|
|
947
|
+
`[Migration]: Going away from SFU... Reason: ${GoAwayReason[reason]}`,
|
|
948
|
+
);
|
|
949
|
+
reconnect('migrate', GoAwayReason[reason]).catch((err) => {
|
|
954
950
|
this.logger(
|
|
955
951
|
'warn',
|
|
956
|
-
|
|
952
|
+
`[Migration]: Failed to migrate to another SFU.`,
|
|
957
953
|
err,
|
|
958
954
|
);
|
|
959
955
|
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
sfuClient.signalWs.addEventListener('close', (e) => {
|
|
959
|
+
// unregister the "goAway" handler, as we won't need it anymore for this connection.
|
|
960
|
+
// the upcoming re-join will register a new handler anyway
|
|
961
|
+
unregisterGoAway();
|
|
962
|
+
// when the user has initiated "call.leave()" operation, we shouldn't
|
|
963
|
+
// care for the WS close code and we shouldn't ever attempt to reconnect
|
|
964
|
+
if (this.isLeaving) return;
|
|
965
|
+
// do nothing if the connection was closed on purpose
|
|
966
|
+
if (e.code === StreamSfuClient.NORMAL_CLOSURE) return;
|
|
967
|
+
// do nothing if the connection was closed because of a policy violation
|
|
968
|
+
// e.g., the user has been blocked by an admin or moderator
|
|
969
|
+
if (e.code === KnownCodes.WS_POLICY_VIOLATION) return;
|
|
970
|
+
// When the SFU is being shut down, it sends a goAway message.
|
|
971
|
+
// While we migrate to another SFU, we might have the WS connection
|
|
972
|
+
// to the old SFU closed abruptly. In this case, we don't want
|
|
973
|
+
// to reconnect to the old SFU, but rather to the new one.
|
|
974
|
+
const isMigratingAway =
|
|
975
|
+
e.code === KnownCodes.WS_CLOSED_ABRUPTLY && sfuClient.isMigratingAway;
|
|
976
|
+
const isFastReconnecting =
|
|
977
|
+
e.code === KnownCodes.WS_CLOSED_ABRUPTLY &&
|
|
978
|
+
sfuClient.isFastReconnecting;
|
|
979
|
+
if (isMigratingAway || isFastReconnecting) return;
|
|
980
|
+
|
|
981
|
+
// do nothing if the connection was closed because of a fast reconnect
|
|
982
|
+
if (e.code === StreamSfuClient.ERROR_CONNECTION_BROKEN) return;
|
|
983
|
+
|
|
984
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
985
|
+
sfuClient.isFastReconnecting = this.reconnectAttempts === 0;
|
|
986
|
+
const strategy = sfuClient.isFastReconnecting ? 'fast' : 'full';
|
|
987
|
+
reconnect(strategy, `SFU closed the WS with code: ${e.code}`).catch(
|
|
988
|
+
(err) => {
|
|
989
|
+
this.logger(
|
|
990
|
+
'error',
|
|
991
|
+
`[Rejoin]: ${strategy} rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
992
|
+
err,
|
|
993
|
+
);
|
|
994
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
995
|
+
},
|
|
996
|
+
);
|
|
997
|
+
} else {
|
|
998
|
+
this.logger(
|
|
999
|
+
'error',
|
|
1000
|
+
'[Rejoin]: Reconnect attempts exceeded. Giving up...',
|
|
1001
|
+
);
|
|
1002
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// handlers for connection online/offline events
|
|
1008
|
+
const unsubscribeOnlineEvent = this.streamClient.on(
|
|
1009
|
+
'connection.changed',
|
|
1010
|
+
async (e) => {
|
|
1011
|
+
if (e.type !== 'connection.changed') return;
|
|
1012
|
+
if (!e.online) return;
|
|
1013
|
+
unsubscribeOnlineEvent();
|
|
1014
|
+
const currentCallingState = this.state.callingState;
|
|
1015
|
+
const shouldReconnect =
|
|
1016
|
+
currentCallingState === CallingState.OFFLINE ||
|
|
1017
|
+
currentCallingState === CallingState.RECONNECTING_FAILED;
|
|
1018
|
+
if (!shouldReconnect) return;
|
|
1019
|
+
this.logger('info', '[Rejoin]: Going online...');
|
|
1020
|
+
let isFirstReconnectAttempt = true;
|
|
1021
|
+
do {
|
|
1022
|
+
try {
|
|
1023
|
+
sfuClient.isFastReconnecting = isFirstReconnectAttempt;
|
|
1024
|
+
await reconnect(
|
|
1025
|
+
isFirstReconnectAttempt ? 'fast' : 'full',
|
|
1026
|
+
'Network: online',
|
|
1027
|
+
);
|
|
1028
|
+
return; // break the loop if rejoin is successful
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
this.logger(
|
|
1031
|
+
'error',
|
|
1032
|
+
`[Rejoin][Network]: Rejoin failed for attempt ${this.reconnectAttempts}`,
|
|
1033
|
+
err,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
// wait for a bit before trying to reconnect again
|
|
1037
|
+
await sleep(retryInterval(this.reconnectAttempts));
|
|
1038
|
+
isFirstReconnectAttempt = false;
|
|
1039
|
+
} while (this.reconnectAttempts < this.maxReconnectAttempts);
|
|
1040
|
+
|
|
1041
|
+
// if we're here, it means that we've exhausted all the reconnect attempts
|
|
1042
|
+
this.logger('error', `[Rejoin][Network]: Rejoin failed. Giving up.`);
|
|
1043
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
1044
|
+
},
|
|
1045
|
+
);
|
|
1046
|
+
const unsubscribeOfflineEvent = this.streamClient.on(
|
|
1047
|
+
'connection.changed',
|
|
1048
|
+
(e) => {
|
|
1049
|
+
if (e.type !== 'connection.changed') return;
|
|
1050
|
+
if (e.online) return;
|
|
1051
|
+
unsubscribeOfflineEvent();
|
|
1052
|
+
this.state.setCallingState(CallingState.OFFLINE);
|
|
960
1053
|
},
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
this.leaveCallHooks.add(() => {
|
|
1057
|
+
unsubscribeOnlineEvent();
|
|
1058
|
+
unsubscribeOfflineEvent();
|
|
961
1059
|
});
|
|
962
1060
|
|
|
1061
|
+
if (!this.subscriber) {
|
|
1062
|
+
this.subscriber = new Subscriber({
|
|
1063
|
+
sfuClient,
|
|
1064
|
+
dispatcher: this.dispatcher,
|
|
1065
|
+
state: this.state,
|
|
1066
|
+
connectionConfig,
|
|
1067
|
+
onUnrecoverableError: () => {
|
|
1068
|
+
reconnect('full', 'unrecoverable subscriber error').catch((err) => {
|
|
1069
|
+
this.logger('debug', '[Rejoin]: Rejoin failed', err);
|
|
1070
|
+
});
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
963
1075
|
// anonymous users can't publish anything hence, there is no need
|
|
964
1076
|
// to create Publisher Peer Connection for them
|
|
965
1077
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
966
|
-
if (!isAnonymous) {
|
|
967
|
-
if (closePreviousInstances && this.publisher) {
|
|
968
|
-
this.publisher.close({ stopTracks: false });
|
|
969
|
-
}
|
|
1078
|
+
if (!this.publisher && !isAnonymous) {
|
|
970
1079
|
const audioSettings = this.state.settings?.audio;
|
|
971
1080
|
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
972
1081
|
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
@@ -977,29 +1086,25 @@ export class Call {
|
|
|
977
1086
|
connectionConfig,
|
|
978
1087
|
isDtxEnabled,
|
|
979
1088
|
isRedEnabled,
|
|
980
|
-
logTag: String(this.reconnectAttempts),
|
|
981
1089
|
onUnrecoverableError: () => {
|
|
982
|
-
|
|
983
|
-
this.logger(
|
|
984
|
-
'warn',
|
|
985
|
-
'[Reconnect] Error reconnecting after a publisher error',
|
|
986
|
-
err,
|
|
987
|
-
);
|
|
1090
|
+
reconnect('full', 'unrecoverable publisher error').catch((err) => {
|
|
1091
|
+
this.logger('debug', '[Rejoin]: Rejoin failed', err);
|
|
988
1092
|
});
|
|
989
1093
|
},
|
|
990
1094
|
});
|
|
991
1095
|
}
|
|
992
1096
|
|
|
993
|
-
this.statsReporter
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1097
|
+
if (!this.statsReporter) {
|
|
1098
|
+
this.statsReporter = createStatsReporter({
|
|
1099
|
+
subscriber: this.subscriber,
|
|
1100
|
+
publisher: this.publisher,
|
|
1101
|
+
state: this.state,
|
|
1102
|
+
datacenter: this.sfuClient.edgeName,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1000
1105
|
|
|
1001
|
-
|
|
1002
|
-
if (
|
|
1106
|
+
const clientDetails = getClientDetails();
|
|
1107
|
+
if (!this.sfuStatsReporter && statsOptions) {
|
|
1003
1108
|
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
|
|
1004
1109
|
clientDetails,
|
|
1005
1110
|
options: statsOptions,
|
|
@@ -1008,316 +1113,152 @@ export class Call {
|
|
|
1008
1113
|
});
|
|
1009
1114
|
this.sfuStatsReporter.start();
|
|
1010
1115
|
}
|
|
1011
|
-
};
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* Retrieves credentials for joining the call.
|
|
1015
|
-
*
|
|
1016
|
-
* @internal
|
|
1017
|
-
*
|
|
1018
|
-
* @param data the join call data.
|
|
1019
|
-
*/
|
|
1020
|
-
doJoinRequest = async (data?: JoinCallData): Promise<JoinCallResponse> => {
|
|
1021
|
-
const location = await this.streamClient.getLocationHint();
|
|
1022
|
-
const request: JoinCallRequest = { ...data, location };
|
|
1023
|
-
const joinResponse = await this.streamClient.post<
|
|
1024
|
-
JoinCallResponse,
|
|
1025
|
-
JoinCallRequest
|
|
1026
|
-
>(`${this.streamClientBasePath}/join`, request);
|
|
1027
|
-
this.state.updateFromCallResponse(joinResponse.call);
|
|
1028
|
-
this.state.setMembers(joinResponse.members);
|
|
1029
|
-
this.state.setOwnCapabilities(joinResponse.own_capabilities);
|
|
1030
|
-
|
|
1031
|
-
if (data?.ring && !this.ringing) {
|
|
1032
|
-
this.ringingSubject.next(true);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (this.ringing && !this.isCreatedByMe) {
|
|
1036
|
-
// signals other users that I have accepted the incoming call.
|
|
1037
|
-
await this.accept();
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
if (this.streamClient._hasConnectionID()) {
|
|
1041
|
-
this.watching = true;
|
|
1042
|
-
this.clientStore.registerCall(this);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
return joinResponse;
|
|
1046
|
-
};
|
|
1047
1116
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1117
|
+
try {
|
|
1118
|
+
// 1. wait for the signal server to be ready before sending "joinRequest"
|
|
1119
|
+
sfuClient.signalReady
|
|
1120
|
+
.catch((err) => this.logger('error', 'Signal ready failed', err))
|
|
1121
|
+
// prepare a generic SDP and send it to the SFU.
|
|
1122
|
+
// this is a throw-away SDP that the SFU will use to determine
|
|
1123
|
+
// the capabilities of the client (codec support, etc.)
|
|
1124
|
+
.then(() => getGenericSdp('recvonly'))
|
|
1125
|
+
.then((sdp) => {
|
|
1126
|
+
const subscriptions = getCurrentValue(this.trackSubscriptionsSubject);
|
|
1127
|
+
const migration: Migration | undefined = isMigrating
|
|
1128
|
+
? {
|
|
1129
|
+
fromSfuId: data?.migrating_from || '',
|
|
1130
|
+
subscriptions: subscriptions.data || [],
|
|
1131
|
+
announcedTracks: this.publisher?.getCurrentTrackInfos() || [],
|
|
1132
|
+
}
|
|
1133
|
+
: undefined;
|
|
1062
1134
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
private reconnect = async (
|
|
1071
|
-
strategy: WebsocketReconnectStrategy,
|
|
1072
|
-
): Promise<void> => {
|
|
1073
|
-
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
1074
|
-
this.logger(
|
|
1075
|
-
'info',
|
|
1076
|
-
`[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`,
|
|
1077
|
-
);
|
|
1135
|
+
return sfuClient.join({
|
|
1136
|
+
subscriberSdp: sdp || '',
|
|
1137
|
+
clientDetails,
|
|
1138
|
+
migration,
|
|
1139
|
+
fastReconnect: previousSfuClient?.isFastReconnecting ?? false,
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1078
1142
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
break;
|
|
1099
|
-
default:
|
|
1100
|
-
ensureExhausted(
|
|
1101
|
-
this.reconnectStrategy,
|
|
1102
|
-
'Unknown reconnection strategy',
|
|
1103
|
-
);
|
|
1104
|
-
break;
|
|
1143
|
+
// 2. in parallel, wait for the SFU to send us the "joinResponse"
|
|
1144
|
+
// this will throw an error if the SFU rejects the join request or
|
|
1145
|
+
// fails to respond in time
|
|
1146
|
+
const { callState, reconnected } = await this.waitForJoinResponse();
|
|
1147
|
+
if (isReconnecting) {
|
|
1148
|
+
this.logger('debug', '[Rejoin] fast reconnected:', reconnected);
|
|
1149
|
+
}
|
|
1150
|
+
if (isMigrating) {
|
|
1151
|
+
await this.subscriber.migrateTo(sfuClient, connectionConfig);
|
|
1152
|
+
await this.publisher?.migrateTo(sfuClient, connectionConfig);
|
|
1153
|
+
} else if (isReconnecting) {
|
|
1154
|
+
if (reconnected) {
|
|
1155
|
+
// update the SFU client instance on the subscriber and publisher
|
|
1156
|
+
this.subscriber.setSfuClient(sfuClient);
|
|
1157
|
+
// publisher might not be there (anonymous users)
|
|
1158
|
+
if (this.publisher) {
|
|
1159
|
+
this.publisher.setSfuClient(sfuClient);
|
|
1160
|
+
// and perform a full ICE restart on the publisher
|
|
1161
|
+
await this.publisher.restartIce();
|
|
1105
1162
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
this.reconnectAttempts++;
|
|
1163
|
+
} else if (previousSfuClient?.isFastReconnecting) {
|
|
1164
|
+
// reconnection wasn't possible, so we need to do a full rejoin
|
|
1165
|
+
return await reconnect('full', 're-attempting').catch((err) => {
|
|
1166
|
+
this.logger(
|
|
1167
|
+
'error',
|
|
1168
|
+
`[Rejoin]: Rejoin failed forced full rejoin.`,
|
|
1169
|
+
err,
|
|
1170
|
+
);
|
|
1171
|
+
});
|
|
1116
1172
|
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
/**
|
|
1147
|
-
* Initiates the reconnection flow with the "migrate" strategy.
|
|
1148
|
-
* @internal
|
|
1149
|
-
*/
|
|
1150
|
-
private reconnectMigrate = async () => {
|
|
1151
|
-
const currentSfuClient = this.sfuClient;
|
|
1152
|
-
if (!currentSfuClient) {
|
|
1153
|
-
throw new Error('Cannot migrate without an active SFU client');
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.MIGRATE;
|
|
1157
|
-
this.state.setCallingState(CallingState.MIGRATING);
|
|
1158
|
-
const currentSubscriber = this.subscriber;
|
|
1159
|
-
const currentPublisher = this.publisher;
|
|
1160
|
-
|
|
1161
|
-
currentSubscriber?.detachEventHandlers();
|
|
1162
|
-
currentPublisher?.detachEventHandlers();
|
|
1173
|
+
}
|
|
1174
|
+
const currentParticipants = callState?.participants || [];
|
|
1175
|
+
const participantCount = callState?.participantCount;
|
|
1176
|
+
const startedAt = callState?.startedAt
|
|
1177
|
+
? Timestamp.toDate(callState.startedAt)
|
|
1178
|
+
: new Date();
|
|
1179
|
+
const pins = callState?.pins ?? [];
|
|
1180
|
+
this.state.setParticipants(() => {
|
|
1181
|
+
const participantLookup = this.state.getParticipantLookupBySessionId();
|
|
1182
|
+
return currentParticipants.map<StreamVideoParticipant>((p) => {
|
|
1183
|
+
// We need to preserve the local state of the participant
|
|
1184
|
+
// (e.g. videoDimension, visibilityState, pinnedAt, etc.)
|
|
1185
|
+
// as it doesn't exist on the server.
|
|
1186
|
+
const existingParticipant = participantLookup[p.sessionId];
|
|
1187
|
+
return Object.assign(p, existingParticipant, {
|
|
1188
|
+
isLocalParticipant: p.sessionId === sfuClient.sessionId,
|
|
1189
|
+
viewportVisibilityState:
|
|
1190
|
+
existingParticipant?.viewportVisibilityState ?? {
|
|
1191
|
+
videoTrack: VisibilityState.UNKNOWN,
|
|
1192
|
+
screenShareTrack: VisibilityState.UNKNOWN,
|
|
1193
|
+
},
|
|
1194
|
+
} satisfies Partial<StreamVideoParticipant>);
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
this.state.setParticipantCount(participantCount?.total || 0);
|
|
1198
|
+
this.state.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
1199
|
+
this.state.setStartedAt(startedAt);
|
|
1200
|
+
this.state.setServerSidePins(pins);
|
|
1163
1201
|
|
|
1164
|
-
|
|
1202
|
+
this.reconnectAttempts = 0; // reset the reconnect attempts counter
|
|
1203
|
+
this.state.setCallingState(CallingState.JOINED);
|
|
1165
1204
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1205
|
+
try {
|
|
1206
|
+
await this.initCamera({ setStatus: true });
|
|
1207
|
+
await this.initMic({ setStatus: true });
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
this.logger(
|
|
1210
|
+
'warn',
|
|
1211
|
+
'Camera and/or mic init failed during join call',
|
|
1212
|
+
error,
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1174
1215
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1216
|
+
// 3. once we have the "joinResponse", and possibly reconciled the local state
|
|
1217
|
+
// we schedule a fast subscription update for all remote participants
|
|
1218
|
+
// that were visible before we reconnected or migrated to a new SFU.
|
|
1219
|
+
const { remoteParticipants } = this.state;
|
|
1220
|
+
if (remoteParticipants.length > 0) {
|
|
1221
|
+
this.updateSubscriptions(remoteParticipants, DebounceType.FAST);
|
|
1222
|
+
}
|
|
1177
1223
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
//
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1224
|
+
this.logger('info', `Joined call ${this.cid}`);
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
// join failed, try to rejoin
|
|
1227
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
1228
|
+
this.logger(
|
|
1229
|
+
'error',
|
|
1230
|
+
`[Rejoin]: Rejoin ${this.reconnectAttempts} failed.`,
|
|
1231
|
+
err,
|
|
1232
|
+
);
|
|
1233
|
+
await reconnect('full', 'previous attempt failed');
|
|
1234
|
+
this.logger(
|
|
1235
|
+
'info',
|
|
1236
|
+
`[Rejoin]: Rejoin ${this.reconnectAttempts} successful!`,
|
|
1237
|
+
);
|
|
1238
|
+
} else {
|
|
1239
|
+
this.logger(
|
|
1240
|
+
'error',
|
|
1241
|
+
`[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
1242
|
+
);
|
|
1243
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
1244
|
+
throw new Error('Join failed');
|
|
1245
|
+
}
|
|
1189
1246
|
}
|
|
1190
1247
|
};
|
|
1191
1248
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
// handles the legacy "goAway" event
|
|
1199
|
-
const unregisterGoAway = this.on('goAway', () => {
|
|
1200
|
-
this.reconnect(WebsocketReconnectStrategy.MIGRATE).catch((err) => {
|
|
1201
|
-
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
1249
|
+
private waitForJoinResponse = (timeout: number = 5000) => {
|
|
1250
|
+
return new Promise<JoinResponse>((resolve, reject) => {
|
|
1251
|
+
const unsubscribe = this.on('joinResponse', (event) => {
|
|
1252
|
+
clearTimeout(timeoutId);
|
|
1253
|
+
unsubscribe();
|
|
1254
|
+
resolve(event);
|
|
1202
1255
|
});
|
|
1203
|
-
});
|
|
1204
1256
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
1210
|
-
this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
|
|
1211
|
-
this.logger('warn', `Can't leave call after disconnect request`, err);
|
|
1212
|
-
});
|
|
1213
|
-
} else {
|
|
1214
|
-
this.reconnect(strategy).catch((err) => {
|
|
1215
|
-
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1257
|
+
const timeoutId = setTimeout(() => {
|
|
1258
|
+
unsubscribe();
|
|
1259
|
+
reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
1260
|
+
}, timeout);
|
|
1218
1261
|
});
|
|
1219
|
-
|
|
1220
|
-
const unregisterNetworkChanged = this.streamClient.on(
|
|
1221
|
-
'network.changed',
|
|
1222
|
-
(e) => {
|
|
1223
|
-
if (!e.online) {
|
|
1224
|
-
this.logger('debug', '[Reconnect] Going offline');
|
|
1225
|
-
if (!this.hasJoinedOnce) return;
|
|
1226
|
-
this.lastOfflineTimestamp = Date.now();
|
|
1227
|
-
// create a new task that would resolve when the network is available
|
|
1228
|
-
const networkAvailableTask = promiseWithResolvers();
|
|
1229
|
-
networkAvailableTask.promise.then(() => {
|
|
1230
|
-
let strategy = WebsocketReconnectStrategy.FAST;
|
|
1231
|
-
if (this.lastOfflineTimestamp) {
|
|
1232
|
-
const offline = (Date.now() - this.lastOfflineTimestamp) / 1000;
|
|
1233
|
-
if (offline > this.fastReconnectDeadlineSeconds) {
|
|
1234
|
-
// We shouldn't attempt FAST if we have exceeded the deadline.
|
|
1235
|
-
// The SFU would have already wiped out the session.
|
|
1236
|
-
strategy = WebsocketReconnectStrategy.REJOIN;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
this.reconnect(strategy).catch((err) => {
|
|
1241
|
-
this.logger(
|
|
1242
|
-
'warn',
|
|
1243
|
-
'[Reconnect] Error restoring connection after going online',
|
|
1244
|
-
err,
|
|
1245
|
-
);
|
|
1246
|
-
});
|
|
1247
|
-
});
|
|
1248
|
-
this.networkAvailableTask = networkAvailableTask;
|
|
1249
|
-
this.sfuStatsReporter?.stop();
|
|
1250
|
-
this.state.setCallingState(CallingState.OFFLINE);
|
|
1251
|
-
} else {
|
|
1252
|
-
this.logger('debug', '[Reconnect] Going online');
|
|
1253
|
-
// TODO try to remove this .close call
|
|
1254
|
-
this.sfuClient?.close(
|
|
1255
|
-
4002,
|
|
1256
|
-
'Closing WS to reconnect after going online',
|
|
1257
|
-
);
|
|
1258
|
-
// we went online, release the previous waiters and reset the state
|
|
1259
|
-
this.networkAvailableTask?.resolve();
|
|
1260
|
-
this.networkAvailableTask = undefined;
|
|
1261
|
-
this.sfuStatsReporter?.start();
|
|
1262
|
-
}
|
|
1263
|
-
},
|
|
1264
|
-
);
|
|
1265
|
-
|
|
1266
|
-
this.leaveCallHooks.add(unregisterGoAway);
|
|
1267
|
-
this.leaveCallHooks.add(unregisterOnError);
|
|
1268
|
-
this.leaveCallHooks.add(unregisterNetworkChanged);
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
/**
|
|
1272
|
-
* Restores the published tracks after a reconnection.
|
|
1273
|
-
* @internal
|
|
1274
|
-
*/
|
|
1275
|
-
private restorePublishedTracks = async () => {
|
|
1276
|
-
// the tracks need to be restored in their original order of publishing
|
|
1277
|
-
// otherwise, we might get `m-lines order mismatch` errors
|
|
1278
|
-
for (const trackType of this.trackPublishOrder) {
|
|
1279
|
-
switch (trackType) {
|
|
1280
|
-
case TrackType.AUDIO:
|
|
1281
|
-
const audioStream = this.microphone.state.mediaStream;
|
|
1282
|
-
if (audioStream) {
|
|
1283
|
-
await this.publishAudioStream(audioStream);
|
|
1284
|
-
}
|
|
1285
|
-
break;
|
|
1286
|
-
case TrackType.VIDEO:
|
|
1287
|
-
const videoStream = this.camera.state.mediaStream;
|
|
1288
|
-
if (videoStream) {
|
|
1289
|
-
await this.publishVideoStream(videoStream, {
|
|
1290
|
-
preferredCodec: this.camera.preferredCodec,
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
break;
|
|
1294
|
-
case TrackType.SCREEN_SHARE:
|
|
1295
|
-
const screenShareStream = this.screenShare.state.mediaStream;
|
|
1296
|
-
if (screenShareStream) {
|
|
1297
|
-
await this.publishScreenShareStream(screenShareStream, {
|
|
1298
|
-
screenShareSettings: this.screenShare.getSettings(),
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
break;
|
|
1302
|
-
// screen share audio can't exist without a screen share, so we handle it there
|
|
1303
|
-
case TrackType.SCREEN_SHARE_AUDIO:
|
|
1304
|
-
case TrackType.UNSPECIFIED:
|
|
1305
|
-
break;
|
|
1306
|
-
default:
|
|
1307
|
-
ensureExhausted(trackType, 'Unknown track type');
|
|
1308
|
-
break;
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
};
|
|
1312
|
-
|
|
1313
|
-
/**
|
|
1314
|
-
* Restores the subscribed tracks after a reconnection.
|
|
1315
|
-
* @internal
|
|
1316
|
-
*/
|
|
1317
|
-
private restoreSubscribedTracks = () => {
|
|
1318
|
-
const { remoteParticipants } = this.state;
|
|
1319
|
-
if (remoteParticipants.length <= 0) return;
|
|
1320
|
-
this.updateSubscriptions(remoteParticipants, DebounceType.FAST);
|
|
1321
1262
|
};
|
|
1322
1263
|
|
|
1323
1264
|
/**
|
|
@@ -1336,7 +1277,7 @@ export class Call {
|
|
|
1336
1277
|
) => {
|
|
1337
1278
|
// we should wait until we get a JoinResponse from the SFU,
|
|
1338
1279
|
// otherwise we risk breaking the ICETrickle flow.
|
|
1339
|
-
await this.
|
|
1280
|
+
await this.assertCallJoined();
|
|
1340
1281
|
if (!this.publisher) {
|
|
1341
1282
|
this.logger('error', 'Trying to publish video before join is completed');
|
|
1342
1283
|
throw new Error(`Call not joined yet.`);
|
|
@@ -1348,9 +1289,6 @@ export class Call {
|
|
|
1348
1289
|
return;
|
|
1349
1290
|
}
|
|
1350
1291
|
|
|
1351
|
-
if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
|
|
1352
|
-
this.trackPublishOrder.push(TrackType.VIDEO);
|
|
1353
|
-
}
|
|
1354
1292
|
await this.publisher.publishStream(
|
|
1355
1293
|
videoStream,
|
|
1356
1294
|
videoTrack,
|
|
@@ -1371,7 +1309,7 @@ export class Call {
|
|
|
1371
1309
|
publishAudioStream = async (audioStream: MediaStream) => {
|
|
1372
1310
|
// we should wait until we get a JoinResponse from the SFU,
|
|
1373
1311
|
// otherwise we risk breaking the ICETrickle flow.
|
|
1374
|
-
await this.
|
|
1312
|
+
await this.assertCallJoined();
|
|
1375
1313
|
if (!this.publisher) {
|
|
1376
1314
|
this.logger('error', 'Trying to publish audio before join is completed');
|
|
1377
1315
|
throw new Error(`Call not joined yet.`);
|
|
@@ -1383,9 +1321,6 @@ export class Call {
|
|
|
1383
1321
|
return;
|
|
1384
1322
|
}
|
|
1385
1323
|
|
|
1386
|
-
if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
|
|
1387
|
-
this.trackPublishOrder.push(TrackType.AUDIO);
|
|
1388
|
-
}
|
|
1389
1324
|
await this.publisher.publishStream(
|
|
1390
1325
|
audioStream,
|
|
1391
1326
|
audioTrack,
|
|
@@ -1408,7 +1343,7 @@ export class Call {
|
|
|
1408
1343
|
) => {
|
|
1409
1344
|
// we should wait until we get a JoinResponse from the SFU,
|
|
1410
1345
|
// otherwise we risk breaking the ICETrickle flow.
|
|
1411
|
-
await this.
|
|
1346
|
+
await this.assertCallJoined();
|
|
1412
1347
|
if (!this.publisher) {
|
|
1413
1348
|
this.logger(
|
|
1414
1349
|
'error',
|
|
@@ -1426,9 +1361,6 @@ export class Call {
|
|
|
1426
1361
|
return;
|
|
1427
1362
|
}
|
|
1428
1363
|
|
|
1429
|
-
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
|
|
1430
|
-
this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
|
|
1431
|
-
}
|
|
1432
1364
|
await this.publisher.publishStream(
|
|
1433
1365
|
screenShareStream,
|
|
1434
1366
|
screenShareTrack,
|
|
@@ -1438,9 +1370,6 @@ export class Call {
|
|
|
1438
1370
|
|
|
1439
1371
|
const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
|
|
1440
1372
|
if (screenShareAudioTrack) {
|
|
1441
|
-
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
1442
|
-
this.trackPublishOrder.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
1443
|
-
}
|
|
1444
1373
|
await this.publisher.publishStream(
|
|
1445
1374
|
screenShareStream,
|
|
1446
1375
|
screenShareAudioTrack,
|
|
@@ -1497,15 +1426,31 @@ export class Call {
|
|
|
1497
1426
|
* @param type the debounce type to use for the update.
|
|
1498
1427
|
*/
|
|
1499
1428
|
updateSubscriptionsPartial = (
|
|
1500
|
-
trackType: VideoTrackType,
|
|
1429
|
+
trackType: VideoTrackType | 'video' | 'screen',
|
|
1501
1430
|
changes: SubscriptionChanges,
|
|
1502
1431
|
type: DebounceType = DebounceType.SLOW,
|
|
1503
1432
|
) => {
|
|
1433
|
+
if (trackType === 'video') {
|
|
1434
|
+
this.logger(
|
|
1435
|
+
'warn',
|
|
1436
|
+
`updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'videoTrack'`,
|
|
1437
|
+
);
|
|
1438
|
+
trackType = 'videoTrack';
|
|
1439
|
+
} else if (trackType === 'screen') {
|
|
1440
|
+
this.logger(
|
|
1441
|
+
'warn',
|
|
1442
|
+
`updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'screenShareTrack'`,
|
|
1443
|
+
);
|
|
1444
|
+
trackType = 'screenShareTrack';
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1504
1447
|
const participants = this.state.updateParticipants(
|
|
1505
1448
|
Object.entries(changes).reduce<StreamVideoParticipantPatches>(
|
|
1506
1449
|
(acc, [sessionId, change]) => {
|
|
1507
|
-
if (change.dimension) {
|
|
1450
|
+
if (change.dimension?.height) {
|
|
1508
1451
|
change.dimension.height = Math.ceil(change.dimension.height);
|
|
1452
|
+
}
|
|
1453
|
+
if (change.dimension?.width) {
|
|
1509
1454
|
change.dimension.width = Math.ceil(change.dimension.width);
|
|
1510
1455
|
}
|
|
1511
1456
|
const prop: keyof StreamVideoParticipant | undefined =
|
|
@@ -1525,7 +1470,9 @@ export class Call {
|
|
|
1525
1470
|
),
|
|
1526
1471
|
);
|
|
1527
1472
|
|
|
1528
|
-
|
|
1473
|
+
if (participants) {
|
|
1474
|
+
this.updateSubscriptions(participants, type);
|
|
1475
|
+
}
|
|
1529
1476
|
};
|
|
1530
1477
|
|
|
1531
1478
|
private updateSubscriptions = (
|
|
@@ -1618,12 +1565,12 @@ export class Call {
|
|
|
1618
1565
|
return this.publisher?.updateVideoPublishQuality(enabledLayers);
|
|
1619
1566
|
};
|
|
1620
1567
|
|
|
1621
|
-
private
|
|
1568
|
+
private assertCallJoined = () => {
|
|
1622
1569
|
return new Promise<void>((resolve) => {
|
|
1623
1570
|
this.state.callingState$
|
|
1624
1571
|
.pipe(
|
|
1625
1572
|
takeWhile((state) => state !== CallingState.JOINED, true),
|
|
1626
|
-
filter((
|
|
1573
|
+
filter((s) => s === CallingState.JOINED),
|
|
1627
1574
|
)
|
|
1628
1575
|
.subscribe(() => resolve());
|
|
1629
1576
|
});
|
|
@@ -2145,16 +2092,16 @@ export class Call {
|
|
|
2145
2092
|
*
|
|
2146
2093
|
* @internal
|
|
2147
2094
|
*/
|
|
2148
|
-
applyDeviceConfig = async (
|
|
2149
|
-
await this.initCamera({ setStatus:
|
|
2095
|
+
applyDeviceConfig = async () => {
|
|
2096
|
+
await this.initCamera({ setStatus: false }).catch((err) => {
|
|
2150
2097
|
this.logger('warn', 'Camera init failed', err);
|
|
2151
2098
|
});
|
|
2152
|
-
await this.initMic({ setStatus:
|
|
2099
|
+
await this.initMic({ setStatus: false }).catch((err) => {
|
|
2153
2100
|
this.logger('warn', 'Mic init failed', err);
|
|
2154
2101
|
});
|
|
2155
2102
|
};
|
|
2156
2103
|
|
|
2157
|
-
private
|
|
2104
|
+
private async initCamera(options: { setStatus: boolean }) {
|
|
2158
2105
|
// Wait for any in progress camera operation
|
|
2159
2106
|
await this.camera.statusChangeSettled();
|
|
2160
2107
|
|
|
@@ -2184,7 +2131,7 @@ export class Call {
|
|
|
2184
2131
|
if (options.setStatus) {
|
|
2185
2132
|
// Publish already that was set before we joined
|
|
2186
2133
|
if (
|
|
2187
|
-
this.camera.enabled &&
|
|
2134
|
+
this.camera.state.status === 'enabled' &&
|
|
2188
2135
|
this.camera.state.mediaStream &&
|
|
2189
2136
|
!this.publisher?.isPublishing(TrackType.VIDEO)
|
|
2190
2137
|
) {
|
|
@@ -2201,9 +2148,9 @@ export class Call {
|
|
|
2201
2148
|
await this.camera.enable();
|
|
2202
2149
|
}
|
|
2203
2150
|
}
|
|
2204
|
-
}
|
|
2151
|
+
}
|
|
2205
2152
|
|
|
2206
|
-
private
|
|
2153
|
+
private async initMic(options: { setStatus: boolean }) {
|
|
2207
2154
|
// Wait for any in progress mic operation
|
|
2208
2155
|
await this.microphone.statusChangeSettled();
|
|
2209
2156
|
|
|
@@ -2217,7 +2164,7 @@ export class Call {
|
|
|
2217
2164
|
if (options.setStatus) {
|
|
2218
2165
|
// Publish media stream that was set before we joined
|
|
2219
2166
|
if (
|
|
2220
|
-
this.microphone.enabled &&
|
|
2167
|
+
this.microphone.state.status === 'enabled' &&
|
|
2221
2168
|
this.microphone.state.mediaStream &&
|
|
2222
2169
|
!this.publisher?.isPublishing(TrackType.AUDIO)
|
|
2223
2170
|
) {
|
|
@@ -2232,7 +2179,7 @@ export class Call {
|
|
|
2232
2179
|
await this.microphone.enable();
|
|
2233
2180
|
}
|
|
2234
2181
|
}
|
|
2235
|
-
}
|
|
2182
|
+
}
|
|
2236
2183
|
|
|
2237
2184
|
/**
|
|
2238
2185
|
* Will begin tracking the given element for visibility changes within the
|