@stream-io/video-client 1.42.1 → 1.42.3

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/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './src/CallType';
10
10
  export * from './src/StreamVideoClient';
11
11
  export * from './src/StreamSfuClient';
12
12
  export * from './src/devices';
13
+ export * from './src/errors';
13
14
  export * from './src/store';
14
15
  export * from './src/sorting';
15
16
  export * from './src/helpers/client-details';
package/dist/index.es.js CHANGED
@@ -4272,6 +4272,12 @@ const sfuEventKinds = {
4272
4272
  changePublishOptions: undefined,
4273
4273
  inboundStateNotification: undefined,
4274
4274
  };
4275
+ /**
4276
+ * Determines if a given event name belongs to the category of SFU events.
4277
+ *
4278
+ * @param eventName the name of the event to check.
4279
+ * @returns true if the event name is an SFU event, otherwise false.
4280
+ */
4275
4281
  const isSfuEvent = (eventName) => {
4276
4282
  return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
4277
4283
  };
@@ -4279,33 +4285,70 @@ class Dispatcher {
4279
4285
  constructor() {
4280
4286
  this.logger = videoLoggerSystem.getLogger('Dispatcher');
4281
4287
  this.subscribers = {};
4282
- this.dispatch = (message, tag = '0') => {
4288
+ /**
4289
+ * Dispatch an event to all subscribers.
4290
+ *
4291
+ * @param message the event payload to dispatch.
4292
+ * @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
4293
+ */
4294
+ this.dispatch = (message, tag = '*') => {
4283
4295
  const eventKind = message.eventPayload.oneofKind;
4284
4296
  if (!eventKind)
4285
4297
  return;
4286
4298
  const payload = message.eventPayload[eventKind];
4287
4299
  this.logger.debug(`Dispatching ${eventKind}, tag=${tag}`, payload);
4288
- const listeners = this.subscribers[eventKind];
4289
- if (!listeners)
4300
+ const handlers = this.subscribers[eventKind];
4301
+ if (!handlers)
4290
4302
  return;
4291
- for (const fn of listeners) {
4303
+ this.emit(payload, handlers[tag]);
4304
+ if (tag !== '*')
4305
+ this.emit(payload, handlers['*']);
4306
+ };
4307
+ /**
4308
+ * Emit an event to a list of listeners.
4309
+ *
4310
+ * @param payload the event payload to emit.
4311
+ * @param listeners the list of listeners to emit the event to.
4312
+ */
4313
+ this.emit = (payload, listeners = []) => {
4314
+ for (const listener of listeners) {
4292
4315
  try {
4293
- fn(payload);
4316
+ listener(payload);
4294
4317
  }
4295
4318
  catch (e) {
4296
4319
  this.logger.warn('Listener failed with error', e);
4297
4320
  }
4298
4321
  }
4299
4322
  };
4300
- this.on = (eventName, fn) => {
4323
+ /**
4324
+ * Subscribe to an event.
4325
+ *
4326
+ * @param eventName the name of the event to subscribe to.
4327
+ * @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
4328
+ * @param fn the callback function to invoke when the event is emitted.
4329
+ * @returns a function that can be called to unsubscribe from the event.
4330
+ */
4331
+ this.on = (eventName, tag, fn) => {
4301
4332
  var _a;
4302
- ((_a = this.subscribers)[eventName] ?? (_a[eventName] = [])).push(fn);
4333
+ const bucket = ((_a = this.subscribers)[eventName] ?? (_a[eventName] = {}));
4334
+ (bucket[tag] ?? (bucket[tag] = [])).push(fn);
4303
4335
  return () => {
4304
- this.off(eventName, fn);
4336
+ this.off(eventName, tag, fn);
4305
4337
  };
4306
4338
  };
4307
- this.off = (eventName, fn) => {
4308
- this.subscribers[eventName] = (this.subscribers[eventName] || []).filter((f) => f !== fn);
4339
+ /**
4340
+ * Unsubscribe from an event.
4341
+ *
4342
+ * @param eventName the name of the event to unsubscribe from.
4343
+ * @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
4344
+ * @param fn the callback function to remove from the event listeners.
4345
+ */
4346
+ this.off = (eventName, tag, fn) => {
4347
+ const bucket = this.subscribers[eventName];
4348
+ const listeners = bucket?.[tag];
4349
+ if (!listeners)
4350
+ return;
4351
+ bucket[tag] = listeners.filter((f) => f !== fn);
4309
4352
  };
4310
4353
  }
4311
4354
  }
@@ -6189,7 +6232,7 @@ const getSdkVersion = (sdk) => {
6189
6232
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6190
6233
  };
6191
6234
 
6192
- const version = "1.42.1";
6235
+ const version = "1.42.3";
6193
6236
  const [major, minor, patch] = version.split('.');
6194
6237
  let sdkInfo = {
6195
6238
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -7256,7 +7299,7 @@ class BasePeerConnection {
7256
7299
  * Consecutive events are queued and executed one after the other.
7257
7300
  */
7258
7301
  this.on = (event, fn) => {
7259
- this.subscriptions.push(this.dispatcher.on(event, (e) => {
7302
+ this.subscriptions.push(this.dispatcher.on(event, this.tag, (e) => {
7260
7303
  const lockKey = `pc.${this.lock}.${event}`;
7261
7304
  withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
7262
7305
  if (this.isDisposed)
@@ -7441,6 +7484,7 @@ class BasePeerConnection {
7441
7484
  this.dispatcher = dispatcher;
7442
7485
  this.iceRestartDelay = iceRestartDelay;
7443
7486
  this.clientPublishOptions = clientPublishOptions;
7487
+ this.tag = tag;
7444
7488
  this.onReconnectionNeeded = onReconnectionNeeded;
7445
7489
  this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
7446
7490
  this.pc = this.createPeerConnection(connectionConfig);
@@ -8443,6 +8487,21 @@ const getTimers = lazy(() => {
8443
8487
  return new WorkerTimer({ useWorker: timerWorkerEnabled });
8444
8488
  });
8445
8489
 
8490
+ class SfuJoinError extends Error {
8491
+ constructor(event) {
8492
+ super(event.error?.message || 'Join Error');
8493
+ this.errorEvent = event;
8494
+ this.unrecoverable =
8495
+ event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
8496
+ }
8497
+ static isJoinErrorCode(event) {
8498
+ const code = event.error?.code;
8499
+ return (code === ErrorCode.SFU_FULL ||
8500
+ code === ErrorCode.SFU_SHUTTING_DOWN ||
8501
+ code === ErrorCode.CALL_PARTICIPANT_LIMIT_REACHED);
8502
+ }
8503
+ }
8504
+
8446
8505
  /**
8447
8506
  * The client used for exchanging information with the SFU.
8448
8507
  */
@@ -8619,7 +8678,7 @@ class StreamSfuClient {
8619
8678
  const { timeout = 7 * 1000 } = opts;
8620
8679
  this.migrationTask?.reject(new Error('Cancelled previous migration'));
8621
8680
  const task = (this.migrationTask = promiseWithResolvers());
8622
- const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
8681
+ const unsubscribe = this.dispatcher.on('participantMigrationComplete', this.tag, () => {
8623
8682
  unsubscribe();
8624
8683
  clearTimeout(this.migrateAwayTimeout);
8625
8684
  task.resolve();
@@ -8645,27 +8704,29 @@ class StreamSfuClient {
8645
8704
  // be replaced with a new one in case a second join request is made
8646
8705
  const current = this.joinResponseTask;
8647
8706
  let timeoutId = undefined;
8648
- const unsubscribeJoinErrorEvents = this.dispatcher.on('error', (event) => {
8649
- const { error, reconnectStrategy } = event;
8650
- if (!error)
8651
- return;
8652
- if (reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT) {
8653
- clearTimeout(timeoutId);
8654
- unsubscribe?.();
8655
- unsubscribeJoinErrorEvents();
8707
+ let unsubscribeJoinResponse = undefined;
8708
+ let unsubscribeJoinErrorEvents = undefined;
8709
+ const cleanupJoinSubscriptions = () => {
8710
+ clearTimeout(timeoutId);
8711
+ timeoutId = undefined;
8712
+ unsubscribeJoinErrorEvents?.();
8713
+ unsubscribeJoinErrorEvents = undefined;
8714
+ unsubscribeJoinResponse?.();
8715
+ unsubscribeJoinResponse = undefined;
8716
+ };
8717
+ unsubscribeJoinErrorEvents = this.dispatcher.on('error', this.tag, (event) => {
8718
+ if (SfuJoinError.isJoinErrorCode(event)) {
8719
+ cleanupJoinSubscriptions();
8656
8720
  current.reject(new SfuJoinError(event));
8657
8721
  }
8658
8722
  });
8659
- const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
8660
- clearTimeout(timeoutId);
8661
- unsubscribe();
8662
- unsubscribeJoinErrorEvents();
8723
+ unsubscribeJoinResponse = this.dispatcher.on('joinResponse', this.tag, (joinResponse) => {
8724
+ cleanupJoinSubscriptions();
8663
8725
  this.keepAlive();
8664
8726
  current.resolve(joinResponse);
8665
8727
  });
8666
8728
  timeoutId = setTimeout(() => {
8667
- unsubscribe();
8668
- unsubscribeJoinErrorEvents();
8729
+ cleanupJoinSubscriptions();
8669
8730
  const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
8670
8731
  this.tracer?.trace('joinRequestTimeout', message);
8671
8732
  current.reject(new Error(message));
@@ -8760,8 +8821,8 @@ class StreamSfuClient {
8760
8821
  // In that case, those events (ICE candidates) need to be buffered
8761
8822
  // and later added to the appropriate PeerConnection
8762
8823
  // once the remoteDescription is known and set.
8763
- this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (iceTrickle) => {
8764
- this.iceTrickleBuffer.push(iceTrickle);
8824
+ this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', tag, (t) => {
8825
+ this.iceTrickleBuffer.push(t);
8765
8826
  });
8766
8827
  // listen to network changes to handle offline state
8767
8828
  // we shouldn't attempt to recover websocket connection when offline
@@ -8810,14 +8871,6 @@ StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
8810
8871
  * The close code used when the client fails to join the call (on the SFU).
8811
8872
  */
8812
8873
  StreamSfuClient.JOIN_FAILED = 4101;
8813
- class SfuJoinError extends Error {
8814
- constructor(event) {
8815
- super(event.error?.message || 'Join Error');
8816
- this.errorEvent = event;
8817
- this.unrecoverable =
8818
- event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
8819
- }
8820
- }
8821
8874
 
8822
8875
  /**
8823
8876
  * Event handler that watched the delivery of `call.accepted`.
@@ -8966,7 +9019,7 @@ const removeFromIfPresent = (arr, ...values) => {
8966
9019
  };
8967
9020
 
8968
9021
  const watchConnectionQualityChanged = (dispatcher, state) => {
8969
- return dispatcher.on('connectionQualityChanged', (e) => {
9022
+ return dispatcher.on('connectionQualityChanged', '*', (e) => {
8970
9023
  const { connectionQualityUpdates } = e;
8971
9024
  if (!connectionQualityUpdates)
8972
9025
  return;
@@ -8984,7 +9037,7 @@ const watchConnectionQualityChanged = (dispatcher, state) => {
8984
9037
  * health check events that our SFU sends.
8985
9038
  */
8986
9039
  const watchParticipantCountChanged = (dispatcher, state) => {
8987
- return dispatcher.on('healthCheckResponse', (e) => {
9040
+ return dispatcher.on('healthCheckResponse', '*', (e) => {
8988
9041
  const { participantCount } = e;
8989
9042
  if (participantCount) {
8990
9043
  state.setParticipantCount(participantCount.total);
@@ -8993,7 +9046,7 @@ const watchParticipantCountChanged = (dispatcher, state) => {
8993
9046
  });
8994
9047
  };
8995
9048
  const watchLiveEnded = (dispatcher, call) => {
8996
- return dispatcher.on('error', (e) => {
9049
+ return dispatcher.on('error', '*', (e) => {
8997
9050
  if (e.error && e.error.code !== ErrorCode.LIVE_ENDED)
8998
9051
  return;
8999
9052
  call.state.setBackstage(true);
@@ -9008,7 +9061,7 @@ const watchLiveEnded = (dispatcher, call) => {
9008
9061
  * Watches and logs the errors reported by the currently connected SFU.
9009
9062
  */
9010
9063
  const watchSfuErrorReports = (dispatcher) => {
9011
- return dispatcher.on('error', (e) => {
9064
+ return dispatcher.on('error', '*', (e) => {
9012
9065
  if (!e.error)
9013
9066
  return;
9014
9067
  const logger = videoLoggerSystem.getLogger('SfuClient');
@@ -9207,7 +9260,7 @@ const reconcileOrphanedTracks = (state, participant) => {
9207
9260
  * Watches for `dominantSpeakerChanged` events.
9208
9261
  */
9209
9262
  const watchDominantSpeakerChanged = (dispatcher, state) => {
9210
- return dispatcher.on('dominantSpeakerChanged', (e) => {
9263
+ return dispatcher.on('dominantSpeakerChanged', '*', (e) => {
9211
9264
  const { sessionId } = e;
9212
9265
  if (sessionId === state.dominantSpeaker?.sessionId)
9213
9266
  return;
@@ -9234,7 +9287,7 @@ const watchDominantSpeakerChanged = (dispatcher, state) => {
9234
9287
  * Watches for `audioLevelChanged` events.
9235
9288
  */
9236
9289
  const watchAudioLevelChanged = (dispatcher, state) => {
9237
- return dispatcher.on('audioLevelChanged', (e) => {
9290
+ return dispatcher.on('audioLevelChanged', '*', (e) => {
9238
9291
  const { audioLevels } = e;
9239
9292
  state.updateParticipants(audioLevels.reduce((patches, current) => {
9240
9293
  patches[current.sessionId] = {
@@ -12394,6 +12447,7 @@ class Call {
12394
12447
  this.hasJoinedOnce = false;
12395
12448
  this.deviceSettingsAppliedOnce = false;
12396
12449
  this.initialized = false;
12450
+ this.acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
12397
12451
  this.joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
12398
12452
  /**
12399
12453
  * A list hooks/functions to invoke when the call is left.
@@ -12590,7 +12644,7 @@ class Call {
12590
12644
  */
12591
12645
  this.on = (eventName, fn) => {
12592
12646
  if (isSfuEvent(eventName)) {
12593
- return this.dispatcher.on(eventName, fn);
12647
+ return this.dispatcher.on(eventName, '*', fn);
12594
12648
  }
12595
12649
  const offHandler = this.streamClient.on(eventName, (e) => {
12596
12650
  const event = e;
@@ -12612,7 +12666,7 @@ class Call {
12612
12666
  */
12613
12667
  this.off = (eventName, fn) => {
12614
12668
  if (isSfuEvent(eventName)) {
12615
- return this.dispatcher.off(eventName, fn);
12669
+ return this.dispatcher.off(eventName, '*', fn);
12616
12670
  }
12617
12671
  // unsubscribe from the stream client event by using the 'off' reference
12618
12672
  const registeredOffHandler = this.streamClientEventHandlers.get(fn);
@@ -12808,8 +12862,10 @@ class Call {
12808
12862
  * Unless you are implementing a custom "ringing" flow, you should not use this method.
12809
12863
  */
12810
12864
  this.accept = async () => {
12811
- this.tracer.trace('call.accept', '');
12812
- return this.streamClient.post(`${this.streamClientBasePath}/accept`);
12865
+ return withoutConcurrency(this.acceptRejectConcurrencyTag, () => {
12866
+ this.tracer.trace('call.accept', '');
12867
+ return this.streamClient.post(`${this.streamClientBasePath}/accept`);
12868
+ });
12813
12869
  };
12814
12870
  /**
12815
12871
  * Marks the incoming call as rejected.
@@ -12821,8 +12877,10 @@ class Call {
12821
12877
  * @param reason the reason for rejecting the call.
12822
12878
  */
12823
12879
  this.reject = async (reason = 'decline') => {
12824
- this.tracer.trace('call.reject', reason);
12825
- return this.streamClient.post(`${this.streamClientBasePath}/reject`, { reason: reason });
12880
+ return withoutConcurrency(this.acceptRejectConcurrencyTag, () => {
12881
+ this.tracer.trace('call.reject', reason);
12882
+ return this.streamClient.post(`${this.streamClientBasePath}/reject`, { reason });
12883
+ });
12826
12884
  };
12827
12885
  /**
12828
12886
  * Will start to watch for call related WebSocket events and initiate a call session with the server.
@@ -12848,6 +12906,7 @@ class Call {
12848
12906
  this.logger.trace(`Joining call (${attempt})`, this.cid);
12849
12907
  await this.doJoin(data);
12850
12908
  delete joinData.migrating_from;
12909
+ delete joinData.migrating_from_list;
12851
12910
  break;
12852
12911
  }
12853
12912
  catch (err) {
@@ -12859,11 +12918,15 @@ class Call {
12859
12918
  // to join the call due to some reason (e.g., ended call, expired token...)
12860
12919
  throw err;
12861
12920
  }
12921
+ // immediately switch to a different SFU in case of recoverable join error
12922
+ const switchSfu = err instanceof SfuJoinError &&
12923
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
12862
12924
  const sfuId = this.credentials?.server.edge_name || '';
12863
12925
  const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
12864
12926
  sfuJoinFailures.set(sfuId, failures);
12865
- if (failures >= 2) {
12927
+ if (switchSfu || failures >= 2) {
12866
12928
  joinData.migrating_from = sfuId;
12929
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
12867
12930
  }
12868
12931
  if (attempt === maxJoinRetries - 1) {
12869
12932
  throw err;
@@ -13391,12 +13454,17 @@ class Call {
13391
13454
  const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
13392
13455
  try {
13393
13456
  const currentSfu = currentSfuClient.edgeName;
13394
- await this.doJoin({ ...this.joinCallData, migrating_from: currentSfu });
13457
+ await this.doJoin({
13458
+ ...this.joinCallData,
13459
+ migrating_from: currentSfu,
13460
+ migrating_from_list: [currentSfu],
13461
+ });
13395
13462
  }
13396
13463
  finally {
13397
13464
  // cleanup the migration_from field after the migration is complete or failed
13398
13465
  // as we don't want to keep dirty data in the join call data
13399
13466
  delete this.joinCallData?.migrating_from;
13467
+ delete this.joinCallData?.migrating_from_list;
13400
13468
  }
13401
13469
  await this.restorePublishedTracks();
13402
13470
  this.restoreSubscribedTracks();
@@ -13431,6 +13499,11 @@ class Call {
13431
13499
  // handles the "error" event, through which the SFU can request a reconnect
13432
13500
  const unregisterOnError = this.on('error', (e) => {
13433
13501
  const { reconnectStrategy: strategy, error } = e;
13502
+ // SFU_FULL is a join error, and when emitted, although it specifies a
13503
+ // `migrate` strategy, we should actually perform a REJOIN to a new SFU.
13504
+ // This is now handled separately in the `call.join()` method.
13505
+ if (SfuJoinError.isJoinErrorCode(e))
13506
+ return;
13434
13507
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
13435
13508
  return;
13436
13509
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
@@ -15527,7 +15600,7 @@ class StreamClient {
15527
15600
  this.getUserAgent = () => {
15528
15601
  if (!this.cachedUserAgent) {
15529
15602
  const { clientAppIdentifier = {} } = this.options;
15530
- const { sdkName = 'js', sdkVersion = "1.42.1", ...extras } = clientAppIdentifier;
15603
+ const { sdkName = 'js', sdkVersion = "1.42.3", ...extras } = clientAppIdentifier;
15531
15604
  this.cachedUserAgent = [
15532
15605
  `stream-video-${sdkName}-v${sdkVersion}`,
15533
15606
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),