@stream-io/video-client 1.60.0 → 1.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/index.browser.es.js +264 -57
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +264 -56
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +264 -57
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +11 -5
- package/dist/src/rtc/Publisher.d.ts +18 -0
- package/dist/src/rtc/TransceiverCache.d.ts +10 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
- package/dist/src/types.d.ts +30 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/Call.ts +180 -48
- package/src/__tests__/Call.lifecycle.test.ts +479 -9
- package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
- package/src/reporting/ClientEventReporter.ts +7 -2
- package/src/reporting/__tests__/ClientEventReporter.test.ts +2 -2
- package/src/rtc/BasePeerConnection.ts +6 -1
- package/src/rtc/Publisher.ts +66 -12
- package/src/rtc/TransceiverCache.ts +17 -0
- package/src/rtc/__tests__/Publisher.test.ts +259 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
- package/src/rtc/e2ee/E2EEManager.ts +10 -1
- package/src/rtc/e2ee/EncryptionManager.ts +1 -1
- package/src/rtc/e2ee/SPEC.md +1 -1
- package/src/types.ts +37 -1
package/dist/index.browser.es.js
CHANGED
|
@@ -5081,7 +5081,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5081
5081
|
}
|
|
5082
5082
|
};
|
|
5083
5083
|
|
|
5084
|
-
const version = "1.
|
|
5084
|
+
const version = "1.61.1";
|
|
5085
5085
|
const [major, minor, patch] = version.split('.');
|
|
5086
5086
|
let sdkInfo = {
|
|
5087
5087
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -8754,7 +8754,12 @@ class BasePeerConnection {
|
|
|
8754
8754
|
}
|
|
8755
8755
|
// we can't recover from a failed connection state (contrary to ICE)
|
|
8756
8756
|
if (state === 'failed') {
|
|
8757
|
-
|
|
8757
|
+
const reason = this.iceHasEverConnected
|
|
8758
|
+
? ReconnectReason.CONNECTION_FAILED
|
|
8759
|
+
: ReconnectReason.ICE_NEVER_CONNECTED;
|
|
8760
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
8761
|
+
this.preConnectStuckTimeout = undefined;
|
|
8762
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, reason, this.peerType);
|
|
8758
8763
|
return;
|
|
8759
8764
|
}
|
|
8760
8765
|
this.handleConnectionStateUpdate(state);
|
|
@@ -9146,6 +9151,25 @@ class TransceiverCache {
|
|
|
9146
9151
|
this.cache.push(bundle);
|
|
9147
9152
|
this.transceiverOrder.push(bundle.transceiver);
|
|
9148
9153
|
};
|
|
9154
|
+
/**
|
|
9155
|
+
* Retires a failed sender from the reusable cache.
|
|
9156
|
+
*
|
|
9157
|
+
* A transceiver that never reached an m-section also leaves the creation
|
|
9158
|
+
* history: once stopped, it is left out of every later offer, so counting
|
|
9159
|
+
* it would shift the `mid` index of every transceiver added after it. One
|
|
9160
|
+
* that is already associated keeps its slot, since its m-section survives
|
|
9161
|
+
* as a rejected one.
|
|
9162
|
+
*/
|
|
9163
|
+
remove = (transceiver) => {
|
|
9164
|
+
const index = this.cache.findIndex((b) => b.transceiver === transceiver);
|
|
9165
|
+
if (index !== -1)
|
|
9166
|
+
this.cache.splice(index, 1);
|
|
9167
|
+
if (transceiver.mid)
|
|
9168
|
+
return;
|
|
9169
|
+
const orderIndex = this.transceiverOrder.indexOf(transceiver);
|
|
9170
|
+
if (orderIndex !== -1)
|
|
9171
|
+
this.transceiverOrder.splice(orderIndex, 1);
|
|
9172
|
+
};
|
|
9149
9173
|
/**
|
|
9150
9174
|
* Gets the transceiver for the given publish option.
|
|
9151
9175
|
*/
|
|
@@ -9317,7 +9341,9 @@ class Publisher extends BasePeerConnection {
|
|
|
9317
9341
|
const sendEncodings = isSvcCodec(publishOption.codec?.name)
|
|
9318
9342
|
? toSvcEncodings(encodings)
|
|
9319
9343
|
: encodings;
|
|
9320
|
-
|
|
9344
|
+
// An E2EE sender must never have media before its transform is attached,
|
|
9345
|
+
// including when another track or an ICE restart negotiates this PC.
|
|
9346
|
+
const transceiver = this.pc.addTransceiver(this.e2ee ? track.kind : track, {
|
|
9321
9347
|
direction: 'sendonly',
|
|
9322
9348
|
sendEncodings,
|
|
9323
9349
|
});
|
|
@@ -9327,16 +9353,63 @@ class Publisher extends BasePeerConnection {
|
|
|
9327
9353
|
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9328
9354
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
9329
9355
|
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9330
|
-
|
|
9356
|
+
if (this.e2ee) {
|
|
9357
|
+
await this.initEncryptedSender(transceiver, track, publishOption);
|
|
9358
|
+
}
|
|
9359
|
+
else {
|
|
9360
|
+
await this.setDegradationPreference(transceiver.sender, publishOption);
|
|
9361
|
+
}
|
|
9362
|
+
await this.negotiate();
|
|
9363
|
+
};
|
|
9364
|
+
/**
|
|
9365
|
+
* Attaches the encryptor, applies sender parameters, then attaches the track.
|
|
9366
|
+
*
|
|
9367
|
+
* `encrypt` must run before the first await: Chrome short-circuits a sender
|
|
9368
|
+
* whose encoded streams aren't created in the task that created it, and the
|
|
9369
|
+
* track goes on last so the sender has no media until both steps succeed.
|
|
9370
|
+
*/
|
|
9371
|
+
initEncryptedSender = async (transceiver, track, publishOption) => {
|
|
9372
|
+
try {
|
|
9373
|
+
const { sender } = transceiver;
|
|
9374
|
+
const codec = publishOption.codec?.name.toLowerCase();
|
|
9375
|
+
this.e2ee.encrypt(sender, codec, TrackType[publishOption.trackType]);
|
|
9376
|
+
this.logger.debug('E2EE encryptor attached to sender');
|
|
9377
|
+
await this.setDegradationPreference(sender, publishOption);
|
|
9378
|
+
await sender.replaceTrack(track);
|
|
9379
|
+
}
|
|
9380
|
+
catch (err) {
|
|
9381
|
+
this.retireTransceiver(transceiver, track);
|
|
9382
|
+
throw err;
|
|
9383
|
+
}
|
|
9384
|
+
};
|
|
9385
|
+
setDegradationPreference = async (sender, publishOption) => {
|
|
9386
|
+
const params = sender.getParameters();
|
|
9331
9387
|
params.degradationPreference =
|
|
9332
9388
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9333
9389
|
'maintain-framerate';
|
|
9334
|
-
await
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9390
|
+
await sender.setParameters(params);
|
|
9391
|
+
};
|
|
9392
|
+
/**
|
|
9393
|
+
* Discards a sender whose E2EE initialization failed.
|
|
9394
|
+
*
|
|
9395
|
+
* A partially initialized sender may carry no transform, so publish() must
|
|
9396
|
+
* not reuse it: retiring it from the cache is what keeps a later publish or
|
|
9397
|
+
* ICE restart from putting cleartext on the wire. Cleanup cannot throw, so
|
|
9398
|
+
* the initialization failure is what reaches the caller.
|
|
9399
|
+
*/
|
|
9400
|
+
retireTransceiver = (transceiver, track) => {
|
|
9401
|
+
this.transceiverCache.remove(transceiver);
|
|
9402
|
+
this.trackIdToTrackType.delete(track.id);
|
|
9403
|
+
// on React Native a clone shares its native source, so stopping it here
|
|
9404
|
+
// would stop the track the caller passed in. dispose() releases it.
|
|
9405
|
+
if (!isReactNative())
|
|
9406
|
+
this.stopTrack(track);
|
|
9407
|
+
try {
|
|
9408
|
+
transceiver.stop();
|
|
9409
|
+
}
|
|
9410
|
+
catch (err) {
|
|
9411
|
+
this.logger.debug('Failed to stop a retired transceiver', err);
|
|
9338
9412
|
}
|
|
9339
|
-
await this.negotiate();
|
|
9340
9413
|
};
|
|
9341
9414
|
/**
|
|
9342
9415
|
* Updates the transceiver with the given track and track type.
|
|
@@ -15902,6 +15975,11 @@ class Call {
|
|
|
15902
15975
|
this.audioBindingsWatchdog?.dispose();
|
|
15903
15976
|
await this.dynascaleManager?.dispose();
|
|
15904
15977
|
this.state.setCallingState(CallingState.LEFT);
|
|
15978
|
+
// `ringingSubject` is cleared further down, so this still reads true for
|
|
15979
|
+
// a call that was ringing.
|
|
15980
|
+
if (this.ringing) {
|
|
15981
|
+
globalThis.streamRNVideoSDK?.ringingCallLifecycle?.onLeave(this);
|
|
15982
|
+
}
|
|
15905
15983
|
this.state.setParticipants([]);
|
|
15906
15984
|
this.state.dispose();
|
|
15907
15985
|
// Reset reconnect-related accumulators so a future `call.join()` on the
|
|
@@ -16070,8 +16148,11 @@ class Call {
|
|
|
16070
16148
|
* @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise.
|
|
16071
16149
|
*/
|
|
16072
16150
|
get = async (params) => {
|
|
16151
|
+
const getLeaveGeneration = this.leaveGeneration;
|
|
16073
16152
|
await this.setup();
|
|
16074
16153
|
const response = await this.streamClient.get(this.streamClientBasePath, params);
|
|
16154
|
+
if (this.leaveGeneration !== getLeaveGeneration)
|
|
16155
|
+
return response;
|
|
16075
16156
|
this.updateFromCallStateResponse(response);
|
|
16076
16157
|
if (params?.ring) {
|
|
16077
16158
|
this.ringingSubject.next(true);
|
|
@@ -16176,42 +16257,80 @@ class Call {
|
|
|
16176
16257
|
/**
|
|
16177
16258
|
* Will start to watch for call related WebSocket events and initiate a call session with the server.
|
|
16178
16259
|
*
|
|
16260
|
+
* One instance is one call flow: discard it after leaving, cancelling, or a
|
|
16261
|
+
* failed join, and create a fresh one for a later flow. Reconnection inside a
|
|
16262
|
+
* live call is handled here and needs no new instance.
|
|
16263
|
+
*
|
|
16179
16264
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
16180
16265
|
*/
|
|
16181
16266
|
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
|
|
16182
16267
|
const callingState = this.state.callingState;
|
|
16268
|
+
// Ahead of the failure boundary below on purpose: a duplicate join on a
|
|
16269
|
+
// live call is refused without tearing that call down.
|
|
16183
16270
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
16184
16271
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
16185
16272
|
}
|
|
16273
|
+
// snapshot before the first await: a leave() landing at any point from
|
|
16274
|
+
// here on supersedes this join, and the retry loop must bail out instead
|
|
16275
|
+
// of resurrecting a call that leave() already tore down.
|
|
16276
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
16277
|
+
const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
|
|
16186
16278
|
// we need this to be set before the callingx.joinCall() is
|
|
16187
16279
|
// called to avoid registering the test call in the CallKit/Telecom
|
|
16188
16280
|
this.allowOwnTracksLoopback = allowOwnTracksLoopback;
|
|
16189
16281
|
if (data?.ring) {
|
|
16190
16282
|
this.ringingSubject.next(true);
|
|
16191
16283
|
}
|
|
16284
|
+
// A ringing call is joined by the SDK rather than by app code, so React
|
|
16285
|
+
// Native prepares it from in here - there is no earlier point at which the
|
|
16286
|
+
// app holds the call. Read after `data.ring` above, which is what makes an
|
|
16287
|
+
// outgoing call ringing in the first place.
|
|
16288
|
+
const ringingLifecycle = this.ringing
|
|
16289
|
+
? globalThis.streamRNVideoSDK?.ringingCallLifecycle
|
|
16290
|
+
: undefined;
|
|
16192
16291
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
16193
|
-
if (callingX) {
|
|
16194
|
-
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
16195
|
-
await callingX.joinCall(this, this.clientStore.calls);
|
|
16196
|
-
}
|
|
16197
|
-
await this.setup();
|
|
16198
|
-
this.clientEventReporter.registerCall(this.cid, {
|
|
16199
|
-
callType: this.type,
|
|
16200
|
-
callId: this.id,
|
|
16201
|
-
getCallSessionId: () => this.state.session?.id ?? '',
|
|
16202
|
-
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
16203
|
-
});
|
|
16204
|
-
this.joinResponseTimeout = joinResponseTimeout;
|
|
16205
|
-
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
16206
|
-
// we will count the number of join failures per SFU.
|
|
16207
|
-
// once the number of failures reaches 2, we will piggyback on the `migrating_from`
|
|
16208
|
-
// field to force the coordinator to provide us another SFU
|
|
16209
|
-
const sfuJoinFailures = new Map();
|
|
16210
|
-
const joinData = data;
|
|
16211
|
-
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
16212
16292
|
try {
|
|
16293
|
+
if (ringingLifecycle) {
|
|
16294
|
+
await ringingLifecycle.beforeJoin(this);
|
|
16295
|
+
if (supersededByLeave()) {
|
|
16296
|
+
this.logger.debug('Join superseded by leave; not joining');
|
|
16297
|
+
return;
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16300
|
+
if (callingX) {
|
|
16301
|
+
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
16302
|
+
await callingX.joinCall(this, this.clientStore.calls, supersededByLeave);
|
|
16303
|
+
if (supersededByLeave()) {
|
|
16304
|
+
this.logger.debug('Join superseded by leave; not setting up');
|
|
16305
|
+
return;
|
|
16306
|
+
}
|
|
16307
|
+
}
|
|
16308
|
+
await this.setup();
|
|
16309
|
+
if (supersededByLeave()) {
|
|
16310
|
+
this.logger.debug('Join superseded by leave; not registering');
|
|
16311
|
+
return;
|
|
16312
|
+
}
|
|
16313
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
16314
|
+
callType: this.type,
|
|
16315
|
+
callId: this.id,
|
|
16316
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
16317
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
16318
|
+
});
|
|
16319
|
+
this.joinResponseTimeout = joinResponseTimeout;
|
|
16320
|
+
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
16321
|
+
// we will count the number of join failures per SFU.
|
|
16322
|
+
// once the number of failures reaches 2, we will piggyback on the `migrating_from`
|
|
16323
|
+
// field to force the coordinator to provide us another SFU
|
|
16324
|
+
const sfuJoinFailures = new Map();
|
|
16325
|
+
const joinData = data;
|
|
16326
|
+
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
16213
16327
|
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
|
|
16214
16328
|
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
16329
|
+
// Also covers a leave that lands during the backoff below.
|
|
16330
|
+
if (supersededByLeave()) {
|
|
16331
|
+
this.logger.debug('Join superseded by leave; not attempting');
|
|
16332
|
+
return;
|
|
16333
|
+
}
|
|
16215
16334
|
try {
|
|
16216
16335
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
16217
16336
|
await this.doJoin(data);
|
|
@@ -16220,6 +16339,10 @@ class Call {
|
|
|
16220
16339
|
return;
|
|
16221
16340
|
}
|
|
16222
16341
|
catch (err) {
|
|
16342
|
+
if (supersededByLeave()) {
|
|
16343
|
+
this.logger.debug('Join superseded by leave; not retrying');
|
|
16344
|
+
return;
|
|
16345
|
+
}
|
|
16223
16346
|
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
16224
16347
|
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
16225
16348
|
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
@@ -16249,6 +16372,9 @@ class Call {
|
|
|
16249
16372
|
}
|
|
16250
16373
|
catch (error) {
|
|
16251
16374
|
callingX?.endCall(this, 'error');
|
|
16375
|
+
// Ends the failed ringing flow and releases what its setup installed.
|
|
16376
|
+
// Never rejects, so `error` is what the caller sees.
|
|
16377
|
+
await ringingLifecycle?.onJoinFailed(this);
|
|
16252
16378
|
throw error;
|
|
16253
16379
|
}
|
|
16254
16380
|
});
|
|
@@ -16271,6 +16397,10 @@ class Call {
|
|
|
16271
16397
|
// globals resolve to the call's factory. Idempotent across
|
|
16272
16398
|
// reconnect/migration attempts.
|
|
16273
16399
|
await this.ensureMediaFactory();
|
|
16400
|
+
if (supersededByLeave()) {
|
|
16401
|
+
this.logger.debug('Join superseded by leave; not wiring media');
|
|
16402
|
+
return;
|
|
16403
|
+
}
|
|
16274
16404
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
16275
16405
|
if (callingX) {
|
|
16276
16406
|
callingX.wireAudioEngineSubscription();
|
|
@@ -16286,11 +16416,17 @@ class Call {
|
|
|
16286
16416
|
data?.migrating_from) {
|
|
16287
16417
|
try {
|
|
16288
16418
|
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
16419
|
+
if (!joinResponse || supersededByLeave()) {
|
|
16420
|
+
this.logger.debug('Join superseded by leave; not creating SFU client');
|
|
16421
|
+
return;
|
|
16422
|
+
}
|
|
16289
16423
|
this.credentials = joinResponse.credentials;
|
|
16290
16424
|
statsOptions = joinResponse.stats_options;
|
|
16291
16425
|
this.lastStatsOptions = statsOptions;
|
|
16292
16426
|
}
|
|
16293
16427
|
catch (error) {
|
|
16428
|
+
if (supersededByLeave())
|
|
16429
|
+
return;
|
|
16294
16430
|
// prevent triggering reconnect flow if the state is OFFLINE
|
|
16295
16431
|
const avoidRestoreState = this.state.callingState === CallingState.OFFLINE;
|
|
16296
16432
|
if (!avoidRestoreState) {
|
|
@@ -16338,6 +16474,10 @@ class Call {
|
|
|
16338
16474
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
16339
16475
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
16340
16476
|
]);
|
|
16477
|
+
if (supersededByLeave()) {
|
|
16478
|
+
this.logger.debug('Join superseded by leave; not joining SFU');
|
|
16479
|
+
return;
|
|
16480
|
+
}
|
|
16341
16481
|
const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
16342
16482
|
const reconnectDetails = isReconnecting
|
|
16343
16483
|
? this.getReconnectDetails(previousSfuClient?.edgeName, previousSessionId)
|
|
@@ -16363,6 +16503,10 @@ class Call {
|
|
|
16363
16503
|
capabilities,
|
|
16364
16504
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
16365
16505
|
}));
|
|
16506
|
+
if (supersededByLeave()) {
|
|
16507
|
+
this.logger.debug('Join superseded by leave; not applying SFU response');
|
|
16508
|
+
return;
|
|
16509
|
+
}
|
|
16366
16510
|
this.currentPublishOptions = publishOptions;
|
|
16367
16511
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
16368
16512
|
if (callState) {
|
|
@@ -16370,6 +16514,10 @@ class Call {
|
|
|
16370
16514
|
}
|
|
16371
16515
|
}
|
|
16372
16516
|
catch (error) {
|
|
16517
|
+
if (supersededByLeave()) {
|
|
16518
|
+
this.logger.debug('Join superseded by leave; ignoring SFU failure');
|
|
16519
|
+
return;
|
|
16520
|
+
}
|
|
16373
16521
|
this.logger.warn('Join SFU request failed', error);
|
|
16374
16522
|
sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
|
|
16375
16523
|
// restore the previous call state if the join-flow fails
|
|
@@ -16377,8 +16525,8 @@ class Call {
|
|
|
16377
16525
|
throw error;
|
|
16378
16526
|
}
|
|
16379
16527
|
}
|
|
16380
|
-
// If the user left while this join was in flight, bail before re-setting JOINED
|
|
16381
|
-
// peer-connection setup below
|
|
16528
|
+
// If the user left while this join was in flight, bail before re-setting JOINED
|
|
16529
|
+
// or starting peer-connection setup below.
|
|
16382
16530
|
if (supersededByLeave()) {
|
|
16383
16531
|
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
16384
16532
|
return;
|
|
@@ -16407,6 +16555,10 @@ class Call {
|
|
|
16407
16555
|
unifiedSessionId: this.unifiedSessionId,
|
|
16408
16556
|
});
|
|
16409
16557
|
}
|
|
16558
|
+
if (supersededByLeave()) {
|
|
16559
|
+
this.logger.debug('Join superseded by leave; not completing join flow');
|
|
16560
|
+
return;
|
|
16561
|
+
}
|
|
16410
16562
|
// make sure we only track connection timing if we are not calling this method as part of a reconnection flow
|
|
16411
16563
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
16412
16564
|
this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
|
|
@@ -16552,15 +16704,20 @@ class Call {
|
|
|
16552
16704
|
* @internal
|
|
16553
16705
|
*/
|
|
16554
16706
|
initPublisherAndSubscriber = async (opts) => {
|
|
16707
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
16555
16708
|
const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
|
|
16556
16709
|
const { enable_rtc_stats: enableTracing, reporting_interval_ms: reportingIntervalMs, } = statsOptions;
|
|
16557
16710
|
// Flush the previous reporter's final sample while its peer connections are
|
|
16558
16711
|
// still alive, before we dispose them below. Awaits only the sampling step.
|
|
16559
16712
|
await this.sfuStatsReporter?.flush();
|
|
16713
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16714
|
+
return;
|
|
16560
16715
|
this.sfuStatsReporter?.stop();
|
|
16561
16716
|
this.sfuStatsReporter = undefined;
|
|
16562
16717
|
if (closePreviousInstances && this.subscriber) {
|
|
16563
16718
|
await this.subscriber.dispose();
|
|
16719
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16720
|
+
return;
|
|
16564
16721
|
this.state.removeAllOrphanedTracks();
|
|
16565
16722
|
}
|
|
16566
16723
|
const basePeerConnectionOptions = {
|
|
@@ -16603,6 +16760,8 @@ class Call {
|
|
|
16603
16760
|
if (!isAnonymous) {
|
|
16604
16761
|
if (closePreviousInstances && this.publisher) {
|
|
16605
16762
|
await this.publisher.dispose();
|
|
16763
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16764
|
+
return;
|
|
16606
16765
|
}
|
|
16607
16766
|
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
|
|
16608
16767
|
selfSubEnabled: this.allowOwnTracksLoopback,
|
|
@@ -16639,12 +16798,22 @@ class Call {
|
|
|
16639
16798
|
*
|
|
16640
16799
|
* @internal
|
|
16641
16800
|
* @param data the join call data.
|
|
16801
|
+
* @returns The coordinator response, or undefined if leave superseded the request before it was sent.
|
|
16642
16802
|
*/
|
|
16643
16803
|
doJoinRequest = async (data) => {
|
|
16804
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
16644
16805
|
const location = await this.streamClient.getLocationHint();
|
|
16806
|
+
if (this.leaveGeneration !== joinLeaveGeneration) {
|
|
16807
|
+
this.logger.debug('Join superseded by leave; not sending coordinator request');
|
|
16808
|
+
return;
|
|
16809
|
+
}
|
|
16645
16810
|
const e2ee = !!this.e2eeManager;
|
|
16646
16811
|
const request = { ...data, location, e2ee };
|
|
16647
16812
|
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
16813
|
+
if (this.leaveGeneration !== joinLeaveGeneration) {
|
|
16814
|
+
this.logger.debug('Join superseded by leave; not applying coordinator response');
|
|
16815
|
+
return joinResponse;
|
|
16816
|
+
}
|
|
16648
16817
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
16649
16818
|
this.state.setMembers(joinResponse.members);
|
|
16650
16819
|
this.state.setOwnCapabilities(joinResponse.own_capabilities);
|
|
@@ -16655,6 +16824,8 @@ class Call {
|
|
|
16655
16824
|
if (!isReconnecting && this.ringing && !this.isCreatedByMe) {
|
|
16656
16825
|
// signals other users that I have accepted the incoming call.
|
|
16657
16826
|
await this.accept();
|
|
16827
|
+
if (this.leaveGeneration !== joinLeaveGeneration)
|
|
16828
|
+
return joinResponse;
|
|
16658
16829
|
}
|
|
16659
16830
|
if (this.streamClient._hasConnectionID()) {
|
|
16660
16831
|
this.watching = true;
|
|
@@ -16744,6 +16915,7 @@ class Call {
|
|
|
16744
16915
|
if (callingState === CallingState.JOINING ||
|
|
16745
16916
|
callingState === CallingState.RECONNECTING ||
|
|
16746
16917
|
callingState === CallingState.MIGRATING ||
|
|
16918
|
+
callingState === CallingState.LEFT ||
|
|
16747
16919
|
callingState === CallingState.RECONNECTING_FAILED)
|
|
16748
16920
|
return;
|
|
16749
16921
|
// Drop redundant reconnect calls. If a reconnect is already queued or
|
|
@@ -16752,19 +16924,27 @@ class Call {
|
|
|
16752
16924
|
// `POST /join` per entry) once the call is already healthy again.
|
|
16753
16925
|
if (hasPending(this.reconnectConcurrencyTag))
|
|
16754
16926
|
return;
|
|
16927
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16928
|
+
const supersededByLeave = () => this.leaveGeneration !== reconnectLeaveGeneration;
|
|
16755
16929
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
16930
|
+
if (supersededByLeave())
|
|
16931
|
+
return;
|
|
16756
16932
|
const reconnectStartTime = Date.now();
|
|
16757
16933
|
this.reconnectStrategy = strategy;
|
|
16758
16934
|
this.reconnectReason = reason;
|
|
16759
16935
|
const sfuRejoinFailures = new Map();
|
|
16760
16936
|
const markAsReconnectingFailed = async () => {
|
|
16937
|
+
if (supersededByLeave())
|
|
16938
|
+
return;
|
|
16761
16939
|
try {
|
|
16762
16940
|
// attempt to fetch the call data from the server, as the call
|
|
16763
16941
|
// state might have changed while we were reconnecting or were offline
|
|
16764
16942
|
await this.get();
|
|
16765
16943
|
}
|
|
16766
16944
|
finally {
|
|
16767
|
-
|
|
16945
|
+
if (!supersededByLeave()) {
|
|
16946
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
16947
|
+
}
|
|
16768
16948
|
}
|
|
16769
16949
|
};
|
|
16770
16950
|
const giveUpAndLeave = async (message) => {
|
|
@@ -16820,6 +17000,8 @@ class Call {
|
|
|
16820
17000
|
try {
|
|
16821
17001
|
// wait until the network is available
|
|
16822
17002
|
await this.networkAvailableTask?.promise;
|
|
17003
|
+
if (supersededByLeave())
|
|
17004
|
+
return;
|
|
16823
17005
|
this.logger.info(`[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[this.reconnectStrategy]}`);
|
|
16824
17006
|
switch (this.reconnectStrategy) {
|
|
16825
17007
|
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
@@ -16859,6 +17041,8 @@ class Call {
|
|
|
16859
17041
|
break; // do-while loop, reconnection worked, exit the loop
|
|
16860
17042
|
}
|
|
16861
17043
|
catch (error) {
|
|
17044
|
+
if (supersededByLeave())
|
|
17045
|
+
return;
|
|
16862
17046
|
if (attemptedStrategy === WebsocketReconnectStrategy.REJOIN) {
|
|
16863
17047
|
const failedSfu = this.credentials?.server.edge_name;
|
|
16864
17048
|
if (failedSfu) {
|
|
@@ -16889,6 +17073,8 @@ class Call {
|
|
|
16889
17073
|
}
|
|
16890
17074
|
// exponential backoff with jitter, capped at 5 s
|
|
16891
17075
|
await sleep(retryInterval(attempt));
|
|
17076
|
+
if (supersededByLeave())
|
|
17077
|
+
return;
|
|
16892
17078
|
const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
16893
17079
|
const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
|
|
16894
17080
|
this.fastReconnectDeadlineSeconds;
|
|
@@ -16907,7 +17093,8 @@ class Call {
|
|
|
16907
17093
|
this.reconnectStrategy = nextStrategy;
|
|
16908
17094
|
this.logger.info(`[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
|
|
16909
17095
|
}
|
|
16910
|
-
} while (
|
|
17096
|
+
} while (!supersededByLeave() &&
|
|
17097
|
+
this.state.callingState !== CallingState.JOINED &&
|
|
16911
17098
|
this.state.callingState !== CallingState.RECONNECTING_FAILED &&
|
|
16912
17099
|
this.state.callingState !== CallingState.LEFT);
|
|
16913
17100
|
this.logger.info('[Reconnect] Reconnection flow finished');
|
|
@@ -16918,10 +17105,13 @@ class Call {
|
|
|
16918
17105
|
* @internal
|
|
16919
17106
|
*/
|
|
16920
17107
|
reconnectFast = async () => {
|
|
17108
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16921
17109
|
const reconnectStartTime = Date.now();
|
|
16922
17110
|
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
16923
17111
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
16924
17112
|
await this.doJoin(this.joinCallData);
|
|
17113
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
17114
|
+
return;
|
|
16925
17115
|
await this.get(); // fetch the latest call state, as it might have changed
|
|
16926
17116
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.FAST, (Date.now() - reconnectStartTime) / 1000);
|
|
16927
17117
|
};
|
|
@@ -16930,6 +17120,7 @@ class Call {
|
|
|
16930
17120
|
* @internal
|
|
16931
17121
|
*/
|
|
16932
17122
|
reconnectRejoin = async () => {
|
|
17123
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16933
17124
|
const reconnectStartTime = Date.now();
|
|
16934
17125
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
16935
17126
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
@@ -16937,7 +17128,11 @@ class Call {
|
|
|
16937
17128
|
? 'network-available'
|
|
16938
17129
|
: 'full-rejoin';
|
|
16939
17130
|
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
|
|
17131
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
17132
|
+
return;
|
|
16940
17133
|
await this.restorePublishedTracks();
|
|
17134
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
17135
|
+
return;
|
|
16941
17136
|
this.restoreSubscribedTracks();
|
|
16942
17137
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
16943
17138
|
};
|
|
@@ -16946,6 +17141,7 @@ class Call {
|
|
|
16946
17141
|
* @internal
|
|
16947
17142
|
*/
|
|
16948
17143
|
reconnectMigrate = async () => {
|
|
17144
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
16949
17145
|
const reconnectStartTime = Date.now();
|
|
16950
17146
|
const currentSfuClient = this.sfuClient;
|
|
16951
17147
|
if (!currentSfuClient) {
|
|
@@ -16959,26 +17155,32 @@ class Call {
|
|
|
16959
17155
|
currentPublisher?.detachEventHandlers();
|
|
16960
17156
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
16961
17157
|
try {
|
|
16962
|
-
|
|
16963
|
-
|
|
16964
|
-
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
|
|
16973
|
-
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
17158
|
+
try {
|
|
17159
|
+
const currentSfu = currentSfuClient.edgeName;
|
|
17160
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
|
|
17161
|
+
...this.joinCallData,
|
|
17162
|
+
migrating_from: currentSfu,
|
|
17163
|
+
migrating_from_list: [currentSfu],
|
|
17164
|
+
}));
|
|
17165
|
+
}
|
|
17166
|
+
finally {
|
|
17167
|
+
// cleanup the migration_from field after the migration is complete or failed
|
|
17168
|
+
// as we don't want to keep dirty data in the join call data
|
|
17169
|
+
delete this.joinCallData?.migrating_from;
|
|
17170
|
+
delete this.joinCallData?.migrating_from_list;
|
|
17171
|
+
}
|
|
17172
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
17173
|
+
return;
|
|
17174
|
+
await this.restorePublishedTracks();
|
|
17175
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
17176
|
+
return;
|
|
17177
|
+
this.restoreSubscribedTracks();
|
|
16978
17178
|
// Wait for the migration to complete, then close the previous SFU client
|
|
16979
17179
|
// and the peer connection instances. In case of failure, the migration
|
|
16980
17180
|
// task would throw an error and REJOIN would be attempted.
|
|
16981
17181
|
await migrationTask();
|
|
17182
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration)
|
|
17183
|
+
return;
|
|
16982
17184
|
// in MIGRATE, we can consider the call as joined only after
|
|
16983
17185
|
// `participantMigrationComplete` event is received, signaled by
|
|
16984
17186
|
// the `migrationTask`
|
|
@@ -17263,10 +17465,11 @@ class Call {
|
|
|
17263
17465
|
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
17264
17466
|
* configured for E2EE.
|
|
17265
17467
|
*
|
|
17266
|
-
* The
|
|
17267
|
-
*
|
|
17268
|
-
*
|
|
17269
|
-
*
|
|
17468
|
+
* The application owns the manager's lifetime - the SDK never disposes it, and
|
|
17469
|
+
* closing the peer connections does not release it. Discard the manager
|
|
17470
|
+
* together with this call instance once its flow has ended, and create a fresh
|
|
17471
|
+
* pair for a later flow: a disposed manager throws from `encrypt`/`decrypt`
|
|
17472
|
+
* rather than silently publishing nothing.
|
|
17270
17473
|
*
|
|
17271
17474
|
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
17272
17475
|
* built-in AES-GCM scheme, or pass your own implementation.
|
|
@@ -19435,11 +19638,11 @@ class StreamClient {
|
|
|
19435
19638
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19436
19639
|
};
|
|
19437
19640
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19438
|
-
"1.
|
|
19641
|
+
"1.61.1";
|
|
19439
19642
|
getUserAgent = () => {
|
|
19440
19643
|
if (!this.cachedUserAgent) {
|
|
19441
19644
|
const { clientAppIdentifier = {} } = this.options;
|
|
19442
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
19645
|
+
const { sdkName = 'js', sdkVersion = "1.61.1", ...extras } = clientAppIdentifier;
|
|
19443
19646
|
this.cachedUserAgent = [
|
|
19444
19647
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19445
19648
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19974,7 +20177,11 @@ class ClientEventReporter {
|
|
|
19974
20177
|
return;
|
|
19975
20178
|
}
|
|
19976
20179
|
if (event.state === 'failed') {
|
|
19977
|
-
|
|
20180
|
+
const iceConnected = event.iceConnectionState === 'connected' ||
|
|
20181
|
+
event.iceConnectionState === 'completed';
|
|
20182
|
+
this.emitPeerConnectionFailure(cid, role, iceConnected ? 'DTLS_CONNECTIVITY_FAILED' : 'ICE_CONNECTIVITY_FAILED', iceConnected
|
|
20183
|
+
? 'DTLS connectivity checks failed'
|
|
20184
|
+
: 'ICE connectivity checks failed');
|
|
19978
20185
|
return;
|
|
19979
20186
|
}
|
|
19980
20187
|
switch (event.state) {
|
|
@@ -21148,9 +21355,9 @@ class EncryptionManager extends TypedEventEmitter {
|
|
|
21148
21355
|
}
|
|
21149
21356
|
if ((this.piped ??= new WeakSet()).has(target))
|
|
21150
21357
|
return;
|
|
21151
|
-
this.piped.add(target);
|
|
21152
21358
|
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
21153
21359
|
const { readable, writable } = target.createEncodedStreams();
|
|
21360
|
+
this.piped.add(target);
|
|
21154
21361
|
this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
|
|
21155
21362
|
};
|
|
21156
21363
|
/**
|
|
@@ -21206,5 +21413,5 @@ class EncryptionManager extends TypedEventEmitter {
|
|
|
21206
21413
|
};
|
|
21207
21414
|
}
|
|
21208
21415
|
|
|
21209
|
-
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 };
|
|
21416
|
+
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, TypedEventEmitter, 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 };
|
|
21210
21417
|
//# sourceMappingURL=index.browser.es.js.map
|