@stream-io/video-client 1.59.0 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +544 -111
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +544 -110
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +544 -111
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +23 -3
- package/dist/src/coordinator/connection/types.d.ts +16 -0
- package/dist/src/coordinator/connection/utils.d.ts +10 -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/sorting/participants.d.ts +13 -0
- package/dist/src/stats/utils.d.ts +6 -0
- package/dist/src/store/CallState.d.ts +10 -1
- package/package.json +1 -1
- package/src/Call.ts +93 -61
- package/src/StreamSfuClient.ts +6 -2
- package/src/__tests__/Call.autodrop.test.ts +116 -64
- package/src/__tests__/Call.getRingState.test.ts +87 -0
- package/src/__tests__/Call.lifecycle.test.ts +23 -0
- package/src/__tests__/Call.ringSettled.test.ts +71 -0
- package/src/__tests__/StreamSfuClient.test.ts +48 -0
- package/src/__tests__/StreamVideoClient.ringing.test.ts +32 -20
- package/src/coordinator/connection/__tests__/eventListenerIsolation.test.ts +115 -0
- package/src/coordinator/connection/client.ts +3 -2
- package/src/coordinator/connection/types.ts +19 -0
- package/src/coordinator/connection/utils.ts +28 -0
- package/src/events/__tests__/call.test.ts +2 -217
- package/src/events/call.ts +1 -73
- package/src/events/callEventHandlers.ts +12 -23
- package/src/gen/coordinator/index.ts +67 -0
- 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/sorting/__tests__/presets.test.ts +188 -3
- package/src/sorting/__tests__/sorting.test.ts +22 -1
- package/src/sorting/participants.ts +20 -0
- package/src/sorting/presets.ts +4 -0
- package/src/stats/__tests__/utils.test.ts +33 -0
- package/src/stats/utils.ts +15 -0
- package/src/store/CallState.ts +25 -0
- package/src/store/__tests__/CallState.test.ts +61 -2
package/dist/index.cjs.js
CHANGED
|
@@ -4431,6 +4431,28 @@ function isErrorResponse(res) {
|
|
|
4431
4431
|
function isCloseEvent(res) {
|
|
4432
4432
|
return res.code !== undefined;
|
|
4433
4433
|
}
|
|
4434
|
+
/**
|
|
4435
|
+
* Invokes a single event listener in isolation, so that a faulty listener
|
|
4436
|
+
* cannot abort the dispatch loop nor the bookkeeping that follows it.
|
|
4437
|
+
*
|
|
4438
|
+
* @param listener the listener to invoke.
|
|
4439
|
+
* @param event the event to pass to the listener.
|
|
4440
|
+
* @param logger the logger to report listener failures with.
|
|
4441
|
+
*/
|
|
4442
|
+
const invokeEventListener = (listener, event, logger) => {
|
|
4443
|
+
try {
|
|
4444
|
+
const result = listener(event);
|
|
4445
|
+
if (result && typeof result.then === 'function') {
|
|
4446
|
+
Promise.resolve(result).catch((error) => report(logger, error, event));
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
catch (error) {
|
|
4450
|
+
report(logger, error, event);
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
const report = (logger, error, event) => {
|
|
4454
|
+
logger.error('Unhandled error in event listener', error, event);
|
|
4455
|
+
};
|
|
4434
4456
|
|
|
4435
4457
|
/**
|
|
4436
4458
|
* Checks whether we are using React Native
|
|
@@ -5079,7 +5101,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5079
5101
|
}
|
|
5080
5102
|
};
|
|
5081
5103
|
|
|
5082
|
-
const version = "1.
|
|
5104
|
+
const version = "1.60.0";
|
|
5083
5105
|
const [major, minor, patch] = version.split('.');
|
|
5084
5106
|
let sdkInfo = {
|
|
5085
5107
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5928,6 +5950,27 @@ const publishingAudio = (a, b) => {
|
|
|
5928
5950
|
return 1;
|
|
5929
5951
|
return 0;
|
|
5930
5952
|
};
|
|
5953
|
+
/**
|
|
5954
|
+
* A comparator which prioritizes participants who are publishing any track
|
|
5955
|
+
* over participants who are not publishing at all.
|
|
5956
|
+
*
|
|
5957
|
+
* Unlike `publishingVideo` and `publishingAudio`, this comparator does not
|
|
5958
|
+
* distinguish between track types. Muting the microphone or turning off the
|
|
5959
|
+
* camera does not change a participant's rank as long as they still publish
|
|
5960
|
+
* something, which keeps the ordering stable across ordinary device toggles.
|
|
5961
|
+
*
|
|
5962
|
+
* @param a the first participant.
|
|
5963
|
+
* @param b the second participant.
|
|
5964
|
+
*/
|
|
5965
|
+
const publishing = (a, b) => {
|
|
5966
|
+
const hasA = a.publishedTracks.length > 0;
|
|
5967
|
+
const hasB = b.publishedTracks.length > 0;
|
|
5968
|
+
if (hasA && !hasB)
|
|
5969
|
+
return -1;
|
|
5970
|
+
if (!hasA && hasB)
|
|
5971
|
+
return 1;
|
|
5972
|
+
return 0;
|
|
5973
|
+
};
|
|
5931
5974
|
/**
|
|
5932
5975
|
* A comparator which prioritizes participants who are pinned.
|
|
5933
5976
|
*
|
|
@@ -6038,11 +6081,11 @@ const withVideoIngressSource = withParticipantSource(ParticipantSource.RTMP, Par
|
|
|
6038
6081
|
/**
|
|
6039
6082
|
* The default sorting preset.
|
|
6040
6083
|
*/
|
|
6041
|
-
const defaultSortPreset = combineComparators(screenSharing, pinned, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6084
|
+
const defaultSortPreset = combineComparators(screenSharing, pinned, publishing, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6042
6085
|
/**
|
|
6043
6086
|
* The sorting preset for speaker layout.
|
|
6044
6087
|
*/
|
|
6045
|
-
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6088
|
+
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, publishing, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6046
6089
|
/**
|
|
6047
6090
|
* The sorting preset for layouts that don't render all participants but
|
|
6048
6091
|
* instead, render them in pages.
|
|
@@ -6051,7 +6094,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
|
|
|
6051
6094
|
/**
|
|
6052
6095
|
* The sorting preset for livestreams and audio rooms.
|
|
6053
6096
|
*/
|
|
6054
|
-
const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6097
|
+
const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6055
6098
|
|
|
6056
6099
|
/**
|
|
6057
6100
|
* Returns the default egress object - when no egress data is available.
|
|
@@ -7091,6 +7134,30 @@ class CallState {
|
|
|
7091
7134
|
this.setCurrentValue(this.captioningSubject, call.captioning);
|
|
7092
7135
|
this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
7093
7136
|
};
|
|
7137
|
+
/**
|
|
7138
|
+
* Merges a polled ring state into the current session. Only the ring fields
|
|
7139
|
+
* are touched: the endpoint does not return the session roster.
|
|
7140
|
+
*
|
|
7141
|
+
* @internal
|
|
7142
|
+
*
|
|
7143
|
+
* @param ringState the ring state as returned by the coordinator.
|
|
7144
|
+
*/
|
|
7145
|
+
updateFromRingState = (ringState) => {
|
|
7146
|
+
this.setCurrentValue(this.sessionSubject, (session) => {
|
|
7147
|
+
if (!session || session.id !== ringState.session_id)
|
|
7148
|
+
return session;
|
|
7149
|
+
return {
|
|
7150
|
+
...session,
|
|
7151
|
+
accepted_by: ringState.accepted_by,
|
|
7152
|
+
rejected_by: ringState.rejected_by,
|
|
7153
|
+
missed_by: ringState.missed_by,
|
|
7154
|
+
ended_at: ringState.session_ended_at ?? session.ended_at,
|
|
7155
|
+
};
|
|
7156
|
+
});
|
|
7157
|
+
if (ringState.call_ended_at) {
|
|
7158
|
+
this.setEndedAt(new Date(ringState.call_ended_at));
|
|
7159
|
+
}
|
|
7160
|
+
};
|
|
7094
7161
|
/**
|
|
7095
7162
|
* Updates the call state with the data received from the SFU server.
|
|
7096
7163
|
*
|
|
@@ -7402,6 +7469,19 @@ const getSdkName = (sdk) => {
|
|
|
7402
7469
|
const getSdkVersion = (sdk) => {
|
|
7403
7470
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
7404
7471
|
};
|
|
7472
|
+
/**
|
|
7473
|
+
* Returns the identifier the SFU receives in the `X-Stream-Client` header,
|
|
7474
|
+
* e.g. `stream-video-react-v1.43.1`. Matches the format the coordinator
|
|
7475
|
+
* already gets through the client's user agent.
|
|
7476
|
+
*/
|
|
7477
|
+
const getStreamClientId = (sdk) => {
|
|
7478
|
+
const sdkPlatforms = {
|
|
7479
|
+
[SdkType.REACT]: 'react',
|
|
7480
|
+
[SdkType.REACT_NATIVE]: 'react-native',
|
|
7481
|
+
};
|
|
7482
|
+
const platform = (sdk && sdkPlatforms[sdk.type]) || 'js';
|
|
7483
|
+
return `stream-video-${platform}-v${getSdkVersion(sdk)}`;
|
|
7484
|
+
};
|
|
7405
7485
|
|
|
7406
7486
|
/**
|
|
7407
7487
|
* Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
|
|
@@ -10283,7 +10363,10 @@ class StreamSfuClient {
|
|
|
10283
10363
|
this.rpc = createSignalClient({
|
|
10284
10364
|
baseUrl: server.url,
|
|
10285
10365
|
interceptors: [
|
|
10286
|
-
withHeaders({
|
|
10366
|
+
withHeaders({
|
|
10367
|
+
Authorization: `Bearer ${token}`,
|
|
10368
|
+
'X-Stream-Client': getStreamClientId(getSdkInfo()),
|
|
10369
|
+
}),
|
|
10287
10370
|
this.tracer && withRequestTracer(this.tracer.trace),
|
|
10288
10371
|
this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
|
|
10289
10372
|
withTimeout(rpcRequestTimeout, this.tracer?.trace),
|
|
@@ -10620,64 +10703,370 @@ class StreamSfuClient {
|
|
|
10620
10703
|
}
|
|
10621
10704
|
|
|
10622
10705
|
/**
|
|
10623
|
-
*
|
|
10624
|
-
*
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10706
|
+
* Decides what a ringing call should do next, based on the current call state.
|
|
10707
|
+
*
|
|
10708
|
+
* The `call.accepted`, `call.rejected` and `call.missed` handlers and the ring
|
|
10709
|
+
* state poller both run this. They differ only in how the state got there: the
|
|
10710
|
+
* handlers rely on `CallState.updateFromEvent`, which runs before them, and the
|
|
10711
|
+
* poller applies the polled ring state itself.
|
|
10712
|
+
*
|
|
10713
|
+
* @param call the call to reconcile.
|
|
10714
|
+
* @param joinSource which of the two triggered this run, reported on the
|
|
10715
|
+
* caller's join: `ring-ws` for the event handlers, `ring-poll-api` for the
|
|
10716
|
+
* poller.
|
|
10717
|
+
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10718
|
+
* terminal: the caller should keep trying while the ring is open.
|
|
10719
|
+
*/
|
|
10720
|
+
const reconcileRingState = async (call, joinSource) => {
|
|
10721
|
+
if (call.state.callingState !== exports.CallingState.RINGING)
|
|
10722
|
+
return true;
|
|
10723
|
+
return call.isCreatedByMe
|
|
10724
|
+
? reconcileAsCaller(call, joinSource)
|
|
10725
|
+
: reconcileAsCallee(call);
|
|
10726
|
+
};
|
|
10727
|
+
const reconcileAsCaller = async (call, joinSource) => {
|
|
10728
|
+
const { session, members, endedAt } = call.state;
|
|
10729
|
+
const currentUserId = call.currentUserId;
|
|
10730
|
+
// checked before `accepted_by`: an ended session cannot be joined
|
|
10731
|
+
if (endedAt || session?.ended_at) {
|
|
10732
|
+
call.logger.info('ring: the call has ended, leaving');
|
|
10733
|
+
// `leave` reports the remote end to callingx off `reason: 'ended'`
|
|
10734
|
+
return leave(call, {
|
|
10735
|
+
reject: false,
|
|
10736
|
+
reason: 'ended',
|
|
10737
|
+
message: 'ring: call ended',
|
|
10738
|
+
});
|
|
10739
|
+
}
|
|
10740
|
+
const acceptedBy = session?.accepted_by ?? {};
|
|
10741
|
+
if (Object.keys(acceptedBy).some((userId) => userId !== currentUserId)) {
|
|
10742
|
+
call.logger.info('ring: the call was accepted, joining');
|
|
10743
|
+
try {
|
|
10744
|
+
await call.join({ joinSource });
|
|
10635
10745
|
}
|
|
10636
|
-
|
|
10746
|
+
catch (err) {
|
|
10747
|
+
// `doJoin` restores the ringing state when a join fails, so the ring is
|
|
10748
|
+
// still open. Report it unsettled and let the next poll retry.
|
|
10749
|
+
call.logger.error('Failed to join an accepted call', err);
|
|
10750
|
+
return false;
|
|
10751
|
+
}
|
|
10752
|
+
return true;
|
|
10753
|
+
}
|
|
10754
|
+
const otherMembers = members
|
|
10755
|
+
.filter((member) => member.user_id !== currentUserId)
|
|
10756
|
+
.map((member) => member.user_id);
|
|
10757
|
+
if (otherMembers.length === 0)
|
|
10758
|
+
return false;
|
|
10759
|
+
const rejectedBy = session?.rejected_by ?? {};
|
|
10760
|
+
if (otherMembers.every((userId) => rejectedBy[userId])) {
|
|
10761
|
+
call.logger.info('ring: everyone rejected, leaving');
|
|
10762
|
+
return leave(call, {
|
|
10763
|
+
reject: true,
|
|
10764
|
+
reason: 'cancel',
|
|
10765
|
+
message: 'ring: everyone rejected',
|
|
10766
|
+
});
|
|
10767
|
+
}
|
|
10768
|
+
return false;
|
|
10769
|
+
};
|
|
10770
|
+
// the current user's own accept or reject, on this or another device, is
|
|
10771
|
+
// handled by `resolveOwnRingOutcome`, and `call.ended` by `watchCallEnded`.
|
|
10772
|
+
const reconcileAsCallee = async (call) => {
|
|
10773
|
+
const createdById = call.state.createdBy?.id;
|
|
10774
|
+
const rejectedBy = call.state.session?.rejected_by ?? {};
|
|
10775
|
+
if (createdById && rejectedBy[createdById]) {
|
|
10776
|
+
call.logger.info('ring: the caller cancelled, leaving');
|
|
10777
|
+
return leave(call, {
|
|
10778
|
+
reason: 'ended',
|
|
10779
|
+
message: 'ring: creator rejected',
|
|
10780
|
+
});
|
|
10781
|
+
}
|
|
10782
|
+
return false;
|
|
10783
|
+
};
|
|
10784
|
+
// `false` when the call could not be left, so the ring stays open for a retry.
|
|
10785
|
+
const leave = async (call, options) => {
|
|
10786
|
+
try {
|
|
10787
|
+
await call.leave(options);
|
|
10788
|
+
return true;
|
|
10789
|
+
}
|
|
10790
|
+
catch (err) {
|
|
10791
|
+
call.logger.error('Failed to leave a ringing call', err);
|
|
10792
|
+
return false;
|
|
10793
|
+
}
|
|
10637
10794
|
};
|
|
10795
|
+
|
|
10796
|
+
// `doJoin` restores the previous calling state when a join fails, so JOINING can
|
|
10797
|
+
// go back to RINGING. Only a state the ring cannot return from ends the poller.
|
|
10798
|
+
const ringIsOver = (callingState) => callingState !== exports.CallingState.RINGING &&
|
|
10799
|
+
callingState !== exports.CallingState.JOINING;
|
|
10638
10800
|
/**
|
|
10639
|
-
*
|
|
10640
|
-
*
|
|
10801
|
+
* Polls the coordinator for the outcome of a ring the current user started.
|
|
10802
|
+
*
|
|
10803
|
+
* `call.accepted`, `call.rejected` and `call.missed` are delivered best-effort,
|
|
10804
|
+
* with no store-and-forward, so a caller that drops one is left on a ringing
|
|
10805
|
+
* screen while the callee is already in the call. After a quiet period this
|
|
10806
|
+
* reads the ring state until a terminal outcome or the end of the ring window.
|
|
10641
10807
|
*/
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10808
|
+
class RingStatePoller {
|
|
10809
|
+
logger = videoLoggerSystem.getLogger('RingStatePoller');
|
|
10810
|
+
call;
|
|
10811
|
+
startAfterMs;
|
|
10812
|
+
intervalMs;
|
|
10813
|
+
sessionId;
|
|
10814
|
+
deadlineAt = 0;
|
|
10815
|
+
idleTimeoutId;
|
|
10816
|
+
intervalId;
|
|
10817
|
+
stopped = false;
|
|
10818
|
+
wasPolling = false;
|
|
10819
|
+
inFlight = false;
|
|
10820
|
+
unsubscribe = [];
|
|
10821
|
+
constructor(call, options = {}) {
|
|
10822
|
+
this.call = call;
|
|
10823
|
+
this.startAfterMs = options.startAfterMs ?? 15_000;
|
|
10824
|
+
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10825
|
+
}
|
|
10826
|
+
/**
|
|
10827
|
+
* Starts polling. Does nothing when the call has no session yet.
|
|
10828
|
+
*/
|
|
10829
|
+
start = () => {
|
|
10830
|
+
if (this.stopped || this.sessionId)
|
|
10831
|
+
return;
|
|
10832
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
10646
10833
|
return;
|
|
10647
|
-
|
|
10648
|
-
const
|
|
10649
|
-
if (!
|
|
10650
|
-
|
|
10834
|
+
// captured once: `call.ended` clears the call's current session
|
|
10835
|
+
const sessionId = this.call.state.session?.id;
|
|
10836
|
+
if (!sessionId) {
|
|
10837
|
+
this.logger.warn('the call has no session');
|
|
10651
10838
|
return;
|
|
10652
10839
|
}
|
|
10653
|
-
|
|
10654
|
-
const
|
|
10655
|
-
|
|
10656
|
-
|
|
10840
|
+
this.sessionId = sessionId;
|
|
10841
|
+
const ring = this.call.state.settings?.ring;
|
|
10842
|
+
const maxDurationMs = ring?.auto_cancel_timeout_ms || ring?.missed_call_timeout_ms || 30_000;
|
|
10843
|
+
this.deadlineAt = Date.now() + maxDurationMs;
|
|
10844
|
+
// an incoming event proves the socket is alive, so the quiet period starts
|
|
10845
|
+
// over: in a group ring a single rejection does not settle the ring.
|
|
10846
|
+
this.unsubscribe.push(this.call.on('call.accepted', () => this.armIdleWindow()), this.call.on('call.rejected', () => this.armIdleWindow()), this.call.on('call.missed', () => this.armIdleWindow()),
|
|
10847
|
+
// the ring is over, whichever way it went. Joining an accepted call
|
|
10848
|
+
// never goes through `leave`, so this is the only signal for it.
|
|
10849
|
+
createSubscription(this.call.state.callingState$, (callingState) => {
|
|
10850
|
+
if (ringIsOver(callingState))
|
|
10851
|
+
this.stop();
|
|
10852
|
+
}));
|
|
10853
|
+
this.armIdleWindow();
|
|
10854
|
+
};
|
|
10855
|
+
/**
|
|
10856
|
+
* Pauses polling, keeping the captured session, the deadline and the event
|
|
10857
|
+
* subscriptions. Resuming does not extend the ring window.
|
|
10858
|
+
*/
|
|
10859
|
+
pause = () => {
|
|
10860
|
+
if (this.stopped)
|
|
10861
|
+
return;
|
|
10862
|
+
this.wasPolling = this.intervalId !== undefined;
|
|
10863
|
+
const timers = getTimers();
|
|
10864
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10865
|
+
timers.clearInterval(this.intervalId);
|
|
10866
|
+
this.idleTimeoutId = undefined;
|
|
10867
|
+
this.intervalId = undefined;
|
|
10868
|
+
};
|
|
10869
|
+
/**
|
|
10870
|
+
* Resumes a paused poller. Goes straight back to polling if it was already
|
|
10871
|
+
* past the quiet period, so a pause does not buy the ring another one.
|
|
10872
|
+
*/
|
|
10873
|
+
resume = () => {
|
|
10874
|
+
if (this.stopped || !this.sessionId)
|
|
10875
|
+
return;
|
|
10876
|
+
if (this.idleTimeoutId !== undefined || this.intervalId !== undefined) {
|
|
10657
10877
|
return;
|
|
10658
10878
|
}
|
|
10659
|
-
if (
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
.every((m) => rejectedBy[m.user_id]);
|
|
10663
|
-
if (everyoneElseRejected) {
|
|
10664
|
-
call.logger.info('everyone rejected, leaving the call');
|
|
10665
|
-
await call.leave({
|
|
10666
|
-
reject: true,
|
|
10667
|
-
reason: 'cancel',
|
|
10668
|
-
message: 'ring: everyone rejected',
|
|
10669
|
-
});
|
|
10670
|
-
}
|
|
10879
|
+
if (!this.wasPolling) {
|
|
10880
|
+
this.armIdleWindow();
|
|
10881
|
+
return;
|
|
10671
10882
|
}
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10883
|
+
this.intervalId = getTimers().setInterval(this.runTick, this.intervalMs);
|
|
10884
|
+
this.runTick();
|
|
10885
|
+
};
|
|
10886
|
+
/**
|
|
10887
|
+
* Stops polling. The poller cannot be restarted.
|
|
10888
|
+
*/
|
|
10889
|
+
stop = () => {
|
|
10890
|
+
if (this.stopped)
|
|
10891
|
+
return;
|
|
10892
|
+
this.stopped = true;
|
|
10893
|
+
const timers = getTimers();
|
|
10894
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10895
|
+
timers.clearInterval(this.intervalId);
|
|
10896
|
+
this.idleTimeoutId = undefined;
|
|
10897
|
+
this.intervalId = undefined;
|
|
10898
|
+
this.unsubscribe.forEach((off) => off());
|
|
10899
|
+
this.unsubscribe = [];
|
|
10900
|
+
};
|
|
10901
|
+
armIdleWindow = () => {
|
|
10902
|
+
if (this.stopped)
|
|
10903
|
+
return;
|
|
10904
|
+
const timers = getTimers();
|
|
10905
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10906
|
+
timers.clearInterval(this.intervalId);
|
|
10907
|
+
this.intervalId = undefined;
|
|
10908
|
+
this.idleTimeoutId = timers.setTimeout(() => {
|
|
10909
|
+
this.idleTimeoutId = undefined;
|
|
10910
|
+
if (this.stopped)
|
|
10911
|
+
return;
|
|
10912
|
+
this.intervalId = timers.setInterval(this.runTick, this.intervalMs);
|
|
10913
|
+
this.runTick();
|
|
10914
|
+
}, this.startAfterMs);
|
|
10915
|
+
};
|
|
10916
|
+
runTick = () => {
|
|
10917
|
+
this.tick().catch((err) => {
|
|
10918
|
+
this.logger.warn('Failed to poll the ring state', err);
|
|
10919
|
+
});
|
|
10920
|
+
};
|
|
10921
|
+
tick = async () => {
|
|
10922
|
+
if (this.stopped || this.inFlight || !this.sessionId)
|
|
10923
|
+
return;
|
|
10924
|
+
const { callingState } = this.call.state;
|
|
10925
|
+
if (ringIsOver(callingState) || Date.now() >= this.deadlineAt) {
|
|
10926
|
+
this.stop();
|
|
10927
|
+
return;
|
|
10928
|
+
}
|
|
10929
|
+
// mid-join: nothing to reconcile until we know whether it succeeded
|
|
10930
|
+
if (callingState !== exports.CallingState.RINGING)
|
|
10931
|
+
return;
|
|
10932
|
+
this.inFlight = true;
|
|
10933
|
+
try {
|
|
10934
|
+
const ringState = await this.call.getRingState(this.sessionId);
|
|
10935
|
+
if (this.stopped)
|
|
10936
|
+
return;
|
|
10937
|
+
this.call.state.updateFromRingState(ringState);
|
|
10938
|
+
if (await reconcileRingState(this.call, 'ring-poll-api'))
|
|
10939
|
+
this.stop();
|
|
10940
|
+
}
|
|
10941
|
+
catch (err) {
|
|
10942
|
+
// a missing session, or one of another call, will never resolve
|
|
10943
|
+
const status = err instanceof ErrorFromResponse ? err.status : undefined;
|
|
10944
|
+
if (status === 400 || status === 404) {
|
|
10945
|
+
this.logger.warn('Stopped polling the ring state', err);
|
|
10946
|
+
this.stop();
|
|
10677
10947
|
}
|
|
10948
|
+
else {
|
|
10949
|
+
this.logger.debug('Failed to poll the ring state', err);
|
|
10950
|
+
}
|
|
10951
|
+
}
|
|
10952
|
+
finally {
|
|
10953
|
+
this.inFlight = false;
|
|
10678
10954
|
}
|
|
10679
10955
|
};
|
|
10956
|
+
}
|
|
10957
|
+
|
|
10958
|
+
/**
|
|
10959
|
+
* Drops a call that has been ringing for too long: the caller cancels after
|
|
10960
|
+
* `auto_cancel_timeout_ms`, a callee after `incoming_call_timeout_ms`.
|
|
10961
|
+
*
|
|
10962
|
+
* Either timeout being `0` means the call rings until something else settles it.
|
|
10963
|
+
*/
|
|
10964
|
+
class RingTimeout {
|
|
10965
|
+
call;
|
|
10966
|
+
timeoutId;
|
|
10967
|
+
deadlineAt;
|
|
10968
|
+
stopped = false;
|
|
10969
|
+
constructor(call) {
|
|
10970
|
+
this.call = call;
|
|
10971
|
+
}
|
|
10972
|
+
/**
|
|
10973
|
+
* Schedules an auto-drop timeout based on the call settings.
|
|
10974
|
+
* Applicable only for ringing calls.
|
|
10975
|
+
*/
|
|
10976
|
+
start = () => {
|
|
10977
|
+
if (this.stopped || this.timeoutId !== undefined)
|
|
10978
|
+
return;
|
|
10979
|
+
// ignore if the call is not ringing
|
|
10980
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
10981
|
+
return;
|
|
10982
|
+
const ring = this.call.state.settings?.ring;
|
|
10983
|
+
if (!ring)
|
|
10984
|
+
return;
|
|
10985
|
+
const isCaller = this.call.isCreatedByMe;
|
|
10986
|
+
const timeoutMs = isCaller
|
|
10987
|
+
? ring.auto_cancel_timeout_ms
|
|
10988
|
+
: ring.incoming_call_timeout_ms;
|
|
10989
|
+
// 0 means no auto-drop
|
|
10990
|
+
if (timeoutMs <= 0)
|
|
10991
|
+
return;
|
|
10992
|
+
const now = Date.now();
|
|
10993
|
+
this.deadlineAt ??= now + timeoutMs;
|
|
10994
|
+
const delayMs = Math.max(0, this.deadlineAt - now);
|
|
10995
|
+
const timers = getTimers();
|
|
10996
|
+
this.timeoutId = timers.setTimeout(() => {
|
|
10997
|
+
this.timeoutId = undefined;
|
|
10998
|
+
// A failed timeout-triggered leave can arm a fresh retry window.
|
|
10999
|
+
this.deadlineAt = undefined;
|
|
11000
|
+
if (this.stopped)
|
|
11001
|
+
return;
|
|
11002
|
+
// the call might have stopped ringing by this point, e.g. it was already
|
|
11003
|
+
// accepted and joined
|
|
11004
|
+
if (this.call.state.callingState !== exports.CallingState.RINGING)
|
|
11005
|
+
return;
|
|
11006
|
+
this.call
|
|
11007
|
+
.leave({
|
|
11008
|
+
reject: true,
|
|
11009
|
+
reason: 'timeout',
|
|
11010
|
+
message: `ringing timeout - ${isCaller
|
|
11011
|
+
? 'no one accepted'
|
|
11012
|
+
: `user didn't interact with incoming call screen`}`,
|
|
11013
|
+
})
|
|
11014
|
+
.catch((err) => {
|
|
11015
|
+
videoLoggerSystem
|
|
11016
|
+
.getLogger('RingTimeout')
|
|
11017
|
+
.error('Failed to drop the call', err);
|
|
11018
|
+
});
|
|
11019
|
+
}, delayMs);
|
|
11020
|
+
};
|
|
11021
|
+
/**
|
|
11022
|
+
* Pauses the timeout while preserving its original deadline.
|
|
11023
|
+
*/
|
|
11024
|
+
pause = () => {
|
|
11025
|
+
if (this.stopped || this.timeoutId === undefined)
|
|
11026
|
+
return;
|
|
11027
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
11028
|
+
this.timeoutId = undefined;
|
|
11029
|
+
};
|
|
11030
|
+
/**
|
|
11031
|
+
* Cancels a scheduled auto-drop timeout. It cannot be armed again.
|
|
11032
|
+
*/
|
|
11033
|
+
stop = () => {
|
|
11034
|
+
if (this.stopped)
|
|
11035
|
+
return;
|
|
11036
|
+
this.stopped = true;
|
|
11037
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
11038
|
+
this.timeoutId = undefined;
|
|
11039
|
+
this.deadlineAt = undefined;
|
|
11040
|
+
};
|
|
11041
|
+
}
|
|
11042
|
+
|
|
11043
|
+
/**
|
|
11044
|
+
* Decides what a ringing call should do about the current user's own accept or
|
|
11045
|
+
* reject, which may have happened on another device.
|
|
11046
|
+
*
|
|
11047
|
+
* Accepting on *this* device also lands in `accepted_by`, so an acceptance only
|
|
11048
|
+
* means another device took the call when this one is still ringing.
|
|
11049
|
+
*
|
|
11050
|
+
* Only meaningful for a ringing call; the caller checks that.
|
|
11051
|
+
*/
|
|
11052
|
+
const resolveOwnRingOutcome = ({ session, currentUserId, callingState, }) => {
|
|
11053
|
+
if (!currentUserId)
|
|
11054
|
+
return { settledByMe: false };
|
|
11055
|
+
const acceptedByMe = Boolean(session?.accepted_by[currentUserId]);
|
|
11056
|
+
const rejectedByMe = Boolean(session?.rejected_by[currentUserId]);
|
|
11057
|
+
if (!acceptedByMe && !rejectedByMe)
|
|
11058
|
+
return { settledByMe: false };
|
|
11059
|
+
const answeredElsewhere = acceptedByMe && callingState === exports.CallingState.RINGING;
|
|
11060
|
+
return {
|
|
11061
|
+
settledByMe: true,
|
|
11062
|
+
leaveReason: answeredElsewhere
|
|
11063
|
+
? 'answeredElsewhere'
|
|
11064
|
+
: rejectedByMe
|
|
11065
|
+
? 'rejected'
|
|
11066
|
+
: undefined,
|
|
11067
|
+
};
|
|
10680
11068
|
};
|
|
11069
|
+
|
|
10681
11070
|
/**
|
|
10682
11071
|
* Event handler that watches the delivery of `call.ended` Websocket event.
|
|
10683
11072
|
*/
|
|
@@ -11078,14 +11467,18 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
11078
11467
|
* @param call the call to register event handlers for.
|
|
11079
11468
|
*/
|
|
11080
11469
|
const registerRingingCallEventHandlers = (call) => {
|
|
11081
|
-
const
|
|
11082
|
-
'
|
|
11083
|
-
|
|
11470
|
+
const reconcile = () => {
|
|
11471
|
+
reconcileRingState(call, 'ring-ws').catch((err) => {
|
|
11472
|
+
call.logger.error('Failed to reconcile the ring state', err);
|
|
11473
|
+
});
|
|
11084
11474
|
};
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11475
|
+
// each event needs its own closure. `call.missed` is deliberately absent:
|
|
11476
|
+
// nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
|
|
11477
|
+
// "nobody answered" case until the server-owned ring timeout lands.
|
|
11478
|
+
const eventHandlers = [
|
|
11479
|
+
call.on('call.accepted', () => reconcile()),
|
|
11480
|
+
call.on('call.rejected', () => reconcile()),
|
|
11481
|
+
];
|
|
11089
11482
|
return () => {
|
|
11090
11483
|
eventHandlers.forEach((unsubscribe) => unsubscribe());
|
|
11091
11484
|
};
|
|
@@ -15133,7 +15526,8 @@ class Call {
|
|
|
15133
15526
|
statsReporter;
|
|
15134
15527
|
sfuStatsReporter;
|
|
15135
15528
|
lastStatsOptions;
|
|
15136
|
-
|
|
15529
|
+
ringTimeout;
|
|
15530
|
+
ringStatePoller;
|
|
15137
15531
|
clientStore;
|
|
15138
15532
|
streamClient;
|
|
15139
15533
|
clientEventReporter;
|
|
@@ -15311,22 +15705,19 @@ class Call {
|
|
|
15311
15705
|
createSubscription(this.state.session$, (session) => {
|
|
15312
15706
|
if (!this.ringing)
|
|
15313
15707
|
return;
|
|
15314
|
-
const
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
if (
|
|
15708
|
+
const { settledByMe, leaveReason } = resolveOwnRingOutcome({
|
|
15709
|
+
session,
|
|
15710
|
+
currentUserId: this.currentUserId,
|
|
15711
|
+
callingState: this.state.callingState,
|
|
15712
|
+
});
|
|
15713
|
+
if (settledByMe)
|
|
15320
15714
|
this.cancelAutoDrop();
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15328
|
-
});
|
|
15329
|
-
}
|
|
15715
|
+
if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag))
|
|
15716
|
+
return;
|
|
15717
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
|
|
15718
|
+
this.leave().catch(() => {
|
|
15719
|
+
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15720
|
+
});
|
|
15330
15721
|
}));
|
|
15331
15722
|
};
|
|
15332
15723
|
handleRingingCall = () => {
|
|
@@ -15372,6 +15763,7 @@ class Call {
|
|
|
15372
15763
|
this.state.setCallingState(exports.CallingState.RINGING);
|
|
15373
15764
|
}
|
|
15374
15765
|
this.scheduleAutoDrop();
|
|
15766
|
+
this.scheduleRingStatePolling();
|
|
15375
15767
|
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
15376
15768
|
}
|
|
15377
15769
|
};
|
|
@@ -15458,6 +15850,12 @@ class Call {
|
|
|
15458
15850
|
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
15459
15851
|
throw new Error('Cannot leave call that has already been left.');
|
|
15460
15852
|
}
|
|
15853
|
+
// before the first await: the calling state stays RINGING well into the
|
|
15854
|
+
// teardown, so pause both watchdogs before they can race this leave. They
|
|
15855
|
+
// keep their deadlines, so a failed leave resumes them without handing the
|
|
15856
|
+
// ring another window.
|
|
15857
|
+
this.ringTimeout?.pause();
|
|
15858
|
+
this.ringStatePoller?.pause();
|
|
15461
15859
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
15462
15860
|
const callingState = this.state.callingState;
|
|
15463
15861
|
if (callingState === exports.CallingState.LEFT) {
|
|
@@ -15547,6 +15945,7 @@ class Call {
|
|
|
15547
15945
|
this.unifiedSessionId = undefined;
|
|
15548
15946
|
this.ringingSubject.next(false);
|
|
15549
15947
|
this.cancelAutoDrop();
|
|
15948
|
+
this.cancelRingStatePolling();
|
|
15550
15949
|
this.clientStore.unregisterCall(this);
|
|
15551
15950
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15552
15951
|
isRingingTypeCall: this.ringing,
|
|
@@ -15585,6 +15984,15 @@ class Call {
|
|
|
15585
15984
|
this.logger.warn('Failed to dispose media engine', err);
|
|
15586
15985
|
});
|
|
15587
15986
|
}
|
|
15987
|
+
}).catch((err) => {
|
|
15988
|
+
if (!hasPending(this.joinLeaveConcurrencyTag) &&
|
|
15989
|
+
this.state.callingState === exports.CallingState.RINGING) {
|
|
15990
|
+
// resume, never re-arm: a fresh watchdog would restart the ring
|
|
15991
|
+
// window this leave was already most of the way through
|
|
15992
|
+
this.ringTimeout?.start();
|
|
15993
|
+
this.ringStatePoller?.resume();
|
|
15994
|
+
}
|
|
15995
|
+
throw err;
|
|
15588
15996
|
});
|
|
15589
15997
|
};
|
|
15590
15998
|
/**
|
|
@@ -15735,6 +16143,21 @@ class Call {
|
|
|
15735
16143
|
ring = async (data = {}) => {
|
|
15736
16144
|
return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
|
|
15737
16145
|
};
|
|
16146
|
+
/**
|
|
16147
|
+
* Returns who accepted, rejected or missed the ring for a call session.
|
|
16148
|
+
* Safe to poll: it performs no writes and emits no events.
|
|
16149
|
+
*
|
|
16150
|
+
* @param callSessionId the call session to read. Defaults to the current one.
|
|
16151
|
+
* Pass it explicitly to read a session that has already ended, as ending a
|
|
16152
|
+
* call clears its current session.
|
|
16153
|
+
*/
|
|
16154
|
+
getRingState = async (callSessionId) => {
|
|
16155
|
+
const sessionId = callSessionId ?? this.state.session?.id;
|
|
16156
|
+
if (!sessionId) {
|
|
16157
|
+
throw new Error('Cannot read the ring state: the call has no session');
|
|
16158
|
+
}
|
|
16159
|
+
return this.streamClient.get(`${this.streamClientBasePath}/ring_state`, { call_session_id: sessionId });
|
|
16160
|
+
};
|
|
15738
16161
|
/**
|
|
15739
16162
|
* A shortcut for {@link Call.get} with `notify` parameter set to `true`.
|
|
15740
16163
|
* Will send a `call.notification` event to the call members.
|
|
@@ -15775,7 +16198,7 @@ class Call {
|
|
|
15775
16198
|
*
|
|
15776
16199
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15777
16200
|
*/
|
|
15778
|
-
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
16201
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
|
|
15779
16202
|
const callingState = this.state.callingState;
|
|
15780
16203
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
15781
16204
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15807,7 +16230,7 @@ class Call {
|
|
|
15807
16230
|
const joinData = data;
|
|
15808
16231
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
15809
16232
|
try {
|
|
15810
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
16233
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
|
|
15811
16234
|
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
15812
16235
|
try {
|
|
15813
16236
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
@@ -16533,7 +16956,7 @@ class Call {
|
|
|
16533
16956
|
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
16534
16957
|
? 'network-available'
|
|
16535
16958
|
: 'full-rejoin';
|
|
16536
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
16959
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
|
|
16537
16960
|
await this.restorePublishedTracks();
|
|
16538
16961
|
this.restoreSubscribedTracks();
|
|
16539
16962
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -16557,7 +16980,7 @@ class Call {
|
|
|
16557
16980
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
16558
16981
|
try {
|
|
16559
16982
|
const currentSfu = currentSfuClient.edgeName;
|
|
16560
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
16983
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
|
|
16561
16984
|
...this.joinCallData,
|
|
16562
16985
|
migrating_from: currentSfu,
|
|
16563
16986
|
migrating_from_list: [currentSfu],
|
|
@@ -17330,40 +17753,36 @@ class Call {
|
|
|
17330
17753
|
*/
|
|
17331
17754
|
scheduleAutoDrop = () => {
|
|
17332
17755
|
this.cancelAutoDrop();
|
|
17333
|
-
|
|
17334
|
-
|
|
17335
|
-
return;
|
|
17336
|
-
// ignore if the call is not ringing
|
|
17337
|
-
if (this.state.callingState !== exports.CallingState.RINGING)
|
|
17338
|
-
return;
|
|
17339
|
-
const timeoutInMs = this.isCreatedByMe
|
|
17340
|
-
? settings.ring.auto_cancel_timeout_ms
|
|
17341
|
-
: settings.ring.incoming_call_timeout_ms;
|
|
17342
|
-
// 0 means no auto-drop
|
|
17343
|
-
if (timeoutInMs <= 0)
|
|
17344
|
-
return;
|
|
17345
|
-
this.dropTimeout = setTimeout(() => {
|
|
17346
|
-
// the call might have stopped ringing by this point,
|
|
17347
|
-
// e.g. it was already accepted and joined
|
|
17348
|
-
if (this.state.callingState !== exports.CallingState.RINGING)
|
|
17349
|
-
return;
|
|
17350
|
-
this.leave({
|
|
17351
|
-
reject: true,
|
|
17352
|
-
reason: 'timeout',
|
|
17353
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
17354
|
-
? 'no one accepted'
|
|
17355
|
-
: `user didn't interact with incoming call screen`}`,
|
|
17356
|
-
}).catch((err) => {
|
|
17357
|
-
this.logger.error('Failed to drop call', err);
|
|
17358
|
-
});
|
|
17359
|
-
}, timeoutInMs);
|
|
17756
|
+
this.ringTimeout = new RingTimeout(this);
|
|
17757
|
+
this.ringTimeout.start();
|
|
17360
17758
|
};
|
|
17361
17759
|
/**
|
|
17362
17760
|
* Cancels a scheduled auto-drop timeout.
|
|
17363
17761
|
*/
|
|
17364
17762
|
cancelAutoDrop = () => {
|
|
17365
|
-
|
|
17366
|
-
this.
|
|
17763
|
+
this.ringTimeout?.stop();
|
|
17764
|
+
this.ringTimeout = undefined;
|
|
17765
|
+
};
|
|
17766
|
+
/**
|
|
17767
|
+
* Starts polling for the ring outcome. Applicable only to ringing calls the
|
|
17768
|
+
* current user created.
|
|
17769
|
+
*/
|
|
17770
|
+
scheduleRingStatePolling = () => {
|
|
17771
|
+
this.cancelRingStatePolling();
|
|
17772
|
+
if (!this.isCreatedByMe)
|
|
17773
|
+
return;
|
|
17774
|
+
const options = this.streamClient.options.ringStatePolling;
|
|
17775
|
+
if (options === false)
|
|
17776
|
+
return;
|
|
17777
|
+
this.ringStatePoller = new RingStatePoller(this, options);
|
|
17778
|
+
this.ringStatePoller.start();
|
|
17779
|
+
};
|
|
17780
|
+
/**
|
|
17781
|
+
* Cancels the ring state polling.
|
|
17782
|
+
*/
|
|
17783
|
+
cancelRingStatePolling = () => {
|
|
17784
|
+
this.ringStatePoller?.stop();
|
|
17785
|
+
this.ringStatePoller = undefined;
|
|
17367
17786
|
};
|
|
17368
17787
|
/**
|
|
17369
17788
|
* Retrieves the list of recordings for the current call or call session.
|
|
@@ -19010,11 +19429,11 @@ class StreamClient {
|
|
|
19010
19429
|
return;
|
|
19011
19430
|
// call generic listeners
|
|
19012
19431
|
for (const listener of this.listeners.all || []) {
|
|
19013
|
-
listener
|
|
19432
|
+
invokeEventListener(listener, event, this.logger);
|
|
19014
19433
|
}
|
|
19015
19434
|
// call type specific listeners
|
|
19016
19435
|
for (const listener of this.listeners[event.type] || []) {
|
|
19017
|
-
listener
|
|
19436
|
+
invokeEventListener(listener, event, this.logger);
|
|
19018
19437
|
}
|
|
19019
19438
|
};
|
|
19020
19439
|
/**
|
|
@@ -19034,11 +19453,11 @@ class StreamClient {
|
|
|
19034
19453
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19035
19454
|
};
|
|
19036
19455
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19037
|
-
"1.
|
|
19456
|
+
"1.60.0";
|
|
19038
19457
|
getUserAgent = () => {
|
|
19039
19458
|
if (!this.cachedUserAgent) {
|
|
19040
19459
|
const { clientAppIdentifier = {} } = this.options;
|
|
19041
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
19460
|
+
const { sdkName = 'js', sdkVersion = "1.60.0", ...extras } = clientAppIdentifier;
|
|
19042
19461
|
this.cachedUserAgent = [
|
|
19043
19462
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19044
19463
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19171,6 +19590,7 @@ class ClientEventReporter {
|
|
|
19171
19590
|
callContexts = new Map();
|
|
19172
19591
|
joinAttemptIds = new Map();
|
|
19173
19592
|
joinReasons = new Map();
|
|
19593
|
+
joinSources = new Map();
|
|
19174
19594
|
coordinatorPairs = new Map();
|
|
19175
19595
|
wsPairs = new Map();
|
|
19176
19596
|
peerConnectionPairs = new Map();
|
|
@@ -19285,6 +19705,7 @@ class ClientEventReporter {
|
|
|
19285
19705
|
this.callContexts.delete(cid);
|
|
19286
19706
|
this.joinAttemptIds.delete(cid);
|
|
19287
19707
|
this.joinReasons.delete(cid);
|
|
19708
|
+
this.joinSources.delete(cid);
|
|
19288
19709
|
this.coordinatorPairs.delete(cid);
|
|
19289
19710
|
this.wsPairs.delete(cid);
|
|
19290
19711
|
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
@@ -19309,7 +19730,12 @@ class ClientEventReporter {
|
|
|
19309
19730
|
this.logger.warn('Failed to start join correlation', err);
|
|
19310
19731
|
}
|
|
19311
19732
|
};
|
|
19312
|
-
withJoinLifecycle = async (cid,
|
|
19733
|
+
withJoinLifecycle = async (cid, options, op) => {
|
|
19734
|
+
const { joinReason, joinSource } = options;
|
|
19735
|
+
if (joinSource)
|
|
19736
|
+
this.joinSources.set(cid, joinSource);
|
|
19737
|
+
else
|
|
19738
|
+
this.joinSources.delete(cid);
|
|
19313
19739
|
this.startCorrelation(cid, joinReason);
|
|
19314
19740
|
try {
|
|
19315
19741
|
return await op();
|
|
@@ -19318,6 +19744,9 @@ class ClientEventReporter {
|
|
|
19318
19744
|
this.closeCallPairs(cid);
|
|
19319
19745
|
throw err;
|
|
19320
19746
|
}
|
|
19747
|
+
finally {
|
|
19748
|
+
this.joinSources.delete(cid);
|
|
19749
|
+
}
|
|
19321
19750
|
};
|
|
19322
19751
|
track = async (cid, stage, op) => {
|
|
19323
19752
|
this.beginAttempt(cid, stage);
|
|
@@ -19441,6 +19870,7 @@ class ClientEventReporter {
|
|
|
19441
19870
|
startedAt: Date.now(),
|
|
19442
19871
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19443
19872
|
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
19873
|
+
joinSourceSnapshot: this.joinSources.get(cid),
|
|
19444
19874
|
};
|
|
19445
19875
|
this.coordinatorPairs.set(cid, pair);
|
|
19446
19876
|
this.sendForCall(cid, {
|
|
@@ -19448,6 +19878,7 @@ class ClientEventReporter {
|
|
|
19448
19878
|
...(pair.joinReasonSnapshot && {
|
|
19449
19879
|
join_reason: pair.joinReasonSnapshot,
|
|
19450
19880
|
}),
|
|
19881
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19451
19882
|
event_type: 'initiated',
|
|
19452
19883
|
});
|
|
19453
19884
|
}
|
|
@@ -19462,6 +19893,7 @@ class ClientEventReporter {
|
|
|
19462
19893
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19463
19894
|
...this.sessionIdField(cid),
|
|
19464
19895
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19896
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19465
19897
|
event_type: 'completed',
|
|
19466
19898
|
outcome: 'success',
|
|
19467
19899
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -19480,6 +19912,7 @@ class ClientEventReporter {
|
|
|
19480
19912
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19481
19913
|
...this.sessionIdField(cid),
|
|
19482
19914
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19915
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19483
19916
|
event_type: 'completed',
|
|
19484
19917
|
outcome: 'failure',
|
|
19485
19918
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -20949,6 +21382,7 @@ exports.name = name;
|
|
|
20949
21382
|
exports.noopComparator = noopComparator;
|
|
20950
21383
|
exports.paginatedLayoutSortPreset = paginatedLayoutSortPreset;
|
|
20951
21384
|
exports.pinned = pinned;
|
|
21385
|
+
exports.publishing = publishing;
|
|
20952
21386
|
exports.publishingAudio = publishingAudio;
|
|
20953
21387
|
exports.publishingVideo = publishingVideo;
|
|
20954
21388
|
exports.reactionType = reactionType;
|