@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.es.js
CHANGED
|
@@ -4412,6 +4412,28 @@ function isErrorResponse(res) {
|
|
|
4412
4412
|
function isCloseEvent(res) {
|
|
4413
4413
|
return res.code !== undefined;
|
|
4414
4414
|
}
|
|
4415
|
+
/**
|
|
4416
|
+
* Invokes a single event listener in isolation, so that a faulty listener
|
|
4417
|
+
* cannot abort the dispatch loop nor the bookkeeping that follows it.
|
|
4418
|
+
*
|
|
4419
|
+
* @param listener the listener to invoke.
|
|
4420
|
+
* @param event the event to pass to the listener.
|
|
4421
|
+
* @param logger the logger to report listener failures with.
|
|
4422
|
+
*/
|
|
4423
|
+
const invokeEventListener = (listener, event, logger) => {
|
|
4424
|
+
try {
|
|
4425
|
+
const result = listener(event);
|
|
4426
|
+
if (result && typeof result.then === 'function') {
|
|
4427
|
+
Promise.resolve(result).catch((error) => report(logger, error, event));
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
catch (error) {
|
|
4431
|
+
report(logger, error, event);
|
|
4432
|
+
}
|
|
4433
|
+
};
|
|
4434
|
+
const report = (logger, error, event) => {
|
|
4435
|
+
logger.error('Unhandled error in event listener', error, event);
|
|
4436
|
+
};
|
|
4415
4437
|
|
|
4416
4438
|
/**
|
|
4417
4439
|
* Checks whether we are using React Native
|
|
@@ -5060,7 +5082,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5060
5082
|
}
|
|
5061
5083
|
};
|
|
5062
5084
|
|
|
5063
|
-
const version = "1.
|
|
5085
|
+
const version = "1.60.0";
|
|
5064
5086
|
const [major, minor, patch] = version.split('.');
|
|
5065
5087
|
let sdkInfo = {
|
|
5066
5088
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5909,6 +5931,27 @@ const publishingAudio = (a, b) => {
|
|
|
5909
5931
|
return 1;
|
|
5910
5932
|
return 0;
|
|
5911
5933
|
};
|
|
5934
|
+
/**
|
|
5935
|
+
* A comparator which prioritizes participants who are publishing any track
|
|
5936
|
+
* over participants who are not publishing at all.
|
|
5937
|
+
*
|
|
5938
|
+
* Unlike `publishingVideo` and `publishingAudio`, this comparator does not
|
|
5939
|
+
* distinguish between track types. Muting the microphone or turning off the
|
|
5940
|
+
* camera does not change a participant's rank as long as they still publish
|
|
5941
|
+
* something, which keeps the ordering stable across ordinary device toggles.
|
|
5942
|
+
*
|
|
5943
|
+
* @param a the first participant.
|
|
5944
|
+
* @param b the second participant.
|
|
5945
|
+
*/
|
|
5946
|
+
const publishing = (a, b) => {
|
|
5947
|
+
const hasA = a.publishedTracks.length > 0;
|
|
5948
|
+
const hasB = b.publishedTracks.length > 0;
|
|
5949
|
+
if (hasA && !hasB)
|
|
5950
|
+
return -1;
|
|
5951
|
+
if (!hasA && hasB)
|
|
5952
|
+
return 1;
|
|
5953
|
+
return 0;
|
|
5954
|
+
};
|
|
5912
5955
|
/**
|
|
5913
5956
|
* A comparator which prioritizes participants who are pinned.
|
|
5914
5957
|
*
|
|
@@ -6019,11 +6062,11 @@ const withVideoIngressSource = withParticipantSource(ParticipantSource.RTMP, Par
|
|
|
6019
6062
|
/**
|
|
6020
6063
|
* The default sorting preset.
|
|
6021
6064
|
*/
|
|
6022
|
-
const defaultSortPreset = combineComparators(screenSharing, pinned, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6065
|
+
const defaultSortPreset = combineComparators(screenSharing, pinned, publishing, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6023
6066
|
/**
|
|
6024
6067
|
* The sorting preset for speaker layout.
|
|
6025
6068
|
*/
|
|
6026
|
-
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6069
|
+
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, publishing, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6027
6070
|
/**
|
|
6028
6071
|
* The sorting preset for layouts that don't render all participants but
|
|
6029
6072
|
* instead, render them in pages.
|
|
@@ -6032,7 +6075,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
|
|
|
6032
6075
|
/**
|
|
6033
6076
|
* The sorting preset for livestreams and audio rooms.
|
|
6034
6077
|
*/
|
|
6035
|
-
const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6078
|
+
const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6036
6079
|
|
|
6037
6080
|
/**
|
|
6038
6081
|
* Returns the default egress object - when no egress data is available.
|
|
@@ -7072,6 +7115,30 @@ class CallState {
|
|
|
7072
7115
|
this.setCurrentValue(this.captioningSubject, call.captioning);
|
|
7073
7116
|
this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
7074
7117
|
};
|
|
7118
|
+
/**
|
|
7119
|
+
* Merges a polled ring state into the current session. Only the ring fields
|
|
7120
|
+
* are touched: the endpoint does not return the session roster.
|
|
7121
|
+
*
|
|
7122
|
+
* @internal
|
|
7123
|
+
*
|
|
7124
|
+
* @param ringState the ring state as returned by the coordinator.
|
|
7125
|
+
*/
|
|
7126
|
+
updateFromRingState = (ringState) => {
|
|
7127
|
+
this.setCurrentValue(this.sessionSubject, (session) => {
|
|
7128
|
+
if (!session || session.id !== ringState.session_id)
|
|
7129
|
+
return session;
|
|
7130
|
+
return {
|
|
7131
|
+
...session,
|
|
7132
|
+
accepted_by: ringState.accepted_by,
|
|
7133
|
+
rejected_by: ringState.rejected_by,
|
|
7134
|
+
missed_by: ringState.missed_by,
|
|
7135
|
+
ended_at: ringState.session_ended_at ?? session.ended_at,
|
|
7136
|
+
};
|
|
7137
|
+
});
|
|
7138
|
+
if (ringState.call_ended_at) {
|
|
7139
|
+
this.setEndedAt(new Date(ringState.call_ended_at));
|
|
7140
|
+
}
|
|
7141
|
+
};
|
|
7075
7142
|
/**
|
|
7076
7143
|
* Updates the call state with the data received from the SFU server.
|
|
7077
7144
|
*
|
|
@@ -7383,6 +7450,19 @@ const getSdkName = (sdk) => {
|
|
|
7383
7450
|
const getSdkVersion = (sdk) => {
|
|
7384
7451
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
7385
7452
|
};
|
|
7453
|
+
/**
|
|
7454
|
+
* Returns the identifier the SFU receives in the `X-Stream-Client` header,
|
|
7455
|
+
* e.g. `stream-video-react-v1.43.1`. Matches the format the coordinator
|
|
7456
|
+
* already gets through the client's user agent.
|
|
7457
|
+
*/
|
|
7458
|
+
const getStreamClientId = (sdk) => {
|
|
7459
|
+
const sdkPlatforms = {
|
|
7460
|
+
[SdkType.REACT]: 'react',
|
|
7461
|
+
[SdkType.REACT_NATIVE]: 'react-native',
|
|
7462
|
+
};
|
|
7463
|
+
const platform = (sdk && sdkPlatforms[sdk.type]) || 'js';
|
|
7464
|
+
return `stream-video-${platform}-v${getSdkVersion(sdk)}`;
|
|
7465
|
+
};
|
|
7386
7466
|
|
|
7387
7467
|
/**
|
|
7388
7468
|
* Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
|
|
@@ -10264,7 +10344,10 @@ class StreamSfuClient {
|
|
|
10264
10344
|
this.rpc = createSignalClient({
|
|
10265
10345
|
baseUrl: server.url,
|
|
10266
10346
|
interceptors: [
|
|
10267
|
-
withHeaders({
|
|
10347
|
+
withHeaders({
|
|
10348
|
+
Authorization: `Bearer ${token}`,
|
|
10349
|
+
'X-Stream-Client': getStreamClientId(getSdkInfo()),
|
|
10350
|
+
}),
|
|
10268
10351
|
this.tracer && withRequestTracer(this.tracer.trace),
|
|
10269
10352
|
this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
|
|
10270
10353
|
withTimeout(rpcRequestTimeout, this.tracer?.trace),
|
|
@@ -10601,64 +10684,370 @@ class StreamSfuClient {
|
|
|
10601
10684
|
}
|
|
10602
10685
|
|
|
10603
10686
|
/**
|
|
10604
|
-
*
|
|
10605
|
-
*
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10687
|
+
* Decides what a ringing call should do next, based on the current call state.
|
|
10688
|
+
*
|
|
10689
|
+
* The `call.accepted`, `call.rejected` and `call.missed` handlers and the ring
|
|
10690
|
+
* state poller both run this. They differ only in how the state got there: the
|
|
10691
|
+
* handlers rely on `CallState.updateFromEvent`, which runs before them, and the
|
|
10692
|
+
* poller applies the polled ring state itself.
|
|
10693
|
+
*
|
|
10694
|
+
* @param call the call to reconcile.
|
|
10695
|
+
* @param joinSource which of the two triggered this run, reported on the
|
|
10696
|
+
* caller's join: `ring-ws` for the event handlers, `ring-poll-api` for the
|
|
10697
|
+
* poller.
|
|
10698
|
+
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10699
|
+
* terminal: the caller should keep trying while the ring is open.
|
|
10700
|
+
*/
|
|
10701
|
+
const reconcileRingState = async (call, joinSource) => {
|
|
10702
|
+
if (call.state.callingState !== CallingState.RINGING)
|
|
10703
|
+
return true;
|
|
10704
|
+
return call.isCreatedByMe
|
|
10705
|
+
? reconcileAsCaller(call, joinSource)
|
|
10706
|
+
: reconcileAsCallee(call);
|
|
10707
|
+
};
|
|
10708
|
+
const reconcileAsCaller = async (call, joinSource) => {
|
|
10709
|
+
const { session, members, endedAt } = call.state;
|
|
10710
|
+
const currentUserId = call.currentUserId;
|
|
10711
|
+
// checked before `accepted_by`: an ended session cannot be joined
|
|
10712
|
+
if (endedAt || session?.ended_at) {
|
|
10713
|
+
call.logger.info('ring: the call has ended, leaving');
|
|
10714
|
+
// `leave` reports the remote end to callingx off `reason: 'ended'`
|
|
10715
|
+
return leave(call, {
|
|
10716
|
+
reject: false,
|
|
10717
|
+
reason: 'ended',
|
|
10718
|
+
message: 'ring: call ended',
|
|
10719
|
+
});
|
|
10720
|
+
}
|
|
10721
|
+
const acceptedBy = session?.accepted_by ?? {};
|
|
10722
|
+
if (Object.keys(acceptedBy).some((userId) => userId !== currentUserId)) {
|
|
10723
|
+
call.logger.info('ring: the call was accepted, joining');
|
|
10724
|
+
try {
|
|
10725
|
+
await call.join({ joinSource });
|
|
10616
10726
|
}
|
|
10617
|
-
|
|
10727
|
+
catch (err) {
|
|
10728
|
+
// `doJoin` restores the ringing state when a join fails, so the ring is
|
|
10729
|
+
// still open. Report it unsettled and let the next poll retry.
|
|
10730
|
+
call.logger.error('Failed to join an accepted call', err);
|
|
10731
|
+
return false;
|
|
10732
|
+
}
|
|
10733
|
+
return true;
|
|
10734
|
+
}
|
|
10735
|
+
const otherMembers = members
|
|
10736
|
+
.filter((member) => member.user_id !== currentUserId)
|
|
10737
|
+
.map((member) => member.user_id);
|
|
10738
|
+
if (otherMembers.length === 0)
|
|
10739
|
+
return false;
|
|
10740
|
+
const rejectedBy = session?.rejected_by ?? {};
|
|
10741
|
+
if (otherMembers.every((userId) => rejectedBy[userId])) {
|
|
10742
|
+
call.logger.info('ring: everyone rejected, leaving');
|
|
10743
|
+
return leave(call, {
|
|
10744
|
+
reject: true,
|
|
10745
|
+
reason: 'cancel',
|
|
10746
|
+
message: 'ring: everyone rejected',
|
|
10747
|
+
});
|
|
10748
|
+
}
|
|
10749
|
+
return false;
|
|
10750
|
+
};
|
|
10751
|
+
// the current user's own accept or reject, on this or another device, is
|
|
10752
|
+
// handled by `resolveOwnRingOutcome`, and `call.ended` by `watchCallEnded`.
|
|
10753
|
+
const reconcileAsCallee = async (call) => {
|
|
10754
|
+
const createdById = call.state.createdBy?.id;
|
|
10755
|
+
const rejectedBy = call.state.session?.rejected_by ?? {};
|
|
10756
|
+
if (createdById && rejectedBy[createdById]) {
|
|
10757
|
+
call.logger.info('ring: the caller cancelled, leaving');
|
|
10758
|
+
return leave(call, {
|
|
10759
|
+
reason: 'ended',
|
|
10760
|
+
message: 'ring: creator rejected',
|
|
10761
|
+
});
|
|
10762
|
+
}
|
|
10763
|
+
return false;
|
|
10764
|
+
};
|
|
10765
|
+
// `false` when the call could not be left, so the ring stays open for a retry.
|
|
10766
|
+
const leave = async (call, options) => {
|
|
10767
|
+
try {
|
|
10768
|
+
await call.leave(options);
|
|
10769
|
+
return true;
|
|
10770
|
+
}
|
|
10771
|
+
catch (err) {
|
|
10772
|
+
call.logger.error('Failed to leave a ringing call', err);
|
|
10773
|
+
return false;
|
|
10774
|
+
}
|
|
10618
10775
|
};
|
|
10776
|
+
|
|
10777
|
+
// `doJoin` restores the previous calling state when a join fails, so JOINING can
|
|
10778
|
+
// go back to RINGING. Only a state the ring cannot return from ends the poller.
|
|
10779
|
+
const ringIsOver = (callingState) => callingState !== CallingState.RINGING &&
|
|
10780
|
+
callingState !== CallingState.JOINING;
|
|
10619
10781
|
/**
|
|
10620
|
-
*
|
|
10621
|
-
*
|
|
10782
|
+
* Polls the coordinator for the outcome of a ring the current user started.
|
|
10783
|
+
*
|
|
10784
|
+
* `call.accepted`, `call.rejected` and `call.missed` are delivered best-effort,
|
|
10785
|
+
* with no store-and-forward, so a caller that drops one is left on a ringing
|
|
10786
|
+
* screen while the callee is already in the call. After a quiet period this
|
|
10787
|
+
* reads the ring state until a terminal outcome or the end of the ring window.
|
|
10622
10788
|
*/
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10789
|
+
class RingStatePoller {
|
|
10790
|
+
logger = videoLoggerSystem.getLogger('RingStatePoller');
|
|
10791
|
+
call;
|
|
10792
|
+
startAfterMs;
|
|
10793
|
+
intervalMs;
|
|
10794
|
+
sessionId;
|
|
10795
|
+
deadlineAt = 0;
|
|
10796
|
+
idleTimeoutId;
|
|
10797
|
+
intervalId;
|
|
10798
|
+
stopped = false;
|
|
10799
|
+
wasPolling = false;
|
|
10800
|
+
inFlight = false;
|
|
10801
|
+
unsubscribe = [];
|
|
10802
|
+
constructor(call, options = {}) {
|
|
10803
|
+
this.call = call;
|
|
10804
|
+
this.startAfterMs = options.startAfterMs ?? 15_000;
|
|
10805
|
+
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10806
|
+
}
|
|
10807
|
+
/**
|
|
10808
|
+
* Starts polling. Does nothing when the call has no session yet.
|
|
10809
|
+
*/
|
|
10810
|
+
start = () => {
|
|
10811
|
+
if (this.stopped || this.sessionId)
|
|
10812
|
+
return;
|
|
10813
|
+
if (this.call.state.callingState !== CallingState.RINGING)
|
|
10627
10814
|
return;
|
|
10628
|
-
|
|
10629
|
-
const
|
|
10630
|
-
if (!
|
|
10631
|
-
|
|
10815
|
+
// captured once: `call.ended` clears the call's current session
|
|
10816
|
+
const sessionId = this.call.state.session?.id;
|
|
10817
|
+
if (!sessionId) {
|
|
10818
|
+
this.logger.warn('the call has no session');
|
|
10632
10819
|
return;
|
|
10633
10820
|
}
|
|
10634
|
-
|
|
10635
|
-
const
|
|
10636
|
-
|
|
10637
|
-
|
|
10821
|
+
this.sessionId = sessionId;
|
|
10822
|
+
const ring = this.call.state.settings?.ring;
|
|
10823
|
+
const maxDurationMs = ring?.auto_cancel_timeout_ms || ring?.missed_call_timeout_ms || 30_000;
|
|
10824
|
+
this.deadlineAt = Date.now() + maxDurationMs;
|
|
10825
|
+
// an incoming event proves the socket is alive, so the quiet period starts
|
|
10826
|
+
// over: in a group ring a single rejection does not settle the ring.
|
|
10827
|
+
this.unsubscribe.push(this.call.on('call.accepted', () => this.armIdleWindow()), this.call.on('call.rejected', () => this.armIdleWindow()), this.call.on('call.missed', () => this.armIdleWindow()),
|
|
10828
|
+
// the ring is over, whichever way it went. Joining an accepted call
|
|
10829
|
+
// never goes through `leave`, so this is the only signal for it.
|
|
10830
|
+
createSubscription(this.call.state.callingState$, (callingState) => {
|
|
10831
|
+
if (ringIsOver(callingState))
|
|
10832
|
+
this.stop();
|
|
10833
|
+
}));
|
|
10834
|
+
this.armIdleWindow();
|
|
10835
|
+
};
|
|
10836
|
+
/**
|
|
10837
|
+
* Pauses polling, keeping the captured session, the deadline and the event
|
|
10838
|
+
* subscriptions. Resuming does not extend the ring window.
|
|
10839
|
+
*/
|
|
10840
|
+
pause = () => {
|
|
10841
|
+
if (this.stopped)
|
|
10842
|
+
return;
|
|
10843
|
+
this.wasPolling = this.intervalId !== undefined;
|
|
10844
|
+
const timers = getTimers();
|
|
10845
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10846
|
+
timers.clearInterval(this.intervalId);
|
|
10847
|
+
this.idleTimeoutId = undefined;
|
|
10848
|
+
this.intervalId = undefined;
|
|
10849
|
+
};
|
|
10850
|
+
/**
|
|
10851
|
+
* Resumes a paused poller. Goes straight back to polling if it was already
|
|
10852
|
+
* past the quiet period, so a pause does not buy the ring another one.
|
|
10853
|
+
*/
|
|
10854
|
+
resume = () => {
|
|
10855
|
+
if (this.stopped || !this.sessionId)
|
|
10856
|
+
return;
|
|
10857
|
+
if (this.idleTimeoutId !== undefined || this.intervalId !== undefined) {
|
|
10638
10858
|
return;
|
|
10639
10859
|
}
|
|
10640
|
-
if (
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
.every((m) => rejectedBy[m.user_id]);
|
|
10644
|
-
if (everyoneElseRejected) {
|
|
10645
|
-
call.logger.info('everyone rejected, leaving the call');
|
|
10646
|
-
await call.leave({
|
|
10647
|
-
reject: true,
|
|
10648
|
-
reason: 'cancel',
|
|
10649
|
-
message: 'ring: everyone rejected',
|
|
10650
|
-
});
|
|
10651
|
-
}
|
|
10860
|
+
if (!this.wasPolling) {
|
|
10861
|
+
this.armIdleWindow();
|
|
10862
|
+
return;
|
|
10652
10863
|
}
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10864
|
+
this.intervalId = getTimers().setInterval(this.runTick, this.intervalMs);
|
|
10865
|
+
this.runTick();
|
|
10866
|
+
};
|
|
10867
|
+
/**
|
|
10868
|
+
* Stops polling. The poller cannot be restarted.
|
|
10869
|
+
*/
|
|
10870
|
+
stop = () => {
|
|
10871
|
+
if (this.stopped)
|
|
10872
|
+
return;
|
|
10873
|
+
this.stopped = true;
|
|
10874
|
+
const timers = getTimers();
|
|
10875
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10876
|
+
timers.clearInterval(this.intervalId);
|
|
10877
|
+
this.idleTimeoutId = undefined;
|
|
10878
|
+
this.intervalId = undefined;
|
|
10879
|
+
this.unsubscribe.forEach((off) => off());
|
|
10880
|
+
this.unsubscribe = [];
|
|
10881
|
+
};
|
|
10882
|
+
armIdleWindow = () => {
|
|
10883
|
+
if (this.stopped)
|
|
10884
|
+
return;
|
|
10885
|
+
const timers = getTimers();
|
|
10886
|
+
timers.clearTimeout(this.idleTimeoutId);
|
|
10887
|
+
timers.clearInterval(this.intervalId);
|
|
10888
|
+
this.intervalId = undefined;
|
|
10889
|
+
this.idleTimeoutId = timers.setTimeout(() => {
|
|
10890
|
+
this.idleTimeoutId = undefined;
|
|
10891
|
+
if (this.stopped)
|
|
10892
|
+
return;
|
|
10893
|
+
this.intervalId = timers.setInterval(this.runTick, this.intervalMs);
|
|
10894
|
+
this.runTick();
|
|
10895
|
+
}, this.startAfterMs);
|
|
10896
|
+
};
|
|
10897
|
+
runTick = () => {
|
|
10898
|
+
this.tick().catch((err) => {
|
|
10899
|
+
this.logger.warn('Failed to poll the ring state', err);
|
|
10900
|
+
});
|
|
10901
|
+
};
|
|
10902
|
+
tick = async () => {
|
|
10903
|
+
if (this.stopped || this.inFlight || !this.sessionId)
|
|
10904
|
+
return;
|
|
10905
|
+
const { callingState } = this.call.state;
|
|
10906
|
+
if (ringIsOver(callingState) || Date.now() >= this.deadlineAt) {
|
|
10907
|
+
this.stop();
|
|
10908
|
+
return;
|
|
10909
|
+
}
|
|
10910
|
+
// mid-join: nothing to reconcile until we know whether it succeeded
|
|
10911
|
+
if (callingState !== CallingState.RINGING)
|
|
10912
|
+
return;
|
|
10913
|
+
this.inFlight = true;
|
|
10914
|
+
try {
|
|
10915
|
+
const ringState = await this.call.getRingState(this.sessionId);
|
|
10916
|
+
if (this.stopped)
|
|
10917
|
+
return;
|
|
10918
|
+
this.call.state.updateFromRingState(ringState);
|
|
10919
|
+
if (await reconcileRingState(this.call, 'ring-poll-api'))
|
|
10920
|
+
this.stop();
|
|
10921
|
+
}
|
|
10922
|
+
catch (err) {
|
|
10923
|
+
// a missing session, or one of another call, will never resolve
|
|
10924
|
+
const status = err instanceof ErrorFromResponse ? err.status : undefined;
|
|
10925
|
+
if (status === 400 || status === 404) {
|
|
10926
|
+
this.logger.warn('Stopped polling the ring state', err);
|
|
10927
|
+
this.stop();
|
|
10658
10928
|
}
|
|
10929
|
+
else {
|
|
10930
|
+
this.logger.debug('Failed to poll the ring state', err);
|
|
10931
|
+
}
|
|
10932
|
+
}
|
|
10933
|
+
finally {
|
|
10934
|
+
this.inFlight = false;
|
|
10659
10935
|
}
|
|
10660
10936
|
};
|
|
10937
|
+
}
|
|
10938
|
+
|
|
10939
|
+
/**
|
|
10940
|
+
* Drops a call that has been ringing for too long: the caller cancels after
|
|
10941
|
+
* `auto_cancel_timeout_ms`, a callee after `incoming_call_timeout_ms`.
|
|
10942
|
+
*
|
|
10943
|
+
* Either timeout being `0` means the call rings until something else settles it.
|
|
10944
|
+
*/
|
|
10945
|
+
class RingTimeout {
|
|
10946
|
+
call;
|
|
10947
|
+
timeoutId;
|
|
10948
|
+
deadlineAt;
|
|
10949
|
+
stopped = false;
|
|
10950
|
+
constructor(call) {
|
|
10951
|
+
this.call = call;
|
|
10952
|
+
}
|
|
10953
|
+
/**
|
|
10954
|
+
* Schedules an auto-drop timeout based on the call settings.
|
|
10955
|
+
* Applicable only for ringing calls.
|
|
10956
|
+
*/
|
|
10957
|
+
start = () => {
|
|
10958
|
+
if (this.stopped || this.timeoutId !== undefined)
|
|
10959
|
+
return;
|
|
10960
|
+
// ignore if the call is not ringing
|
|
10961
|
+
if (this.call.state.callingState !== CallingState.RINGING)
|
|
10962
|
+
return;
|
|
10963
|
+
const ring = this.call.state.settings?.ring;
|
|
10964
|
+
if (!ring)
|
|
10965
|
+
return;
|
|
10966
|
+
const isCaller = this.call.isCreatedByMe;
|
|
10967
|
+
const timeoutMs = isCaller
|
|
10968
|
+
? ring.auto_cancel_timeout_ms
|
|
10969
|
+
: ring.incoming_call_timeout_ms;
|
|
10970
|
+
// 0 means no auto-drop
|
|
10971
|
+
if (timeoutMs <= 0)
|
|
10972
|
+
return;
|
|
10973
|
+
const now = Date.now();
|
|
10974
|
+
this.deadlineAt ??= now + timeoutMs;
|
|
10975
|
+
const delayMs = Math.max(0, this.deadlineAt - now);
|
|
10976
|
+
const timers = getTimers();
|
|
10977
|
+
this.timeoutId = timers.setTimeout(() => {
|
|
10978
|
+
this.timeoutId = undefined;
|
|
10979
|
+
// A failed timeout-triggered leave can arm a fresh retry window.
|
|
10980
|
+
this.deadlineAt = undefined;
|
|
10981
|
+
if (this.stopped)
|
|
10982
|
+
return;
|
|
10983
|
+
// the call might have stopped ringing by this point, e.g. it was already
|
|
10984
|
+
// accepted and joined
|
|
10985
|
+
if (this.call.state.callingState !== CallingState.RINGING)
|
|
10986
|
+
return;
|
|
10987
|
+
this.call
|
|
10988
|
+
.leave({
|
|
10989
|
+
reject: true,
|
|
10990
|
+
reason: 'timeout',
|
|
10991
|
+
message: `ringing timeout - ${isCaller
|
|
10992
|
+
? 'no one accepted'
|
|
10993
|
+
: `user didn't interact with incoming call screen`}`,
|
|
10994
|
+
})
|
|
10995
|
+
.catch((err) => {
|
|
10996
|
+
videoLoggerSystem
|
|
10997
|
+
.getLogger('RingTimeout')
|
|
10998
|
+
.error('Failed to drop the call', err);
|
|
10999
|
+
});
|
|
11000
|
+
}, delayMs);
|
|
11001
|
+
};
|
|
11002
|
+
/**
|
|
11003
|
+
* Pauses the timeout while preserving its original deadline.
|
|
11004
|
+
*/
|
|
11005
|
+
pause = () => {
|
|
11006
|
+
if (this.stopped || this.timeoutId === undefined)
|
|
11007
|
+
return;
|
|
11008
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
11009
|
+
this.timeoutId = undefined;
|
|
11010
|
+
};
|
|
11011
|
+
/**
|
|
11012
|
+
* Cancels a scheduled auto-drop timeout. It cannot be armed again.
|
|
11013
|
+
*/
|
|
11014
|
+
stop = () => {
|
|
11015
|
+
if (this.stopped)
|
|
11016
|
+
return;
|
|
11017
|
+
this.stopped = true;
|
|
11018
|
+
getTimers().clearTimeout(this.timeoutId);
|
|
11019
|
+
this.timeoutId = undefined;
|
|
11020
|
+
this.deadlineAt = undefined;
|
|
11021
|
+
};
|
|
11022
|
+
}
|
|
11023
|
+
|
|
11024
|
+
/**
|
|
11025
|
+
* Decides what a ringing call should do about the current user's own accept or
|
|
11026
|
+
* reject, which may have happened on another device.
|
|
11027
|
+
*
|
|
11028
|
+
* Accepting on *this* device also lands in `accepted_by`, so an acceptance only
|
|
11029
|
+
* means another device took the call when this one is still ringing.
|
|
11030
|
+
*
|
|
11031
|
+
* Only meaningful for a ringing call; the caller checks that.
|
|
11032
|
+
*/
|
|
11033
|
+
const resolveOwnRingOutcome = ({ session, currentUserId, callingState, }) => {
|
|
11034
|
+
if (!currentUserId)
|
|
11035
|
+
return { settledByMe: false };
|
|
11036
|
+
const acceptedByMe = Boolean(session?.accepted_by[currentUserId]);
|
|
11037
|
+
const rejectedByMe = Boolean(session?.rejected_by[currentUserId]);
|
|
11038
|
+
if (!acceptedByMe && !rejectedByMe)
|
|
11039
|
+
return { settledByMe: false };
|
|
11040
|
+
const answeredElsewhere = acceptedByMe && callingState === CallingState.RINGING;
|
|
11041
|
+
return {
|
|
11042
|
+
settledByMe: true,
|
|
11043
|
+
leaveReason: answeredElsewhere
|
|
11044
|
+
? 'answeredElsewhere'
|
|
11045
|
+
: rejectedByMe
|
|
11046
|
+
? 'rejected'
|
|
11047
|
+
: undefined,
|
|
11048
|
+
};
|
|
10661
11049
|
};
|
|
11050
|
+
|
|
10662
11051
|
/**
|
|
10663
11052
|
* Event handler that watches the delivery of `call.ended` Websocket event.
|
|
10664
11053
|
*/
|
|
@@ -11059,14 +11448,18 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
11059
11448
|
* @param call the call to register event handlers for.
|
|
11060
11449
|
*/
|
|
11061
11450
|
const registerRingingCallEventHandlers = (call) => {
|
|
11062
|
-
const
|
|
11063
|
-
'
|
|
11064
|
-
|
|
11451
|
+
const reconcile = () => {
|
|
11452
|
+
reconcileRingState(call, 'ring-ws').catch((err) => {
|
|
11453
|
+
call.logger.error('Failed to reconcile the ring state', err);
|
|
11454
|
+
});
|
|
11065
11455
|
};
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11456
|
+
// each event needs its own closure. `call.missed` is deliberately absent:
|
|
11457
|
+
// nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
|
|
11458
|
+
// "nobody answered" case until the server-owned ring timeout lands.
|
|
11459
|
+
const eventHandlers = [
|
|
11460
|
+
call.on('call.accepted', () => reconcile()),
|
|
11461
|
+
call.on('call.rejected', () => reconcile()),
|
|
11462
|
+
];
|
|
11070
11463
|
return () => {
|
|
11071
11464
|
eventHandlers.forEach((unsubscribe) => unsubscribe());
|
|
11072
11465
|
};
|
|
@@ -15114,7 +15507,8 @@ class Call {
|
|
|
15114
15507
|
statsReporter;
|
|
15115
15508
|
sfuStatsReporter;
|
|
15116
15509
|
lastStatsOptions;
|
|
15117
|
-
|
|
15510
|
+
ringTimeout;
|
|
15511
|
+
ringStatePoller;
|
|
15118
15512
|
clientStore;
|
|
15119
15513
|
streamClient;
|
|
15120
15514
|
clientEventReporter;
|
|
@@ -15292,22 +15686,19 @@ class Call {
|
|
|
15292
15686
|
createSubscription(this.state.session$, (session) => {
|
|
15293
15687
|
if (!this.ringing)
|
|
15294
15688
|
return;
|
|
15295
|
-
const
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
if (
|
|
15689
|
+
const { settledByMe, leaveReason } = resolveOwnRingOutcome({
|
|
15690
|
+
session,
|
|
15691
|
+
currentUserId: this.currentUserId,
|
|
15692
|
+
callingState: this.state.callingState,
|
|
15693
|
+
});
|
|
15694
|
+
if (settledByMe)
|
|
15301
15695
|
this.cancelAutoDrop();
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15309
|
-
});
|
|
15310
|
-
}
|
|
15696
|
+
if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag))
|
|
15697
|
+
return;
|
|
15698
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
|
|
15699
|
+
this.leave().catch(() => {
|
|
15700
|
+
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
15701
|
+
});
|
|
15311
15702
|
}));
|
|
15312
15703
|
};
|
|
15313
15704
|
handleRingingCall = () => {
|
|
@@ -15353,6 +15744,7 @@ class Call {
|
|
|
15353
15744
|
this.state.setCallingState(CallingState.RINGING);
|
|
15354
15745
|
}
|
|
15355
15746
|
this.scheduleAutoDrop();
|
|
15747
|
+
this.scheduleRingStatePolling();
|
|
15356
15748
|
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
15357
15749
|
}
|
|
15358
15750
|
};
|
|
@@ -15439,6 +15831,12 @@ class Call {
|
|
|
15439
15831
|
if (this.state.callingState === CallingState.LEFT) {
|
|
15440
15832
|
throw new Error('Cannot leave call that has already been left.');
|
|
15441
15833
|
}
|
|
15834
|
+
// before the first await: the calling state stays RINGING well into the
|
|
15835
|
+
// teardown, so pause both watchdogs before they can race this leave. They
|
|
15836
|
+
// keep their deadlines, so a failed leave resumes them without handing the
|
|
15837
|
+
// ring another window.
|
|
15838
|
+
this.ringTimeout?.pause();
|
|
15839
|
+
this.ringStatePoller?.pause();
|
|
15442
15840
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
15443
15841
|
const callingState = this.state.callingState;
|
|
15444
15842
|
if (callingState === CallingState.LEFT) {
|
|
@@ -15528,6 +15926,7 @@ class Call {
|
|
|
15528
15926
|
this.unifiedSessionId = undefined;
|
|
15529
15927
|
this.ringingSubject.next(false);
|
|
15530
15928
|
this.cancelAutoDrop();
|
|
15929
|
+
this.cancelRingStatePolling();
|
|
15531
15930
|
this.clientStore.unregisterCall(this);
|
|
15532
15931
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15533
15932
|
isRingingTypeCall: this.ringing,
|
|
@@ -15566,6 +15965,15 @@ class Call {
|
|
|
15566
15965
|
this.logger.warn('Failed to dispose media engine', err);
|
|
15567
15966
|
});
|
|
15568
15967
|
}
|
|
15968
|
+
}).catch((err) => {
|
|
15969
|
+
if (!hasPending(this.joinLeaveConcurrencyTag) &&
|
|
15970
|
+
this.state.callingState === CallingState.RINGING) {
|
|
15971
|
+
// resume, never re-arm: a fresh watchdog would restart the ring
|
|
15972
|
+
// window this leave was already most of the way through
|
|
15973
|
+
this.ringTimeout?.start();
|
|
15974
|
+
this.ringStatePoller?.resume();
|
|
15975
|
+
}
|
|
15976
|
+
throw err;
|
|
15569
15977
|
});
|
|
15570
15978
|
};
|
|
15571
15979
|
/**
|
|
@@ -15716,6 +16124,21 @@ class Call {
|
|
|
15716
16124
|
ring = async (data = {}) => {
|
|
15717
16125
|
return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
|
|
15718
16126
|
};
|
|
16127
|
+
/**
|
|
16128
|
+
* Returns who accepted, rejected or missed the ring for a call session.
|
|
16129
|
+
* Safe to poll: it performs no writes and emits no events.
|
|
16130
|
+
*
|
|
16131
|
+
* @param callSessionId the call session to read. Defaults to the current one.
|
|
16132
|
+
* Pass it explicitly to read a session that has already ended, as ending a
|
|
16133
|
+
* call clears its current session.
|
|
16134
|
+
*/
|
|
16135
|
+
getRingState = async (callSessionId) => {
|
|
16136
|
+
const sessionId = callSessionId ?? this.state.session?.id;
|
|
16137
|
+
if (!sessionId) {
|
|
16138
|
+
throw new Error('Cannot read the ring state: the call has no session');
|
|
16139
|
+
}
|
|
16140
|
+
return this.streamClient.get(`${this.streamClientBasePath}/ring_state`, { call_session_id: sessionId });
|
|
16141
|
+
};
|
|
15719
16142
|
/**
|
|
15720
16143
|
* A shortcut for {@link Call.get} with `notify` parameter set to `true`.
|
|
15721
16144
|
* Will send a `call.notification` event to the call members.
|
|
@@ -15756,7 +16179,7 @@ class Call {
|
|
|
15756
16179
|
*
|
|
15757
16180
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15758
16181
|
*/
|
|
15759
|
-
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
16182
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
|
|
15760
16183
|
const callingState = this.state.callingState;
|
|
15761
16184
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
15762
16185
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15788,7 +16211,7 @@ class Call {
|
|
|
15788
16211
|
const joinData = data;
|
|
15789
16212
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
15790
16213
|
try {
|
|
15791
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
16214
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
|
|
15792
16215
|
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
15793
16216
|
try {
|
|
15794
16217
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
@@ -16514,7 +16937,7 @@ class Call {
|
|
|
16514
16937
|
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
16515
16938
|
? 'network-available'
|
|
16516
16939
|
: 'full-rejoin';
|
|
16517
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
16940
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
|
|
16518
16941
|
await this.restorePublishedTracks();
|
|
16519
16942
|
this.restoreSubscribedTracks();
|
|
16520
16943
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -16538,7 +16961,7 @@ class Call {
|
|
|
16538
16961
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
16539
16962
|
try {
|
|
16540
16963
|
const currentSfu = currentSfuClient.edgeName;
|
|
16541
|
-
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
16964
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
|
|
16542
16965
|
...this.joinCallData,
|
|
16543
16966
|
migrating_from: currentSfu,
|
|
16544
16967
|
migrating_from_list: [currentSfu],
|
|
@@ -17311,40 +17734,36 @@ class Call {
|
|
|
17311
17734
|
*/
|
|
17312
17735
|
scheduleAutoDrop = () => {
|
|
17313
17736
|
this.cancelAutoDrop();
|
|
17314
|
-
|
|
17315
|
-
|
|
17316
|
-
return;
|
|
17317
|
-
// ignore if the call is not ringing
|
|
17318
|
-
if (this.state.callingState !== CallingState.RINGING)
|
|
17319
|
-
return;
|
|
17320
|
-
const timeoutInMs = this.isCreatedByMe
|
|
17321
|
-
? settings.ring.auto_cancel_timeout_ms
|
|
17322
|
-
: settings.ring.incoming_call_timeout_ms;
|
|
17323
|
-
// 0 means no auto-drop
|
|
17324
|
-
if (timeoutInMs <= 0)
|
|
17325
|
-
return;
|
|
17326
|
-
this.dropTimeout = setTimeout(() => {
|
|
17327
|
-
// the call might have stopped ringing by this point,
|
|
17328
|
-
// e.g. it was already accepted and joined
|
|
17329
|
-
if (this.state.callingState !== CallingState.RINGING)
|
|
17330
|
-
return;
|
|
17331
|
-
this.leave({
|
|
17332
|
-
reject: true,
|
|
17333
|
-
reason: 'timeout',
|
|
17334
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
17335
|
-
? 'no one accepted'
|
|
17336
|
-
: `user didn't interact with incoming call screen`}`,
|
|
17337
|
-
}).catch((err) => {
|
|
17338
|
-
this.logger.error('Failed to drop call', err);
|
|
17339
|
-
});
|
|
17340
|
-
}, timeoutInMs);
|
|
17737
|
+
this.ringTimeout = new RingTimeout(this);
|
|
17738
|
+
this.ringTimeout.start();
|
|
17341
17739
|
};
|
|
17342
17740
|
/**
|
|
17343
17741
|
* Cancels a scheduled auto-drop timeout.
|
|
17344
17742
|
*/
|
|
17345
17743
|
cancelAutoDrop = () => {
|
|
17346
|
-
|
|
17347
|
-
this.
|
|
17744
|
+
this.ringTimeout?.stop();
|
|
17745
|
+
this.ringTimeout = undefined;
|
|
17746
|
+
};
|
|
17747
|
+
/**
|
|
17748
|
+
* Starts polling for the ring outcome. Applicable only to ringing calls the
|
|
17749
|
+
* current user created.
|
|
17750
|
+
*/
|
|
17751
|
+
scheduleRingStatePolling = () => {
|
|
17752
|
+
this.cancelRingStatePolling();
|
|
17753
|
+
if (!this.isCreatedByMe)
|
|
17754
|
+
return;
|
|
17755
|
+
const options = this.streamClient.options.ringStatePolling;
|
|
17756
|
+
if (options === false)
|
|
17757
|
+
return;
|
|
17758
|
+
this.ringStatePoller = new RingStatePoller(this, options);
|
|
17759
|
+
this.ringStatePoller.start();
|
|
17760
|
+
};
|
|
17761
|
+
/**
|
|
17762
|
+
* Cancels the ring state polling.
|
|
17763
|
+
*/
|
|
17764
|
+
cancelRingStatePolling = () => {
|
|
17765
|
+
this.ringStatePoller?.stop();
|
|
17766
|
+
this.ringStatePoller = undefined;
|
|
17348
17767
|
};
|
|
17349
17768
|
/**
|
|
17350
17769
|
* Retrieves the list of recordings for the current call or call session.
|
|
@@ -18991,11 +19410,11 @@ class StreamClient {
|
|
|
18991
19410
|
return;
|
|
18992
19411
|
// call generic listeners
|
|
18993
19412
|
for (const listener of this.listeners.all || []) {
|
|
18994
|
-
listener
|
|
19413
|
+
invokeEventListener(listener, event, this.logger);
|
|
18995
19414
|
}
|
|
18996
19415
|
// call type specific listeners
|
|
18997
19416
|
for (const listener of this.listeners[event.type] || []) {
|
|
18998
|
-
listener
|
|
19417
|
+
invokeEventListener(listener, event, this.logger);
|
|
18999
19418
|
}
|
|
19000
19419
|
};
|
|
19001
19420
|
/**
|
|
@@ -19015,11 +19434,11 @@ class StreamClient {
|
|
|
19015
19434
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19016
19435
|
};
|
|
19017
19436
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19018
|
-
"1.
|
|
19437
|
+
"1.60.0";
|
|
19019
19438
|
getUserAgent = () => {
|
|
19020
19439
|
if (!this.cachedUserAgent) {
|
|
19021
19440
|
const { clientAppIdentifier = {} } = this.options;
|
|
19022
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
19441
|
+
const { sdkName = 'js', sdkVersion = "1.60.0", ...extras } = clientAppIdentifier;
|
|
19023
19442
|
this.cachedUserAgent = [
|
|
19024
19443
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19025
19444
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19152,6 +19571,7 @@ class ClientEventReporter {
|
|
|
19152
19571
|
callContexts = new Map();
|
|
19153
19572
|
joinAttemptIds = new Map();
|
|
19154
19573
|
joinReasons = new Map();
|
|
19574
|
+
joinSources = new Map();
|
|
19155
19575
|
coordinatorPairs = new Map();
|
|
19156
19576
|
wsPairs = new Map();
|
|
19157
19577
|
peerConnectionPairs = new Map();
|
|
@@ -19266,6 +19686,7 @@ class ClientEventReporter {
|
|
|
19266
19686
|
this.callContexts.delete(cid);
|
|
19267
19687
|
this.joinAttemptIds.delete(cid);
|
|
19268
19688
|
this.joinReasons.delete(cid);
|
|
19689
|
+
this.joinSources.delete(cid);
|
|
19269
19690
|
this.coordinatorPairs.delete(cid);
|
|
19270
19691
|
this.wsPairs.delete(cid);
|
|
19271
19692
|
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
@@ -19290,7 +19711,12 @@ class ClientEventReporter {
|
|
|
19290
19711
|
this.logger.warn('Failed to start join correlation', err);
|
|
19291
19712
|
}
|
|
19292
19713
|
};
|
|
19293
|
-
withJoinLifecycle = async (cid,
|
|
19714
|
+
withJoinLifecycle = async (cid, options, op) => {
|
|
19715
|
+
const { joinReason, joinSource } = options;
|
|
19716
|
+
if (joinSource)
|
|
19717
|
+
this.joinSources.set(cid, joinSource);
|
|
19718
|
+
else
|
|
19719
|
+
this.joinSources.delete(cid);
|
|
19294
19720
|
this.startCorrelation(cid, joinReason);
|
|
19295
19721
|
try {
|
|
19296
19722
|
return await op();
|
|
@@ -19299,6 +19725,9 @@ class ClientEventReporter {
|
|
|
19299
19725
|
this.closeCallPairs(cid);
|
|
19300
19726
|
throw err;
|
|
19301
19727
|
}
|
|
19728
|
+
finally {
|
|
19729
|
+
this.joinSources.delete(cid);
|
|
19730
|
+
}
|
|
19302
19731
|
};
|
|
19303
19732
|
track = async (cid, stage, op) => {
|
|
19304
19733
|
this.beginAttempt(cid, stage);
|
|
@@ -19422,6 +19851,7 @@ class ClientEventReporter {
|
|
|
19422
19851
|
startedAt: Date.now(),
|
|
19423
19852
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19424
19853
|
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
19854
|
+
joinSourceSnapshot: this.joinSources.get(cid),
|
|
19425
19855
|
};
|
|
19426
19856
|
this.coordinatorPairs.set(cid, pair);
|
|
19427
19857
|
this.sendForCall(cid, {
|
|
@@ -19429,6 +19859,7 @@ class ClientEventReporter {
|
|
|
19429
19859
|
...(pair.joinReasonSnapshot && {
|
|
19430
19860
|
join_reason: pair.joinReasonSnapshot,
|
|
19431
19861
|
}),
|
|
19862
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19432
19863
|
event_type: 'initiated',
|
|
19433
19864
|
});
|
|
19434
19865
|
}
|
|
@@ -19443,6 +19874,7 @@ class ClientEventReporter {
|
|
|
19443
19874
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19444
19875
|
...this.sessionIdField(cid),
|
|
19445
19876
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19877
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19446
19878
|
event_type: 'completed',
|
|
19447
19879
|
outcome: 'success',
|
|
19448
19880
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -19461,6 +19893,7 @@ class ClientEventReporter {
|
|
|
19461
19893
|
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
19462
19894
|
...this.sessionIdField(cid),
|
|
19463
19895
|
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
19896
|
+
...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
|
|
19464
19897
|
event_type: 'completed',
|
|
19465
19898
|
outcome: 'failure',
|
|
19466
19899
|
retry_count_attempt: pair.attempts - 1,
|
|
@@ -20805,5 +21238,5 @@ var e2eeWorker$1 = /*#__PURE__*/Object.freeze({
|
|
|
20805
21238
|
e2eeWorker: e2eeWorker
|
|
20806
21239
|
});
|
|
20807
21240
|
|
|
20808
|
-
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
|
|
21241
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishing, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
|
|
20809
21242
|
//# sourceMappingURL=index.es.js.map
|