@stream-io/video-client 2.0.0-beta.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/index.browser.es.js +1126 -791
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1126 -791
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1126 -791
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +37 -11
- package/dist/src/StreamVideoClient.d.ts +15 -21
- package/dist/src/coordinator/connection/types.d.ts +16 -0
- package/dist/src/events/call.d.ts +0 -11
- package/dist/src/gen/coordinator/index.d.ts +73 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
- package/dist/src/ringing/RingStatePoller.d.ts +46 -0
- package/dist/src/ringing/RingTimeout.d.ts +27 -0
- package/dist/src/ringing/index.d.ts +4 -0
- package/dist/src/ringing/reconcileRingState.d.ts +18 -0
- package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
- package/dist/src/rtc/Publisher.d.ts +18 -0
- package/dist/src/rtc/TransceiverCache.d.ts +10 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
- package/dist/src/stats/utils.d.ts +6 -0
- package/dist/src/store/CallState.d.ts +118 -267
- package/dist/src/store/ClientState.d.ts +62 -0
- package/dist/src/store/index.d.ts +1 -1
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/types.d.ts +22 -0
- package/dist/src/types.d.ts +33 -4
- package/index.ts +1 -0
- package/package.json +2 -2
- package/src/Call.ts +285 -125
- package/src/StreamSfuClient.ts +6 -2
- package/src/StreamVideoClient.ts +40 -47
- package/src/__tests__/Call.autodrop.test.ts +118 -66
- package/src/__tests__/Call.getRingState.test.ts +87 -0
- package/src/__tests__/Call.lifecycle.test.ts +499 -6
- package/src/__tests__/Call.publishing.test.ts +2 -2
- package/src/__tests__/Call.ringSettled.test.ts +71 -0
- package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
- package/src/__tests__/StreamSfuClient.test.ts +48 -0
- package/src/__tests__/StreamVideoClient.disconnect.test.ts +62 -0
- package/src/coordinator/connection/types.ts +19 -0
- package/src/devices/__tests__/CameraManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -2
- package/src/devices/__tests__/ScreenShareManager.test.ts +3 -3
- package/src/devices/__tests__/SpeakerManager.test.ts +4 -4
- package/src/events/__tests__/call.test.ts +5 -220
- package/src/events/call.ts +1 -73
- package/src/events/callEventHandlers.ts +12 -23
- package/src/gen/coordinator/index.ts +67 -0
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +2 -2
- package/src/helpers/__tests__/DynascaleManager.test.ts +2 -2
- package/src/helpers/__tests__/ViewportTracker.test.ts +2 -2
- package/src/reporting/ClientEventReporter.ts +30 -4
- package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
- package/src/ringing/RingStatePoller.ts +177 -0
- package/src/ringing/RingTimeout.ts +90 -0
- package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
- package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
- package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
- package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
- package/src/ringing/index.ts +4 -0
- package/src/ringing/reconcileRingState.ts +108 -0
- package/src/ringing/resolveOwnRingOutcome.ts +58 -0
- package/src/rtc/Publisher.ts +66 -12
- package/src/rtc/TransceiverCache.ts +17 -0
- package/src/rtc/__tests__/Call.reconnect.test.ts +5 -5
- package/src/rtc/__tests__/Publisher.test.ts +209 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
- package/src/rtc/e2ee/E2EEManager.ts +10 -1
- package/src/rtc/e2ee/EncryptionManager.ts +1 -1
- package/src/rtc/e2ee/SPEC.md +1 -1
- package/src/stats/__tests__/utils.test.ts +33 -0
- package/src/stats/utils.ts +15 -0
- package/src/store/CallState.ts +305 -550
- package/src/store/ClientState.ts +101 -0
- package/src/store/__tests__/CallState.test.ts +60 -1
- package/src/store/__tests__/ClientState.test.ts +35 -0
- package/src/store/__tests__/rxUtils.test.ts +67 -1
- package/src/store/index.ts +1 -1
- package/src/store/rxUtils.ts +8 -4
- package/src/store/subjects.ts +30 -0
- package/src/store/types.ts +45 -0
- package/src/types.ts +40 -4
- package/dist/src/store/stateStore.d.ts +0 -84
- package/src/store/stateStore.ts +0 -156
package/dist/index.cjs.js
CHANGED
|
@@ -5101,7 +5101,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5101
5101
|
}
|
|
5102
5102
|
};
|
|
5103
5103
|
|
|
5104
|
-
const version = "2.0.0-beta.
|
|
5104
|
+
const version = "2.0.0-beta.1";
|
|
5105
5105
|
const [major, minor, patch] = version.split('.');
|
|
5106
5106
|
let sdkInfo = {
|
|
5107
5107
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5481,11 +5481,14 @@ const isFunctionPatch = (update) => typeof update === 'function';
|
|
|
5481
5481
|
* @param observable$ the observable to get the value from.
|
|
5482
5482
|
*/
|
|
5483
5483
|
const getCurrentValue = (observable$) => {
|
|
5484
|
+
if (observable$ instanceof rxjs.BehaviorSubject) {
|
|
5485
|
+
return observable$.getValue();
|
|
5486
|
+
}
|
|
5484
5487
|
let value;
|
|
5485
5488
|
let err = undefined;
|
|
5486
|
-
|
|
5489
|
+
observable$
|
|
5487
5490
|
.subscribe({
|
|
5488
|
-
next: (
|
|
5491
|
+
next: (v) => {
|
|
5489
5492
|
value = v;
|
|
5490
5493
|
},
|
|
5491
5494
|
error: (e) => {
|
|
@@ -5634,38 +5637,39 @@ exports.CallingState = void 0;
|
|
|
5634
5637
|
CallingState["OFFLINE"] = "offline";
|
|
5635
5638
|
})(exports.CallingState || (exports.CallingState = {}));
|
|
5636
5639
|
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
/**
|
|
5665
|
-
* The currently connected user.
|
|
5666
|
-
*/
|
|
5640
|
+
function subject(initialValue) {
|
|
5641
|
+
return new rxjs.BehaviorSubject(initialValue);
|
|
5642
|
+
}
|
|
5643
|
+
/**
|
|
5644
|
+
* Creates an Observable from the given subject by piping it to the
|
|
5645
|
+
* `distinctUntilChanged()` operator.
|
|
5646
|
+
*/
|
|
5647
|
+
const duc = (source, comparator) => source.pipe(rxjs.distinctUntilChanged(comparator));
|
|
5648
|
+
/**
|
|
5649
|
+
* Multicasts the given Observable, replaying the latest computed value to
|
|
5650
|
+
* every new subscriber instead of re-running the pipeline for each of them.
|
|
5651
|
+
*/
|
|
5652
|
+
const shared = (source) => source.pipe(rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
5653
|
+
|
|
5654
|
+
/**
|
|
5655
|
+
* Holds the state of the client, and the calls it created or tracks.
|
|
5656
|
+
* You can subscribe to the exposed observables to get notified about changes.
|
|
5657
|
+
*/
|
|
5658
|
+
class ClientState {
|
|
5659
|
+
logger = videoLoggerSystem.getLogger('ClientState');
|
|
5660
|
+
connectedUserSubject = subject();
|
|
5661
|
+
callsSubject = subject([]);
|
|
5662
|
+
/** Data describing a user successfully connected over WS to the coordinator. */
|
|
5663
|
+
connectedUser$ = this.connectedUserSubject.asObservable();
|
|
5664
|
+
/** A list of {@link Call} objects created/tracked by this client. */
|
|
5665
|
+
calls$ = this.callsSubject.asObservable();
|
|
5666
|
+
/** The currently connected user. */
|
|
5667
5667
|
get connectedUser() {
|
|
5668
|
-
return this.connectedUserSubject
|
|
5668
|
+
return getCurrentValue(this.connectedUserSubject);
|
|
5669
|
+
}
|
|
5670
|
+
/** A list of {@link Call} objects created/tracked by this client. */
|
|
5671
|
+
get calls() {
|
|
5672
|
+
return getCurrentValue(this.callsSubject);
|
|
5669
5673
|
}
|
|
5670
5674
|
/**
|
|
5671
5675
|
* Sets the currently connected user.
|
|
@@ -5676,22 +5680,19 @@ class StreamVideoWriteableStateStore {
|
|
|
5676
5680
|
setConnectedUser = (user) => {
|
|
5677
5681
|
return setCurrentValue(this.connectedUserSubject, user);
|
|
5678
5682
|
};
|
|
5679
|
-
/**
|
|
5680
|
-
* A list of {@link Call} objects created/tracked by this client.
|
|
5681
|
-
*/
|
|
5682
|
-
get calls() {
|
|
5683
|
-
return getCurrentValue(this.callsSubject);
|
|
5684
|
-
}
|
|
5685
5683
|
/**
|
|
5686
5684
|
* Sets the list of {@link Call} objects created/tracked by this client.
|
|
5687
|
-
*
|
|
5685
|
+
*
|
|
5686
|
+
* @internal
|
|
5687
|
+
* @param calls the calls to set.
|
|
5688
5688
|
*/
|
|
5689
5689
|
setCalls = (calls) => {
|
|
5690
5690
|
return setCurrentValue(this.callsSubject, calls);
|
|
5691
5691
|
};
|
|
5692
5692
|
/**
|
|
5693
|
-
* Adds a {@link Call} object to the list of
|
|
5693
|
+
* Adds a {@link Call} object to the list of calls tracked by this client.
|
|
5694
5694
|
*
|
|
5695
|
+
* @internal
|
|
5695
5696
|
* @param call the call to add.
|
|
5696
5697
|
*/
|
|
5697
5698
|
registerCall = (call) => {
|
|
@@ -5702,6 +5703,7 @@ class StreamVideoWriteableStateStore {
|
|
|
5702
5703
|
/**
|
|
5703
5704
|
* Registers a {@link Call} object if it doesn't exist, otherwise updates it.
|
|
5704
5705
|
*
|
|
5706
|
+
* @internal
|
|
5705
5707
|
* @param call the call to register or update.
|
|
5706
5708
|
*/
|
|
5707
5709
|
registerOrUpdateCall = (call) => {
|
|
@@ -5713,17 +5715,17 @@ class StreamVideoWriteableStateStore {
|
|
|
5713
5715
|
}
|
|
5714
5716
|
};
|
|
5715
5717
|
/**
|
|
5716
|
-
* Removes a {@link Call} object from the list of
|
|
5718
|
+
* Removes a {@link Call} object from the list of calls tracked by this client.
|
|
5717
5719
|
*
|
|
5718
|
-
* @
|
|
5720
|
+
* @internal
|
|
5721
|
+
* @param call the call to remove.
|
|
5719
5722
|
*/
|
|
5720
5723
|
unregisterCall = (call) => {
|
|
5721
|
-
|
|
5722
|
-
logger.trace(`Unregistering call: ${call.cid}`);
|
|
5724
|
+
this.logger.trace(`Unregistering call: ${call.cid}`);
|
|
5723
5725
|
return this.setCalls((calls) => calls.filter((c) => c !== call));
|
|
5724
5726
|
};
|
|
5725
5727
|
/**
|
|
5726
|
-
* Finds a {@link Call} object in the list of
|
|
5728
|
+
* Finds a {@link Call} object in the list of calls tracked by this client.
|
|
5727
5729
|
*
|
|
5728
5730
|
* @param type the type of call to find.
|
|
5729
5731
|
* @param id the id of the call to find.
|
|
@@ -5732,38 +5734,6 @@ class StreamVideoWriteableStateStore {
|
|
|
5732
5734
|
return this.calls.find((c) => c.type === type && c.id === id);
|
|
5733
5735
|
};
|
|
5734
5736
|
}
|
|
5735
|
-
/**
|
|
5736
|
-
* A reactive store that exposes state variables in a reactive manner.
|
|
5737
|
-
* You can subscribe to changes of the different state variables.
|
|
5738
|
-
* This central store contains all the state variables related to [`StreamVideoClient`](./StreamVideClient.md) and [`Call`](./Call.md).
|
|
5739
|
-
*/
|
|
5740
|
-
class StreamVideoReadOnlyStateStore {
|
|
5741
|
-
/**
|
|
5742
|
-
* Data describing a user successfully connected over WS to coordinator server.
|
|
5743
|
-
*/
|
|
5744
|
-
connectedUser$;
|
|
5745
|
-
/**
|
|
5746
|
-
* A list of {@link Call} objects created/tracked by this client.
|
|
5747
|
-
*/
|
|
5748
|
-
calls$;
|
|
5749
|
-
constructor(store) {
|
|
5750
|
-
// convert and expose subjects as observables
|
|
5751
|
-
this.connectedUser$ = store.connectedUserSubject.asObservable();
|
|
5752
|
-
this.calls$ = store.callsSubject.asObservable();
|
|
5753
|
-
}
|
|
5754
|
-
/**
|
|
5755
|
-
* The current user connected over WS to the backend.
|
|
5756
|
-
*/
|
|
5757
|
-
get connectedUser() {
|
|
5758
|
-
return getCurrentValue(this.connectedUser$);
|
|
5759
|
-
}
|
|
5760
|
-
/**
|
|
5761
|
-
* A list of {@link Call} objects created/tracked by this client.
|
|
5762
|
-
*/
|
|
5763
|
-
get calls() {
|
|
5764
|
-
return getCurrentValue(this.calls$);
|
|
5765
|
-
}
|
|
5766
|
-
}
|
|
5767
5737
|
|
|
5768
5738
|
/**
|
|
5769
5739
|
* Creates a new combined {@link Comparator<T>} which sorts items by the given comparators.
|
|
@@ -6096,9 +6066,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
|
|
|
6096
6066
|
*/
|
|
6097
6067
|
const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6098
6068
|
|
|
6099
|
-
/**
|
|
6100
|
-
* Returns the default egress object - when no egress data is available.
|
|
6101
|
-
*/
|
|
6069
|
+
/** Returns the default egress object - when no egress data is available. */
|
|
6102
6070
|
const defaultEgress = {
|
|
6103
6071
|
broadcasting: false,
|
|
6104
6072
|
hls: { playlist_url: '', status: '' },
|
|
@@ -6109,93 +6077,120 @@ const defaultEgress = {
|
|
|
6109
6077
|
* @react You don't have to use this class directly, as we are exposing the state through Hooks.
|
|
6110
6078
|
*/
|
|
6111
6079
|
class CallState {
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
// We keep these tracks around until we can associate them with a participant.
|
|
6145
|
-
orphanedTracks = [];
|
|
6080
|
+
logger = videoLoggerSystem.getLogger('CallState');
|
|
6081
|
+
/** A list of comparators that are used to sort the participants. */
|
|
6082
|
+
sortParticipantsBy = defaultSortPreset;
|
|
6083
|
+
backstageSubject = subject(true);
|
|
6084
|
+
blockedUserIdsSubject = subject([]);
|
|
6085
|
+
createdAtSubject = subject(new Date());
|
|
6086
|
+
endedAtSubject = subject();
|
|
6087
|
+
startsAtSubject = subject();
|
|
6088
|
+
updatedAtSubject = subject(new Date());
|
|
6089
|
+
createdBySubject = subject();
|
|
6090
|
+
customSubject = subject({});
|
|
6091
|
+
egressSubject = subject();
|
|
6092
|
+
ingressSubject = subject();
|
|
6093
|
+
recordingSubject = subject(false);
|
|
6094
|
+
individualRecordingSubject = subject(false);
|
|
6095
|
+
rawRecordingSubject = subject(false);
|
|
6096
|
+
sessionSubject = subject();
|
|
6097
|
+
settingsSubject = subject();
|
|
6098
|
+
transcribingSubject = subject(false);
|
|
6099
|
+
captioningSubject = subject(false);
|
|
6100
|
+
e2eeEnabledSubject = subject(false);
|
|
6101
|
+
endedBySubject = subject();
|
|
6102
|
+
thumbnailsSubject = subject();
|
|
6103
|
+
membersSubject = subject([]);
|
|
6104
|
+
ownCapabilitiesSubject = subject([]);
|
|
6105
|
+
callingStateSubject = subject(exports.CallingState.UNKNOWN);
|
|
6106
|
+
startedAtSubject = subject();
|
|
6107
|
+
participantCountSubject = subject(0);
|
|
6108
|
+
anonymousParticipantCountSubject = subject(0);
|
|
6109
|
+
participantsSubject = subject([]);
|
|
6110
|
+
callStatsReportSubject = subject();
|
|
6111
|
+
closedCaptionsSubject = subject([]);
|
|
6146
6112
|
callGrantsSubject = new rxjs.ReplaySubject(1);
|
|
6147
|
-
|
|
6113
|
+
/** The backstage state. */
|
|
6114
|
+
backstage$ = duc(this.backstageSubject);
|
|
6115
|
+
/** Will provide the list of blocked user IDs. */
|
|
6116
|
+
blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
|
|
6117
|
+
/** Will provide the time when this call has been created. */
|
|
6118
|
+
createdAt$ = this.createdAtSubject.asObservable();
|
|
6119
|
+
/** Will provide the time when this call has been ended. */
|
|
6120
|
+
endedAt$ = this.endedAtSubject.asObservable();
|
|
6121
|
+
/** Will provide the time when this call has been scheduled to start. */
|
|
6122
|
+
startsAt$ = this.startsAtSubject.asObservable();
|
|
6123
|
+
/** Will provide the time when this call has been updated. */
|
|
6124
|
+
updatedAt$ = this.updatedAtSubject.asObservable();
|
|
6125
|
+
/** Will provide the user who created this call. */
|
|
6126
|
+
createdBy$ = this.createdBySubject.asObservable();
|
|
6127
|
+
/** Will provide the custom data of this call. */
|
|
6128
|
+
custom$ = this.customSubject.asObservable();
|
|
6129
|
+
/** Will provide the egress data of this call. */
|
|
6130
|
+
egress$ = this.egressSubject.asObservable();
|
|
6131
|
+
/** Will provide the ingress data of this call. */
|
|
6132
|
+
ingress$ = this.ingressSubject.asObservable();
|
|
6133
|
+
/** Will provide the recording state of this call. */
|
|
6134
|
+
recording$ = duc(this.recordingSubject);
|
|
6135
|
+
/** Will provide the recording state of this call. */
|
|
6136
|
+
individualRecording$ = duc(this.individualRecordingSubject);
|
|
6137
|
+
/** Will provide the recording state of this call. */
|
|
6138
|
+
rawRecording$ = duc(this.rawRecordingSubject);
|
|
6139
|
+
/** Will provide the session data of this call. */
|
|
6140
|
+
session$ = this.sessionSubject.asObservable();
|
|
6141
|
+
/** Will provide the settings of this call. */
|
|
6142
|
+
settings$ = this.settingsSubject.asObservable();
|
|
6143
|
+
/** Will provide the transcribing state of this call. */
|
|
6144
|
+
transcribing$ = duc(this.transcribingSubject);
|
|
6145
|
+
/** Will provide the closed captioning state of this call. */
|
|
6146
|
+
captioning$ = duc(this.captioningSubject);
|
|
6147
|
+
/**
|
|
6148
|
+
* Whether end-to-end encryption is active for this call, as reported by the
|
|
6149
|
+
* SFU in the join response.
|
|
6150
|
+
*/
|
|
6151
|
+
e2eeEnabled$ = duc(this.e2eeEnabledSubject);
|
|
6152
|
+
/** Will provide the user who ended this call. */
|
|
6153
|
+
endedBy$ = this.endedBySubject.asObservable();
|
|
6154
|
+
/** Will provide the thumbnails of this call. */
|
|
6155
|
+
thumbnails$ = this.thumbnailsSubject.asObservable();
|
|
6156
|
+
/** The list of members in the current call. */
|
|
6157
|
+
members$ = this.membersSubject.asObservable();
|
|
6158
|
+
/** The calling state. */
|
|
6159
|
+
callingState$ = duc(this.callingStateSubject);
|
|
6148
6160
|
/**
|
|
6149
6161
|
* The time the call session actually started.
|
|
6150
6162
|
* Useful for displaying the call duration.
|
|
6151
6163
|
*/
|
|
6152
|
-
startedAt
|
|
6164
|
+
startedAt$ = this.startedAtSubject.asObservable();
|
|
6153
6165
|
/**
|
|
6154
6166
|
* The server-side counted number of participants connected to the current call.
|
|
6155
6167
|
* This number includes the anonymous participants as well.
|
|
6156
6168
|
*/
|
|
6157
|
-
participantCount
|
|
6169
|
+
participantCount$ = duc(this.participantCountSubject);
|
|
6158
6170
|
/**
|
|
6159
6171
|
* The server-side counted number of anonymous participants connected to the current call.
|
|
6160
6172
|
* This number excludes the regular participants.
|
|
6161
6173
|
*/
|
|
6162
|
-
anonymousParticipantCount
|
|
6174
|
+
anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
|
|
6163
6175
|
/**
|
|
6164
6176
|
* All participants of the current call (this includes the current user and other participants as well),
|
|
6165
6177
|
* unsorted. This observable only updates when participants join or leave the call.
|
|
6166
6178
|
*/
|
|
6167
|
-
rawParticipants
|
|
6168
|
-
/**
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
* Pinned participants of the current call.
|
|
6183
|
-
*/
|
|
6184
|
-
pinnedParticipants$;
|
|
6185
|
-
/**
|
|
6186
|
-
* The currently elected dominant speaker in the current call.
|
|
6187
|
-
*/
|
|
6188
|
-
dominantSpeaker$;
|
|
6189
|
-
/**
|
|
6190
|
-
* Emits true whenever there is an active screen sharing session within
|
|
6191
|
-
* the current call. Useful for displaying a "screen sharing" indicator and
|
|
6192
|
-
* switching the layout to a screen sharing layout.
|
|
6193
|
-
*
|
|
6194
|
-
* The actual screen sharing track isn't exposed here, but can be retrieved
|
|
6195
|
-
* from the list of call participants. We also don't want to be limiting
|
|
6196
|
-
* to the number of share screen tracks are displayed in a call.
|
|
6197
|
-
*/
|
|
6198
|
-
hasOngoingScreenShare$;
|
|
6179
|
+
rawParticipants$ = shared(this.participantsSubject.asObservable());
|
|
6180
|
+
/** All participants of the current call sorted according to the current `sortByParticipantsBy` setting. */
|
|
6181
|
+
participants$ = shared(this.participantsSubject.asObservable().pipe(
|
|
6182
|
+
// maintain stable-sort by mutating the participants stored in the original subject
|
|
6183
|
+
rxjs.map((ps) => ps.sort(this.sortParticipantsBy))));
|
|
6184
|
+
/** The local participant of the current call. */
|
|
6185
|
+
localParticipant$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.find((p) => p.isLocalParticipant))));
|
|
6186
|
+
/** Remote participants of the current call (all except the local participant). */
|
|
6187
|
+
remoteParticipants$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.filter((p) => !p.isLocalParticipant))));
|
|
6188
|
+
/** Pinned participants of the current call. */
|
|
6189
|
+
pinnedParticipants$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.filter((p) => !!p.pin))));
|
|
6190
|
+
/** The currently elected dominant speaker in the current call. */
|
|
6191
|
+
dominantSpeaker$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.find((p) => p.isDominantSpeaker))));
|
|
6192
|
+
/** Emits true whenever there is an active screen sharing session within the current call. */
|
|
6193
|
+
hasOngoingScreenShare$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.some((p) => hasScreenShare(p))), rxjs.distinctUntilChanged()));
|
|
6199
6194
|
/**
|
|
6200
6195
|
* The latest stats report of the current call.
|
|
6201
6196
|
* When stats gathering is enabled, this observable will emit a new value
|
|
@@ -6204,232 +6199,66 @@ class CallState {
|
|
|
6204
6199
|
* Consumers of this observable can implement their own batching logic
|
|
6205
6200
|
* in case they want to show historical stats data.
|
|
6206
6201
|
*/
|
|
6207
|
-
callStatsReport
|
|
6208
|
-
/**
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
/**
|
|
6245
|
-
* Will provide the user who created this call.
|
|
6246
|
-
*/
|
|
6247
|
-
createdBy$;
|
|
6248
|
-
/**
|
|
6249
|
-
* Will provide the custom data of this call.
|
|
6250
|
-
*/
|
|
6251
|
-
custom$;
|
|
6252
|
-
/**
|
|
6253
|
-
* Will provide the egress data of this call.
|
|
6254
|
-
*/
|
|
6255
|
-
egress$;
|
|
6256
|
-
/**
|
|
6257
|
-
* Will provide the ingress data of this call.
|
|
6258
|
-
*/
|
|
6259
|
-
ingress$;
|
|
6260
|
-
/**
|
|
6261
|
-
* Will provide the recording state of this call.
|
|
6262
|
-
*/
|
|
6263
|
-
recording$;
|
|
6264
|
-
/**
|
|
6265
|
-
* Will provide the recording state of this call.
|
|
6266
|
-
*/
|
|
6267
|
-
individualRecording$;
|
|
6268
|
-
/**
|
|
6269
|
-
* Will provide the recording state of this call.
|
|
6270
|
-
*/
|
|
6271
|
-
rawRecording$;
|
|
6272
|
-
/**
|
|
6273
|
-
* Will provide the session data of this call.
|
|
6274
|
-
*/
|
|
6275
|
-
session$;
|
|
6276
|
-
/**
|
|
6277
|
-
* Will provide the settings of this call.
|
|
6278
|
-
*/
|
|
6279
|
-
settings$;
|
|
6280
|
-
/**
|
|
6281
|
-
* Will provide the transcribing state of this call.
|
|
6282
|
-
*/
|
|
6283
|
-
transcribing$;
|
|
6284
|
-
/**
|
|
6285
|
-
* Will provide the closed captioning state of this call.
|
|
6286
|
-
*/
|
|
6287
|
-
captioning$;
|
|
6288
|
-
/**
|
|
6289
|
-
* Whether end-to-end encryption is active for this call, as reported by the
|
|
6290
|
-
* SFU in the join response.
|
|
6291
|
-
*/
|
|
6292
|
-
e2eeEnabled$;
|
|
6293
|
-
/**
|
|
6294
|
-
* Will provide the user who ended this call.
|
|
6295
|
-
*/
|
|
6296
|
-
endedBy$;
|
|
6297
|
-
/**
|
|
6298
|
-
* Will provide the thumbnails of this call.
|
|
6299
|
-
*/
|
|
6300
|
-
thumbnails$;
|
|
6301
|
-
/**
|
|
6302
|
-
* The queue of closed captions.
|
|
6303
|
-
*/
|
|
6304
|
-
closedCaptions$;
|
|
6305
|
-
logger = videoLoggerSystem.getLogger('CallState');
|
|
6306
|
-
/**
|
|
6307
|
-
* A list of comparators that are used to sort the participants.
|
|
6308
|
-
*/
|
|
6309
|
-
sortParticipantsBy = defaultSortPreset;
|
|
6310
|
-
/**
|
|
6311
|
-
* The closed captions configuration.
|
|
6312
|
-
*/
|
|
6202
|
+
callStatsReport$ = this.callStatsReportSubject.asObservable();
|
|
6203
|
+
/** The queue of closed captions. */
|
|
6204
|
+
closedCaptions$ = this.closedCaptionsSubject.asObservable();
|
|
6205
|
+
/** The list of capabilities of the current user. */
|
|
6206
|
+
ownCapabilities$ = shared(rxjs.combineLatest([
|
|
6207
|
+
this.ownCapabilitiesSubject,
|
|
6208
|
+
this.callGrantsSubject.pipe(rxjs.startWith(undefined)),
|
|
6209
|
+
]).pipe(rxjs.map(([capabilities, grants]) => {
|
|
6210
|
+
if (!grants)
|
|
6211
|
+
return capabilities;
|
|
6212
|
+
const { canPublishAudio, canPublishVideo, canScreenshare } = grants;
|
|
6213
|
+
const update = {
|
|
6214
|
+
[OwnCapability.SEND_AUDIO]: canPublishAudio,
|
|
6215
|
+
[OwnCapability.SEND_VIDEO]: canPublishVideo,
|
|
6216
|
+
[OwnCapability.SCREENSHARE]: canScreenshare,
|
|
6217
|
+
};
|
|
6218
|
+
const nextCapabilities = [...capabilities];
|
|
6219
|
+
for (const _capability in update) {
|
|
6220
|
+
const capability = _capability;
|
|
6221
|
+
const allowed = update[capability];
|
|
6222
|
+
// grants take precedence over capabilities, reconstruct the capabilities
|
|
6223
|
+
if (allowed && !nextCapabilities.includes(capability)) {
|
|
6224
|
+
nextCapabilities.push(capability);
|
|
6225
|
+
}
|
|
6226
|
+
else if (!allowed && nextCapabilities.includes(capability)) {
|
|
6227
|
+
const index = nextCapabilities.indexOf(capability);
|
|
6228
|
+
nextCapabilities.splice(index, 1);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
return nextCapabilities;
|
|
6232
|
+
}), rxjs.distinctUntilChanged(isShallowArrayEqual)));
|
|
6233
|
+
// These are tracks that were delivered to the Subscriber's onTrack event
|
|
6234
|
+
// that we couldn't associate with a participant yet.
|
|
6235
|
+
// This happens when the participantJoined event hasn't been received yet.
|
|
6236
|
+
// We keep these tracks around until we can associate them with a participant.
|
|
6237
|
+
orphanedTracks = [];
|
|
6238
|
+
/** The closed captions configuration. */
|
|
6313
6239
|
closedCaptionsSettings;
|
|
6314
6240
|
closedCaptionsTasks = new Map();
|
|
6315
6241
|
eventHandlers;
|
|
6316
|
-
/**
|
|
6317
|
-
* Creates a new instance of the CallState class.
|
|
6318
|
-
*
|
|
6319
|
-
*/
|
|
6242
|
+
/** Creates a new instance of the CallState class. */
|
|
6320
6243
|
constructor() {
|
|
6321
|
-
this.rawParticipants$ = this.participantsSubject
|
|
6322
|
-
.asObservable()
|
|
6323
|
-
.pipe(rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6324
|
-
this.participants$ = this.participantsSubject.asObservable().pipe(
|
|
6325
|
-
// maintain stable-sort by mutating the participants stored
|
|
6326
|
-
// in the original subject
|
|
6327
|
-
rxjs.map((ps) => ps.sort(this.sortParticipantsBy)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6328
|
-
this.localParticipant$ = this.participants$.pipe(rxjs.map((participants) => participants.find((p) => p.isLocalParticipant)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6329
|
-
this.remoteParticipants$ = this.participants$.pipe(rxjs.map((participants) => participants.filter((p) => !p.isLocalParticipant)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6330
|
-
this.pinnedParticipants$ = this.participants$.pipe(rxjs.map((participants) => participants.filter((p) => !!p.pin)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6331
|
-
this.dominantSpeaker$ = this.participants$.pipe(rxjs.map((participants) => participants.find((p) => p.isDominantSpeaker)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6332
|
-
this.hasOngoingScreenShare$ = this.participants$.pipe(rxjs.map((participants) => participants.some((p) => hasScreenShare(p))), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6333
|
-
// dates
|
|
6334
|
-
this.createdAt$ = this.createdAtSubject.asObservable();
|
|
6335
|
-
this.endedAt$ = this.endedAtSubject.asObservable();
|
|
6336
|
-
this.startsAt$ = this.startsAtSubject.asObservable();
|
|
6337
|
-
this.startedAt$ = this.startedAtSubject.asObservable();
|
|
6338
|
-
this.updatedAt$ = this.updatedAtSubject.asObservable();
|
|
6339
|
-
this.callStatsReport$ = this.callStatsReportSubject.asObservable();
|
|
6340
|
-
this.members$ = this.membersSubject.asObservable();
|
|
6341
|
-
// complex objects should work as streams of data
|
|
6342
|
-
this.createdBy$ = this.createdBySubject.asObservable();
|
|
6343
|
-
this.custom$ = this.customSubject.asObservable();
|
|
6344
|
-
this.egress$ = this.egressSubject.asObservable();
|
|
6345
|
-
this.ingress$ = this.ingressSubject.asObservable();
|
|
6346
|
-
this.session$ = this.sessionSubject.asObservable();
|
|
6347
|
-
this.settings$ = this.settingsSubject.asObservable();
|
|
6348
|
-
this.endedBy$ = this.endedBySubject.asObservable();
|
|
6349
|
-
this.thumbnails$ = this.thumbnailsSubject.asObservable();
|
|
6350
|
-
this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
|
|
6351
|
-
/**
|
|
6352
|
-
* Creates an Observable from the given subject by piping to the
|
|
6353
|
-
* `distinctUntilChanged()` operator.
|
|
6354
|
-
*/
|
|
6355
|
-
const duc = (subject, comparator) => subject.pipe(rxjs.distinctUntilChanged(comparator));
|
|
6356
|
-
// primitive values should only emit once the value they hold changes
|
|
6357
|
-
this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
|
|
6358
|
-
this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
|
|
6359
|
-
this.backstage$ = duc(this.backstageSubject);
|
|
6360
|
-
this.callingState$ = duc(this.callingStateSubject);
|
|
6361
|
-
this.ownCapabilities$ = rxjs.combineLatest([
|
|
6362
|
-
this.ownCapabilitiesSubject,
|
|
6363
|
-
this.callGrantsSubject.pipe(rxjs.startWith(undefined)),
|
|
6364
|
-
]).pipe(rxjs.map(([capabilities, grants]) => {
|
|
6365
|
-
if (!grants)
|
|
6366
|
-
return capabilities;
|
|
6367
|
-
const { canPublishAudio, canPublishVideo, canScreenshare } = grants;
|
|
6368
|
-
const update = {
|
|
6369
|
-
[OwnCapability.SEND_AUDIO]: canPublishAudio,
|
|
6370
|
-
[OwnCapability.SEND_VIDEO]: canPublishVideo,
|
|
6371
|
-
[OwnCapability.SCREENSHARE]: canScreenshare,
|
|
6372
|
-
};
|
|
6373
|
-
const nextCapabilities = [...capabilities];
|
|
6374
|
-
for (const _capability in update) {
|
|
6375
|
-
const capability = _capability;
|
|
6376
|
-
const allowed = update[capability];
|
|
6377
|
-
// grants take precedence over capabilities, reconstruct the capabilities
|
|
6378
|
-
if (allowed && !nextCapabilities.includes(capability)) {
|
|
6379
|
-
nextCapabilities.push(capability);
|
|
6380
|
-
}
|
|
6381
|
-
else if (!allowed && nextCapabilities.includes(capability)) {
|
|
6382
|
-
const index = nextCapabilities.indexOf(capability);
|
|
6383
|
-
nextCapabilities.splice(index, 1);
|
|
6384
|
-
}
|
|
6385
|
-
}
|
|
6386
|
-
return nextCapabilities;
|
|
6387
|
-
}), rxjs.distinctUntilChanged(isShallowArrayEqual), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6388
|
-
this.participantCount$ = duc(this.participantCountSubject);
|
|
6389
|
-
this.recording$ = duc(this.recordingSubject);
|
|
6390
|
-
this.individualRecording$ = duc(this.individualRecordingSubject);
|
|
6391
|
-
this.rawRecording$ = duc(this.rawRecordingSubject);
|
|
6392
|
-
this.transcribing$ = duc(this.transcribingSubject);
|
|
6393
|
-
this.captioning$ = duc(this.captioningSubject);
|
|
6394
|
-
this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
|
|
6395
6244
|
this.eventHandlers = {
|
|
6396
|
-
// these events are not updating the call state:
|
|
6397
|
-
'call.frame_recording_ready': undefined,
|
|
6398
|
-
'call.kicked_user': undefined,
|
|
6399
|
-
'call.moderation_blur': undefined,
|
|
6400
|
-
'call.moderation_warning': undefined,
|
|
6401
|
-
'call.permission_request': undefined,
|
|
6402
|
-
'call.recording_ready': undefined,
|
|
6403
|
-
'call.rtmp_broadcast_failed': undefined,
|
|
6404
|
-
'call.rtmp_broadcast_started': undefined,
|
|
6405
|
-
'call.rtmp_broadcast_stopped': undefined,
|
|
6406
|
-
'call.stats_report_ready': undefined,
|
|
6407
|
-
'call.transcription_ready': undefined,
|
|
6408
|
-
'call.user_feedback_submitted': undefined,
|
|
6409
|
-
'call.user_muted': undefined,
|
|
6410
|
-
'connection.error': undefined,
|
|
6411
|
-
'connection.ok': undefined,
|
|
6412
|
-
'health.check': undefined,
|
|
6413
|
-
'user.updated': undefined,
|
|
6414
|
-
custom: undefined,
|
|
6415
|
-
// events that update call state:
|
|
6416
6245
|
'call.accepted': (e) => this.updateFromCallResponse(e.call),
|
|
6417
6246
|
'call.blocked_user': this.blockUser,
|
|
6418
6247
|
'call.closed_caption': this.updateFromClosedCaptions,
|
|
6419
6248
|
'call.closed_captions_failed': () => {
|
|
6420
|
-
|
|
6249
|
+
setCurrentValue(this.captioningSubject, false);
|
|
6421
6250
|
},
|
|
6422
6251
|
'call.closed_captions_started': () => {
|
|
6423
|
-
|
|
6252
|
+
setCurrentValue(this.captioningSubject, true);
|
|
6424
6253
|
},
|
|
6425
6254
|
'call.closed_captions_stopped': () => {
|
|
6426
|
-
|
|
6255
|
+
setCurrentValue(this.captioningSubject, false);
|
|
6427
6256
|
},
|
|
6428
6257
|
'call.created': (e) => this.updateFromCallResponse(e.call),
|
|
6429
6258
|
'call.deleted': (e) => this.updateFromCallResponse(e.call),
|
|
6430
6259
|
'call.ended': (e) => {
|
|
6431
6260
|
this.updateFromCallResponse(e.call);
|
|
6432
|
-
|
|
6261
|
+
setCurrentValue(this.endedBySubject, e.user);
|
|
6433
6262
|
},
|
|
6434
6263
|
'call.frame_recording_failed': (e) => {
|
|
6435
6264
|
this.updateFromCallResponse(e.call);
|
|
@@ -6474,21 +6303,19 @@ class CallState {
|
|
|
6474
6303
|
'call.session_participant_left': this.updateFromSessionParticipantLeft,
|
|
6475
6304
|
'call.session_started': (e) => this.updateFromCallResponse(e.call),
|
|
6476
6305
|
'call.transcription_started': () => {
|
|
6477
|
-
|
|
6306
|
+
setCurrentValue(this.transcribingSubject, true);
|
|
6478
6307
|
},
|
|
6479
6308
|
'call.transcription_stopped': () => {
|
|
6480
|
-
|
|
6309
|
+
setCurrentValue(this.transcribingSubject, false);
|
|
6481
6310
|
},
|
|
6482
6311
|
'call.transcription_failed': () => {
|
|
6483
|
-
|
|
6312
|
+
setCurrentValue(this.transcribingSubject, false);
|
|
6484
6313
|
},
|
|
6485
6314
|
'call.unblocked_user': this.unblockUser,
|
|
6486
6315
|
'call.updated': (e) => this.updateFromCallResponse(e.call),
|
|
6487
6316
|
};
|
|
6488
6317
|
}
|
|
6489
|
-
/**
|
|
6490
|
-
* Runs the cleanup tasks.
|
|
6491
|
-
*/
|
|
6318
|
+
/** Runs the cleanup tasks. */
|
|
6492
6319
|
dispose = () => {
|
|
6493
6320
|
for (const [ccKey, taskId] of this.closedCaptionsTasks.entries()) {
|
|
6494
6321
|
clearTimeout(taskId);
|
|
@@ -6505,39 +6332,18 @@ class CallState {
|
|
|
6505
6332
|
setSortParticipantsBy = (comparator) => {
|
|
6506
6333
|
this.sortParticipantsBy = comparator;
|
|
6507
6334
|
// trigger re-sorting of participants
|
|
6508
|
-
|
|
6335
|
+
setCurrentValue(this.participantsSubject, (ps) => ps);
|
|
6509
6336
|
};
|
|
6510
|
-
/**
|
|
6511
|
-
* Returns the comparator currently used to sort the participants.
|
|
6512
|
-
*/
|
|
6337
|
+
/** Returns the comparator currently used to sort the participants. */
|
|
6513
6338
|
getSortParticipantsBy = () => {
|
|
6514
6339
|
return this.sortParticipantsBy;
|
|
6515
6340
|
};
|
|
6516
|
-
/**
|
|
6517
|
-
* Gets the current value of an observable, or undefined if the observable has
|
|
6518
|
-
* not emitted a value yet.
|
|
6519
|
-
*
|
|
6520
|
-
* @param observable$ the observable to get the value from.
|
|
6521
|
-
*/
|
|
6522
|
-
getCurrentValue = getCurrentValue;
|
|
6523
|
-
/**
|
|
6524
|
-
* Updates the value of the provided Subject.
|
|
6525
|
-
* An `update` can either be a new value or a function which takes
|
|
6526
|
-
* the current value and returns a new value.
|
|
6527
|
-
*
|
|
6528
|
-
* @internal
|
|
6529
|
-
*
|
|
6530
|
-
* @param subject the subject to update.
|
|
6531
|
-
* @param update the update to apply to the subject.
|
|
6532
|
-
* @return the updated value.
|
|
6533
|
-
*/
|
|
6534
|
-
setCurrentValue = setCurrentValue;
|
|
6535
6341
|
/**
|
|
6536
6342
|
* The server-side counted number of participants connected to the current call.
|
|
6537
6343
|
* This number includes the anonymous participants as well.
|
|
6538
6344
|
*/
|
|
6539
6345
|
get participantCount() {
|
|
6540
|
-
return
|
|
6346
|
+
return getCurrentValue(this.participantCountSubject);
|
|
6541
6347
|
}
|
|
6542
6348
|
/**
|
|
6543
6349
|
* Sets the number of participants in the current call.
|
|
@@ -6546,14 +6352,14 @@ class CallState {
|
|
|
6546
6352
|
* @param count the number of participants.
|
|
6547
6353
|
*/
|
|
6548
6354
|
setParticipantCount = (count) => {
|
|
6549
|
-
return
|
|
6355
|
+
return setCurrentValue(this.participantCountSubject, count);
|
|
6550
6356
|
};
|
|
6551
6357
|
/**
|
|
6552
6358
|
* The time the call session actually started.
|
|
6553
6359
|
* Useful for displaying the call duration.
|
|
6554
6360
|
*/
|
|
6555
6361
|
get startedAt() {
|
|
6556
|
-
return
|
|
6362
|
+
return getCurrentValue(this.startedAtSubject);
|
|
6557
6363
|
}
|
|
6558
6364
|
/**
|
|
6559
6365
|
* Sets the time the call session actually started.
|
|
@@ -6562,13 +6368,11 @@ class CallState {
|
|
|
6562
6368
|
* @param startedAt the time the call session actually started.
|
|
6563
6369
|
*/
|
|
6564
6370
|
setStartedAt = (startedAt) => {
|
|
6565
|
-
return
|
|
6371
|
+
return setCurrentValue(this.startedAtSubject, startedAt);
|
|
6566
6372
|
};
|
|
6567
|
-
/**
|
|
6568
|
-
* Returns whether closed captions are enabled in the current call.
|
|
6569
|
-
*/
|
|
6373
|
+
/** Returns whether closed captions are enabled in the current call. */
|
|
6570
6374
|
get captioning() {
|
|
6571
|
-
return
|
|
6375
|
+
return getCurrentValue(this.captioningSubject);
|
|
6572
6376
|
}
|
|
6573
6377
|
/**
|
|
6574
6378
|
* Sets the closed captioning state of the current call.
|
|
@@ -6584,7 +6388,7 @@ class CallState {
|
|
|
6584
6388
|
* This number includes the anonymous participants as well.
|
|
6585
6389
|
*/
|
|
6586
6390
|
get anonymousParticipantCount() {
|
|
6587
|
-
return
|
|
6391
|
+
return getCurrentValue(this.anonymousParticipantCountSubject);
|
|
6588
6392
|
}
|
|
6589
6393
|
/**
|
|
6590
6394
|
* Sets the number of anonymous participants in the current call.
|
|
@@ -6593,19 +6397,15 @@ class CallState {
|
|
|
6593
6397
|
* @param count the number of anonymous participants.
|
|
6594
6398
|
*/
|
|
6595
6399
|
setAnonymousParticipantCount = (count) => {
|
|
6596
|
-
return
|
|
6400
|
+
return setCurrentValue(this.anonymousParticipantCountSubject, count);
|
|
6597
6401
|
};
|
|
6598
|
-
/**
|
|
6599
|
-
* The list of participants in the current call.
|
|
6600
|
-
*/
|
|
6402
|
+
/** The list of participants in the current call. */
|
|
6601
6403
|
get participants() {
|
|
6602
|
-
return
|
|
6404
|
+
return getCurrentValue(this.participants$);
|
|
6603
6405
|
}
|
|
6604
|
-
/**
|
|
6605
|
-
* The stable list of participants in the current call, unsorted.
|
|
6606
|
-
*/
|
|
6406
|
+
/** The stable list of participants in the current call, unsorted. */
|
|
6607
6407
|
get rawParticipants() {
|
|
6608
|
-
return
|
|
6408
|
+
return getCurrentValue(this.rawParticipants$);
|
|
6609
6409
|
}
|
|
6610
6410
|
/**
|
|
6611
6411
|
* Returns the current participants array directly from the BehaviorSubject.
|
|
@@ -6626,43 +6426,31 @@ class CallState {
|
|
|
6626
6426
|
* @param participants the list of participants.
|
|
6627
6427
|
*/
|
|
6628
6428
|
setParticipants = (participants) => {
|
|
6629
|
-
return
|
|
6429
|
+
return setCurrentValue(this.participantsSubject, participants);
|
|
6630
6430
|
};
|
|
6631
|
-
/**
|
|
6632
|
-
* The local participant in the current call.
|
|
6633
|
-
*/
|
|
6431
|
+
/** The local participant in the current call. */
|
|
6634
6432
|
get localParticipant() {
|
|
6635
|
-
return
|
|
6433
|
+
return getCurrentValue(this.localParticipant$);
|
|
6636
6434
|
}
|
|
6637
|
-
/**
|
|
6638
|
-
* The list of remote participants in the current call.
|
|
6639
|
-
*/
|
|
6435
|
+
/** The list of remote participants in the current call. */
|
|
6640
6436
|
get remoteParticipants() {
|
|
6641
|
-
return
|
|
6437
|
+
return getCurrentValue(this.remoteParticipants$);
|
|
6642
6438
|
}
|
|
6643
|
-
/**
|
|
6644
|
-
* The dominant speaker in the current call.
|
|
6645
|
-
*/
|
|
6439
|
+
/** The dominant speaker in the current call. */
|
|
6646
6440
|
get dominantSpeaker() {
|
|
6647
|
-
return
|
|
6441
|
+
return getCurrentValue(this.dominantSpeaker$);
|
|
6648
6442
|
}
|
|
6649
|
-
/**
|
|
6650
|
-
* The list of pinned participants in the current call.
|
|
6651
|
-
*/
|
|
6443
|
+
/** The list of pinned participants in the current call. */
|
|
6652
6444
|
get pinnedParticipants() {
|
|
6653
|
-
return
|
|
6445
|
+
return getCurrentValue(this.pinnedParticipants$);
|
|
6654
6446
|
}
|
|
6655
|
-
/**
|
|
6656
|
-
* Tell if there is an ongoing screen share in this call.
|
|
6657
|
-
*/
|
|
6447
|
+
/** Tell if there is an ongoing screen share in this call. */
|
|
6658
6448
|
get hasOngoingScreenShare() {
|
|
6659
|
-
return
|
|
6449
|
+
return getCurrentValue(this.hasOngoingScreenShare$);
|
|
6660
6450
|
}
|
|
6661
|
-
/**
|
|
6662
|
-
* The calling state.
|
|
6663
|
-
*/
|
|
6451
|
+
/** The calling state. */
|
|
6664
6452
|
get callingState() {
|
|
6665
|
-
return
|
|
6453
|
+
return getCurrentValue(this.callingStateSubject);
|
|
6666
6454
|
}
|
|
6667
6455
|
/**
|
|
6668
6456
|
* Sets the calling state.
|
|
@@ -6671,13 +6459,11 @@ class CallState {
|
|
|
6671
6459
|
* @param state the new calling state.
|
|
6672
6460
|
*/
|
|
6673
6461
|
setCallingState = (state) => {
|
|
6674
|
-
return
|
|
6462
|
+
return setCurrentValue(this.callingStateSubject, state);
|
|
6675
6463
|
};
|
|
6676
|
-
/**
|
|
6677
|
-
* The call stats report.
|
|
6678
|
-
*/
|
|
6464
|
+
/** The call stats report. */
|
|
6679
6465
|
get callStatsReport() {
|
|
6680
|
-
return
|
|
6466
|
+
return getCurrentValue(this.callStatsReportSubject);
|
|
6681
6467
|
}
|
|
6682
6468
|
/**
|
|
6683
6469
|
* Returns whether the call stats report is being observed or not.
|
|
@@ -6693,13 +6479,11 @@ class CallState {
|
|
|
6693
6479
|
* @param report the report to set.
|
|
6694
6480
|
*/
|
|
6695
6481
|
setCallStatsReport = (report) => {
|
|
6696
|
-
return
|
|
6482
|
+
return setCurrentValue(this.callStatsReportSubject, report);
|
|
6697
6483
|
};
|
|
6698
|
-
/**
|
|
6699
|
-
* The members of the current call.
|
|
6700
|
-
*/
|
|
6484
|
+
/** The members of the current call. */
|
|
6701
6485
|
get members() {
|
|
6702
|
-
return
|
|
6486
|
+
return getCurrentValue(this.membersSubject);
|
|
6703
6487
|
}
|
|
6704
6488
|
/**
|
|
6705
6489
|
* Sets the members of the current call.
|
|
@@ -6708,13 +6492,11 @@ class CallState {
|
|
|
6708
6492
|
* @param members the members to set.
|
|
6709
6493
|
*/
|
|
6710
6494
|
setMembers = (members) => {
|
|
6711
|
-
|
|
6495
|
+
setCurrentValue(this.membersSubject, members);
|
|
6712
6496
|
};
|
|
6713
|
-
/**
|
|
6714
|
-
* The capabilities of the current user for the current call.
|
|
6715
|
-
*/
|
|
6497
|
+
/** The capabilities of the current user for the current call. */
|
|
6716
6498
|
get ownCapabilities() {
|
|
6717
|
-
return
|
|
6499
|
+
return getCurrentValue(this.ownCapabilities$);
|
|
6718
6500
|
}
|
|
6719
6501
|
/**
|
|
6720
6502
|
* Sets the own capabilities.
|
|
@@ -6723,7 +6505,7 @@ class CallState {
|
|
|
6723
6505
|
* @param capabilities the capabilities to set.
|
|
6724
6506
|
*/
|
|
6725
6507
|
setOwnCapabilities = (capabilities) => {
|
|
6726
|
-
return
|
|
6508
|
+
return setCurrentValue(this.ownCapabilitiesSubject, capabilities);
|
|
6727
6509
|
};
|
|
6728
6510
|
/**
|
|
6729
6511
|
* Sets the call grants (used for own capabilities).
|
|
@@ -6732,141 +6514,105 @@ class CallState {
|
|
|
6732
6514
|
* @param grants the grants to set.
|
|
6733
6515
|
*/
|
|
6734
6516
|
setCallGrants = (grants) => {
|
|
6735
|
-
return
|
|
6517
|
+
return setCurrentValue(this.callGrantsSubject, grants);
|
|
6736
6518
|
};
|
|
6737
|
-
/**
|
|
6738
|
-
* The backstage state.
|
|
6739
|
-
*/
|
|
6519
|
+
/** The backstage state. */
|
|
6740
6520
|
get backstage() {
|
|
6741
|
-
return
|
|
6521
|
+
return getCurrentValue(this.backstageSubject);
|
|
6742
6522
|
}
|
|
6743
6523
|
/**
|
|
6744
6524
|
* Sets the backstage state.
|
|
6745
6525
|
* @param backstage the backstage state.
|
|
6746
6526
|
*/
|
|
6747
6527
|
setBackstage = (backstage) => {
|
|
6748
|
-
return
|
|
6528
|
+
return setCurrentValue(this.backstageSubject, backstage);
|
|
6749
6529
|
};
|
|
6750
|
-
/**
|
|
6751
|
-
* Will provide the list of blocked user IDs.
|
|
6752
|
-
*/
|
|
6530
|
+
/** Will provide the list of blocked user IDs. */
|
|
6753
6531
|
get blockedUserIds() {
|
|
6754
|
-
return
|
|
6532
|
+
return getCurrentValue(this.blockedUserIdsSubject);
|
|
6755
6533
|
}
|
|
6756
|
-
/**
|
|
6757
|
-
* Will provide the time when this call has been created.
|
|
6758
|
-
*/
|
|
6534
|
+
/** Will provide the time when this call has been created. */
|
|
6759
6535
|
get createdAt() {
|
|
6760
|
-
return
|
|
6536
|
+
return getCurrentValue(this.createdAtSubject);
|
|
6761
6537
|
}
|
|
6762
|
-
/**
|
|
6763
|
-
* Will provide the time when this call has been ended.
|
|
6764
|
-
*/
|
|
6538
|
+
/** Will provide the time when this call has been ended. */
|
|
6765
6539
|
get endedAt() {
|
|
6766
|
-
return
|
|
6540
|
+
return getCurrentValue(this.endedAtSubject);
|
|
6767
6541
|
}
|
|
6768
6542
|
/**
|
|
6769
6543
|
* Sets the time when this call has been ended.
|
|
6770
6544
|
* @param endedAt the time when this call has been ended.
|
|
6771
6545
|
*/
|
|
6772
6546
|
setEndedAt = (endedAt) => {
|
|
6773
|
-
return
|
|
6547
|
+
return setCurrentValue(this.endedAtSubject, endedAt);
|
|
6774
6548
|
};
|
|
6775
|
-
/**
|
|
6776
|
-
* Will provide the time when this call has been scheduled to start.
|
|
6777
|
-
*/
|
|
6549
|
+
/** Will provide the time when this call has been scheduled to start. */
|
|
6778
6550
|
get startsAt() {
|
|
6779
|
-
return
|
|
6551
|
+
return getCurrentValue(this.startsAtSubject);
|
|
6780
6552
|
}
|
|
6781
|
-
/**
|
|
6782
|
-
* Will provide the time when this call has been updated.
|
|
6783
|
-
*/
|
|
6553
|
+
/** Will provide the time when this call has been updated. */
|
|
6784
6554
|
get updatedAt() {
|
|
6785
|
-
return
|
|
6555
|
+
return getCurrentValue(this.updatedAtSubject);
|
|
6786
6556
|
}
|
|
6787
|
-
/**
|
|
6788
|
-
* Will provide the user who created this call.
|
|
6789
|
-
*/
|
|
6557
|
+
/** Will provide the user who created this call. */
|
|
6790
6558
|
get createdBy() {
|
|
6791
|
-
return
|
|
6559
|
+
return getCurrentValue(this.createdBySubject);
|
|
6792
6560
|
}
|
|
6793
|
-
/**
|
|
6794
|
-
* Will provide the custom data of this call.
|
|
6795
|
-
*/
|
|
6561
|
+
/** Will provide the custom data of this call. */
|
|
6796
6562
|
get custom() {
|
|
6797
|
-
return
|
|
6563
|
+
return getCurrentValue(this.customSubject);
|
|
6798
6564
|
}
|
|
6799
6565
|
/**
|
|
6800
6566
|
* Will provide the egress data of this call.
|
|
6801
6567
|
*/
|
|
6802
6568
|
get egress() {
|
|
6803
|
-
return
|
|
6569
|
+
return getCurrentValue(this.egressSubject);
|
|
6804
6570
|
}
|
|
6805
|
-
/**
|
|
6806
|
-
* Will provide the ingress data of this call.
|
|
6807
|
-
*/
|
|
6571
|
+
/** Will provide the ingress data of this call. */
|
|
6808
6572
|
get ingress() {
|
|
6809
|
-
return
|
|
6573
|
+
return getCurrentValue(this.ingressSubject);
|
|
6810
6574
|
}
|
|
6811
|
-
/**
|
|
6812
|
-
* Will provide the composite recording state of this call.
|
|
6813
|
-
*/
|
|
6575
|
+
/** Will provide the composite recording state of this call. */
|
|
6814
6576
|
get recording() {
|
|
6815
|
-
return
|
|
6577
|
+
return getCurrentValue(this.recordingSubject);
|
|
6816
6578
|
}
|
|
6817
|
-
/**
|
|
6818
|
-
* Will provide the individual recording state of this call.
|
|
6819
|
-
*/
|
|
6579
|
+
/** Will provide the individual recording state of this call. */
|
|
6820
6580
|
get individualRecording() {
|
|
6821
|
-
return
|
|
6581
|
+
return getCurrentValue(this.individualRecordingSubject);
|
|
6822
6582
|
}
|
|
6823
|
-
/**
|
|
6824
|
-
* Will provide the raw recording state of this call.
|
|
6825
|
-
*/
|
|
6583
|
+
/** Will provide the raw recording state of this call. */
|
|
6826
6584
|
get rawRecording() {
|
|
6827
|
-
return
|
|
6585
|
+
return getCurrentValue(this.rawRecordingSubject);
|
|
6828
6586
|
}
|
|
6829
|
-
/**
|
|
6830
|
-
* Will provide the session data of this call.
|
|
6831
|
-
*/
|
|
6587
|
+
/** Will provide the session data of this call. */
|
|
6832
6588
|
get session() {
|
|
6833
|
-
return
|
|
6589
|
+
return getCurrentValue(this.sessionSubject);
|
|
6834
6590
|
}
|
|
6835
|
-
/**
|
|
6836
|
-
* Will provide the settings of this call.
|
|
6837
|
-
*/
|
|
6591
|
+
/** Will provide the settings of this call. */
|
|
6838
6592
|
get settings() {
|
|
6839
|
-
return
|
|
6593
|
+
return getCurrentValue(this.settingsSubject);
|
|
6840
6594
|
}
|
|
6841
|
-
/**
|
|
6842
|
-
* Will provide the transcribing state of this call.
|
|
6843
|
-
*/
|
|
6595
|
+
/** Will provide the transcribing state of this call. */
|
|
6844
6596
|
get transcribing() {
|
|
6845
|
-
return
|
|
6597
|
+
return getCurrentValue(this.transcribingSubject);
|
|
6846
6598
|
}
|
|
6847
|
-
/**
|
|
6848
|
-
* Whether end-to-end encryption is active for this call.
|
|
6849
|
-
*/
|
|
6599
|
+
/** Whether end-to-end encryption is active for this call. */
|
|
6850
6600
|
get e2eeEnabled() {
|
|
6851
|
-
return
|
|
6601
|
+
return getCurrentValue(this.e2eeEnabledSubject);
|
|
6852
6602
|
}
|
|
6853
|
-
/**
|
|
6854
|
-
* Will provide the user who ended this call.
|
|
6855
|
-
*/
|
|
6603
|
+
/** Will provide the user who ended this call. */
|
|
6856
6604
|
get endedBy() {
|
|
6857
|
-
return
|
|
6605
|
+
return getCurrentValue(this.endedBySubject);
|
|
6858
6606
|
}
|
|
6859
6607
|
/**
|
|
6860
6608
|
* Will provide the thumbnails of this call, if enabled in the call settings.
|
|
6861
6609
|
*/
|
|
6862
6610
|
get thumbnails() {
|
|
6863
|
-
return
|
|
6611
|
+
return getCurrentValue(this.thumbnailsSubject);
|
|
6864
6612
|
}
|
|
6865
|
-
/**
|
|
6866
|
-
* Returns the current queue of closed captions.
|
|
6867
|
-
*/
|
|
6613
|
+
/** Returns the current queue of closed captions. */
|
|
6868
6614
|
get closedCaptions() {
|
|
6869
|
-
return
|
|
6615
|
+
return getCurrentValue(this.closedCaptionsSubject);
|
|
6870
6616
|
}
|
|
6871
6617
|
/**
|
|
6872
6618
|
* Will try to find the participant with the given sessionId in the current call.
|
|
@@ -6877,9 +6623,7 @@ class CallState {
|
|
|
6877
6623
|
findParticipantBySessionId = (sessionId) => {
|
|
6878
6624
|
return this.participants.find((p) => p.sessionId === sessionId);
|
|
6879
6625
|
};
|
|
6880
|
-
/**
|
|
6881
|
-
* Returns a new lookup table of participants indexed by their session ID.
|
|
6882
|
-
*/
|
|
6626
|
+
/** Returns a new lookup table of participants indexed by their session ID. */
|
|
6883
6627
|
getParticipantLookupBySessionId = () => {
|
|
6884
6628
|
return this.participants.reduce((lookupTable, participant) => {
|
|
6885
6629
|
lookupTable[participant.sessionId] = participant;
|
|
@@ -7114,25 +6858,49 @@ class CallState {
|
|
|
7114
6858
|
*/
|
|
7115
6859
|
updateFromCallResponse = (call) => {
|
|
7116
6860
|
this.setBackstage(call.backstage);
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
6861
|
+
setCurrentValue(this.blockedUserIdsSubject, call.blocked_user_ids);
|
|
6862
|
+
setCurrentValue(this.createdAtSubject, new Date(call.created_at));
|
|
6863
|
+
setCurrentValue(this.updatedAtSubject, new Date(call.updated_at));
|
|
6864
|
+
setCurrentValue(this.startsAtSubject, call.starts_at ? new Date(call.starts_at) : undefined);
|
|
7121
6865
|
this.setEndedAt(call.ended_at ? new Date(call.ended_at) : undefined);
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
6866
|
+
setCurrentValue(this.createdBySubject, call.created_by);
|
|
6867
|
+
setCurrentValue(this.customSubject, call.custom);
|
|
6868
|
+
setCurrentValue(this.egressSubject, call.egress);
|
|
6869
|
+
setCurrentValue(this.ingressSubject, call.ingress);
|
|
7126
6870
|
const { individual_recording, composite_recording, raw_recording } = call.egress;
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
const s =
|
|
6871
|
+
setCurrentValue(this.recordingSubject, call.recording || composite_recording?.status === 'running');
|
|
6872
|
+
setCurrentValue(this.individualRecordingSubject, individual_recording?.status === 'running');
|
|
6873
|
+
setCurrentValue(this.rawRecordingSubject, raw_recording?.status === 'running');
|
|
6874
|
+
const s = setCurrentValue(this.sessionSubject, call.session);
|
|
7131
6875
|
this.updateParticipantCountFromSession(s);
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
6876
|
+
setCurrentValue(this.settingsSubject, call.settings);
|
|
6877
|
+
setCurrentValue(this.transcribingSubject, call.transcribing);
|
|
6878
|
+
setCurrentValue(this.captioningSubject, call.captioning);
|
|
6879
|
+
setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
6880
|
+
};
|
|
6881
|
+
/**
|
|
6882
|
+
* Merges a polled ring state into the current session. Only the ring fields
|
|
6883
|
+
* are touched: the endpoint does not return the session roster.
|
|
6884
|
+
*
|
|
6885
|
+
* @internal
|
|
6886
|
+
*
|
|
6887
|
+
* @param ringState the ring state as returned by the coordinator.
|
|
6888
|
+
*/
|
|
6889
|
+
updateFromRingState = (ringState) => {
|
|
6890
|
+
setCurrentValue(this.sessionSubject, (session) => {
|
|
6891
|
+
if (!session || session.id !== ringState.session_id)
|
|
6892
|
+
return session;
|
|
6893
|
+
return {
|
|
6894
|
+
...session,
|
|
6895
|
+
accepted_by: ringState.accepted_by,
|
|
6896
|
+
rejected_by: ringState.rejected_by,
|
|
6897
|
+
missed_by: ringState.missed_by,
|
|
6898
|
+
ended_at: ringState.session_ended_at ?? session.ended_at,
|
|
6899
|
+
};
|
|
6900
|
+
});
|
|
6901
|
+
if (ringState.call_ended_at) {
|
|
6902
|
+
this.setEndedAt(new Date(ringState.call_ended_at));
|
|
6903
|
+
}
|
|
7136
6904
|
};
|
|
7137
6905
|
/**
|
|
7138
6906
|
* Updates the call state with the data received from the SFU server.
|
|
@@ -7170,21 +6938,21 @@ class CallState {
|
|
|
7170
6938
|
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
7171
6939
|
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
7172
6940
|
this.setServerSidePins(pins);
|
|
7173
|
-
|
|
6941
|
+
setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
|
|
7174
6942
|
};
|
|
7175
6943
|
updateFromMemberRemoved = (event) => {
|
|
7176
6944
|
this.updateFromCallResponse(event.call);
|
|
7177
|
-
|
|
6945
|
+
setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
|
|
7178
6946
|
};
|
|
7179
6947
|
updateFromMemberAdded = (event) => {
|
|
7180
6948
|
this.updateFromCallResponse(event.call);
|
|
7181
|
-
|
|
6949
|
+
setCurrentValue(this.membersSubject, (members) => [
|
|
7182
6950
|
...members,
|
|
7183
6951
|
...event.members,
|
|
7184
6952
|
]);
|
|
7185
6953
|
};
|
|
7186
6954
|
updateFromHLSBroadcastStopped = () => {
|
|
7187
|
-
|
|
6955
|
+
setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
7188
6956
|
...egress,
|
|
7189
6957
|
broadcasting: false,
|
|
7190
6958
|
hls: {
|
|
@@ -7194,7 +6962,7 @@ class CallState {
|
|
|
7194
6962
|
}));
|
|
7195
6963
|
};
|
|
7196
6964
|
updateFromHLSBroadcastingFailed = () => {
|
|
7197
|
-
|
|
6965
|
+
setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
7198
6966
|
...egress,
|
|
7199
6967
|
broadcasting: false,
|
|
7200
6968
|
hls: {
|
|
@@ -7206,13 +6974,13 @@ class CallState {
|
|
|
7206
6974
|
updateFromRecordingEvent = (type, running) => {
|
|
7207
6975
|
// handle the legacy format, where `type` is absent in the emitted events
|
|
7208
6976
|
if (type === undefined || type === 'composite') {
|
|
7209
|
-
|
|
6977
|
+
setCurrentValue(this.recordingSubject, running);
|
|
7210
6978
|
}
|
|
7211
6979
|
else if (type === 'individual') {
|
|
7212
|
-
|
|
6980
|
+
setCurrentValue(this.individualRecordingSubject, running);
|
|
7213
6981
|
}
|
|
7214
6982
|
else if (type === 'raw') {
|
|
7215
|
-
|
|
6983
|
+
setCurrentValue(this.rawRecordingSubject, running);
|
|
7216
6984
|
}
|
|
7217
6985
|
else {
|
|
7218
6986
|
ensureExhausted(type, 'Unknown recording type');
|
|
@@ -7229,7 +6997,7 @@ class CallState {
|
|
|
7229
6997
|
this.setAnonymousParticipantCount(session.anonymous_participant_count || 0);
|
|
7230
6998
|
};
|
|
7231
6999
|
updateFromSessionParticipantCountUpdate = (event) => {
|
|
7232
|
-
const s =
|
|
7000
|
+
const s = setCurrentValue(this.sessionSubject, (session) => {
|
|
7233
7001
|
if (!session)
|
|
7234
7002
|
return session;
|
|
7235
7003
|
return {
|
|
@@ -7241,7 +7009,7 @@ class CallState {
|
|
|
7241
7009
|
this.updateParticipantCountFromSession(s);
|
|
7242
7010
|
};
|
|
7243
7011
|
updateFromSessionParticipantLeft = (event) => {
|
|
7244
|
-
const s =
|
|
7012
|
+
const s = setCurrentValue(this.sessionSubject, (session) => {
|
|
7245
7013
|
if (!session)
|
|
7246
7014
|
return session;
|
|
7247
7015
|
const { participants, participants_count_by_role } = session;
|
|
@@ -7258,7 +7026,7 @@ class CallState {
|
|
|
7258
7026
|
this.updateParticipantCountFromSession(s);
|
|
7259
7027
|
};
|
|
7260
7028
|
updateFromSessionParticipantJoined = (event) => {
|
|
7261
|
-
const s =
|
|
7029
|
+
const s = setCurrentValue(this.sessionSubject, (session) => {
|
|
7262
7030
|
if (!session)
|
|
7263
7031
|
return session;
|
|
7264
7032
|
const { participants, participants_count_by_role } = session;
|
|
@@ -7295,7 +7063,7 @@ class CallState {
|
|
|
7295
7063
|
};
|
|
7296
7064
|
updateMembers = (event) => {
|
|
7297
7065
|
this.updateFromCallResponse(event.call);
|
|
7298
|
-
|
|
7066
|
+
setCurrentValue(this.membersSubject, (members) => members.map((member) => {
|
|
7299
7067
|
const memberUpdate = event.members.find((m) => m.user_id === member.user_id);
|
|
7300
7068
|
return memberUpdate ? memberUpdate : member;
|
|
7301
7069
|
}));
|
|
@@ -7320,14 +7088,14 @@ class CallState {
|
|
|
7320
7088
|
});
|
|
7321
7089
|
};
|
|
7322
7090
|
unblockUser = (event) => {
|
|
7323
|
-
|
|
7091
|
+
setCurrentValue(this.blockedUserIdsSubject, (current) => {
|
|
7324
7092
|
if (!current)
|
|
7325
7093
|
return current;
|
|
7326
7094
|
return current.filter((id) => id !== event.user.id);
|
|
7327
7095
|
});
|
|
7328
7096
|
};
|
|
7329
7097
|
blockUser = (event) => {
|
|
7330
|
-
|
|
7098
|
+
setCurrentValue(this.blockedUserIdsSubject, (current) => [
|
|
7331
7099
|
...(current || []),
|
|
7332
7100
|
event.user.id,
|
|
7333
7101
|
]);
|
|
@@ -7338,7 +7106,7 @@ class CallState {
|
|
|
7338
7106
|
}
|
|
7339
7107
|
};
|
|
7340
7108
|
updateFromClosedCaptions = (event) => {
|
|
7341
|
-
|
|
7109
|
+
setCurrentValue(this.closedCaptionsSubject, (queue) => {
|
|
7342
7110
|
const { closed_caption } = event;
|
|
7343
7111
|
const keyOf = (c) => `${c.speaker_id}/${c.start_time}`;
|
|
7344
7112
|
const currentKey = keyOf(closed_caption);
|
|
@@ -7350,7 +7118,7 @@ class CallState {
|
|
|
7350
7118
|
// schedule the removal of the closed caption after the retention time
|
|
7351
7119
|
if (visibilityDurationMs > 0) {
|
|
7352
7120
|
const taskId = setTimeout(() => {
|
|
7353
|
-
|
|
7121
|
+
setCurrentValue(this.closedCaptionsSubject, (captions) => captions.filter((caption) => caption !== closed_caption));
|
|
7354
7122
|
this.closedCaptionsTasks.delete(currentKey);
|
|
7355
7123
|
}, visibilityDurationMs);
|
|
7356
7124
|
this.closedCaptionsTasks.set(currentKey, taskId);
|
|
@@ -7445,6 +7213,19 @@ const getSdkName = (sdk) => {
|
|
|
7445
7213
|
const getSdkVersion = (sdk) => {
|
|
7446
7214
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
7447
7215
|
};
|
|
7216
|
+
/**
|
|
7217
|
+
* Returns the identifier the SFU receives in the `X-Stream-Client` header,
|
|
7218
|
+
* e.g. `stream-video-react-v1.43.1`. Matches the format the coordinator
|
|
7219
|
+
* already gets through the client's user agent.
|
|
7220
|
+
*/
|
|
7221
|
+
const getStreamClientId = (sdk) => {
|
|
7222
|
+
const sdkPlatforms = {
|
|
7223
|
+
[SdkType.REACT]: 'react',
|
|
7224
|
+
[SdkType.REACT_NATIVE]: 'react-native',
|
|
7225
|
+
};
|
|
7226
|
+
const platform = (sdk && sdkPlatforms[sdk.type]) || 'js';
|
|
7227
|
+
return `stream-video-${platform}-v${getSdkVersion(sdk)}`;
|
|
7228
|
+
};
|
|
7448
7229
|
|
|
7449
7230
|
/**
|
|
7450
7231
|
* Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
|
|
@@ -9129,6 +8910,25 @@ class TransceiverCache {
|
|
|
9129
8910
|
this.cache.push(bundle);
|
|
9130
8911
|
this.transceiverOrder.push(bundle.transceiver);
|
|
9131
8912
|
};
|
|
8913
|
+
/**
|
|
8914
|
+
* Retires a failed sender from the reusable cache.
|
|
8915
|
+
*
|
|
8916
|
+
* A transceiver that never reached an m-section also leaves the creation
|
|
8917
|
+
* history: once stopped, it is left out of every later offer, so counting
|
|
8918
|
+
* it would shift the `mid` index of every transceiver added after it. One
|
|
8919
|
+
* that is already associated keeps its slot, since its m-section survives
|
|
8920
|
+
* as a rejected one.
|
|
8921
|
+
*/
|
|
8922
|
+
remove = (transceiver) => {
|
|
8923
|
+
const index = this.cache.findIndex((b) => b.transceiver === transceiver);
|
|
8924
|
+
if (index !== -1)
|
|
8925
|
+
this.cache.splice(index, 1);
|
|
8926
|
+
if (transceiver.mid)
|
|
8927
|
+
return;
|
|
8928
|
+
const orderIndex = this.transceiverOrder.indexOf(transceiver);
|
|
8929
|
+
if (orderIndex !== -1)
|
|
8930
|
+
this.transceiverOrder.splice(orderIndex, 1);
|
|
8931
|
+
};
|
|
9132
8932
|
/**
|
|
9133
8933
|
* Gets the transceiver for the given publish option.
|
|
9134
8934
|
*/
|
|
@@ -9300,7 +9100,9 @@ class Publisher extends BasePeerConnection {
|
|
|
9300
9100
|
const sendEncodings = isSvcCodec(publishOption.codec?.name)
|
|
9301
9101
|
? toSvcEncodings(encodings)
|
|
9302
9102
|
: encodings;
|
|
9303
|
-
|
|
9103
|
+
// An E2EE sender must never have media before its transform is attached,
|
|
9104
|
+
// including when another track or an ICE restart negotiates this PC.
|
|
9105
|
+
const transceiver = this.pc.addTransceiver(this.e2ee ? track.kind : track, {
|
|
9304
9106
|
direction: 'sendonly',
|
|
9305
9107
|
sendEncodings,
|
|
9306
9108
|
});
|
|
@@ -9310,16 +9112,63 @@ class Publisher extends BasePeerConnection {
|
|
|
9310
9112
|
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9311
9113
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
9312
9114
|
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9313
|
-
|
|
9115
|
+
if (this.e2ee) {
|
|
9116
|
+
await this.initEncryptedSender(transceiver, track, publishOption);
|
|
9117
|
+
}
|
|
9118
|
+
else {
|
|
9119
|
+
await this.setDegradationPreference(transceiver.sender, publishOption);
|
|
9120
|
+
}
|
|
9121
|
+
await this.negotiate();
|
|
9122
|
+
};
|
|
9123
|
+
/**
|
|
9124
|
+
* Attaches the encryptor, applies sender parameters, then attaches the track.
|
|
9125
|
+
*
|
|
9126
|
+
* `encrypt` must run before the first await: Chrome short-circuits a sender
|
|
9127
|
+
* whose encoded streams aren't created in the task that created it, and the
|
|
9128
|
+
* track goes on last so the sender has no media until both steps succeed.
|
|
9129
|
+
*/
|
|
9130
|
+
initEncryptedSender = async (transceiver, track, publishOption) => {
|
|
9131
|
+
try {
|
|
9132
|
+
const { sender } = transceiver;
|
|
9133
|
+
const codec = publishOption.codec?.name.toLowerCase();
|
|
9134
|
+
this.e2ee.encrypt(sender, codec, TrackType[publishOption.trackType]);
|
|
9135
|
+
this.logger.debug('E2EE encryptor attached to sender');
|
|
9136
|
+
await this.setDegradationPreference(sender, publishOption);
|
|
9137
|
+
await sender.replaceTrack(track);
|
|
9138
|
+
}
|
|
9139
|
+
catch (err) {
|
|
9140
|
+
this.retireTransceiver(transceiver, track);
|
|
9141
|
+
throw err;
|
|
9142
|
+
}
|
|
9143
|
+
};
|
|
9144
|
+
setDegradationPreference = async (sender, publishOption) => {
|
|
9145
|
+
const params = sender.getParameters();
|
|
9314
9146
|
params.degradationPreference =
|
|
9315
9147
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9316
9148
|
'maintain-framerate';
|
|
9317
|
-
await
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9149
|
+
await sender.setParameters(params);
|
|
9150
|
+
};
|
|
9151
|
+
/**
|
|
9152
|
+
* Discards a sender whose E2EE initialization failed.
|
|
9153
|
+
*
|
|
9154
|
+
* A partially initialized sender may carry no transform, so publish() must
|
|
9155
|
+
* not reuse it: retiring it from the cache is what keeps a later publish or
|
|
9156
|
+
* ICE restart from putting cleartext on the wire. Cleanup cannot throw, so
|
|
9157
|
+
* the initialization failure is what reaches the caller.
|
|
9158
|
+
*/
|
|
9159
|
+
retireTransceiver = (transceiver, track) => {
|
|
9160
|
+
this.transceiverCache.remove(transceiver);
|
|
9161
|
+
this.trackIdToTrackType.delete(track.id);
|
|
9162
|
+
// on React Native a clone shares its native source, so stopping it here
|
|
9163
|
+
// would stop the track the caller passed in. dispose() releases it.
|
|
9164
|
+
if (!isReactNative())
|
|
9165
|
+
this.stopTrack(track);
|
|
9166
|
+
try {
|
|
9167
|
+
transceiver.stop();
|
|
9168
|
+
}
|
|
9169
|
+
catch (err) {
|
|
9170
|
+
this.logger.debug('Failed to stop a retired transceiver', err);
|
|
9321
9171
|
}
|
|
9322
|
-
await this.negotiate();
|
|
9323
9172
|
};
|
|
9324
9173
|
/**
|
|
9325
9174
|
* Updates the transceiver with the given track and track type.
|
|
@@ -10326,7 +10175,10 @@ class StreamSfuClient {
|
|
|
10326
10175
|
this.rpc = createSignalClient({
|
|
10327
10176
|
baseUrl: server.url,
|
|
10328
10177
|
interceptors: [
|
|
10329
|
-
withHeaders({
|
|
10178
|
+
withHeaders({
|
|
10179
|
+
Authorization: `Bearer ${token}`,
|
|
10180
|
+
'X-Stream-Client': getStreamClientId(getSdkInfo()),
|
|
10181
|
+
}),
|
|
10330
10182
|
this.tracer && withRequestTracer(this.tracer.trace),
|
|
10331
10183
|
this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
|
|
10332
10184
|
withTimeout(rpcRequestTimeout, this.tracer?.trace),
|
|
@@ -10629,98 +10481,404 @@ class StreamSfuClient {
|
|
|
10629
10481
|
},
|
|
10630
10482
|
}));
|
|
10631
10483
|
};
|
|
10632
|
-
send = async (message) => {
|
|
10633
|
-
await this.signalReady(); // wait for the signal ws to be open
|
|
10634
|
-
const msgJson = SfuRequest.toJson(message);
|
|
10635
|
-
if (this.signalWs.readyState !== WebSocket.OPEN) {
|
|
10636
|
-
this.logger.debug('Signal WS is not open. Skipping message', msgJson);
|
|
10484
|
+
send = async (message) => {
|
|
10485
|
+
await this.signalReady(); // wait for the signal ws to be open
|
|
10486
|
+
const msgJson = SfuRequest.toJson(message);
|
|
10487
|
+
if (this.signalWs.readyState !== WebSocket.OPEN) {
|
|
10488
|
+
this.logger.debug('Signal WS is not open. Skipping message', msgJson);
|
|
10489
|
+
return;
|
|
10490
|
+
}
|
|
10491
|
+
this.logger.debug(`Sending message to: ${this.edgeName}`, msgJson);
|
|
10492
|
+
this.signalWs.send(SfuRequest.toBinary(message));
|
|
10493
|
+
};
|
|
10494
|
+
keepAlive = () => {
|
|
10495
|
+
const timers = getTimers();
|
|
10496
|
+
timers.clearInterval(this.keepAliveInterval);
|
|
10497
|
+
this.keepAliveInterval = timers.setInterval(() => {
|
|
10498
|
+
this.ping().catch((e) => {
|
|
10499
|
+
this.logger.error('Error sending healthCheckRequest to SFU', e);
|
|
10500
|
+
});
|
|
10501
|
+
}, this.pingIntervalInMs);
|
|
10502
|
+
};
|
|
10503
|
+
scheduleConnectionCheck = () => {
|
|
10504
|
+
const timers = getTimers();
|
|
10505
|
+
timers.clearInterval(this.connectionCheckInterval);
|
|
10506
|
+
this.connectionCheckInterval = timers.setInterval(() => {
|
|
10507
|
+
if (this.lastMessageTimestamp) {
|
|
10508
|
+
const timeSinceLastMessage = Date.now() - this.lastMessageTimestamp;
|
|
10509
|
+
if (timeSinceLastMessage > this.unhealthyTimeoutInMs) {
|
|
10510
|
+
this.close(StreamSfuClient.ERROR_CONNECTION_UNHEALTHY, `SFU connection unhealthy. Didn't receive any message for ${this.unhealthyTimeoutInMs}ms`);
|
|
10511
|
+
}
|
|
10512
|
+
}
|
|
10513
|
+
}, this.connectionCheckIntervalInMs);
|
|
10514
|
+
};
|
|
10515
|
+
}
|
|
10516
|
+
|
|
10517
|
+
/**
|
|
10518
|
+
* Decides what a ringing call should do next, based on the current call state.
|
|
10519
|
+
*
|
|
10520
|
+
* The `call.accepted`, `call.rejected` and `call.missed` handlers and the ring
|
|
10521
|
+
* state poller both run this. They differ only in how the state got there: the
|
|
10522
|
+
* handlers rely on `CallState.updateFromEvent`, which runs before them, and the
|
|
10523
|
+
* poller applies the polled ring state itself.
|
|
10524
|
+
*
|
|
10525
|
+
* @param call the call to reconcile.
|
|
10526
|
+
* @param joinSource which of the two triggered this run, reported on the
|
|
10527
|
+
* caller's join: `ring-ws` for the event handlers, `ring-poll-api` for the
|
|
10528
|
+
* poller.
|
|
10529
|
+
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10530
|
+
* terminal: the caller should keep trying while the ring is open.
|
|
10531
|
+
*/
|
|
10532
|
+
const reconcileRingState = async (call, joinSource) => {
|
|
10533
|
+
if (call.state.callingState !== exports.CallingState.RINGING)
|
|
10534
|
+
return true;
|
|
10535
|
+
return call.isCreatedByMe
|
|
10536
|
+
? reconcileAsCaller(call, joinSource)
|
|
10537
|
+
: reconcileAsCallee(call);
|
|
10538
|
+
};
|
|
10539
|
+
const reconcileAsCaller = async (call, joinSource) => {
|
|
10540
|
+
const { session, members, endedAt } = call.state;
|
|
10541
|
+
const currentUserId = call.currentUserId;
|
|
10542
|
+
// checked before `accepted_by`: an ended session cannot be joined
|
|
10543
|
+
if (endedAt || session?.ended_at) {
|
|
10544
|
+
call.logger.info('ring: the call has ended, leaving');
|
|
10545
|
+
// `leave` reports the remote end to callingx off `reason: 'ended'`
|
|
10546
|
+
return leave(call, {
|
|
10547
|
+
reject: false,
|
|
10548
|
+
reason: 'ended',
|
|
10549
|
+
message: 'ring: call ended',
|
|
10550
|
+
});
|
|
10551
|
+
}
|
|
10552
|
+
const acceptedBy = session?.accepted_by ?? {};
|
|
10553
|
+
if (Object.keys(acceptedBy).some((userId) => userId !== currentUserId)) {
|
|
10554
|
+
call.logger.info('ring: the call was accepted, joining');
|
|
10555
|
+
try {
|
|
10556
|
+
await call.join({ joinSource });
|
|
10557
|
+
}
|
|
10558
|
+
catch (err) {
|
|
10559
|
+
// `doJoin` restores the ringing state when a join fails, so the ring is
|
|
10560
|
+
// still open. Report it unsettled and let the next poll retry.
|
|
10561
|
+
call.logger.error('Failed to join an accepted call', err);
|
|
10562
|
+
return false;
|
|
10563
|
+
}
|
|
10564
|
+
return true;
|
|
10565
|
+
}
|
|
10566
|
+
const otherMembers = members
|
|
10567
|
+
.filter((member) => member.user_id !== currentUserId)
|
|
10568
|
+
.map((member) => member.user_id);
|
|
10569
|
+
if (otherMembers.length === 0)
|
|
10570
|
+
return false;
|
|
10571
|
+
const rejectedBy = session?.rejected_by ?? {};
|
|
10572
|
+
if (otherMembers.every((userId) => rejectedBy[userId])) {
|
|
10573
|
+
call.logger.info('ring: everyone rejected, leaving');
|
|
10574
|
+
return leave(call, {
|
|
10575
|
+
reject: true,
|
|
10576
|
+
reason: 'cancel',
|
|
10577
|
+
message: 'ring: everyone rejected',
|
|
10578
|
+
});
|
|
10579
|
+
}
|
|
10580
|
+
return false;
|
|
10581
|
+
};
|
|
10582
|
+
// the current user's own accept or reject, on this or another device, is
|
|
10583
|
+
// handled by `resolveOwnRingOutcome`, and `call.ended` by `watchCallEnded`.
|
|
10584
|
+
const reconcileAsCallee = async (call) => {
|
|
10585
|
+
const createdById = call.state.createdBy?.id;
|
|
10586
|
+
const rejectedBy = call.state.session?.rejected_by ?? {};
|
|
10587
|
+
if (createdById && rejectedBy[createdById]) {
|
|
10588
|
+
call.logger.info('ring: the caller cancelled, leaving');
|
|
10589
|
+
return leave(call, {
|
|
10590
|
+
reason: 'ended',
|
|
10591
|
+
message: 'ring: creator rejected',
|
|
10592
|
+
});
|
|
10593
|
+
}
|
|
10594
|
+
return false;
|
|
10595
|
+
};
|
|
10596
|
+
// `false` when the call could not be left, so the ring stays open for a retry.
|
|
10597
|
+
const leave = async (call, options) => {
|
|
10598
|
+
try {
|
|
10599
|
+
await call.leave(options);
|
|
10600
|
+
return true;
|
|
10601
|
+
}
|
|
10602
|
+
catch (err) {
|
|
10603
|
+
call.logger.error('Failed to leave a ringing call', err);
|
|
10604
|
+
return false;
|
|
10605
|
+
}
|
|
10606
|
+
};
|
|
10607
|
+
|
|
10608
|
+
// `doJoin` restores the previous calling state when a join fails, so JOINING can
|
|
10609
|
+
// go back to RINGING. Only a state the ring cannot return from ends the poller.
|
|
10610
|
+
const ringIsOver = (callingState) => callingState !== exports.CallingState.RINGING &&
|
|
10611
|
+
callingState !== exports.CallingState.JOINING;
|
|
10612
|
+
/**
|
|
10613
|
+
* Polls the coordinator for the outcome of a ring the current user started.
|
|
10614
|
+
*
|
|
10615
|
+
* `call.accepted`, `call.rejected` and `call.missed` are delivered best-effort,
|
|
10616
|
+
* with no store-and-forward, so a caller that drops one is left on a ringing
|
|
10617
|
+
* screen while the callee is already in the call. After a quiet period this
|
|
10618
|
+
* reads the ring state until a terminal outcome or the end of the ring window.
|
|
10619
|
+
*/
|
|
10620
|
+
class RingStatePoller {
|
|
10621
|
+
logger = videoLoggerSystem.getLogger('RingStatePoller');
|
|
10622
|
+
call;
|
|
10623
|
+
startAfterMs;
|
|
10624
|
+
intervalMs;
|
|
10625
|
+
sessionId;
|
|
10626
|
+
deadlineAt = 0;
|
|
10627
|
+
idleTimeoutId;
|
|
10628
|
+
intervalId;
|
|
10629
|
+
stopped = false;
|
|
10630
|
+
wasPolling = false;
|
|
10631
|
+
inFlight = false;
|
|
10632
|
+
unsubscribe = [];
|
|
10633
|
+
constructor(call, options = {}) {
|
|
10634
|
+
this.call = call;
|
|
10635
|
+
this.startAfterMs = options.startAfterMs ?? 15_000;
|
|
10636
|
+
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10637
|
+
}
|
|
10638
|
+
/**
|
|
10639
|
+
* Starts polling. Does nothing when the call has no session yet.
|
|
10640
|
+
*/
|
|
10641
|
+
start = () => {
|
|
10642
|
+
if (this.stopped || this.sessionId)
|
|
10643
|
+
return;
|
|
10644
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
10645
|
+
return;
|
|
10646
|
+
// captured once: `call.ended` clears the call's current session
|
|
10647
|
+
const sessionId = this.call.state.session?.id;
|
|
10648
|
+
if (!sessionId) {
|
|
10649
|
+
this.logger.warn('the call has no session');
|
|
10650
|
+
return;
|
|
10651
|
+
}
|
|
10652
|
+
this.sessionId = sessionId;
|
|
10653
|
+
const ring = this.call.state.settings?.ring;
|
|
10654
|
+
const maxDurationMs = ring?.auto_cancel_timeout_ms || ring?.missed_call_timeout_ms || 30_000;
|
|
10655
|
+
this.deadlineAt = Date.now() + maxDurationMs;
|
|
10656
|
+
// an incoming event proves the socket is alive, so the quiet period starts
|
|
10657
|
+
// over: in a group ring a single rejection does not settle the ring.
|
|
10658
|
+
this.unsubscribe.push(this.call.on('call.accepted', () => this.armIdleWindow()), this.call.on('call.rejected', () => this.armIdleWindow()), this.call.on('call.missed', () => this.armIdleWindow()),
|
|
10659
|
+
// the ring is over, whichever way it went. Joining an accepted call
|
|
10660
|
+
// never goes through `leave`, so this is the only signal for it.
|
|
10661
|
+
createSubscription(this.call.state.callingState$, (callingState) => {
|
|
10662
|
+
if (ringIsOver(callingState))
|
|
10663
|
+
this.stop();
|
|
10664
|
+
}));
|
|
10665
|
+
this.armIdleWindow();
|
|
10666
|
+
};
|
|
10667
|
+
/**
|
|
10668
|
+
* Pauses polling, keeping the captured session, the deadline and the event
|
|
10669
|
+
* subscriptions. Resuming does not extend the ring window.
|
|
10670
|
+
*/
|
|
10671
|
+
pause = () => {
|
|
10672
|
+
if (this.stopped)
|
|
10673
|
+
return;
|
|
10674
|
+
this.wasPolling = this.intervalId !== undefined;
|
|
10675
|
+
const timers = getTimers();
|
|
10676
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10677
|
+
timers.clearInterval(this.intervalId);
|
|
10678
|
+
this.idleTimeoutId = undefined;
|
|
10679
|
+
this.intervalId = undefined;
|
|
10680
|
+
};
|
|
10681
|
+
/**
|
|
10682
|
+
* Resumes a paused poller. Goes straight back to polling if it was already
|
|
10683
|
+
* past the quiet period, so a pause does not buy the ring another one.
|
|
10684
|
+
*/
|
|
10685
|
+
resume = () => {
|
|
10686
|
+
if (this.stopped || !this.sessionId)
|
|
10687
|
+
return;
|
|
10688
|
+
if (this.idleTimeoutId !== undefined || this.intervalId !== undefined) {
|
|
10637
10689
|
return;
|
|
10638
10690
|
}
|
|
10639
|
-
this.
|
|
10640
|
-
|
|
10691
|
+
if (!this.wasPolling) {
|
|
10692
|
+
this.armIdleWindow();
|
|
10693
|
+
return;
|
|
10694
|
+
}
|
|
10695
|
+
this.intervalId = getTimers().setInterval(this.runTick, this.intervalMs);
|
|
10696
|
+
this.runTick();
|
|
10641
10697
|
};
|
|
10642
|
-
|
|
10698
|
+
/**
|
|
10699
|
+
* Stops polling. The poller cannot be restarted.
|
|
10700
|
+
*/
|
|
10701
|
+
stop = () => {
|
|
10702
|
+
if (this.stopped)
|
|
10703
|
+
return;
|
|
10704
|
+
this.stopped = true;
|
|
10643
10705
|
const timers = getTimers();
|
|
10644
|
-
timers.
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10706
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10707
|
+
timers.clearInterval(this.intervalId);
|
|
10708
|
+
this.idleTimeoutId = undefined;
|
|
10709
|
+
this.intervalId = undefined;
|
|
10710
|
+
this.unsubscribe.forEach((off) => off());
|
|
10711
|
+
this.unsubscribe = [];
|
|
10650
10712
|
};
|
|
10651
|
-
|
|
10713
|
+
armIdleWindow = () => {
|
|
10714
|
+
if (this.stopped)
|
|
10715
|
+
return;
|
|
10652
10716
|
const timers = getTimers();
|
|
10653
|
-
timers.
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10717
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10718
|
+
timers.clearInterval(this.intervalId);
|
|
10719
|
+
this.intervalId = undefined;
|
|
10720
|
+
this.idleTimeoutId = timers.setTimeout(() => {
|
|
10721
|
+
this.idleTimeoutId = undefined;
|
|
10722
|
+
if (this.stopped)
|
|
10723
|
+
return;
|
|
10724
|
+
this.intervalId = timers.setInterval(this.runTick, this.intervalMs);
|
|
10725
|
+
this.runTick();
|
|
10726
|
+
}, this.startAfterMs);
|
|
10662
10727
|
};
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
* Once the event is received, the call is joined.
|
|
10668
|
-
*/
|
|
10669
|
-
const watchCallAccepted = (call) => {
|
|
10670
|
-
return async function onCallAccepted(event) {
|
|
10671
|
-
// We want to discard the event if it's from the current user
|
|
10672
|
-
if (event.user.id === call.currentUserId)
|
|
10673
|
-
return;
|
|
10674
|
-
const { state } = call;
|
|
10675
|
-
if (event.call.created_by.id === call.currentUserId &&
|
|
10676
|
-
state.callingState === exports.CallingState.RINGING) {
|
|
10677
|
-
await call.join();
|
|
10678
|
-
}
|
|
10728
|
+
runTick = () => {
|
|
10729
|
+
this.tick().catch((err) => {
|
|
10730
|
+
this.logger.warn('Failed to poll the ring state', err);
|
|
10731
|
+
});
|
|
10679
10732
|
};
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
* Event handler that watches delivery of `call.rejected` Websocket event.
|
|
10683
|
-
* Once the event is received, the call is left.
|
|
10684
|
-
*/
|
|
10685
|
-
const watchCallRejected = (call) => {
|
|
10686
|
-
return async function onCallRejected(event) {
|
|
10687
|
-
// We want to discard the event if it's from the current user
|
|
10688
|
-
if (event.user.id === call.currentUserId)
|
|
10733
|
+
tick = async () => {
|
|
10734
|
+
if (this.stopped || this.inFlight || !this.sessionId)
|
|
10689
10735
|
return;
|
|
10690
|
-
const {
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
call.logger.warn('No call session provided. Ignoring call.rejected event.', event);
|
|
10736
|
+
const { callingState } = this.call.state;
|
|
10737
|
+
if (ringIsOver(callingState) || Date.now() >= this.deadlineAt) {
|
|
10738
|
+
this.stop();
|
|
10694
10739
|
return;
|
|
10695
10740
|
}
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
if (callingState !== exports.CallingState.RINGING) {
|
|
10699
|
-
call.logger.info('Call is not in ringing mode (it is either accepted or rejected already). Ignoring call.rejected event.', event);
|
|
10741
|
+
// mid-join: nothing to reconcile until we know whether it succeeded
|
|
10742
|
+
if (callingState !== exports.CallingState.RINGING)
|
|
10700
10743
|
return;
|
|
10744
|
+
this.inFlight = true;
|
|
10745
|
+
try {
|
|
10746
|
+
const ringState = await this.call.getRingState(this.sessionId);
|
|
10747
|
+
if (this.stopped)
|
|
10748
|
+
return;
|
|
10749
|
+
this.call.state.updateFromRingState(ringState);
|
|
10750
|
+
if (await reconcileRingState(this.call, 'ring-poll-api'))
|
|
10751
|
+
this.stop();
|
|
10701
10752
|
}
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
await call.leave({
|
|
10709
|
-
reject: true,
|
|
10710
|
-
reason: 'cancel',
|
|
10711
|
-
message: 'ring: everyone rejected',
|
|
10712
|
-
});
|
|
10753
|
+
catch (err) {
|
|
10754
|
+
// a missing session, or one of another call, will never resolve
|
|
10755
|
+
const status = err instanceof ErrorFromResponse ? err.status : undefined;
|
|
10756
|
+
if (status === 400 || status === 404) {
|
|
10757
|
+
this.logger.warn('Stopped polling the ring state', err);
|
|
10758
|
+
this.stop();
|
|
10713
10759
|
}
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
if (rejectedBy[eventCall.created_by.id]) {
|
|
10717
|
-
call.logger.info('call creator rejected, leaving call');
|
|
10718
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
10719
|
-
await call.leave({ message: 'ring: creator rejected' });
|
|
10760
|
+
else {
|
|
10761
|
+
this.logger.debug('Failed to poll the ring state', err);
|
|
10720
10762
|
}
|
|
10721
10763
|
}
|
|
10764
|
+
finally {
|
|
10765
|
+
this.inFlight = false;
|
|
10766
|
+
}
|
|
10767
|
+
};
|
|
10768
|
+
}
|
|
10769
|
+
|
|
10770
|
+
/**
|
|
10771
|
+
* Drops a call that has been ringing for too long: the caller cancels after
|
|
10772
|
+
* `auto_cancel_timeout_ms`, a callee after `incoming_call_timeout_ms`.
|
|
10773
|
+
*
|
|
10774
|
+
* Either timeout being `0` means the call rings until something else settles it.
|
|
10775
|
+
*/
|
|
10776
|
+
class RingTimeout {
|
|
10777
|
+
call;
|
|
10778
|
+
timeoutId;
|
|
10779
|
+
deadlineAt;
|
|
10780
|
+
stopped = false;
|
|
10781
|
+
constructor(call) {
|
|
10782
|
+
this.call = call;
|
|
10783
|
+
}
|
|
10784
|
+
/**
|
|
10785
|
+
* Schedules an auto-drop timeout based on the call settings.
|
|
10786
|
+
* Applicable only for ringing calls.
|
|
10787
|
+
*/
|
|
10788
|
+
start = () => {
|
|
10789
|
+
if (this.stopped || this.timeoutId !== undefined)
|
|
10790
|
+
return;
|
|
10791
|
+
// ignore if the call is not ringing
|
|
10792
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
10793
|
+
return;
|
|
10794
|
+
const ring = this.call.state.settings?.ring;
|
|
10795
|
+
if (!ring)
|
|
10796
|
+
return;
|
|
10797
|
+
const isCaller = this.call.isCreatedByMe;
|
|
10798
|
+
const timeoutMs = isCaller
|
|
10799
|
+
? ring.auto_cancel_timeout_ms
|
|
10800
|
+
: ring.incoming_call_timeout_ms;
|
|
10801
|
+
// 0 means no auto-drop
|
|
10802
|
+
if (timeoutMs <= 0)
|
|
10803
|
+
return;
|
|
10804
|
+
const now = Date.now();
|
|
10805
|
+
this.deadlineAt ??= now + timeoutMs;
|
|
10806
|
+
const delayMs = Math.max(0, this.deadlineAt - now);
|
|
10807
|
+
const timers = getTimers();
|
|
10808
|
+
this.timeoutId = timers.setTimeout(() => {
|
|
10809
|
+
this.timeoutId = undefined;
|
|
10810
|
+
// A failed timeout-triggered leave can arm a fresh retry window.
|
|
10811
|
+
this.deadlineAt = undefined;
|
|
10812
|
+
if (this.stopped)
|
|
10813
|
+
return;
|
|
10814
|
+
// the call might have stopped ringing by this point, e.g. it was already
|
|
10815
|
+
// accepted and joined
|
|
10816
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
10817
|
+
return;
|
|
10818
|
+
this.call
|
|
10819
|
+
.leave({
|
|
10820
|
+
reject: true,
|
|
10821
|
+
reason: 'timeout',
|
|
10822
|
+
message: `ringing timeout - ${isCaller
|
|
10823
|
+
? 'no one accepted'
|
|
10824
|
+
: `user didn't interact with incoming call screen`}`,
|
|
10825
|
+
})
|
|
10826
|
+
.catch((err) => {
|
|
10827
|
+
videoLoggerSystem
|
|
10828
|
+
.getLogger('RingTimeout')
|
|
10829
|
+
.error('Failed to drop the call', err);
|
|
10830
|
+
});
|
|
10831
|
+
}, delayMs);
|
|
10832
|
+
};
|
|
10833
|
+
/**
|
|
10834
|
+
* Pauses the timeout while preserving its original deadline.
|
|
10835
|
+
*/
|
|
10836
|
+
pause = () => {
|
|
10837
|
+
if (this.stopped || this.timeoutId === undefined)
|
|
10838
|
+
return;
|
|
10839
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
10840
|
+
this.timeoutId = undefined;
|
|
10841
|
+
};
|
|
10842
|
+
/**
|
|
10843
|
+
* Cancels a scheduled auto-drop timeout. It cannot be armed again.
|
|
10844
|
+
*/
|
|
10845
|
+
stop = () => {
|
|
10846
|
+
if (this.stopped)
|
|
10847
|
+
return;
|
|
10848
|
+
this.stopped = true;
|
|
10849
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
10850
|
+
this.timeoutId = undefined;
|
|
10851
|
+
this.deadlineAt = undefined;
|
|
10852
|
+
};
|
|
10853
|
+
}
|
|
10854
|
+
|
|
10855
|
+
/**
|
|
10856
|
+
* Decides what a ringing call should do about the current user's own accept or
|
|
10857
|
+
* reject, which may have happened on another device.
|
|
10858
|
+
*
|
|
10859
|
+
* Accepting on *this* device also lands in `accepted_by`, so an acceptance only
|
|
10860
|
+
* means another device took the call when this one is still ringing.
|
|
10861
|
+
*
|
|
10862
|
+
* Only meaningful for a ringing call; the caller checks that.
|
|
10863
|
+
*/
|
|
10864
|
+
const resolveOwnRingOutcome = ({ session, currentUserId, callingState, }) => {
|
|
10865
|
+
if (!currentUserId)
|
|
10866
|
+
return { settledByMe: false };
|
|
10867
|
+
const acceptedByMe = Boolean(session?.accepted_by[currentUserId]);
|
|
10868
|
+
const rejectedByMe = Boolean(session?.rejected_by[currentUserId]);
|
|
10869
|
+
if (!acceptedByMe && !rejectedByMe)
|
|
10870
|
+
return { settledByMe: false };
|
|
10871
|
+
const answeredElsewhere = acceptedByMe && callingState === exports.CallingState.RINGING;
|
|
10872
|
+
return {
|
|
10873
|
+
settledByMe: true,
|
|
10874
|
+
leaveReason: answeredElsewhere
|
|
10875
|
+
? 'answeredElsewhere'
|
|
10876
|
+
: rejectedByMe
|
|
10877
|
+
? 'rejected'
|
|
10878
|
+
: undefined,
|
|
10722
10879
|
};
|
|
10723
10880
|
};
|
|
10881
|
+
|
|
10724
10882
|
/**
|
|
10725
10883
|
* Event handler that watches the delivery of `call.ended` Websocket event.
|
|
10726
10884
|
*/
|
|
@@ -11121,14 +11279,18 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
11121
11279
|
* @param call the call to register event handlers for.
|
|
11122
11280
|
*/
|
|
11123
11281
|
const registerRingingCallEventHandlers = (call) => {
|
|
11124
|
-
const
|
|
11125
|
-
'
|
|
11126
|
-
|
|
11282
|
+
const reconcile = () => {
|
|
11283
|
+
reconcileRingState(call, 'ring-ws').catch((err) => {
|
|
11284
|
+
call.logger.error('Failed to reconcile the ring state', err);
|
|
11285
|
+
});
|
|
11127
11286
|
};
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11287
|
+
// each event needs its own closure. `call.missed` is deliberately absent:
|
|
11288
|
+
// nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
|
|
11289
|
+
// "nobody answered" case until the server-owned ring timeout lands.
|
|
11290
|
+
const eventHandlers = [
|
|
11291
|
+
call.on('call.accepted', () => reconcile()),
|
|
11292
|
+
call.on('call.rejected', () => reconcile()),
|
|
11293
|
+
];
|
|
11132
11294
|
return () => {
|
|
11133
11295
|
eventHandlers.forEach((unsubscribe) => unsubscribe());
|
|
11134
11296
|
};
|
|
@@ -15176,8 +15338,9 @@ class Call {
|
|
|
15176
15338
|
statsReporter;
|
|
15177
15339
|
sfuStatsReporter;
|
|
15178
15340
|
lastStatsOptions;
|
|
15179
|
-
|
|
15180
|
-
|
|
15341
|
+
ringTimeout;
|
|
15342
|
+
ringStatePoller;
|
|
15343
|
+
clientState;
|
|
15181
15344
|
streamClient;
|
|
15182
15345
|
clientEventReporter;
|
|
15183
15346
|
sfuClient;
|
|
@@ -15244,7 +15407,7 @@ class Call {
|
|
|
15244
15407
|
* Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
|
|
15245
15408
|
* method to construct a `Call` instance.
|
|
15246
15409
|
*/
|
|
15247
|
-
constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy,
|
|
15410
|
+
constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientState, ringing = false, watching = false, }) {
|
|
15248
15411
|
this.type = type;
|
|
15249
15412
|
this.id = id;
|
|
15250
15413
|
this.cid = `${type}:${id}`;
|
|
@@ -15252,7 +15415,7 @@ class Call {
|
|
|
15252
15415
|
this.watching = watching;
|
|
15253
15416
|
this.streamClient = streamClient;
|
|
15254
15417
|
this.clientEventReporter = clientEventReporter;
|
|
15255
|
-
this.
|
|
15418
|
+
this.clientState = clientState;
|
|
15256
15419
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
15257
15420
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
15258
15421
|
const callTypeConfig = CallTypes.get(type);
|
|
@@ -15354,27 +15517,24 @@ class Call {
|
|
|
15354
15517
|
createSubscription(this.state.session$, (session) => {
|
|
15355
15518
|
if (!this.ringing)
|
|
15356
15519
|
return;
|
|
15357
|
-
const
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
|
|
15362
|
-
if (
|
|
15520
|
+
const { settledByMe, leaveReason } = resolveOwnRingOutcome({
|
|
15521
|
+
session,
|
|
15522
|
+
currentUserId: this.currentUserId,
|
|
15523
|
+
callingState: this.state.callingState,
|
|
15524
|
+
});
|
|
15525
|
+
if (settledByMe)
|
|
15363
15526
|
this.cancelAutoDrop();
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15371
|
-
});
|
|
15372
|
-
}
|
|
15527
|
+
if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag))
|
|
15528
|
+
return;
|
|
15529
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
|
|
15530
|
+
this.leave().catch(() => {
|
|
15531
|
+
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15532
|
+
});
|
|
15373
15533
|
}));
|
|
15374
15534
|
};
|
|
15375
15535
|
handleRingingCall = () => {
|
|
15376
15536
|
const callSession = this.state.session;
|
|
15377
|
-
const receiver_id = this.
|
|
15537
|
+
const receiver_id = this.clientState.connectedUser?.id;
|
|
15378
15538
|
const ended_at = callSession?.ended_at;
|
|
15379
15539
|
const created_by_id = this.state.createdBy?.id;
|
|
15380
15540
|
if (this.currentUserId && created_by_id === this.currentUserId) {
|
|
@@ -15415,6 +15575,7 @@ class Call {
|
|
|
15415
15575
|
this.state.setCallingState(exports.CallingState.RINGING);
|
|
15416
15576
|
}
|
|
15417
15577
|
this.scheduleAutoDrop();
|
|
15578
|
+
this.scheduleRingStatePolling();
|
|
15418
15579
|
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
15419
15580
|
}
|
|
15420
15581
|
};
|
|
@@ -15501,6 +15662,12 @@ class Call {
|
|
|
15501
15662
|
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
15502
15663
|
throw new Error('Cannot leave call that has already been left.');
|
|
15503
15664
|
}
|
|
15665
|
+
// before the first await: the calling state stays RINGING well into the
|
|
15666
|
+
// teardown, so pause both watchdogs before they can race this leave. They
|
|
15667
|
+
// keep their deadlines, so a failed leave resumes them without handing the
|
|
15668
|
+
// ring another window.
|
|
15669
|
+
this.ringTimeout?.pause();
|
|
15670
|
+
this.ringStatePoller?.pause();
|
|
15504
15671
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
15505
15672
|
const callingState = this.state.callingState;
|
|
15506
15673
|
if (callingState === exports.CallingState.LEFT) {
|
|
@@ -15567,6 +15734,11 @@ class Call {
|
|
|
15567
15734
|
this.audioBindingsWatchdog?.dispose();
|
|
15568
15735
|
await this.dynascaleManager?.dispose();
|
|
15569
15736
|
this.state.setCallingState(exports.CallingState.LEFT);
|
|
15737
|
+
// `ringingSubject` is cleared further down, so this still reads true for
|
|
15738
|
+
// a call that was ringing.
|
|
15739
|
+
if (this.ringing) {
|
|
15740
|
+
globalThis.streamRNVideoSDK?.ringingCallLifecycle?.onLeave(this);
|
|
15741
|
+
}
|
|
15570
15742
|
this.state.setParticipants([]);
|
|
15571
15743
|
this.state.dispose();
|
|
15572
15744
|
// Reset reconnect-related accumulators so a future `call.join()` on the
|
|
@@ -15590,7 +15762,8 @@ class Call {
|
|
|
15590
15762
|
this.unifiedSessionId = undefined;
|
|
15591
15763
|
this.ringingSubject.next(false);
|
|
15592
15764
|
this.cancelAutoDrop();
|
|
15593
|
-
this.
|
|
15765
|
+
this.cancelRingStatePolling();
|
|
15766
|
+
this.clientState.unregisterCall(this);
|
|
15594
15767
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15595
15768
|
isRingingTypeCall: this.ringing,
|
|
15596
15769
|
shouldStopCallManager: this.callManagerStarted,
|
|
@@ -15628,6 +15801,15 @@ class Call {
|
|
|
15628
15801
|
this.logger.warn('Failed to dispose media engine', err);
|
|
15629
15802
|
});
|
|
15630
15803
|
}
|
|
15804
|
+
}).catch((err) => {
|
|
15805
|
+
if (!hasPending(this.joinLeaveConcurrencyTag) &&
|
|
15806
|
+
this.state.callingState === exports.CallingState.RINGING) {
|
|
15807
|
+
// resume, never re-arm: a fresh watchdog would restart the ring
|
|
15808
|
+
// window this leave was already most of the way through
|
|
15809
|
+
this.ringTimeout?.start();
|
|
15810
|
+
this.ringStatePoller?.resume();
|
|
15811
|
+
}
|
|
15812
|
+
throw err;
|
|
15631
15813
|
});
|
|
15632
15814
|
};
|
|
15633
15815
|
/**
|
|
@@ -15640,7 +15822,7 @@ class Call {
|
|
|
15640
15822
|
* Retrieves the current user ID.
|
|
15641
15823
|
*/
|
|
15642
15824
|
get currentUserId() {
|
|
15643
|
-
return this.
|
|
15825
|
+
return this.clientState.connectedUser?.id;
|
|
15644
15826
|
}
|
|
15645
15827
|
/**
|
|
15646
15828
|
* A flag indicating whether self-subscription is enabled for the call.
|
|
@@ -15701,8 +15883,8 @@ class Call {
|
|
|
15701
15883
|
this.ringingSubject.next(true);
|
|
15702
15884
|
// we remove the instance from the calls list to enable the following filter in useCalls hook
|
|
15703
15885
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
15704
|
-
const calls = this.
|
|
15705
|
-
this.
|
|
15886
|
+
const calls = this.clientState.calls.filter((c) => c.cid !== this.cid);
|
|
15887
|
+
this.clientState.setCalls([this, ...calls]);
|
|
15706
15888
|
await this.applyDeviceConfig(settings, { publish: false });
|
|
15707
15889
|
};
|
|
15708
15890
|
/**
|
|
@@ -15725,15 +15907,18 @@ class Call {
|
|
|
15725
15907
|
* @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise.
|
|
15726
15908
|
*/
|
|
15727
15909
|
get = async (params) => {
|
|
15910
|
+
const getLeaveGeneration = this.leaveGeneration;
|
|
15728
15911
|
await this.setup();
|
|
15729
15912
|
const response = await this.streamClient.get(this.streamClientBasePath, params);
|
|
15913
|
+
if (this.leaveGeneration !== getLeaveGeneration)
|
|
15914
|
+
return response;
|
|
15730
15915
|
this.updateFromCallStateResponse(response);
|
|
15731
15916
|
if (params?.ring) {
|
|
15732
15917
|
this.ringingSubject.next(true);
|
|
15733
15918
|
}
|
|
15734
15919
|
if (this.streamClient._hasConnectionID()) {
|
|
15735
15920
|
this.watching = true;
|
|
15736
|
-
this.
|
|
15921
|
+
this.clientState.registerOrUpdateCall(this);
|
|
15737
15922
|
}
|
|
15738
15923
|
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15739
15924
|
return response;
|
|
@@ -15752,7 +15937,7 @@ class Call {
|
|
|
15752
15937
|
}
|
|
15753
15938
|
if (this.streamClient._hasConnectionID()) {
|
|
15754
15939
|
this.watching = true;
|
|
15755
|
-
this.
|
|
15940
|
+
this.clientState.registerOrUpdateCall(this);
|
|
15756
15941
|
}
|
|
15757
15942
|
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15758
15943
|
return response;
|
|
@@ -15778,6 +15963,21 @@ class Call {
|
|
|
15778
15963
|
ring = async (data = {}) => {
|
|
15779
15964
|
return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
|
|
15780
15965
|
};
|
|
15966
|
+
/**
|
|
15967
|
+
* Returns who accepted, rejected or missed the ring for a call session.
|
|
15968
|
+
* Safe to poll: it performs no writes and emits no events.
|
|
15969
|
+
*
|
|
15970
|
+
* @param callSessionId the call session to read. Defaults to the current one.
|
|
15971
|
+
* Pass it explicitly to read a session that has already ended, as ending a
|
|
15972
|
+
* call clears its current session.
|
|
15973
|
+
*/
|
|
15974
|
+
getRingState = async (callSessionId) => {
|
|
15975
|
+
const sessionId = callSessionId ?? this.state.session?.id;
|
|
15976
|
+
if (!sessionId) {
|
|
15977
|
+
throw new Error('Cannot read the ring state: the call has no session');
|
|
15978
|
+
}
|
|
15979
|
+
return this.streamClient.get(`${this.streamClientBasePath}/ring_state`, { call_session_id: sessionId });
|
|
15980
|
+
};
|
|
15781
15981
|
/**
|
|
15782
15982
|
* A shortcut for {@link Call.get} with `notify` parameter set to `true`.
|
|
15783
15983
|
* Will send a `call.notification` event to the call members.
|
|
@@ -15816,42 +16016,80 @@ class Call {
|
|
|
15816
16016
|
/**
|
|
15817
16017
|
* Will start to watch for call related WebSocket events and initiate a call session with the server.
|
|
15818
16018
|
*
|
|
16019
|
+
* One instance is one call flow: discard it after leaving, cancelling, or a
|
|
16020
|
+
* failed join, and create a fresh one for a later flow. Reconnection inside a
|
|
16021
|
+
* live call is handled here and needs no new instance.
|
|
16022
|
+
*
|
|
15819
16023
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15820
16024
|
*/
|
|
15821
|
-
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
16025
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
|
|
15822
16026
|
const callingState = this.state.callingState;
|
|
16027
|
+
// Ahead of the failure boundary below on purpose: a duplicate join on a
|
|
16028
|
+
// live call is refused without tearing that call down.
|
|
15823
16029
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
15824
16030
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
15825
16031
|
}
|
|
16032
|
+
// snapshot before the first await: a leave() landing at any point from
|
|
16033
|
+
// here on supersedes this join, and the retry loop must bail out instead
|
|
16034
|
+
// of resurrecting a call that leave() already tore down.
|
|
16035
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
16036
|
+
const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
|
|
15826
16037
|
// we need this to be set before the callingx.joinCall() is
|
|
15827
16038
|
// called to avoid registering the test call in the CallKit/Telecom
|
|
15828
16039
|
this.allowOwnTracksLoopback = allowOwnTracksLoopback;
|
|
15829
16040
|
if (data?.ring) {
|
|
15830
16041
|
this.ringingSubject.next(true);
|
|
15831
16042
|
}
|
|
16043
|
+
// A ringing call is joined by the SDK rather than by app code, so React
|
|
16044
|
+
// Native prepares it from in here - there is no earlier point at which the
|
|
16045
|
+
// app holds the call. Read after `data.ring` above, which is what makes an
|
|
16046
|
+
// outgoing call ringing in the first place.
|
|
16047
|
+
const ringingLifecycle = this.ringing
|
|
16048
|
+
? globalThis.streamRNVideoSDK?.ringingCallLifecycle
|
|
16049
|
+
: undefined;
|
|
15832
16050
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
15833
|
-
if (callingX) {
|
|
15834
|
-
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
15835
|
-
await callingX.joinCall(this, this.clientStore.calls);
|
|
15836
|
-
}
|
|
15837
|
-
await this.setup();
|
|
15838
|
-
this.clientEventReporter.registerCall(this.cid, {
|
|
15839
|
-
callType: this.type,
|
|
15840
|
-
callId: this.id,
|
|
15841
|
-
getCallSessionId: () => this.state.session?.id ?? '',
|
|
15842
|
-
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
15843
|
-
});
|
|
15844
|
-
this.joinResponseTimeout = joinResponseTimeout;
|
|
15845
|
-
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
15846
|
-
// we will count the number of join failures per SFU.
|
|
15847
|
-
// once the number of failures reaches 2, we will piggyback on the `migrating_from`
|
|
15848
|
-
// field to force the coordinator to provide us another SFU
|
|
15849
|
-
const sfuJoinFailures = new Map();
|
|
15850
|
-
const joinData = data;
|
|
15851
|
-
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
15852
16051
|
try {
|
|
15853
|
-
|
|
16052
|
+
if (ringingLifecycle) {
|
|
16053
|
+
await ringingLifecycle.beforeJoin(this);
|
|
16054
|
+
if (supersededByLeave()) {
|
|
16055
|
+
this.logger.debug('Join superseded by leave; not joining');
|
|
16056
|
+
return;
|
|
16057
|
+
}
|
|
16058
|
+
}
|
|
16059
|
+
if (callingX) {
|
|
16060
|
+
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
16061
|
+
await callingX.joinCall(this, this.clientState.calls, supersededByLeave);
|
|
16062
|
+
if (supersededByLeave()) {
|
|
16063
|
+
this.logger.debug('Join superseded by leave; not setting up');
|
|
16064
|
+
return;
|
|
16065
|
+
}
|
|
16066
|
+
}
|
|
16067
|
+
await this.setup();
|
|
16068
|
+
if (supersededByLeave()) {
|
|
16069
|
+
this.logger.debug('Join superseded by leave; not registering');
|
|
16070
|
+
return;
|
|
16071
|
+
}
|
|
16072
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
16073
|
+
callType: this.type,
|
|
16074
|
+
callId: this.id,
|
|
16075
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
16076
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
16077
|
+
});
|
|
16078
|
+
this.joinResponseTimeout = joinResponseTimeout;
|
|
16079
|
+
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
16080
|
+
// we will count the number of join failures per SFU.
|
|
16081
|
+
// once the number of failures reaches 2, we will piggyback on the `migrating_from`
|
|
16082
|
+
// field to force the coordinator to provide us another SFU
|
|
16083
|
+
const sfuJoinFailures = new Map();
|
|
16084
|
+
const joinData = data;
|
|
16085
|
+
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
16086
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
|
|
15854
16087
|
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
16088
|
+
// Also covers a leave that lands during the backoff below.
|
|
16089
|
+
if (supersededByLeave()) {
|
|
16090
|
+
this.logger.debug('Join superseded by leave; not attempting');
|
|
16091
|
+
return;
|
|
16092
|
+
}
|
|
15855
16093
|
try {
|
|
15856
16094
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
15857
16095
|
await this.doJoin(data);
|
|
@@ -15860,6 +16098,10 @@ class Call {
|
|
|
15860
16098
|
return;
|
|
15861
16099
|
}
|
|
15862
16100
|
catch (err) {
|
|
16101
|
+
if (supersededByLeave()) {
|
|
16102
|
+
this.logger.debug('Join superseded by leave; not retrying');
|
|
16103
|
+
return;
|
|
16104
|
+
}
|
|
15863
16105
|
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
15864
16106
|
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
15865
16107
|
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
@@ -15889,6 +16131,9 @@ class Call {
|
|
|
15889
16131
|
}
|
|
15890
16132
|
catch (error) {
|
|
15891
16133
|
callingX?.endCall(this, 'error');
|
|
16134
|
+
// Ends the failed ringing flow and releases what its setup installed.
|
|
16135
|
+
// Never rejects, so `error` is what the caller sees.
|
|
16136
|
+
await ringingLifecycle?.onJoinFailed(this);
|
|
15892
16137
|
throw error;
|
|
15893
16138
|
}
|
|
15894
16139
|
});
|
|
@@ -15911,6 +16156,10 @@ class Call {
|
|
|
15911
16156
|
// globals resolve to the call's factory. Idempotent across
|
|
15912
16157
|
// reconnect/migration attempts.
|
|
15913
16158
|
await this.ensureMediaFactory();
|
|
16159
|
+
if (supersededByLeave()) {
|
|
16160
|
+
this.logger.debug('Join superseded by leave; not wiring media');
|
|
16161
|
+
return;
|
|
16162
|
+
}
|
|
15914
16163
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
15915
16164
|
if (callingX) {
|
|
15916
16165
|
callingX.wireAudioEngineSubscription();
|
|
@@ -15926,11 +16175,17 @@ class Call {
|
|
|
15926
16175
|
data?.migrating_from) {
|
|
15927
16176
|
try {
|
|
15928
16177
|
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
16178
|
+
if (!joinResponse || supersededByLeave()) {
|
|
16179
|
+
this.logger.debug('Join superseded by leave; not creating SFU client');
|
|
16180
|
+
return;
|
|
16181
|
+
}
|
|
15929
16182
|
this.credentials = joinResponse.credentials;
|
|
15930
16183
|
statsOptions = joinResponse.stats_options;
|
|
15931
16184
|
this.lastStatsOptions = statsOptions;
|
|
15932
16185
|
}
|
|
15933
16186
|
catch (error) {
|
|
16187
|
+
if (supersededByLeave())
|
|
16188
|
+
return;
|
|
15934
16189
|
// prevent triggering reconnect flow if the state is OFFLINE
|
|
15935
16190
|
const avoidRestoreState = this.state.callingState === exports.CallingState.OFFLINE;
|
|
15936
16191
|
if (!avoidRestoreState) {
|
|
@@ -15978,6 +16233,10 @@ class Call {
|
|
|
15978
16233
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
15979
16234
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
15980
16235
|
]);
|
|
16236
|
+
if (supersededByLeave()) {
|
|
16237
|
+
this.logger.debug('Join superseded by leave; not joining SFU');
|
|
16238
|
+
return;
|
|
16239
|
+
}
|
|
15981
16240
|
const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
15982
16241
|
const reconnectDetails = isReconnecting
|
|
15983
16242
|
? this.getReconnectDetails(previousSfuClient?.edgeName, previousSessionId)
|
|
@@ -16003,6 +16262,10 @@ class Call {
|
|
|
16003
16262
|
capabilities,
|
|
16004
16263
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
16005
16264
|
}));
|
|
16265
|
+
if (supersededByLeave()) {
|
|
16266
|
+
this.logger.debug('Join superseded by leave; not applying SFU response');
|
|
16267
|
+
return;
|
|
16268
|
+
}
|
|
16006
16269
|
this.currentPublishOptions = publishOptions;
|
|
16007
16270
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
16008
16271
|
if (callState) {
|
|
@@ -16010,6 +16273,10 @@ class Call {
|
|
|
16010
16273
|
}
|
|
16011
16274
|
}
|
|
16012
16275
|
catch (error) {
|
|
16276
|
+
if (supersededByLeave()) {
|
|
16277
|
+
this.logger.debug('Join superseded by leave; ignoring SFU failure');
|
|
16278
|
+
return;
|
|
16279
|
+
}
|
|
16013
16280
|
this.logger.warn('Join SFU request failed', error);
|
|
16014
16281
|
sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
|
|
16015
16282
|
// restore the previous call state if the join-flow fails
|
|
@@ -16017,8 +16284,8 @@ class Call {
|
|
|
16017
16284
|
throw error;
|
|
16018
16285
|
}
|
|
16019
16286
|
}
|
|
16020
|
-
// If the user left while this join was in flight, bail before re-setting JOINED
|
|
16021
|
-
// peer-connection setup below
|
|
16287
|
+
// If the user left while this join was in flight, bail before re-setting JOINED
|
|
16288
|
+
// or starting peer-connection setup below.
|
|
16022
16289
|
if (supersededByLeave()) {
|
|
16023
16290
|
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
16024
16291
|
return;
|
|
@@ -16047,6 +16314,10 @@ class Call {
|
|
|
16047
16314
|
unifiedSessionId: this.unifiedSessionId,
|
|
16048
16315
|
});
|
|
16049
16316
|
}
|
|
16317
|
+
if (supersededByLeave()) {
|
|
16318
|
+
this.logger.debug('Join superseded by leave; not completing join flow');
|
|
16319
|
+
return;
|
|
16320
|
+
}
|
|
16050
16321
|
// make sure we only track connection timing if we are not calling this method as part of a reconnection flow
|
|
16051
16322
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
16052
16323
|
this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
|
|
@@ -16192,15 +16463,20 @@ class Call {
|
|
|
16192
16463
|
* @internal
|
|
16193
16464
|
*/
|
|
16194
16465
|
initPublisherAndSubscriber = async (opts) => {
|
|
16466
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
16195
16467
|
const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
|
|
16196
16468
|
const { enable_rtc_stats: enableTracing, reporting_interval_ms: reportingIntervalMs, } = statsOptions;
|
|
16197
16469
|
// Flush the previous reporter's final sample while its peer connections are
|
|
16198
16470
|
// still alive, before we dispose them below. Awaits only the sampling step.
|
|
16199
16471
|
await this.sfuStatsReporter?.flush();
|
|
16472
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16473
|
+
return;
|
|
16200
16474
|
this.sfuStatsReporter?.stop();
|
|
16201
16475
|
this.sfuStatsReporter = undefined;
|
|
16202
16476
|
if (closePreviousInstances && this.subscriber) {
|
|
16203
16477
|
await this.subscriber.dispose();
|
|
16478
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16479
|
+
return;
|
|
16204
16480
|
this.state.removeAllOrphanedTracks();
|
|
16205
16481
|
}
|
|
16206
16482
|
const basePeerConnectionOptions = {
|
|
@@ -16243,6 +16519,8 @@ class Call {
|
|
|
16243
16519
|
if (!isAnonymous) {
|
|
16244
16520
|
if (closePreviousInstances && this.publisher) {
|
|
16245
16521
|
await this.publisher.dispose();
|
|
16522
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16523
|
+
return;
|
|
16246
16524
|
}
|
|
16247
16525
|
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
|
|
16248
16526
|
selfSubEnabled: this.allowOwnTracksLoopback,
|
|
@@ -16279,12 +16557,22 @@ class Call {
|
|
|
16279
16557
|
*
|
|
16280
16558
|
* @internal
|
|
16281
16559
|
* @param data the join call data.
|
|
16560
|
+
* @returns The coordinator response, or undefined if leave superseded the request before it was sent.
|
|
16282
16561
|
*/
|
|
16283
16562
|
doJoinRequest = async (data) => {
|
|
16563
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
16284
16564
|
const location = await this.streamClient.getLocationHint();
|
|
16565
|
+
if (this.leaveGeneration !== joinLeaveGeneration) {
|
|
16566
|
+
this.logger.debug('Join superseded by leave; not sending coordinator request');
|
|
16567
|
+
return;
|
|
16568
|
+
}
|
|
16285
16569
|
const e2ee = !!this.e2eeManager;
|
|
16286
16570
|
const request = { ...data, location, e2ee };
|
|
16287
16571
|
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
16572
|
+
if (this.leaveGeneration !== joinLeaveGeneration) {
|
|
16573
|
+
this.logger.debug('Join superseded by leave; not applying coordinator response');
|
|
16574
|
+
return joinResponse;
|
|
16575
|
+
}
|
|
16288
16576
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
16289
16577
|
this.state.setMembers(joinResponse.members);
|
|
16290
16578
|
this.state.setOwnCapabilities(joinResponse.own_capabilities);
|
|
@@ -16295,10 +16583,12 @@ class Call {
|
|
|
16295
16583
|
if (!isReconnecting && this.ringing && !this.isCreatedByMe) {
|
|
16296
16584
|
// signals other users that I have accepted the incoming call.
|
|
16297
16585
|
await this.accept();
|
|
16586
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16587
|
+
return joinResponse;
|
|
16298
16588
|
}
|
|
16299
16589
|
if (this.streamClient._hasConnectionID()) {
|
|
16300
16590
|
this.watching = true;
|
|
16301
|
-
this.
|
|
16591
|
+
this.clientState.registerOrUpdateCall(this);
|
|
16302
16592
|
}
|
|
16303
16593
|
return joinResponse;
|
|
16304
16594
|
};
|
|
@@ -16384,6 +16674,7 @@ class Call {
|
|
|
16384
16674
|
if (callingState === exports.CallingState.JOINING ||
|
|
16385
16675
|
callingState === exports.CallingState.RECONNECTING ||
|
|
16386
16676
|
callingState === exports.CallingState.MIGRATING ||
|
|
16677
|
+
callingState === exports.CallingState.LEFT ||
|
|
16387
16678
|
callingState === exports.CallingState.RECONNECTING_FAILED)
|
|
16388
16679
|
return;
|
|
16389
16680
|
// Drop redundant reconnect calls. If a reconnect is already queued or
|
|
@@ -16392,19 +16683,27 @@ class Call {
|
|
|
16392
16683
|
// `POST /join` per entry) once the call is already healthy again.
|
|
16393
16684
|
if (hasPending(this.reconnectConcurrencyTag))
|
|
16394
16685
|
return;
|
|
16686
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16687
|
+
const supersededByLeave = () => this.leaveGeneration !== reconnectLeaveGeneration;
|
|
16395
16688
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
16689
|
+
if (supersededByLeave())
|
|
16690
|
+
return;
|
|
16396
16691
|
const reconnectStartTime = Date.now();
|
|
16397
16692
|
this.reconnectStrategy = strategy;
|
|
16398
16693
|
this.reconnectReason = reason;
|
|
16399
16694
|
const sfuRejoinFailures = new Map();
|
|
16400
16695
|
const markAsReconnectingFailed = async () => {
|
|
16696
|
+
if (supersededByLeave())
|
|
16697
|
+
return;
|
|
16401
16698
|
try {
|
|
16402
16699
|
// attempt to fetch the call data from the server, as the call
|
|
16403
16700
|
// state might have changed while we were reconnecting or were offline
|
|
16404
16701
|
await this.get();
|
|
16405
16702
|
}
|
|
16406
16703
|
finally {
|
|
16407
|
-
|
|
16704
|
+
if (!supersededByLeave()) {
|
|
16705
|
+
this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
|
|
16706
|
+
}
|
|
16408
16707
|
}
|
|
16409
16708
|
};
|
|
16410
16709
|
const giveUpAndLeave = async (message) => {
|
|
@@ -16460,6 +16759,8 @@ class Call {
|
|
|
16460
16759
|
try {
|
|
16461
16760
|
// wait until the network is available
|
|
16462
16761
|
await this.networkAvailableTask?.promise;
|
|
16762
|
+
if (supersededByLeave())
|
|
16763
|
+
return;
|
|
16463
16764
|
this.logger.info(`[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[this.reconnectStrategy]}`);
|
|
16464
16765
|
switch (this.reconnectStrategy) {
|
|
16465
16766
|
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
@@ -16499,6 +16800,8 @@ class Call {
|
|
|
16499
16800
|
break; // do-while loop, reconnection worked, exit the loop
|
|
16500
16801
|
}
|
|
16501
16802
|
catch (error) {
|
|
16803
|
+
if (supersededByLeave())
|
|
16804
|
+
return;
|
|
16502
16805
|
if (attemptedStrategy === WebsocketReconnectStrategy.REJOIN) {
|
|
16503
16806
|
const failedSfu = this.credentials?.server.edge_name;
|
|
16504
16807
|
if (failedSfu) {
|
|
@@ -16529,6 +16832,8 @@ class Call {
|
|
|
16529
16832
|
}
|
|
16530
16833
|
// exponential backoff with jitter, capped at 5 s
|
|
16531
16834
|
await sleep(retryInterval(attempt));
|
|
16835
|
+
if (supersededByLeave())
|
|
16836
|
+
return;
|
|
16532
16837
|
const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
16533
16838
|
const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
|
|
16534
16839
|
this.fastReconnectDeadlineSeconds;
|
|
@@ -16547,7 +16852,8 @@ class Call {
|
|
|
16547
16852
|
this.reconnectStrategy = nextStrategy;
|
|
16548
16853
|
this.logger.info(`[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
|
|
16549
16854
|
}
|
|
16550
|
-
} while (
|
|
16855
|
+
} while (!supersededByLeave() &&
|
|
16856
|
+
this.state.callingState !== exports.CallingState.JOINED &&
|
|
16551
16857
|
this.state.callingState !== exports.CallingState.RECONNECTING_FAILED &&
|
|
16552
16858
|
this.state.callingState !== exports.CallingState.LEFT);
|
|
16553
16859
|
this.logger.info('[Reconnect] Reconnection flow finished');
|
|
@@ -16558,10 +16864,13 @@ class Call {
|
|
|
16558
16864
|
* @internal
|
|
16559
16865
|
*/
|
|
16560
16866
|
reconnectFast = async () => {
|
|
16867
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16561
16868
|
const reconnectStartTime = Date.now();
|
|
16562
16869
|
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
16563
16870
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
16564
16871
|
await this.doJoin(this.joinCallData);
|
|
16872
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
16873
|
+
return;
|
|
16565
16874
|
await this.get(); // fetch the latest call state, as it might have changed
|
|
16566
16875
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.FAST, (Date.now() - reconnectStartTime) / 1000);
|
|
16567
16876
|
};
|
|
@@ -16570,14 +16879,19 @@ class Call {
|
|
|
16570
16879
|
* @internal
|
|
16571
16880
|
*/
|
|
16572
16881
|
reconnectRejoin = async () => {
|
|
16882
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16573
16883
|
const reconnectStartTime = Date.now();
|
|
16574
16884
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
16575
16885
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
16576
16886
|
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
16577
16887
|
? 'network-available'
|
|
16578
16888
|
: 'full-rejoin';
|
|
16579
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
16889
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
|
|
16890
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
16891
|
+
return;
|
|
16580
16892
|
await this.restorePublishedTracks();
|
|
16893
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
16894
|
+
return;
|
|
16581
16895
|
this.restoreSubscribedTracks();
|
|
16582
16896
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
16583
16897
|
};
|
|
@@ -16586,6 +16900,7 @@ class Call {
|
|
|
16586
16900
|
* @internal
|
|
16587
16901
|
*/
|
|
16588
16902
|
reconnectMigrate = async () => {
|
|
16903
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16589
16904
|
const reconnectStartTime = Date.now();
|
|
16590
16905
|
const currentSfuClient = this.sfuClient;
|
|
16591
16906
|
if (!currentSfuClient) {
|
|
@@ -16599,26 +16914,32 @@ class Call {
|
|
|
16599
16914
|
currentPublisher?.detachEventHandlers();
|
|
16600
16915
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
16601
16916
|
try {
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
16610
|
-
|
|
16611
|
-
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16917
|
+
try {
|
|
16918
|
+
const currentSfu = currentSfuClient.edgeName;
|
|
16919
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
|
|
16920
|
+
...this.joinCallData,
|
|
16921
|
+
migrating_from: currentSfu,
|
|
16922
|
+
migrating_from_list: [currentSfu],
|
|
16923
|
+
}));
|
|
16924
|
+
}
|
|
16925
|
+
finally {
|
|
16926
|
+
// cleanup the migration_from field after the migration is complete or failed
|
|
16927
|
+
// as we don't want to keep dirty data in the join call data
|
|
16928
|
+
delete this.joinCallData?.migrating_from;
|
|
16929
|
+
delete this.joinCallData?.migrating_from_list;
|
|
16930
|
+
}
|
|
16931
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
16932
|
+
return;
|
|
16933
|
+
await this.restorePublishedTracks();
|
|
16934
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
16935
|
+
return;
|
|
16936
|
+
this.restoreSubscribedTracks();
|
|
16618
16937
|
// Wait for the migration to complete, then close the previous SFU client
|
|
16619
16938
|
// and the peer connection instances. In case of failure, the migration
|
|
16620
16939
|
// task would throw an error and REJOIN would be attempted.
|
|
16621
16940
|
await migrationTask();
|
|
16941
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
16942
|
+
return;
|
|
16622
16943
|
// in MIGRATE, we can consider the call as joined only after
|
|
16623
16944
|
// `participantMigrationComplete` event is received, signaled by
|
|
16624
16945
|
// the `migrationTask`
|
|
@@ -16903,10 +17224,11 @@ class Call {
|
|
|
16903
17224
|
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
16904
17225
|
* configured for E2EE.
|
|
16905
17226
|
*
|
|
16906
|
-
* The
|
|
16907
|
-
*
|
|
16908
|
-
*
|
|
16909
|
-
*
|
|
17227
|
+
* The application owns the manager's lifetime - the SDK never disposes it, and
|
|
17228
|
+
* closing the peer connections does not release it. Discard the manager
|
|
17229
|
+
* together with this call instance once its flow has ended, and create a fresh
|
|
17230
|
+
* pair for a later flow: a disposed manager throws from `encrypt`/`decrypt`
|
|
17231
|
+
* rather than silently publishing nothing.
|
|
16910
17232
|
*
|
|
16911
17233
|
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
16912
17234
|
* built-in AES-GCM scheme, or pass your own implementation.
|
|
@@ -16954,7 +17276,7 @@ class Call {
|
|
|
16954
17276
|
});
|
|
16955
17277
|
};
|
|
16956
17278
|
/**
|
|
16957
|
-
* Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state
|
|
17279
|
+
* Will enhance the reported stats with additional participant-specific information (the `callStatsReport$` state variable).
|
|
16958
17280
|
* This is usually helpful when detailed stats for a specific participant are needed.
|
|
16959
17281
|
*
|
|
16960
17282
|
* @param sessionId the sessionId to start reporting for.
|
|
@@ -17373,40 +17695,36 @@ class Call {
|
|
|
17373
17695
|
*/
|
|
17374
17696
|
scheduleAutoDrop = () => {
|
|
17375
17697
|
this.cancelAutoDrop();
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
return;
|
|
17379
|
-
// ignore if the call is not ringing
|
|
17380
|
-
if (this.state.callingState !== exports.CallingState.RINGING)
|
|
17381
|
-
return;
|
|
17382
|
-
const timeoutInMs = this.isCreatedByMe
|
|
17383
|
-
? settings.ring.auto_cancel_timeout_ms
|
|
17384
|
-
: settings.ring.incoming_call_timeout_ms;
|
|
17385
|
-
// 0 means no auto-drop
|
|
17386
|
-
if (timeoutInMs <= 0)
|
|
17387
|
-
return;
|
|
17388
|
-
this.dropTimeout = setTimeout(() => {
|
|
17389
|
-
// the call might have stopped ringing by this point,
|
|
17390
|
-
// e.g. it was already accepted and joined
|
|
17391
|
-
if (this.state.callingState !== exports.CallingState.RINGING)
|
|
17392
|
-
return;
|
|
17393
|
-
this.leave({
|
|
17394
|
-
reject: true,
|
|
17395
|
-
reason: 'timeout',
|
|
17396
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
17397
|
-
? 'no one accepted'
|
|
17398
|
-
: `user didn't interact with incoming call screen`}`,
|
|
17399
|
-
}).catch((err) => {
|
|
17400
|
-
this.logger.error('Failed to drop call', err);
|
|
17401
|
-
});
|
|
17402
|
-
}, timeoutInMs);
|
|
17698
|
+
this.ringTimeout = new RingTimeout(this);
|
|
17699
|
+
this.ringTimeout.start();
|
|
17403
17700
|
};
|
|
17404
17701
|
/**
|
|
17405
17702
|
* Cancels a scheduled auto-drop timeout.
|
|
17406
17703
|
*/
|
|
17407
17704
|
cancelAutoDrop = () => {
|
|
17408
|
-
|
|
17409
|
-
this.
|
|
17705
|
+
this.ringTimeout?.stop();
|
|
17706
|
+
this.ringTimeout = undefined;
|
|
17707
|
+
};
|
|
17708
|
+
/**
|
|
17709
|
+
* Starts polling for the ring outcome. Applicable only to ringing calls the
|
|
17710
|
+
* current user created.
|
|
17711
|
+
*/
|
|
17712
|
+
scheduleRingStatePolling = () => {
|
|
17713
|
+
this.cancelRingStatePolling();
|
|
17714
|
+
if (!this.isCreatedByMe)
|
|
17715
|
+
return;
|
|
17716
|
+
const options = this.streamClient.options.ringStatePolling;
|
|
17717
|
+
if (options === false)
|
|
17718
|
+
return;
|
|
17719
|
+
this.ringStatePoller = new RingStatePoller(this, options);
|
|
17720
|
+
this.ringStatePoller.start();
|
|
17721
|
+
};
|
|
17722
|
+
/**
|
|
17723
|
+
* Cancels the ring state polling.
|
|
17724
|
+
*/
|
|
17725
|
+
cancelRingStatePolling = () => {
|
|
17726
|
+
this.ringStatePoller?.stop();
|
|
17727
|
+
this.ringStatePoller = undefined;
|
|
17410
17728
|
};
|
|
17411
17729
|
/**
|
|
17412
17730
|
* Retrieves the list of recordings for the current call or call session.
|
|
@@ -19077,11 +19395,11 @@ class StreamClient {
|
|
|
19077
19395
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19078
19396
|
};
|
|
19079
19397
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19080
|
-
"2.0.0-beta.
|
|
19398
|
+
"2.0.0-beta.1";
|
|
19081
19399
|
getUserAgent = () => {
|
|
19082
19400
|
if (!this.cachedUserAgent) {
|
|
19083
19401
|
const { clientAppIdentifier = {} } = this.options;
|
|
19084
|
-
const { sdkName = 'js', sdkVersion = "2.0.0-beta.
|
|
19402
|
+
const { sdkName = 'js', sdkVersion = "2.0.0-beta.1", ...extras } = clientAppIdentifier;
|
|
19085
19403
|
this.cachedUserAgent = [
|
|
19086
19404
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19087
19405
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19214,6 +19532,7 @@ class ClientEventReporter {
|
|
|
19214
19532
|
callContexts = new Map();
|
|
19215
19533
|
joinAttemptIds = new Map();
|
|
19216
19534
|
joinReasons = new Map();
|
|
19535
|
+
joinSources = new Map();
|
|
19217
19536
|
coordinatorPairs = new Map();
|
|
19218
19537
|
wsPairs = new Map();
|
|
19219
19538
|
peerConnectionPairs = new Map();
|
|
@@ -19328,6 +19647,7 @@ class ClientEventReporter {
|
|
|
19328
19647
|
this.callContexts.delete(cid);
|
|
19329
19648
|
this.joinAttemptIds.delete(cid);
|
|
19330
19649
|
this.joinReasons.delete(cid);
|
|
19650
|
+
this.joinSources.delete(cid);
|
|
19331
19651
|
this.coordinatorPairs.delete(cid);
|
|
19332
19652
|
this.wsPairs.delete(cid);
|
|
19333
19653
|
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
@@ -19352,7 +19672,12 @@ class ClientEventReporter {
|
|
|
19352
19672
|
this.logger.warn('Failed to start join correlation', err);
|
|
19353
19673
|
}
|
|
19354
19674
|
};
|
|
19355
|
-
withJoinLifecycle = async (cid,
|
|
19675
|
+
withJoinLifecycle = async (cid, options, op) => {
|
|
19676
|
+
const { joinReason, joinSource } = options;
|
|
19677
|
+
if (joinSource)
|
|
19678
|
+
this.joinSources.set(cid, joinSource);
|
|
19679
|
+
else
|
|
19680
|
+
this.joinSources.delete(cid);
|
|
19356
19681
|
this.startCorrelation(cid, joinReason);
|
|
19357
19682
|
try {
|
|
19358
19683
|
return await op();
|
|
@@ -19361,6 +19686,9 @@ class ClientEventReporter {
|
|
|
19361
19686
|
this.closeCallPairs(cid);
|
|
19362
19687
|
throw err;
|
|
19363
19688
|
}
|
|
19689
|
+
finally {
|
|
19690
|
+
this.joinSources.delete(cid);
|
|
19691
|
+
}
|
|
19364
19692
|
};
|
|
19365
19693
|
track = async (cid, stage, op) => {
|
|
19366
19694
|
this.beginAttempt(cid, stage);
|
|
@@ -19484,6 +19812,7 @@ class ClientEventReporter {
|
|
|
19484
19812
|
startedAt: Date.now(),
|
|
19485
19813
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19486
19814
|
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
19815
|
+
joinSourceSnapshot: this.joinSources.get(cid),
|
|
19487
19816
|
};
|
|
19488
19817
|
this.coordinatorPairs.set(cid, pair);
|
|
19489
19818
|
this.sendForCall(cid, {
|
|
@@ -19491,6 +19820,7 @@ class ClientEventReporter {
|
|
|
19491
19820
|
...(pair.joinReasonSnapshot && {
|
|
19492
19821
|
join_reason: pair.joinReasonSnapshot,
|
|
19493
19822
|
}),
|
|
19823
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19494
19824
|
event_type: 'initiated',
|
|
19495
19825
|
});
|
|
19496
19826
|
}
|
|
@@ -19505,6 +19835,7 @@ class ClientEventReporter {
|
|
|
19505
19835
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19506
19836
|
...this.sessionIdField(cid),
|
|
19507
19837
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19838
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19508
19839
|
event_type: 'completed',
|
|
19509
19840
|
outcome: 'success',
|
|
19510
19841
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -19523,6 +19854,7 @@ class ClientEventReporter {
|
|
|
19523
19854
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19524
19855
|
...this.sessionIdField(cid),
|
|
19525
19856
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19857
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19526
19858
|
event_type: 'completed',
|
|
19527
19859
|
outcome: 'failure',
|
|
19528
19860
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -19823,16 +20155,11 @@ const mapWsJoinError = (err) => {
|
|
|
19823
20155
|
*/
|
|
19824
20156
|
class StreamVideoClient {
|
|
19825
20157
|
/**
|
|
19826
|
-
*
|
|
20158
|
+
* The reactive state of this client.
|
|
19827
20159
|
* You can subscribe to changes of the different state variables.
|
|
19828
|
-
* Our library is built in a way that all state changes are exposed in this store,
|
|
19829
|
-
* o all UI changes in your application should be handled by subscribing to these variables.
|
|
19830
|
-
*
|
|
19831
|
-
* @deprecated use the `client.state` getter.
|
|
19832
20160
|
*/
|
|
19833
|
-
|
|
20161
|
+
state = new ClientState();
|
|
19834
20162
|
logger;
|
|
19835
|
-
writeableStateStore;
|
|
19836
20163
|
streamClient;
|
|
19837
20164
|
clientEventReporter;
|
|
19838
20165
|
effectsRegistered = false;
|
|
@@ -19857,8 +20184,6 @@ class StreamVideoClient {
|
|
|
19857
20184
|
streamClient: this.streamClient,
|
|
19858
20185
|
enabled: clientOptions?.clientEventsReportingEnabled ?? true,
|
|
19859
20186
|
});
|
|
19860
|
-
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
19861
|
-
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|
|
19862
20187
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
19863
20188
|
const user = apiKeyOrArgs.user;
|
|
19864
20189
|
if (user.type === 'anonymous')
|
|
@@ -19895,19 +20220,13 @@ class StreamVideoClient {
|
|
|
19895
20220
|
}
|
|
19896
20221
|
StreamVideoClient._instances.set(instanceKey, this);
|
|
19897
20222
|
};
|
|
19898
|
-
/**
|
|
19899
|
-
* Return the reactive state store, use this if you want to be notified about changes to the client state
|
|
19900
|
-
*/
|
|
19901
|
-
get state() {
|
|
19902
|
-
return this.readOnlyStateStore;
|
|
19903
|
-
}
|
|
19904
20223
|
registerEffects = () => {
|
|
19905
20224
|
if (this.effectsRegistered)
|
|
19906
20225
|
return;
|
|
19907
20226
|
this.eventHandlersToUnregister.push(this.on('call.created', (event) => this.initCallFromEvent(event)), this.on('call.ring', (event) => this.initCallFromEvent(event)), this.on('connection.changed', (event) => {
|
|
19908
20227
|
if (!event.online)
|
|
19909
20228
|
return;
|
|
19910
|
-
const callsToReWatch = this.
|
|
20229
|
+
const callsToReWatch = this.state.calls
|
|
19911
20230
|
.filter((call) => call.watching)
|
|
19912
20231
|
.map((call) => call.cid);
|
|
19913
20232
|
if (callsToReWatch.length <= 0)
|
|
@@ -19927,7 +20246,7 @@ class StreamVideoClient {
|
|
|
19927
20246
|
const concurrencyTag = getCallInitConcurrencyTag(e.call_cid);
|
|
19928
20247
|
await withoutConcurrency(concurrencyTag, async () => {
|
|
19929
20248
|
const ringing = e.type === 'call.ring';
|
|
19930
|
-
let call = this.
|
|
20249
|
+
let call = this.state.findCall(e.call.type, e.call.id);
|
|
19931
20250
|
if (call) {
|
|
19932
20251
|
if (ringing) {
|
|
19933
20252
|
if (this.shouldRejectCall(call.cid)) {
|
|
@@ -19953,7 +20272,7 @@ class StreamVideoClient {
|
|
|
19953
20272
|
type: e.call.type,
|
|
19954
20273
|
id: e.call.id,
|
|
19955
20274
|
members: e.members,
|
|
19956
|
-
|
|
20275
|
+
clientState: this.state,
|
|
19957
20276
|
ringing,
|
|
19958
20277
|
});
|
|
19959
20278
|
if (ringing) {
|
|
@@ -19969,7 +20288,7 @@ class StreamVideoClient {
|
|
|
19969
20288
|
}
|
|
19970
20289
|
else {
|
|
19971
20290
|
call.state.updateFromCallResponse(e.call);
|
|
19972
|
-
this.
|
|
20291
|
+
this.state.registerCall(call);
|
|
19973
20292
|
this.logger.info(`New call created and registered: ${call.cid}`);
|
|
19974
20293
|
}
|
|
19975
20294
|
});
|
|
@@ -20000,7 +20319,7 @@ class StreamVideoClient {
|
|
|
20000
20319
|
filter_conditions: { cid: { $in: callsToReWatch } },
|
|
20001
20320
|
});
|
|
20002
20321
|
for (const c of response.calls) {
|
|
20003
|
-
const call = this.
|
|
20322
|
+
const call = this.state.findCall(c.call.type, c.call.id);
|
|
20004
20323
|
if (call) {
|
|
20005
20324
|
call.updateFromCallStateResponse(c);
|
|
20006
20325
|
}
|
|
@@ -20021,8 +20340,6 @@ class StreamVideoClient {
|
|
|
20021
20340
|
};
|
|
20022
20341
|
/**
|
|
20023
20342
|
* Connects the given user to the client.
|
|
20024
|
-
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
20025
|
-
* If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
|
|
20026
20343
|
*
|
|
20027
20344
|
* @param user the user to connect.
|
|
20028
20345
|
* @param tokenOrProvider a token or a function that returns a token.
|
|
@@ -20066,24 +20383,40 @@ class StreamVideoClient {
|
|
|
20066
20383
|
});
|
|
20067
20384
|
// connectUserResponse will be void if connectUser called twice for the same user
|
|
20068
20385
|
if (connectUserResponse?.me) {
|
|
20069
|
-
this.
|
|
20386
|
+
this.state.setConnectedUser(connectUserResponse.me);
|
|
20070
20387
|
}
|
|
20071
20388
|
this.registerEffects();
|
|
20072
20389
|
return connectUserResponse;
|
|
20073
20390
|
};
|
|
20391
|
+
/**
|
|
20392
|
+
* Leaves every call this client still tracks. Runs while the coordinator
|
|
20393
|
+
* connection is alive, so that the leave requests each call sends reach the
|
|
20394
|
+
* backend.
|
|
20395
|
+
*/
|
|
20396
|
+
leaveAllCalls = async () => {
|
|
20397
|
+
for (const call of this.state.calls) {
|
|
20398
|
+
if (call.state.callingState === exports.CallingState.LEFT)
|
|
20399
|
+
continue;
|
|
20400
|
+
this.logger.info(`User disconnected, leaving call: ${call.cid}`);
|
|
20401
|
+
await call
|
|
20402
|
+
.leave({ message: 'client.disconnectUser() called' })
|
|
20403
|
+
.catch((err) => {
|
|
20404
|
+
this.logger.error(`Error leaving call: ${call.cid}`, err);
|
|
20405
|
+
});
|
|
20406
|
+
}
|
|
20407
|
+
};
|
|
20074
20408
|
/**
|
|
20075
20409
|
* Disconnects the currently connected user from the client.
|
|
20076
|
-
*
|
|
20077
|
-
* If the connection is successfully disconnected, the connected user [state variable](#readonlystatestore) will be updated accordingly
|
|
20410
|
+
* Leaves every call the user is still in, and updates `client.state`accordingly.
|
|
20078
20411
|
*
|
|
20079
20412
|
* @param timeout Max number of ms, to wait for close event of websocket, before forcefully assuming successful disconnection.
|
|
20080
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
|
20081
20413
|
*/
|
|
20082
20414
|
disconnectUser = async (timeout) => {
|
|
20083
20415
|
await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
20084
20416
|
const { user, key } = this.streamClient;
|
|
20085
20417
|
if (!user)
|
|
20086
20418
|
return;
|
|
20419
|
+
await this.leaveAllCalls();
|
|
20087
20420
|
await this.streamClient.disconnectUser(timeout);
|
|
20088
20421
|
if (user.id) {
|
|
20089
20422
|
StreamVideoClient._instances.delete(getInstanceKey(key, user));
|
|
@@ -20091,13 +20424,15 @@ class StreamVideoClient {
|
|
|
20091
20424
|
this.eventHandlersToUnregister.forEach((unregister) => unregister());
|
|
20092
20425
|
this.eventHandlersToUnregister = [];
|
|
20093
20426
|
this.effectsRegistered = false;
|
|
20094
|
-
this.
|
|
20427
|
+
this.state.setConnectedUser(undefined);
|
|
20095
20428
|
});
|
|
20096
20429
|
};
|
|
20097
20430
|
/**
|
|
20098
20431
|
* You can subscribe to WebSocket events provided by the API.
|
|
20099
20432
|
* To remove a subscription, call the `off` method or, execute the returned unsubscribe function.
|
|
20100
|
-
* Please note that subscribing to WebSocket events is an advanced use-case,
|
|
20433
|
+
* Please note that subscribing to WebSocket events is an advanced use-case,
|
|
20434
|
+
* for most use-cases it should be enough to watch for changes in the reactive
|
|
20435
|
+
* `client.state`.
|
|
20101
20436
|
*
|
|
20102
20437
|
* @param eventName the event name or 'all'.
|
|
20103
20438
|
* @param callback the callback which will be called when the event is emitted.
|
|
@@ -20124,7 +20459,7 @@ class StreamVideoClient {
|
|
|
20124
20459
|
*/
|
|
20125
20460
|
call = (type, id, options = {}) => {
|
|
20126
20461
|
const call = options.reuseInstance
|
|
20127
|
-
? this.
|
|
20462
|
+
? this.state.findCall(type, id)
|
|
20128
20463
|
: undefined;
|
|
20129
20464
|
return (call ??
|
|
20130
20465
|
new Call({
|
|
@@ -20132,7 +20467,7 @@ class StreamVideoClient {
|
|
|
20132
20467
|
clientEventReporter: this.clientEventReporter,
|
|
20133
20468
|
id: id,
|
|
20134
20469
|
type: type,
|
|
20135
|
-
|
|
20470
|
+
clientState: this.state,
|
|
20136
20471
|
}));
|
|
20137
20472
|
};
|
|
20138
20473
|
/**
|
|
@@ -20162,7 +20497,7 @@ class StreamVideoClient {
|
|
|
20162
20497
|
members: c.members,
|
|
20163
20498
|
ownCapabilities: c.own_capabilities,
|
|
20164
20499
|
watching: data.watch,
|
|
20165
|
-
|
|
20500
|
+
clientState: this.state,
|
|
20166
20501
|
});
|
|
20167
20502
|
call.state.updateFromCallResponse(c.call);
|
|
20168
20503
|
await call.applyDeviceConfig(c.call.settings, {
|
|
@@ -20171,7 +20506,7 @@ class StreamVideoClient {
|
|
|
20171
20506
|
});
|
|
20172
20507
|
if (data.watch) {
|
|
20173
20508
|
await call.setup();
|
|
20174
|
-
this.
|
|
20509
|
+
this.state.registerCall(call);
|
|
20175
20510
|
}
|
|
20176
20511
|
calls.push(call);
|
|
20177
20512
|
}
|
|
@@ -20270,7 +20605,7 @@ class StreamVideoClient {
|
|
|
20270
20605
|
clientEventReporter: this.clientEventReporter,
|
|
20271
20606
|
type: callType,
|
|
20272
20607
|
id: callId,
|
|
20273
|
-
|
|
20608
|
+
clientState: this.state,
|
|
20274
20609
|
ringing: true,
|
|
20275
20610
|
});
|
|
20276
20611
|
await call.get();
|
|
@@ -20776,9 +21111,9 @@ class EncryptionManager extends TypedEventEmitter {
|
|
|
20776
21111
|
}
|
|
20777
21112
|
if ((this.piped ??= new WeakSet()).has(target))
|
|
20778
21113
|
return;
|
|
20779
|
-
this.piped.add(target);
|
|
20780
21114
|
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
20781
21115
|
const { readable, writable } = target.createEncodedStreams();
|
|
21116
|
+
this.piped.add(target);
|
|
20782
21117
|
this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
|
|
20783
21118
|
};
|
|
20784
21119
|
/**
|
|
@@ -20888,6 +21223,7 @@ exports.CallType = CallType;
|
|
|
20888
21223
|
exports.CallTypes = CallTypes;
|
|
20889
21224
|
exports.CameraManager = CameraManager;
|
|
20890
21225
|
exports.CameraManagerState = CameraManagerState;
|
|
21226
|
+
exports.ClientState = ClientState;
|
|
20891
21227
|
exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnum;
|
|
20892
21228
|
exports.DEFAULT_LOOPBACK_RECORDING_DURATION_MS = DEFAULT_LOOPBACK_RECORDING_DURATION_MS;
|
|
20893
21229
|
exports.DeviceManager = DeviceManager;
|
|
@@ -20937,8 +21273,6 @@ exports.StartClosedCaptionsRequestLanguageEnum = StartClosedCaptionsRequestLangu
|
|
|
20937
21273
|
exports.StartTranscriptionRequestLanguageEnum = StartTranscriptionRequestLanguageEnum;
|
|
20938
21274
|
exports.StreamSfuClient = StreamSfuClient;
|
|
20939
21275
|
exports.StreamVideoClient = StreamVideoClient;
|
|
20940
|
-
exports.StreamVideoReadOnlyStateStore = StreamVideoReadOnlyStateStore;
|
|
20941
|
-
exports.StreamVideoWriteableStateStore = StreamVideoWriteableStateStore;
|
|
20942
21276
|
exports.TranscriptionSettingsRequestClosedCaptionModeEnum = TranscriptionSettingsRequestClosedCaptionModeEnum;
|
|
20943
21277
|
exports.TranscriptionSettingsRequestLanguageEnum = TranscriptionSettingsRequestLanguageEnum;
|
|
20944
21278
|
exports.TranscriptionSettingsRequestModeEnum = TranscriptionSettingsRequestModeEnum;
|
|
@@ -20946,6 +21280,7 @@ exports.TranscriptionSettingsResponseClosedCaptionModeEnum = TranscriptionSettin
|
|
|
20946
21280
|
exports.TranscriptionSettingsResponseLanguageEnum = TranscriptionSettingsResponseLanguageEnum;
|
|
20947
21281
|
exports.TranscriptionSettingsResponseModeEnum = TranscriptionSettingsResponseModeEnum;
|
|
20948
21282
|
exports.TranslationSettingsLanguagesEnum = TranslationSettingsLanguagesEnum;
|
|
21283
|
+
exports.TypedEventEmitter = TypedEventEmitter;
|
|
20949
21284
|
exports.UpdateUserPermissionsRequestGrantPermissionsEnum = UpdateUserPermissionsRequestGrantPermissionsEnum;
|
|
20950
21285
|
exports.UpdateUserPermissionsRequestRevokePermissionsEnum = UpdateUserPermissionsRequestRevokePermissionsEnum;
|
|
20951
21286
|
exports.VideoSettingsRequestCameraFacingEnum = VideoSettingsRequestCameraFacingEnum;
|