@stream-io/video-client 1.48.0 → 1.49.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 +376 -48
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +376 -48
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +376 -48
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +35 -1
- package/dist/src/StreamSfuClient.d.ts +8 -1
- package/dist/src/devices/DeviceManagerState.d.ts +13 -0
- package/dist/src/helpers/SlidingWindowRateLimiter.d.ts +28 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +11 -2
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +33 -1
- package/package.json +1 -1
- package/src/Call.ts +179 -18
- package/src/StreamSfuClient.ts +75 -12
- package/src/__tests__/Call.publishing.test.ts +103 -0
- package/src/__tests__/StreamSfuClient.test.ts +275 -0
- package/src/devices/DeviceManagerState.ts +20 -0
- package/src/devices/__tests__/ScreenShareManager.test.ts +0 -2
- package/src/devices/devices.ts +2 -1
- package/src/helpers/SlidingWindowRateLimiter.ts +49 -0
- package/src/helpers/__tests__/SlidingWindowRateLimiter.test.ts +43 -0
- package/src/rpc/retryable.ts +0 -1
- package/src/rtc/BasePeerConnection.ts +96 -6
- package/src/rtc/Publisher.ts +2 -1
- package/src/rtc/__tests__/Call.reconnect.test.ts +761 -0
- package/src/rtc/__tests__/Publisher.test.ts +210 -0
- package/src/rtc/__tests__/Subscriber.test.ts +56 -0
- package/src/rtc/index.ts +1 -0
- package/src/rtc/types.ts +38 -1
package/dist/index.browser.es.js
CHANGED
|
@@ -4006,8 +4006,6 @@ const retryable = async (rpc, signal, maxRetries = Number.POSITIVE_INFINITY) =>
|
|
|
4006
4006
|
do {
|
|
4007
4007
|
if (attempt > 0)
|
|
4008
4008
|
await sleep(retryInterval(attempt));
|
|
4009
|
-
if (signal?.aborted)
|
|
4010
|
-
throw new Error(signal.reason);
|
|
4011
4009
|
try {
|
|
4012
4010
|
result = await rpc({ attempt });
|
|
4013
4011
|
}
|
|
@@ -4405,6 +4403,21 @@ class Dispatcher {
|
|
|
4405
4403
|
}
|
|
4406
4404
|
}
|
|
4407
4405
|
|
|
4406
|
+
/**
|
|
4407
|
+
* NegotiationError is thrown when there is an error during the negotiation process.
|
|
4408
|
+
* It extends the built-in Error class and includes an SfuError object for more details.
|
|
4409
|
+
*/
|
|
4410
|
+
class NegotiationError extends Error {
|
|
4411
|
+
/**
|
|
4412
|
+
* Creates an instance of NegotiationError.
|
|
4413
|
+
*/
|
|
4414
|
+
constructor(error) {
|
|
4415
|
+
super(error.message);
|
|
4416
|
+
this.name = 'NegotiationError';
|
|
4417
|
+
this.error = error;
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4408
4421
|
/**
|
|
4409
4422
|
* A buffer for ICE Candidates. Used for ICE Trickle:
|
|
4410
4423
|
* - https://bloggeek.me/webrtcglossary/trickle-ice/
|
|
@@ -6222,21 +6235,6 @@ class CallState {
|
|
|
6222
6235
|
}
|
|
6223
6236
|
}
|
|
6224
6237
|
|
|
6225
|
-
/**
|
|
6226
|
-
* NegotiationError is thrown when there is an error during the negotiation process.
|
|
6227
|
-
* It extends the built-in Error class and includes an SfuError object for more details.
|
|
6228
|
-
*/
|
|
6229
|
-
class NegotiationError extends Error {
|
|
6230
|
-
/**
|
|
6231
|
-
* Creates an instance of NegotiationError.
|
|
6232
|
-
*/
|
|
6233
|
-
constructor(error) {
|
|
6234
|
-
super(error.message);
|
|
6235
|
-
this.name = 'NegotiationError';
|
|
6236
|
-
this.error = error;
|
|
6237
|
-
}
|
|
6238
|
-
}
|
|
6239
|
-
|
|
6240
6238
|
/**
|
|
6241
6239
|
* Flatten the stats report into an array of stats objects.
|
|
6242
6240
|
*
|
|
@@ -6284,7 +6282,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6284
6282
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6285
6283
|
};
|
|
6286
6284
|
|
|
6287
|
-
const version = "1.
|
|
6285
|
+
const version = "1.49.0";
|
|
6288
6286
|
const [major, minor, patch] = version.split('.');
|
|
6289
6287
|
let sdkInfo = {
|
|
6290
6288
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7306,6 +7304,30 @@ class Tracer {
|
|
|
7306
7304
|
}
|
|
7307
7305
|
}
|
|
7308
7306
|
|
|
7307
|
+
/**
|
|
7308
|
+
* Canonical reasons the SDK uses to trigger a reconnection. Free-form strings
|
|
7309
|
+
* are still accepted at the callback boundary (e.g. when forwarding an SFU
|
|
7310
|
+
* error message), but only the members below influence reconnect-loop
|
|
7311
|
+
* behavior. In particular, `Call.reconnect` programmatically inspects
|
|
7312
|
+
* `ICE_NEVER_CONNECTED` to drive the unsupported-network detector — pass a
|
|
7313
|
+
* canonical member when you want the SDK to react to the reason; pass a
|
|
7314
|
+
* free-form string when the value is purely diagnostic.
|
|
7315
|
+
*/
|
|
7316
|
+
const ReconnectReason = {
|
|
7317
|
+
/** ICE never reached `connected`/`completed`, escalate to REJOIN. */
|
|
7318
|
+
ICE_NEVER_CONNECTED: 'ice_never_connected',
|
|
7319
|
+
/** RTCPeerConnection.connectionState became `failed`. */
|
|
7320
|
+
CONNECTION_FAILED: 'connection_failed',
|
|
7321
|
+
/** `restartIce()` rejected. */
|
|
7322
|
+
RESTART_ICE_FAILED: 'restart_ice_failed',
|
|
7323
|
+
/** SFU `goAway` event, migrate to a new SFU. */
|
|
7324
|
+
GO_AWAY: 'go_away',
|
|
7325
|
+
/** Network came back online after going offline. */
|
|
7326
|
+
NETWORK_BACK_ONLINE: 'network_back_online',
|
|
7327
|
+
/** SFU error event with no descriptive message. */
|
|
7328
|
+
SFU_ERROR: 'sfu_error',
|
|
7329
|
+
};
|
|
7330
|
+
|
|
7309
7331
|
/**
|
|
7310
7332
|
* A base class for the `Publisher` and `Subscriber` classes.
|
|
7311
7333
|
* @internal
|
|
@@ -7314,7 +7336,8 @@ class BasePeerConnection {
|
|
|
7314
7336
|
/**
|
|
7315
7337
|
* Constructs a new `BasePeerConnection` instance.
|
|
7316
7338
|
*/
|
|
7317
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, }) {
|
|
7339
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, }) {
|
|
7340
|
+
this.iceHasEverConnected = false;
|
|
7318
7341
|
this.isIceRestarting = false;
|
|
7319
7342
|
this.isDisposed = false;
|
|
7320
7343
|
this.trackIdToTrackType = new Map();
|
|
@@ -7337,13 +7360,12 @@ class BasePeerConnection {
|
|
|
7337
7360
|
*/
|
|
7338
7361
|
this.tryRestartIce = () => {
|
|
7339
7362
|
this.restartIce().catch((e) => {
|
|
7340
|
-
|
|
7341
|
-
this.logger.error(reason, e);
|
|
7363
|
+
this.logger.error('restartICE() failed, initiating reconnect', e);
|
|
7342
7364
|
const strategy = e instanceof NegotiationError &&
|
|
7343
7365
|
e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
|
|
7344
7366
|
? WebsocketReconnectStrategy.FAST
|
|
7345
7367
|
: WebsocketReconnectStrategy.REJOIN;
|
|
7346
|
-
this.onReconnectionNeeded?.(strategy,
|
|
7368
|
+
this.onReconnectionNeeded?.(strategy, ReconnectReason.RESTART_ICE_FAILED, this.peerType);
|
|
7347
7369
|
});
|
|
7348
7370
|
};
|
|
7349
7371
|
/**
|
|
@@ -7412,6 +7434,17 @@ class BasePeerConnection {
|
|
|
7412
7434
|
const connectionState = this.pc.connectionState;
|
|
7413
7435
|
return !failedStates.has(iceState) && !failedStates.has(connectionState);
|
|
7414
7436
|
};
|
|
7437
|
+
/**
|
|
7438
|
+
* Returns true only when the peer connection is currently fully established
|
|
7439
|
+
* (ICE `connected`/`completed` AND connection state `connected`).
|
|
7440
|
+
* Transient states like `disconnected`, `checking`, or `new` return false.
|
|
7441
|
+
*/
|
|
7442
|
+
this.isStable = () => {
|
|
7443
|
+
const iceState = this.pc.iceConnectionState;
|
|
7444
|
+
const connectionState = this.pc.connectionState;
|
|
7445
|
+
return ((iceState === 'connected' || iceState === 'completed') &&
|
|
7446
|
+
connectionState === 'connected');
|
|
7447
|
+
};
|
|
7415
7448
|
/**
|
|
7416
7449
|
* Handles the ICECandidate event and
|
|
7417
7450
|
* Initiates an ICE Trickle process with the SFU.
|
|
@@ -7461,7 +7494,7 @@ class BasePeerConnection {
|
|
|
7461
7494
|
}
|
|
7462
7495
|
// we can't recover from a failed connection state (contrary to ICE)
|
|
7463
7496
|
if (state === 'failed') {
|
|
7464
|
-
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN,
|
|
7497
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, ReconnectReason.CONNECTION_FAILED, this.peerType);
|
|
7465
7498
|
return;
|
|
7466
7499
|
}
|
|
7467
7500
|
this.handleConnectionStateUpdate(state);
|
|
@@ -7483,6 +7516,41 @@ class BasePeerConnection {
|
|
|
7483
7516
|
// do nothing when ICE is restarting
|
|
7484
7517
|
if (this.isIceRestarting)
|
|
7485
7518
|
return;
|
|
7519
|
+
// Pre-connect handling: ICE has never reached `connected`/`completed`.
|
|
7520
|
+
// Restart is futile here (the data plane was never established), but
|
|
7521
|
+
// these two terminal-ish states need different treatment:
|
|
7522
|
+
// - `failed` is terminal, escalate to REJOIN so a new SFU/credentials
|
|
7523
|
+
// /PC configuration gets a chance, and let `Call.reconnect` count
|
|
7524
|
+
// this toward the unsupported-network budget.
|
|
7525
|
+
// - `disconnected` is transient, the browser may yet move back to
|
|
7526
|
+
// `checking`/`connected`. Don't restart, don't escalate; wait it
|
|
7527
|
+
// out. If it ultimately fails, ICE will transition to `failed` and
|
|
7528
|
+
// the branch above will take over.
|
|
7529
|
+
if (!this.iceHasEverConnected) {
|
|
7530
|
+
if (state === 'failed') {
|
|
7531
|
+
this.logger.info('ICE failed before connected, escalating to REJOIN');
|
|
7532
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
7533
|
+
this.preConnectStuckTimeout = undefined;
|
|
7534
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, ReconnectReason.ICE_NEVER_CONNECTED, this.peerType);
|
|
7535
|
+
return;
|
|
7536
|
+
}
|
|
7537
|
+
if (state === 'disconnected') {
|
|
7538
|
+
this.logger.info('ICE disconnected before connected, wait to recover');
|
|
7539
|
+
// Watchdog: if the browser stays in `disconnected` without ever
|
|
7540
|
+
// reaching `connected` or transitioning to `failed`, escalate to
|
|
7541
|
+
// REJOIN ourselves so we don't wait silently forever. Rare but
|
|
7542
|
+
// observed on flaky mobile networks.
|
|
7543
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
7544
|
+
this.preConnectStuckTimeout = setTimeout(() => {
|
|
7545
|
+
if (!this.iceHasEverConnected &&
|
|
7546
|
+
this.pc.iceConnectionState === 'disconnected') {
|
|
7547
|
+
this.logger.info('ICE stuck in pre-connect disconnected, escalating to REJOIN');
|
|
7548
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, ReconnectReason.ICE_NEVER_CONNECTED, this.peerType);
|
|
7549
|
+
}
|
|
7550
|
+
}, this.iceRestartDelay * 2);
|
|
7551
|
+
return;
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7486
7554
|
switch (state) {
|
|
7487
7555
|
case 'failed':
|
|
7488
7556
|
// in the `failed` state, we try to restart ICE immediately
|
|
@@ -7502,12 +7570,24 @@ class BasePeerConnection {
|
|
|
7502
7570
|
}, this.iceRestartDelay);
|
|
7503
7571
|
break;
|
|
7504
7572
|
case 'connected':
|
|
7505
|
-
|
|
7573
|
+
case 'completed':
|
|
7574
|
+
// Fire `onIceConnected` exactly once per peer-connection lifetime —
|
|
7575
|
+
// the first time ICE reaches `connected`/`completed` end-to-end.
|
|
7576
|
+
// Used by `Call` to reset the unsupported-network failure counter
|
|
7577
|
+
// only after WebRTC has actually recovered, not merely on SFU join.
|
|
7578
|
+
if (!this.iceHasEverConnected) {
|
|
7579
|
+
this.iceHasEverConnected = true;
|
|
7580
|
+
this.onIceConnected?.(this.peerType);
|
|
7581
|
+
}
|
|
7582
|
+
// clear any scheduled restartICE since the connection is healthy
|
|
7506
7583
|
if (this.iceRestartTimeout) {
|
|
7507
7584
|
this.logger.info('connected connection, canceling restartICE');
|
|
7508
7585
|
clearTimeout(this.iceRestartTimeout);
|
|
7509
7586
|
this.iceRestartTimeout = undefined;
|
|
7510
7587
|
}
|
|
7588
|
+
// clear the pre-connect watchdog if it was armed
|
|
7589
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
7590
|
+
this.preConnectStuckTimeout = undefined;
|
|
7511
7591
|
break;
|
|
7512
7592
|
}
|
|
7513
7593
|
};
|
|
@@ -7540,6 +7620,7 @@ class BasePeerConnection {
|
|
|
7540
7620
|
this.clientPublishOptions = clientPublishOptions;
|
|
7541
7621
|
this.tag = tag;
|
|
7542
7622
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
7623
|
+
this.onIceConnected = onIceConnected;
|
|
7543
7624
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
7544
7625
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
7545
7626
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
@@ -7558,7 +7639,10 @@ class BasePeerConnection {
|
|
|
7558
7639
|
dispose() {
|
|
7559
7640
|
clearTimeout(this.iceRestartTimeout);
|
|
7560
7641
|
this.iceRestartTimeout = undefined;
|
|
7642
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
7643
|
+
this.preConnectStuckTimeout = undefined;
|
|
7561
7644
|
this.onReconnectionNeeded = undefined;
|
|
7645
|
+
this.onIceConnected = undefined;
|
|
7562
7646
|
this.isDisposed = true;
|
|
7563
7647
|
this.detachEventHandlers();
|
|
7564
7648
|
this.pc.close();
|
|
@@ -8212,7 +8296,8 @@ class Publisher extends BasePeerConnection {
|
|
|
8212
8296
|
const trackInfos = [];
|
|
8213
8297
|
for (const publishOption of this.publishOptions) {
|
|
8214
8298
|
const bundle = this.transceiverCache.get(publishOption);
|
|
8215
|
-
|
|
8299
|
+
const track = bundle?.transceiver.sender.track;
|
|
8300
|
+
if (!bundle || !track || track.readyState !== 'live')
|
|
8216
8301
|
continue;
|
|
8217
8302
|
trackInfos.push(this.toTrackInfo(bundle, sdp));
|
|
8218
8303
|
}
|
|
@@ -8557,6 +8642,20 @@ class SfuJoinError extends Error {
|
|
|
8557
8642
|
}
|
|
8558
8643
|
}
|
|
8559
8644
|
|
|
8645
|
+
/**
|
|
8646
|
+
* Creates a fresh `joinResponseTask` with a no-op rejection handler attached
|
|
8647
|
+
* to the underlying promise. The handler marks the rejection path as handled
|
|
8648
|
+
* so a teardown-time reject (e.g., from `close()` during disposal) does not
|
|
8649
|
+
* surface as an `UnhandledPromiseRejection`. Explicit awaiters of
|
|
8650
|
+
* `StreamSfuClient.joinTask` still observe the rejection through their own
|
|
8651
|
+
* `then`/`catch` chain. `.catch()` returns a new promise; the original is
|
|
8652
|
+
* unchanged.
|
|
8653
|
+
*/
|
|
8654
|
+
const makeJoinResponseTask = () => {
|
|
8655
|
+
const task = promiseWithResolvers();
|
|
8656
|
+
task.promise.catch(() => { }); // see the comment above
|
|
8657
|
+
return task;
|
|
8658
|
+
};
|
|
8560
8659
|
/**
|
|
8561
8660
|
* The client used for exchanging information with the SFU.
|
|
8562
8661
|
*/
|
|
@@ -8588,9 +8687,10 @@ class StreamSfuClient {
|
|
|
8588
8687
|
this.subscriptionsConcurrencyTag = Symbol('subscriptionsConcurrencyTag');
|
|
8589
8688
|
/**
|
|
8590
8689
|
* Promise that resolves when the JoinResponse is received.
|
|
8591
|
-
* Rejects after a certain threshold if the response is not received
|
|
8690
|
+
* Rejects after a certain threshold if the response is not received,
|
|
8691
|
+
* or when the SFU client is disposed before a join completes.
|
|
8592
8692
|
*/
|
|
8593
|
-
this.joinResponseTask =
|
|
8693
|
+
this.joinResponseTask = makeJoinResponseTask();
|
|
8594
8694
|
/**
|
|
8595
8695
|
* A controller to abort the current requests.
|
|
8596
8696
|
*/
|
|
@@ -8660,14 +8760,21 @@ class StreamSfuClient {
|
|
|
8660
8760
|
};
|
|
8661
8761
|
this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
|
|
8662
8762
|
this.isClosingClean = code !== StreamSfuClient.ERROR_CONNECTION_UNHEALTHY;
|
|
8663
|
-
|
|
8763
|
+
// Close the WebSocket whether it has fully opened (`OPEN`) or is still
|
|
8764
|
+
// mid-handshake (`CONNECTING`). The WebSocket spec aborts the handshake
|
|
8765
|
+
// when `close()` is called on a CONNECTING socket. Without this, an
|
|
8766
|
+
// SFU socket that opens just after teardown would dispatch events into
|
|
8767
|
+
// a Call instance that has already moved on.
|
|
8768
|
+
const ws = this.signalWs;
|
|
8769
|
+
if (ws.readyState === WebSocket.OPEN ||
|
|
8770
|
+
ws.readyState === WebSocket.CONNECTING) {
|
|
8664
8771
|
this.logger.debug(`Closing SFU WS connection: ${code} - ${reason}`);
|
|
8665
|
-
|
|
8666
|
-
|
|
8772
|
+
ws.close(code, `js-client: ${reason}`);
|
|
8773
|
+
ws.removeEventListener('close', this.handleWebSocketClose);
|
|
8667
8774
|
}
|
|
8668
|
-
this.dispose();
|
|
8775
|
+
this.dispose(reason);
|
|
8669
8776
|
};
|
|
8670
|
-
this.dispose = () => {
|
|
8777
|
+
this.dispose = (reason) => {
|
|
8671
8778
|
this.logger.debug('Disposing SFU client');
|
|
8672
8779
|
this.unsubscribeIceTrickle();
|
|
8673
8780
|
this.unsubscribeNetworkChanged();
|
|
@@ -8676,6 +8783,13 @@ class StreamSfuClient {
|
|
|
8676
8783
|
clearTimeout(this.migrateAwayTimeout);
|
|
8677
8784
|
this.abortController.abort();
|
|
8678
8785
|
this.migrationTask?.resolve();
|
|
8786
|
+
// Settle a pending `joinResponseTask` so `leaveAndClose`, `join()`, and
|
|
8787
|
+
// any other awaiters (`await this.joinTask`) don't hang indefinitely
|
|
8788
|
+
// when the SFU client is torn down before the SFU sent a JoinResponse.
|
|
8789
|
+
if (!this.joinResponseTask.isResolved() &&
|
|
8790
|
+
!this.joinResponseTask.isRejected()) {
|
|
8791
|
+
this.joinResponseTask.reject(new Error(`SFU client disposed before join completed${reason ? `: ${reason}` : ''}`));
|
|
8792
|
+
}
|
|
8679
8793
|
this.iceTrickleBuffer.dispose();
|
|
8680
8794
|
};
|
|
8681
8795
|
this.getTrace = () => {
|
|
@@ -8684,8 +8798,24 @@ class StreamSfuClient {
|
|
|
8684
8798
|
this.leaveAndClose = async (reason) => {
|
|
8685
8799
|
try {
|
|
8686
8800
|
this.isLeaving = true;
|
|
8687
|
-
|
|
8688
|
-
|
|
8801
|
+
// Best-effort: give an in-flight join a short grace period to complete
|
|
8802
|
+
// so we can send a graceful `leaveCallRequest`. Bounded so we never hang
|
|
8803
|
+
// here if the SFU is unresponsive. If the task settles either way during
|
|
8804
|
+
// the wait, the re-check below decides whether to notify.
|
|
8805
|
+
if (!this.joinResponseTask.isResolved() &&
|
|
8806
|
+
!this.joinResponseTask.isRejected()) {
|
|
8807
|
+
await Promise.race([
|
|
8808
|
+
// swallow rejection — we re-check `isResolved()` below to decide
|
|
8809
|
+
this.joinResponseTask.promise.catch(() => { }),
|
|
8810
|
+
sleep(StreamSfuClient.LEAVE_NOTIFY_GRACE_MS),
|
|
8811
|
+
]);
|
|
8812
|
+
}
|
|
8813
|
+
if (this.joinResponseTask.isResolved()) {
|
|
8814
|
+
await this.notifyLeave(reason);
|
|
8815
|
+
}
|
|
8816
|
+
else {
|
|
8817
|
+
this.logger.debug('[leaveAndClose] join not completed within grace period, skipping notifyLeave');
|
|
8818
|
+
}
|
|
8689
8819
|
}
|
|
8690
8820
|
catch (err) {
|
|
8691
8821
|
this.logger.debug('Error notifying SFU about leaving call', err);
|
|
@@ -8754,9 +8884,9 @@ class StreamSfuClient {
|
|
|
8754
8884
|
this.joinResponseTask.isRejected()) {
|
|
8755
8885
|
// we need to lock the RPC requests until we receive a JoinResponse.
|
|
8756
8886
|
// that's why we have this primitive lock mechanism.
|
|
8757
|
-
// the client starts with already initialized joinResponseTask,
|
|
8887
|
+
// the client starts with an already initialized joinResponseTask,
|
|
8758
8888
|
// and this code creates a new one for the next join request.
|
|
8759
|
-
this.joinResponseTask =
|
|
8889
|
+
this.joinResponseTask = makeJoinResponseTask();
|
|
8760
8890
|
}
|
|
8761
8891
|
// capture a reference to the current joinResponseTask as it might
|
|
8762
8892
|
// be replaced with a new one in case a second join request is made
|
|
@@ -8929,6 +9059,12 @@ StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
|
|
|
8929
9059
|
* The close code used when the client fails to join the call (on the SFU).
|
|
8930
9060
|
*/
|
|
8931
9061
|
StreamSfuClient.JOIN_FAILED = 4101;
|
|
9062
|
+
/**
|
|
9063
|
+
* Best-effort grace period in `leaveAndClose` for an in-flight join to
|
|
9064
|
+
* complete before we give up and close without sending `leaveCallRequest`.
|
|
9065
|
+
* Bounded so a stuck join can never hang the leave path.
|
|
9066
|
+
*/
|
|
9067
|
+
StreamSfuClient.LEAVE_NOTIFY_GRACE_MS = 1000;
|
|
8932
9068
|
|
|
8933
9069
|
/**
|
|
8934
9070
|
* Event handler that watched the delivery of `call.accepted`.
|
|
@@ -10324,6 +10460,50 @@ const CallTypes = new CallTypesRegistry([
|
|
|
10324
10460
|
}),
|
|
10325
10461
|
]);
|
|
10326
10462
|
|
|
10463
|
+
/**
|
|
10464
|
+
* A generic sliding-window rate limiter.
|
|
10465
|
+
*
|
|
10466
|
+
* Allows at most `maxAttempts` registrations inside a rolling `windowMs`.
|
|
10467
|
+
* Attempts spaced further apart than `windowMs` are always allowed.
|
|
10468
|
+
*/
|
|
10469
|
+
class SlidingWindowRateLimiter {
|
|
10470
|
+
constructor(maxAttempts, windowMs) {
|
|
10471
|
+
this.timestamps = [];
|
|
10472
|
+
/**
|
|
10473
|
+
* Attempts to register a new event at `now`. Returns `true` if the attempt
|
|
10474
|
+
* fits inside the budget (and records it), or `false` if the budget is
|
|
10475
|
+
* exhausted (in which case no timestamp is recorded).
|
|
10476
|
+
*/
|
|
10477
|
+
this.tryRegister = (now = Date.now()) => {
|
|
10478
|
+
this.prune(now);
|
|
10479
|
+
if (this.timestamps.length >= this.maxAttempts)
|
|
10480
|
+
return false;
|
|
10481
|
+
this.timestamps.push(now);
|
|
10482
|
+
return true;
|
|
10483
|
+
};
|
|
10484
|
+
/**
|
|
10485
|
+
* Clears the attempt history.
|
|
10486
|
+
*/
|
|
10487
|
+
this.reset = () => {
|
|
10488
|
+
this.timestamps = [];
|
|
10489
|
+
};
|
|
10490
|
+
/**
|
|
10491
|
+
* Updates the budget and window size. Existing timestamps are kept; they
|
|
10492
|
+
* will be pruned by the next `tryRegister` call.
|
|
10493
|
+
*/
|
|
10494
|
+
this.setLimits = (maxAttempts, windowMs) => {
|
|
10495
|
+
this.maxAttempts = maxAttempts;
|
|
10496
|
+
this.windowMs = windowMs;
|
|
10497
|
+
};
|
|
10498
|
+
this.prune = (now) => {
|
|
10499
|
+
const cutoff = now - this.windowMs;
|
|
10500
|
+
this.timestamps = this.timestamps.filter((t) => t >= cutoff);
|
|
10501
|
+
};
|
|
10502
|
+
this.maxAttempts = maxAttempts;
|
|
10503
|
+
this.windowMs = windowMs;
|
|
10504
|
+
}
|
|
10505
|
+
}
|
|
10506
|
+
|
|
10327
10507
|
/**
|
|
10328
10508
|
* Deactivates MediaStream (stops and removes tracks) to be later garbage collected
|
|
10329
10509
|
*
|
|
@@ -10705,7 +10885,6 @@ const getScreenShareStream = async (options, tracer) => {
|
|
|
10705
10885
|
const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
|
|
10706
10886
|
try {
|
|
10707
10887
|
const constraints = {
|
|
10708
|
-
// @ts-expect-error - not present in types yet
|
|
10709
10888
|
systemAudio: 'include',
|
|
10710
10889
|
...options,
|
|
10711
10890
|
video: typeof options?.video === 'boolean'
|
|
@@ -10720,6 +10899,8 @@ const getScreenShareStream = async (options, tracer) => {
|
|
|
10720
10899
|
? options.audio
|
|
10721
10900
|
: {
|
|
10722
10901
|
channelCount: { ideal: 2 },
|
|
10902
|
+
// @ts-expect-error not yet present in the types
|
|
10903
|
+
restrictOwnAudio: true,
|
|
10723
10904
|
echoCancellation: false,
|
|
10724
10905
|
autoGainControl: false,
|
|
10725
10906
|
noiseSuppression: false,
|
|
@@ -11364,6 +11545,7 @@ class DeviceManagerState {
|
|
|
11364
11545
|
this.statusSubject = new BehaviorSubject(undefined);
|
|
11365
11546
|
this.optimisticStatusSubject = new BehaviorSubject(undefined);
|
|
11366
11547
|
this.mediaStreamSubject = new BehaviorSubject(undefined);
|
|
11548
|
+
this.rootMediaStreamSubject = new BehaviorSubject(undefined);
|
|
11367
11549
|
this.selectedDeviceSubject = new BehaviorSubject(undefined);
|
|
11368
11550
|
this.defaultConstraintsSubject = new BehaviorSubject(undefined);
|
|
11369
11551
|
/**
|
|
@@ -11371,6 +11553,12 @@ class DeviceManagerState {
|
|
|
11371
11553
|
*
|
|
11372
11554
|
*/
|
|
11373
11555
|
this.mediaStream$ = this.mediaStreamSubject.asObservable();
|
|
11556
|
+
/**
|
|
11557
|
+
* An Observable that emits the raw device media stream (before any filters are applied),
|
|
11558
|
+
* or `undefined` if the device is currently disabled. When no filters are active, this
|
|
11559
|
+
* emits the same stream as `mediaStream$`.
|
|
11560
|
+
*/
|
|
11561
|
+
this.rootMediaStream$ = this.rootMediaStreamSubject.asObservable();
|
|
11374
11562
|
/**
|
|
11375
11563
|
* An Observable that emits the currently selected device
|
|
11376
11564
|
*/
|
|
@@ -11426,6 +11614,14 @@ class DeviceManagerState {
|
|
|
11426
11614
|
get mediaStream() {
|
|
11427
11615
|
return getCurrentValue(this.mediaStream$);
|
|
11428
11616
|
}
|
|
11617
|
+
/**
|
|
11618
|
+
* The raw device media stream (before any filters are applied), or `undefined`
|
|
11619
|
+
* if the device is currently disabled. When no filters are active, this is the
|
|
11620
|
+
* same as `mediaStream`.
|
|
11621
|
+
*/
|
|
11622
|
+
get rootMediaStream() {
|
|
11623
|
+
return getCurrentValue(this.rootMediaStream$);
|
|
11624
|
+
}
|
|
11429
11625
|
/**
|
|
11430
11626
|
* @internal
|
|
11431
11627
|
* @param status
|
|
@@ -11450,6 +11646,7 @@ class DeviceManagerState {
|
|
|
11450
11646
|
*/
|
|
11451
11647
|
setMediaStream(stream, rootStream) {
|
|
11452
11648
|
setCurrentValue(this.mediaStreamSubject, stream);
|
|
11649
|
+
setCurrentValue(this.rootMediaStreamSubject, rootStream);
|
|
11453
11650
|
if (rootStream) {
|
|
11454
11651
|
this.setDevice(this.getDeviceIdFromStream(rootStream));
|
|
11455
11652
|
}
|
|
@@ -12701,6 +12898,16 @@ class Call {
|
|
|
12701
12898
|
this.fastReconnectDeadlineSeconds = 0;
|
|
12702
12899
|
this.disconnectionTimeoutSeconds = 0;
|
|
12703
12900
|
this.lastOfflineTimestamp = 0;
|
|
12901
|
+
// (10 attempts per rolling 120 s window).
|
|
12902
|
+
this.rejoinRateLimiter = new SlidingWindowRateLimiter(10, 120000);
|
|
12903
|
+
// "Network doesn't support WebRTC" detector: counts peer-connection
|
|
12904
|
+
// failures where ICE never reached `connected`/`completed`.
|
|
12905
|
+
this.maxIceFailuresWithoutConnect = 2;
|
|
12906
|
+
this.iceFailuresWithoutConnect = 0;
|
|
12907
|
+
// Consecutive-negotiation-failure detector: stops the reconnect loop when
|
|
12908
|
+
// the SFU keeps failing to negotiate SDP for us.
|
|
12909
|
+
this.maxConsecutiveNegotiationFailures = 3;
|
|
12910
|
+
this.consecutiveNegotiationFailures = 0;
|
|
12704
12911
|
// maintain the order of publishing tracks to restore them after a reconnection
|
|
12705
12912
|
// it shouldn't contain duplicates
|
|
12706
12913
|
this.trackPublishOrder = [];
|
|
@@ -13003,6 +13210,19 @@ class Call {
|
|
|
13003
13210
|
this.state.setCallingState(CallingState.LEFT);
|
|
13004
13211
|
this.state.setParticipants([]);
|
|
13005
13212
|
this.state.dispose();
|
|
13213
|
+
// Reset reconnect-related accumulators so a future `call.join()` on the
|
|
13214
|
+
// same instance starts with a fresh budget. The `Call` may be reused
|
|
13215
|
+
// (see `Call.test.ts` "can reuse call instance") so this is required.
|
|
13216
|
+
// Strategy/reason/attempts must also be cleared: when `leave()` is
|
|
13217
|
+
// reached via `giveUpAndLeave()` the success-path reset at the end of
|
|
13218
|
+
// `joinFlow` never runs, leaving stale values that would make the next
|
|
13219
|
+
// fresh `join()` send a stale `ReconnectDetails` to the SFU.
|
|
13220
|
+
this.rejoinRateLimiter.reset();
|
|
13221
|
+
this.iceFailuresWithoutConnect = 0;
|
|
13222
|
+
this.consecutiveNegotiationFailures = 0;
|
|
13223
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
13224
|
+
this.reconnectReason = '';
|
|
13225
|
+
this.reconnectAttempts = 0;
|
|
13006
13226
|
// Call all leave call hooks, e.g. to clean up global event handlers
|
|
13007
13227
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
13008
13228
|
this.initialized = false;
|
|
@@ -13353,9 +13573,20 @@ class Call {
|
|
|
13353
13573
|
// when performing fast reconnect, or when we reuse the same SFU client,
|
|
13354
13574
|
// (ws remained healthy), we just need to restore the ICE connection
|
|
13355
13575
|
if (performingFastReconnect) {
|
|
13356
|
-
//
|
|
13357
|
-
// we
|
|
13358
|
-
|
|
13576
|
+
// The SFU automatically issues an ICE restart on the subscriber,
|
|
13577
|
+
// so we only need to decide about the publisher. If the publisher's
|
|
13578
|
+
// peer connection is still stable (ICE still connected end-to-end),
|
|
13579
|
+
// the signal WebSocket drop was the only problem — the new WS alone
|
|
13580
|
+
// is enough, and restarting ICE would add unnecessary SDP/ICE churn.
|
|
13581
|
+
const publisherIsStable = this.publisher?.isStable() ?? true;
|
|
13582
|
+
const includePublisher = !!this.publisher?.isPublishing() && !publisherIsStable;
|
|
13583
|
+
if (!includePublisher && this.publisher?.isPublishing()) {
|
|
13584
|
+
this.logger.info('[Reconnect] FAST: skipping publisher ICE restart, publisher PC is stable');
|
|
13585
|
+
}
|
|
13586
|
+
await this.restoreICE(sfuClient, {
|
|
13587
|
+
includeSubscriber: false,
|
|
13588
|
+
includePublisher,
|
|
13589
|
+
});
|
|
13359
13590
|
}
|
|
13360
13591
|
else {
|
|
13361
13592
|
const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
|
|
@@ -13398,6 +13629,15 @@ class Call {
|
|
|
13398
13629
|
// reset the reconnect strategy to unspecified after a successful reconnection
|
|
13399
13630
|
this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
13400
13631
|
this.reconnectReason = '';
|
|
13632
|
+
// A successful SFU join handshake resets the consecutive-negotiation
|
|
13633
|
+
// counter (negotiation just succeeded). It does NOT reset
|
|
13634
|
+
// `iceFailuresWithoutConnect` or the rolling `rejoinRateLimiter`;
|
|
13635
|
+
// those track WebRTC-level health and rejoin frequency, which are not
|
|
13636
|
+
// proven by the SFU handshake alone. ICE-failures-without-connect is
|
|
13637
|
+
// cleared via the `onIceConnected` callback when the peer connection
|
|
13638
|
+
// actually reaches `connected`/`completed` end-to-end. The rejoin
|
|
13639
|
+
// rolling window decays naturally as old timestamps age out.
|
|
13640
|
+
this.consecutiveNegotiationFailures = 0;
|
|
13401
13641
|
this.logger.info(`Joined call ${this.cid}`);
|
|
13402
13642
|
};
|
|
13403
13643
|
/**
|
|
@@ -13515,6 +13755,12 @@ class Call {
|
|
|
13515
13755
|
this.logger.warn(message, err);
|
|
13516
13756
|
});
|
|
13517
13757
|
},
|
|
13758
|
+
onIceConnected: () => {
|
|
13759
|
+
// ICE has reached `connected`/`completed` end-to-end on at least
|
|
13760
|
+
// one peer connection, WebRTC is actually working, so the
|
|
13761
|
+
// "ICE never connected" failure budget can be cleared.
|
|
13762
|
+
this.iceFailuresWithoutConnect = 0;
|
|
13763
|
+
},
|
|
13518
13764
|
};
|
|
13519
13765
|
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
13520
13766
|
// anonymous users can't publish anything hence, there is no need
|
|
@@ -13620,7 +13866,9 @@ class Call {
|
|
|
13620
13866
|
* @internal
|
|
13621
13867
|
*
|
|
13622
13868
|
* @param strategy the reconnection strategy to use.
|
|
13623
|
-
* @param reason the reason for the reconnection.
|
|
13869
|
+
* @param reason the reason for the reconnection. Pass a `ReconnectReason.*`
|
|
13870
|
+
* constant when the SDK should react to it (e.g.
|
|
13871
|
+
* `ICE_NEVER_CONNECTED` increments the unsupported-network counter).
|
|
13624
13872
|
*/
|
|
13625
13873
|
this.reconnect = async (strategy, reason) => {
|
|
13626
13874
|
if (this.state.callingState === CallingState.RECONNECTING ||
|
|
@@ -13641,6 +13889,30 @@ class Call {
|
|
|
13641
13889
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
13642
13890
|
}
|
|
13643
13891
|
};
|
|
13892
|
+
const giveUpAndLeave = async (message) => {
|
|
13893
|
+
this.logger.warn(`[Reconnect] Giving up: ${message}. Leaving the call.`);
|
|
13894
|
+
// If we're mid-iteration, the state can be JOINING; `Call.leave` would
|
|
13895
|
+
// then wait for JOINED before proceeding, but no more attempts will run
|
|
13896
|
+
// so JOINED never comes. Transition to RECONNECTING so `leave()` proceeds.
|
|
13897
|
+
if (this.state.callingState === CallingState.JOINING) {
|
|
13898
|
+
this.state.setCallingState(CallingState.RECONNECTING);
|
|
13899
|
+
}
|
|
13900
|
+
try {
|
|
13901
|
+
await this.leave({ message });
|
|
13902
|
+
}
|
|
13903
|
+
catch (err) {
|
|
13904
|
+
this.logger.warn(`[Reconnect] leave() failed after ${message}`, err);
|
|
13905
|
+
}
|
|
13906
|
+
};
|
|
13907
|
+
// Count this entry into reconnect if it was triggered by a peer
|
|
13908
|
+
// connection that never reached `connected`/`completed`.
|
|
13909
|
+
if (reason === ReconnectReason.ICE_NEVER_CONNECTED) {
|
|
13910
|
+
this.iceFailuresWithoutConnect++;
|
|
13911
|
+
if (this.iceFailuresWithoutConnect >= this.maxIceFailuresWithoutConnect) {
|
|
13912
|
+
await giveUpAndLeave('webrtc_unsupported_network');
|
|
13913
|
+
return;
|
|
13914
|
+
}
|
|
13915
|
+
}
|
|
13644
13916
|
let attempt = 0;
|
|
13645
13917
|
do {
|
|
13646
13918
|
const reconnectingTime = Date.now() - reconnectStartTime;
|
|
@@ -13651,6 +13923,16 @@ class Call {
|
|
|
13651
13923
|
await markAsReconnectingFailed();
|
|
13652
13924
|
return;
|
|
13653
13925
|
}
|
|
13926
|
+
// Rejoin rate limit: bound the number of REJOIN (and MIGRATE)
|
|
13927
|
+
// transitions inside a rolling window. FAST is not counted because
|
|
13928
|
+
// it does not issue a new backend `joinCall`.
|
|
13929
|
+
if (this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN ||
|
|
13930
|
+
this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE) {
|
|
13931
|
+
if (!this.rejoinRateLimiter.tryRegister()) {
|
|
13932
|
+
await giveUpAndLeave('rejoin_attempt_limit_exceeded');
|
|
13933
|
+
return;
|
|
13934
|
+
}
|
|
13935
|
+
}
|
|
13654
13936
|
// we don't increment reconnect attempts for the FAST strategy.
|
|
13655
13937
|
if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
|
|
13656
13938
|
this.reconnectAttempts++;
|
|
@@ -13678,6 +13960,8 @@ class Call {
|
|
|
13678
13960
|
ensureExhausted(this.reconnectStrategy, 'Unknown reconnection strategy');
|
|
13679
13961
|
break;
|
|
13680
13962
|
}
|
|
13963
|
+
// reconnection worked — reset the negotiation-failure streak.
|
|
13964
|
+
this.consecutiveNegotiationFailures = 0;
|
|
13681
13965
|
break; // do-while loop, reconnection worked, exit the loop
|
|
13682
13966
|
}
|
|
13683
13967
|
catch (error) {
|
|
@@ -13692,7 +13976,16 @@ class Call {
|
|
|
13692
13976
|
await markAsReconnectingFailed();
|
|
13693
13977
|
return;
|
|
13694
13978
|
}
|
|
13695
|
-
|
|
13979
|
+
if (error instanceof NegotiationError) {
|
|
13980
|
+
this.consecutiveNegotiationFailures++;
|
|
13981
|
+
if (this.consecutiveNegotiationFailures >=
|
|
13982
|
+
this.maxConsecutiveNegotiationFailures) {
|
|
13983
|
+
await giveUpAndLeave('repeated_negotiation_failures');
|
|
13984
|
+
return;
|
|
13985
|
+
}
|
|
13986
|
+
}
|
|
13987
|
+
// exponential backoff with jitter, capped at 5 s
|
|
13988
|
+
await sleep(retryInterval(attempt));
|
|
13696
13989
|
const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
13697
13990
|
const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
|
|
13698
13991
|
this.fastReconnectDeadlineSeconds;
|
|
@@ -13801,7 +14094,7 @@ class Call {
|
|
|
13801
14094
|
this.registerReconnectHandlers = () => {
|
|
13802
14095
|
// handles the legacy "goAway" event
|
|
13803
14096
|
const unregisterGoAway = this.on('goAway', () => {
|
|
13804
|
-
this.reconnect(WebsocketReconnectStrategy.MIGRATE,
|
|
14097
|
+
this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
|
|
13805
14098
|
});
|
|
13806
14099
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
13807
14100
|
const unregisterOnError = this.on('error', (e) => {
|
|
@@ -13820,7 +14113,7 @@ class Call {
|
|
|
13820
14113
|
});
|
|
13821
14114
|
}
|
|
13822
14115
|
else {
|
|
13823
|
-
this.reconnect(strategy, error?.message ||
|
|
14116
|
+
this.reconnect(strategy, error?.message || ReconnectReason.SFU_ERROR).catch((err) => {
|
|
13824
14117
|
this.logger.warn('[Reconnect] Error reconnecting', err);
|
|
13825
14118
|
});
|
|
13826
14119
|
}
|
|
@@ -13844,7 +14137,7 @@ class Call {
|
|
|
13844
14137
|
strategy = WebsocketReconnectStrategy.REJOIN;
|
|
13845
14138
|
}
|
|
13846
14139
|
}
|
|
13847
|
-
this.reconnect(strategy,
|
|
14140
|
+
this.reconnect(strategy, ReconnectReason.NETWORK_BACK_ONLINE).catch((err) => {
|
|
13848
14141
|
this.logger.warn('[Reconnect] Error reconnecting after going online', err);
|
|
13849
14142
|
});
|
|
13850
14143
|
});
|
|
@@ -13987,10 +14280,12 @@ class Call {
|
|
|
13987
14280
|
* @param trackTypes the track types to update the call state with.
|
|
13988
14281
|
*/
|
|
13989
14282
|
this.updateLocalStreamState = async (mediaStream, ...trackTypes) => {
|
|
13990
|
-
|
|
14283
|
+
const sessionId = this.sfuClient?.sessionId;
|
|
14284
|
+
if (!sessionId)
|
|
13991
14285
|
return;
|
|
13992
14286
|
await this.notifyTrackMuteState(!mediaStream, ...trackTypes);
|
|
13993
|
-
|
|
14287
|
+
if (this.sfuClient?.sessionId !== sessionId)
|
|
14288
|
+
return;
|
|
13994
14289
|
for (const trackType of trackTypes) {
|
|
13995
14290
|
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
13996
14291
|
if (!streamStateProp)
|
|
@@ -14786,6 +15081,39 @@ class Call {
|
|
|
14786
15081
|
this.setDisconnectionTimeout = (timeoutSeconds) => {
|
|
14787
15082
|
this.disconnectionTimeoutSeconds = timeoutSeconds;
|
|
14788
15083
|
};
|
|
15084
|
+
/**
|
|
15085
|
+
* Configures the rolling-window limit for REJOIN/MIGRATE attempts. Once
|
|
15086
|
+
* `maxAttempts` rejoins have been registered inside `windowSeconds`, the
|
|
15087
|
+
* SDK stops retrying and transitions the call to `LEFT` with the
|
|
15088
|
+
* `rejoin_attempt_limit_exceeded` leave message.
|
|
15089
|
+
*
|
|
15090
|
+
* Defaults: 10 attempts per 120 seconds (aligned with the Swift SDK).
|
|
15091
|
+
* Both arguments are clamped to a minimum of 1.
|
|
15092
|
+
*/
|
|
15093
|
+
this.setRejoinAttemptLimit = (maxAttempts, windowSeconds) => {
|
|
15094
|
+
this.rejoinRateLimiter.setLimits(Math.max(1, maxAttempts), Math.max(1, windowSeconds) * 1000);
|
|
15095
|
+
};
|
|
15096
|
+
/**
|
|
15097
|
+
* Configures how many peer-connection failures where ICE never reached
|
|
15098
|
+
* `connected`/`completed` are tolerated before the SDK concludes that the
|
|
15099
|
+
* current network cannot support WebRTC and transitions the call to
|
|
15100
|
+
* `LEFT` with the `webrtc_unsupported_network` leave message.
|
|
15101
|
+
*
|
|
15102
|
+
* Default: 2. Clamped to a minimum of 1.
|
|
15103
|
+
*/
|
|
15104
|
+
this.setMaxIceFailuresWithoutConnect = (n) => {
|
|
15105
|
+
this.maxIceFailuresWithoutConnect = Math.max(1, n);
|
|
15106
|
+
};
|
|
15107
|
+
/**
|
|
15108
|
+
* Configures how many consecutive SDP `NegotiationError`s are tolerated
|
|
15109
|
+
* before the SDK stops retrying and transitions the call to `LEFT` with
|
|
15110
|
+
* the `repeated_negotiation_failures` leave message.
|
|
15111
|
+
*
|
|
15112
|
+
* Default: 3. Clamped to a minimum of 1.
|
|
15113
|
+
*/
|
|
15114
|
+
this.setMaxConsecutiveNegotiationFailures = (n) => {
|
|
15115
|
+
this.maxConsecutiveNegotiationFailures = Math.max(1, n);
|
|
15116
|
+
};
|
|
14789
15117
|
/**
|
|
14790
15118
|
* Enables the provided client capabilities.
|
|
14791
15119
|
*/
|
|
@@ -15968,7 +16296,7 @@ class StreamClient {
|
|
|
15968
16296
|
this.getUserAgent = () => {
|
|
15969
16297
|
if (!this.cachedUserAgent) {
|
|
15970
16298
|
const { clientAppIdentifier = {} } = this.options;
|
|
15971
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
16299
|
+
const { sdkName = 'js', sdkVersion = "1.49.0", ...extras } = clientAppIdentifier;
|
|
15972
16300
|
this.cachedUserAgent = [
|
|
15973
16301
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
15974
16302
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|