@stream-io/video-client 1.42.1 → 1.42.2

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.cjs.js CHANGED
@@ -4291,6 +4291,12 @@ const sfuEventKinds = {
4291
4291
  changePublishOptions: undefined,
4292
4292
  inboundStateNotification: undefined,
4293
4293
  };
4294
+ /**
4295
+ * Determines if a given event name belongs to the category of SFU events.
4296
+ *
4297
+ * @param eventName the name of the event to check.
4298
+ * @returns true if the event name is an SFU event, otherwise false.
4299
+ */
4294
4300
  const isSfuEvent = (eventName) => {
4295
4301
  return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
4296
4302
  };
@@ -4298,33 +4304,70 @@ class Dispatcher {
4298
4304
  constructor() {
4299
4305
  this.logger = videoLoggerSystem.getLogger('Dispatcher');
4300
4306
  this.subscribers = {};
4301
- this.dispatch = (message, tag = '0') => {
4307
+ /**
4308
+ * Dispatch an event to all subscribers.
4309
+ *
4310
+ * @param message the event payload to dispatch.
4311
+ * @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
4312
+ */
4313
+ this.dispatch = (message, tag = '*') => {
4302
4314
  const eventKind = message.eventPayload.oneofKind;
4303
4315
  if (!eventKind)
4304
4316
  return;
4305
4317
  const payload = message.eventPayload[eventKind];
4306
4318
  this.logger.debug(`Dispatching ${eventKind}, tag=${tag}`, payload);
4307
- const listeners = this.subscribers[eventKind];
4308
- if (!listeners)
4319
+ const handlers = this.subscribers[eventKind];
4320
+ if (!handlers)
4309
4321
  return;
4310
- for (const fn of listeners) {
4322
+ this.emit(payload, handlers[tag]);
4323
+ if (tag !== '*')
4324
+ this.emit(payload, handlers['*']);
4325
+ };
4326
+ /**
4327
+ * Emit an event to a list of listeners.
4328
+ *
4329
+ * @param payload the event payload to emit.
4330
+ * @param listeners the list of listeners to emit the event to.
4331
+ */
4332
+ this.emit = (payload, listeners = []) => {
4333
+ for (const listener of listeners) {
4311
4334
  try {
4312
- fn(payload);
4335
+ listener(payload);
4313
4336
  }
4314
4337
  catch (e) {
4315
4338
  this.logger.warn('Listener failed with error', e);
4316
4339
  }
4317
4340
  }
4318
4341
  };
4319
- this.on = (eventName, fn) => {
4342
+ /**
4343
+ * Subscribe to an event.
4344
+ *
4345
+ * @param eventName the name of the event to subscribe to.
4346
+ * @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
4347
+ * @param fn the callback function to invoke when the event is emitted.
4348
+ * @returns a function that can be called to unsubscribe from the event.
4349
+ */
4350
+ this.on = (eventName, tag, fn) => {
4320
4351
  var _a;
4321
- ((_a = this.subscribers)[eventName] ?? (_a[eventName] = [])).push(fn);
4352
+ const bucket = ((_a = this.subscribers)[eventName] ?? (_a[eventName] = {}));
4353
+ (bucket[tag] ?? (bucket[tag] = [])).push(fn);
4322
4354
  return () => {
4323
- this.off(eventName, fn);
4355
+ this.off(eventName, tag, fn);
4324
4356
  };
4325
4357
  };
4326
- this.off = (eventName, fn) => {
4327
- this.subscribers[eventName] = (this.subscribers[eventName] || []).filter((f) => f !== fn);
4358
+ /**
4359
+ * Unsubscribe from an event.
4360
+ *
4361
+ * @param eventName the name of the event to unsubscribe from.
4362
+ * @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
4363
+ * @param fn the callback function to remove from the event listeners.
4364
+ */
4365
+ this.off = (eventName, tag, fn) => {
4366
+ const bucket = this.subscribers[eventName];
4367
+ const listeners = bucket?.[tag];
4368
+ if (!listeners)
4369
+ return;
4370
+ bucket[tag] = listeners.filter((f) => f !== fn);
4328
4371
  };
4329
4372
  }
4330
4373
  }
@@ -6208,7 +6251,7 @@ const getSdkVersion = (sdk) => {
6208
6251
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6209
6252
  };
6210
6253
 
6211
- const version = "1.42.1";
6254
+ const version = "1.42.2";
6212
6255
  const [major, minor, patch] = version.split('.');
6213
6256
  let sdkInfo = {
6214
6257
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -7275,7 +7318,7 @@ class BasePeerConnection {
7275
7318
  * Consecutive events are queued and executed one after the other.
7276
7319
  */
7277
7320
  this.on = (event, fn) => {
7278
- this.subscriptions.push(this.dispatcher.on(event, (e) => {
7321
+ this.subscriptions.push(this.dispatcher.on(event, this.tag, (e) => {
7279
7322
  const lockKey = `pc.${this.lock}.${event}`;
7280
7323
  withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
7281
7324
  if (this.isDisposed)
@@ -7460,6 +7503,7 @@ class BasePeerConnection {
7460
7503
  this.dispatcher = dispatcher;
7461
7504
  this.iceRestartDelay = iceRestartDelay;
7462
7505
  this.clientPublishOptions = clientPublishOptions;
7506
+ this.tag = tag;
7463
7507
  this.onReconnectionNeeded = onReconnectionNeeded;
7464
7508
  this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
7465
7509
  this.pc = this.createPeerConnection(connectionConfig);
@@ -8462,6 +8506,21 @@ const getTimers = lazy(() => {
8462
8506
  return new workerTimer.WorkerTimer({ useWorker: timerWorkerEnabled });
8463
8507
  });
8464
8508
 
8509
+ class SfuJoinError extends Error {
8510
+ constructor(event) {
8511
+ super(event.error?.message || 'Join Error');
8512
+ this.errorEvent = event;
8513
+ this.unrecoverable =
8514
+ event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
8515
+ }
8516
+ static isJoinErrorCode(event) {
8517
+ const code = event.error?.code;
8518
+ return (code === ErrorCode.SFU_FULL ||
8519
+ code === ErrorCode.SFU_SHUTTING_DOWN ||
8520
+ code === ErrorCode.CALL_PARTICIPANT_LIMIT_REACHED);
8521
+ }
8522
+ }
8523
+
8465
8524
  /**
8466
8525
  * The client used for exchanging information with the SFU.
8467
8526
  */
@@ -8638,7 +8697,7 @@ class StreamSfuClient {
8638
8697
  const { timeout = 7 * 1000 } = opts;
8639
8698
  this.migrationTask?.reject(new Error('Cancelled previous migration'));
8640
8699
  const task = (this.migrationTask = promiseWithResolvers());
8641
- const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
8700
+ const unsubscribe = this.dispatcher.on('participantMigrationComplete', this.tag, () => {
8642
8701
  unsubscribe();
8643
8702
  clearTimeout(this.migrateAwayTimeout);
8644
8703
  task.resolve();
@@ -8664,27 +8723,29 @@ class StreamSfuClient {
8664
8723
  // be replaced with a new one in case a second join request is made
8665
8724
  const current = this.joinResponseTask;
8666
8725
  let timeoutId = undefined;
8667
- const unsubscribeJoinErrorEvents = this.dispatcher.on('error', (event) => {
8668
- const { error, reconnectStrategy } = event;
8669
- if (!error)
8670
- return;
8671
- if (reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT) {
8672
- clearTimeout(timeoutId);
8673
- unsubscribe?.();
8674
- unsubscribeJoinErrorEvents();
8726
+ let unsubscribeJoinResponse = undefined;
8727
+ let unsubscribeJoinErrorEvents = undefined;
8728
+ const cleanupJoinSubscriptions = () => {
8729
+ clearTimeout(timeoutId);
8730
+ timeoutId = undefined;
8731
+ unsubscribeJoinErrorEvents?.();
8732
+ unsubscribeJoinErrorEvents = undefined;
8733
+ unsubscribeJoinResponse?.();
8734
+ unsubscribeJoinResponse = undefined;
8735
+ };
8736
+ unsubscribeJoinErrorEvents = this.dispatcher.on('error', this.tag, (event) => {
8737
+ if (SfuJoinError.isJoinErrorCode(event)) {
8738
+ cleanupJoinSubscriptions();
8675
8739
  current.reject(new SfuJoinError(event));
8676
8740
  }
8677
8741
  });
8678
- const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
8679
- clearTimeout(timeoutId);
8680
- unsubscribe();
8681
- unsubscribeJoinErrorEvents();
8742
+ unsubscribeJoinResponse = this.dispatcher.on('joinResponse', this.tag, (joinResponse) => {
8743
+ cleanupJoinSubscriptions();
8682
8744
  this.keepAlive();
8683
8745
  current.resolve(joinResponse);
8684
8746
  });
8685
8747
  timeoutId = setTimeout(() => {
8686
- unsubscribe();
8687
- unsubscribeJoinErrorEvents();
8748
+ cleanupJoinSubscriptions();
8688
8749
  const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
8689
8750
  this.tracer?.trace('joinRequestTimeout', message);
8690
8751
  current.reject(new Error(message));
@@ -8779,8 +8840,8 @@ class StreamSfuClient {
8779
8840
  // In that case, those events (ICE candidates) need to be buffered
8780
8841
  // and later added to the appropriate PeerConnection
8781
8842
  // once the remoteDescription is known and set.
8782
- this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (iceTrickle) => {
8783
- this.iceTrickleBuffer.push(iceTrickle);
8843
+ this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', tag, (t) => {
8844
+ this.iceTrickleBuffer.push(t);
8784
8845
  });
8785
8846
  // listen to network changes to handle offline state
8786
8847
  // we shouldn't attempt to recover websocket connection when offline
@@ -8829,14 +8890,6 @@ StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
8829
8890
  * The close code used when the client fails to join the call (on the SFU).
8830
8891
  */
8831
8892
  StreamSfuClient.JOIN_FAILED = 4101;
8832
- class SfuJoinError extends Error {
8833
- constructor(event) {
8834
- super(event.error?.message || 'Join Error');
8835
- this.errorEvent = event;
8836
- this.unrecoverable =
8837
- event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
8838
- }
8839
- }
8840
8893
 
8841
8894
  /**
8842
8895
  * Event handler that watched the delivery of `call.accepted`.
@@ -8985,7 +9038,7 @@ const removeFromIfPresent = (arr, ...values) => {
8985
9038
  };
8986
9039
 
8987
9040
  const watchConnectionQualityChanged = (dispatcher, state) => {
8988
- return dispatcher.on('connectionQualityChanged', (e) => {
9041
+ return dispatcher.on('connectionQualityChanged', '*', (e) => {
8989
9042
  const { connectionQualityUpdates } = e;
8990
9043
  if (!connectionQualityUpdates)
8991
9044
  return;
@@ -9003,7 +9056,7 @@ const watchConnectionQualityChanged = (dispatcher, state) => {
9003
9056
  * health check events that our SFU sends.
9004
9057
  */
9005
9058
  const watchParticipantCountChanged = (dispatcher, state) => {
9006
- return dispatcher.on('healthCheckResponse', (e) => {
9059
+ return dispatcher.on('healthCheckResponse', '*', (e) => {
9007
9060
  const { participantCount } = e;
9008
9061
  if (participantCount) {
9009
9062
  state.setParticipantCount(participantCount.total);
@@ -9012,7 +9065,7 @@ const watchParticipantCountChanged = (dispatcher, state) => {
9012
9065
  });
9013
9066
  };
9014
9067
  const watchLiveEnded = (dispatcher, call) => {
9015
- return dispatcher.on('error', (e) => {
9068
+ return dispatcher.on('error', '*', (e) => {
9016
9069
  if (e.error && e.error.code !== ErrorCode.LIVE_ENDED)
9017
9070
  return;
9018
9071
  call.state.setBackstage(true);
@@ -9027,7 +9080,7 @@ const watchLiveEnded = (dispatcher, call) => {
9027
9080
  * Watches and logs the errors reported by the currently connected SFU.
9028
9081
  */
9029
9082
  const watchSfuErrorReports = (dispatcher) => {
9030
- return dispatcher.on('error', (e) => {
9083
+ return dispatcher.on('error', '*', (e) => {
9031
9084
  if (!e.error)
9032
9085
  return;
9033
9086
  const logger = videoLoggerSystem.getLogger('SfuClient');
@@ -9226,7 +9279,7 @@ const reconcileOrphanedTracks = (state, participant) => {
9226
9279
  * Watches for `dominantSpeakerChanged` events.
9227
9280
  */
9228
9281
  const watchDominantSpeakerChanged = (dispatcher, state) => {
9229
- return dispatcher.on('dominantSpeakerChanged', (e) => {
9282
+ return dispatcher.on('dominantSpeakerChanged', '*', (e) => {
9230
9283
  const { sessionId } = e;
9231
9284
  if (sessionId === state.dominantSpeaker?.sessionId)
9232
9285
  return;
@@ -9253,7 +9306,7 @@ const watchDominantSpeakerChanged = (dispatcher, state) => {
9253
9306
  * Watches for `audioLevelChanged` events.
9254
9307
  */
9255
9308
  const watchAudioLevelChanged = (dispatcher, state) => {
9256
- return dispatcher.on('audioLevelChanged', (e) => {
9309
+ return dispatcher.on('audioLevelChanged', '*', (e) => {
9257
9310
  const { audioLevels } = e;
9258
9311
  state.updateParticipants(audioLevels.reduce((patches, current) => {
9259
9312
  patches[current.sessionId] = {
@@ -12609,7 +12662,7 @@ class Call {
12609
12662
  */
12610
12663
  this.on = (eventName, fn) => {
12611
12664
  if (isSfuEvent(eventName)) {
12612
- return this.dispatcher.on(eventName, fn);
12665
+ return this.dispatcher.on(eventName, '*', fn);
12613
12666
  }
12614
12667
  const offHandler = this.streamClient.on(eventName, (e) => {
12615
12668
  const event = e;
@@ -12631,7 +12684,7 @@ class Call {
12631
12684
  */
12632
12685
  this.off = (eventName, fn) => {
12633
12686
  if (isSfuEvent(eventName)) {
12634
- return this.dispatcher.off(eventName, fn);
12687
+ return this.dispatcher.off(eventName, '*', fn);
12635
12688
  }
12636
12689
  // unsubscribe from the stream client event by using the 'off' reference
12637
12690
  const registeredOffHandler = this.streamClientEventHandlers.get(fn);
@@ -12867,6 +12920,7 @@ class Call {
12867
12920
  this.logger.trace(`Joining call (${attempt})`, this.cid);
12868
12921
  await this.doJoin(data);
12869
12922
  delete joinData.migrating_from;
12923
+ delete joinData.migrating_from_list;
12870
12924
  break;
12871
12925
  }
12872
12926
  catch (err) {
@@ -12878,11 +12932,15 @@ class Call {
12878
12932
  // to join the call due to some reason (e.g., ended call, expired token...)
12879
12933
  throw err;
12880
12934
  }
12935
+ // immediately switch to a different SFU in case of recoverable join error
12936
+ const switchSfu = err instanceof SfuJoinError &&
12937
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
12881
12938
  const sfuId = this.credentials?.server.edge_name || '';
12882
12939
  const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
12883
12940
  sfuJoinFailures.set(sfuId, failures);
12884
- if (failures >= 2) {
12941
+ if (switchSfu || failures >= 2) {
12885
12942
  joinData.migrating_from = sfuId;
12943
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
12886
12944
  }
12887
12945
  if (attempt === maxJoinRetries - 1) {
12888
12946
  throw err;
@@ -13410,12 +13468,17 @@ class Call {
13410
13468
  const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
13411
13469
  try {
13412
13470
  const currentSfu = currentSfuClient.edgeName;
13413
- await this.doJoin({ ...this.joinCallData, migrating_from: currentSfu });
13471
+ await this.doJoin({
13472
+ ...this.joinCallData,
13473
+ migrating_from: currentSfu,
13474
+ migrating_from_list: [currentSfu],
13475
+ });
13414
13476
  }
13415
13477
  finally {
13416
13478
  // cleanup the migration_from field after the migration is complete or failed
13417
13479
  // as we don't want to keep dirty data in the join call data
13418
13480
  delete this.joinCallData?.migrating_from;
13481
+ delete this.joinCallData?.migrating_from_list;
13419
13482
  }
13420
13483
  await this.restorePublishedTracks();
13421
13484
  this.restoreSubscribedTracks();
@@ -13450,6 +13513,11 @@ class Call {
13450
13513
  // handles the "error" event, through which the SFU can request a reconnect
13451
13514
  const unregisterOnError = this.on('error', (e) => {
13452
13515
  const { reconnectStrategy: strategy, error } = e;
13516
+ // SFU_FULL is a join error, and when emitted, although it specifies a
13517
+ // `migrate` strategy, we should actually perform a REJOIN to a new SFU.
13518
+ // This is now handled separately in the `call.join()` method.
13519
+ if (SfuJoinError.isJoinErrorCode(e))
13520
+ return;
13453
13521
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
13454
13522
  return;
13455
13523
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
@@ -15546,7 +15614,7 @@ class StreamClient {
15546
15614
  this.getUserAgent = () => {
15547
15615
  if (!this.cachedUserAgent) {
15548
15616
  const { clientAppIdentifier = {} } = this.options;
15549
- const { sdkName = 'js', sdkVersion = "1.42.1", ...extras } = clientAppIdentifier;
15617
+ const { sdkName = 'js', sdkVersion = "1.42.2", ...extras } = clientAppIdentifier;
15550
15618
  this.cachedUserAgent = [
15551
15619
  `stream-video-${sdkName}-v${sdkVersion}`,
15552
15620
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),