@stream-io/video-client 1.3.0 → 1.3.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/dist/index.es.js CHANGED
@@ -6731,6 +6731,101 @@ const muteTypeToTrackType = (muteType) => {
6731
6731
  }
6732
6732
  };
6733
6733
 
6734
+ /**
6735
+ * Runs async functions serially. Useful for wrapping async actions that
6736
+ * should never run simultaneously: if marked with the same tag, functions
6737
+ * will run one after another.
6738
+ *
6739
+ * @param tag Async functions with the same tag will run serially. Async functions
6740
+ * with different tags can run in parallel.
6741
+ * @param cb Async function to run.
6742
+ * @returns Promise that resolves when async functions returns.
6743
+ */
6744
+ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
6745
+ /**
6746
+ * Runs async functions serially, and cancels all other actions with the same tag
6747
+ * when a new action is scheduled. Useful for wrapping async actions that override
6748
+ * each other (e.g. enabling and disabling camera).
6749
+ *
6750
+ * If an async function hasn't started yet and was canceled, it will never run.
6751
+ * If an async function is already running and was canceled, it will be notified
6752
+ * via an abort signal passed as an argument.
6753
+ *
6754
+ * @param tag Async functions with the same tag will run serially and are canceled
6755
+ * when a new action with the same tag is scheduled.
6756
+ * @param cb Async function to run. Receives AbortSignal as the only argument.
6757
+ * @returns Promise that resolves when async functions returns. If the function didn't
6758
+ * start and was canceled, will resolve with 'canceled'. If the function started to run,
6759
+ * it's up to the function to decide how to react to cancelation.
6760
+ */
6761
+ const withCancellation = createRunner(wrapWithCancellation);
6762
+ const pendingPromises = new Map();
6763
+ async function settled(tag) {
6764
+ await pendingPromises.get(tag)?.promise;
6765
+ }
6766
+ /**
6767
+ * Implements common functionality of running async functions serially, by chaining
6768
+ * their promises one after another.
6769
+ *
6770
+ * Before running, async function is "wrapped" using the provided wrapper. This wrapper
6771
+ * can add additional steps to run before or after the function.
6772
+ *
6773
+ * When async function is scheduled to run, the previous function is notified
6774
+ * by calling the associated onContinued callback. This behavior of this callback
6775
+ * is defined by the wrapper.
6776
+ */
6777
+ function createRunner(wrapper) {
6778
+ return function run(tag, cb) {
6779
+ const { cb: wrapped, onContinued } = wrapper(tag, cb);
6780
+ const pending = pendingPromises.get(tag);
6781
+ pending?.onContinued();
6782
+ const promise = pending
6783
+ ? pending.promise.then(wrapped, wrapped)
6784
+ : wrapped();
6785
+ pendingPromises.set(tag, { promise, onContinued });
6786
+ return promise;
6787
+ };
6788
+ }
6789
+ /**
6790
+ * Wraps an async function with an additional step run after the function:
6791
+ * if the function is the last in the queue, it cleans up the whole chain
6792
+ * of promises after finishing.
6793
+ */
6794
+ function wrapWithContinuationTracking(tag, cb) {
6795
+ let hasContinuation = false;
6796
+ const wrapped = () => cb().finally(() => {
6797
+ if (!hasContinuation) {
6798
+ pendingPromises.delete(tag);
6799
+ }
6800
+ });
6801
+ const onContinued = () => (hasContinuation = true);
6802
+ return { cb: wrapped, onContinued };
6803
+ }
6804
+ /**
6805
+ * Wraps an async function with additional functionalilty:
6806
+ * 1. Associates an abort signal with every function, that is passed to it
6807
+ * as an argument. When a new function is scheduled to run after the current
6808
+ * one, current signal is aborted.
6809
+ * 2. If current function didn't start and was aborted, in will never start.
6810
+ * 3. If the function is the last in the queue, it cleans up the whole chain
6811
+ * of promises after finishing.
6812
+ */
6813
+ function wrapWithCancellation(tag, cb) {
6814
+ const ac = new AbortController();
6815
+ const wrapped = () => {
6816
+ if (ac.signal.aborted) {
6817
+ return Promise.resolve('canceled');
6818
+ }
6819
+ return cb(ac.signal).finally(() => {
6820
+ if (!ac.signal.aborted) {
6821
+ pendingPromises.delete(tag);
6822
+ }
6823
+ });
6824
+ };
6825
+ const onContinued = () => ac.abort();
6826
+ return { cb: wrapped, onContinued };
6827
+ }
6828
+
6734
6829
  /**
6735
6830
  * Checks if the provided update is a function patch.
6736
6831
  *
@@ -6788,9 +6883,27 @@ const createSubscription = (observable, handler) => {
6788
6883
  subscription.unsubscribe();
6789
6884
  };
6790
6885
  };
6886
+ /**
6887
+ * Creates a subscription and returns a function to unsubscribe. Makes sure that
6888
+ * only one async handler runs at the same time. If updates come in quicker than
6889
+ * it takes for the current handler to finish, other handlers will wait.
6890
+ *
6891
+ * @param observable the observable to subscribe to.
6892
+ * @param handler the async handler to call when the observable emits a value.
6893
+ */
6894
+ const createSafeAsyncSubscription = (observable, handler) => {
6895
+ const tag = Symbol();
6896
+ const subscription = observable.subscribe((value) => {
6897
+ withoutConcurrency(tag, () => handler(value));
6898
+ });
6899
+ return () => {
6900
+ subscription.unsubscribe();
6901
+ };
6902
+ };
6791
6903
 
6792
6904
  var rxUtils = /*#__PURE__*/Object.freeze({
6793
6905
  __proto__: null,
6906
+ createSafeAsyncSubscription: createSafeAsyncSubscription,
6794
6907
  createSubscription: createSubscription,
6795
6908
  getCurrentValue: getCurrentValue,
6796
6909
  setCurrentValue: setCurrentValue
@@ -10920,6 +11033,7 @@ class InputMediaDeviceManager {
10920
11033
  this.subscriptions = [];
10921
11034
  this.isTrackStoppedDueToTrackEnd = false;
10922
11035
  this.filters = [];
11036
+ this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
10923
11037
  /**
10924
11038
  * Disposes the manager.
10925
11039
  *
@@ -10950,26 +11064,20 @@ class InputMediaDeviceManager {
10950
11064
  */
10951
11065
  async enable() {
10952
11066
  if (this.state.optimisticStatus === 'enabled') {
10953
- await this.statusChangePromise;
10954
11067
  return;
10955
11068
  }
10956
- const signal = this.nextAbortableStatusChangeRequest('enabled');
10957
- const doEnable = async () => {
10958
- if (signal.aborted)
10959
- return;
11069
+ this.state.setPendingStatus('enabled');
11070
+ await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
10960
11071
  try {
10961
11072
  await this.unmuteStream();
10962
11073
  this.state.setStatus('enabled');
10963
11074
  }
10964
11075
  finally {
10965
- if (!signal.aborted)
10966
- this.resetStatusChangeRequest();
11076
+ if (!signal.aborted) {
11077
+ this.state.setPendingStatus(this.state.status);
11078
+ }
10967
11079
  }
10968
- };
10969
- this.statusChangePromise = this.statusChangePromise
10970
- ? this.statusChangePromise.then(doEnable)
10971
- : doEnable();
10972
- await this.statusChangePromise;
11080
+ });
10973
11081
  }
10974
11082
  /**
10975
11083
  * Stops or pauses the stream based on state.disableMode
@@ -10978,27 +11086,27 @@ class InputMediaDeviceManager {
10978
11086
  async disable(forceStop = false) {
10979
11087
  this.state.prevStatus = this.state.status;
10980
11088
  if (!forceStop && this.state.optimisticStatus === 'disabled') {
10981
- await this.statusChangePromise;
10982
11089
  return;
10983
11090
  }
10984
- const stopTracks = forceStop || this.state.disableMode === 'stop-tracks';
10985
- const signal = this.nextAbortableStatusChangeRequest('disabled');
10986
- const doDisable = async () => {
10987
- if (signal.aborted)
10988
- return;
11091
+ this.state.setPendingStatus('disabled');
11092
+ await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
10989
11093
  try {
11094
+ const stopTracks = forceStop || this.state.disableMode === 'stop-tracks';
10990
11095
  await this.muteStream(stopTracks);
10991
11096
  this.state.setStatus('disabled');
10992
11097
  }
10993
11098
  finally {
10994
- if (!signal.aborted)
10995
- this.resetStatusChangeRequest();
11099
+ if (!signal.aborted) {
11100
+ this.state.setPendingStatus(this.state.status);
11101
+ }
10996
11102
  }
10997
- };
10998
- this.statusChangePromise = this.statusChangePromise
10999
- ? this.statusChangePromise.then(doDisable)
11000
- : doDisable();
11001
- await this.statusChangePromise;
11103
+ });
11104
+ }
11105
+ /**
11106
+ * Returns a promise that resolves when all pe
11107
+ */
11108
+ async statusChangeSettled() {
11109
+ await settled(this.statusChangeConcurrencyTag);
11002
11110
  }
11003
11111
  /**
11004
11112
  * If status was previously enabled, it will re-enable the device.
@@ -11015,10 +11123,10 @@ class InputMediaDeviceManager {
11015
11123
  */
11016
11124
  async toggle() {
11017
11125
  if (this.state.optimisticStatus === 'enabled') {
11018
- return this.disable();
11126
+ return await this.disable();
11019
11127
  }
11020
11128
  else {
11021
- return this.enable();
11129
+ return await this.enable();
11022
11130
  }
11023
11131
  }
11024
11132
  /**
@@ -11201,9 +11309,7 @@ class InputMediaDeviceManager {
11201
11309
  this.state.setMediaStream(stream, await rootStream);
11202
11310
  this.getTracks().forEach((track) => {
11203
11311
  track.addEventListener('ended', async () => {
11204
- if (this.statusChangePromise) {
11205
- await this.statusChangePromise;
11206
- }
11312
+ await this.statusChangeSettled();
11207
11313
  if (this.state.status === 'enabled') {
11208
11314
  this.isTrackStoppedDueToTrackEnd = true;
11209
11315
  setTimeout(() => {
@@ -11232,7 +11338,7 @@ class InputMediaDeviceManager {
11232
11338
  try {
11233
11339
  if (!deviceId)
11234
11340
  return;
11235
- await this.statusChangePromise;
11341
+ await this.statusChangeSettled();
11236
11342
  let isDeviceDisconnected = false;
11237
11343
  let isDeviceReplaced = false;
11238
11344
  const currentDevice = this.findDeviceInList(currentDevices, deviceId);
@@ -11269,17 +11375,6 @@ class InputMediaDeviceManager {
11269
11375
  findDeviceInList(devices, deviceId) {
11270
11376
  return devices.find((d) => d.deviceId === deviceId && d.kind === this.mediaDeviceKind);
11271
11377
  }
11272
- nextAbortableStatusChangeRequest(status) {
11273
- this.statusChangeAbortController?.abort();
11274
- this.statusChangeAbortController = new AbortController();
11275
- this.state.setPendingStatus(status);
11276
- return this.statusChangeAbortController.signal;
11277
- }
11278
- resetStatusChangeRequest() {
11279
- this.statusChangePromise = undefined;
11280
- this.statusChangeAbortController = undefined;
11281
- this.state.setPendingStatus(this.state.status);
11282
- }
11283
11378
  }
11284
11379
 
11285
11380
  class InputMediaDeviceManagerState {
@@ -11537,9 +11632,9 @@ class CameraManager extends InputMediaDeviceManager {
11537
11632
  async selectTargetResolution(resolution) {
11538
11633
  this.targetResolution.height = resolution.height;
11539
11634
  this.targetResolution.width = resolution.width;
11540
- if (this.statusChangePromise && this.state.optimisticStatus === 'enabled') {
11635
+ if (this.state.optimisticStatus === 'enabled') {
11541
11636
  try {
11542
- await this.statusChangePromise;
11637
+ await this.statusChangeSettled();
11543
11638
  }
11544
11639
  catch (error) {
11545
11640
  // couldn't enable device, target resolution will be applied the next time user attempts to start the device
@@ -11761,6 +11856,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
11761
11856
  : 'stop-tracks') {
11762
11857
  super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
11763
11858
  this.speakingWhileMutedNotificationEnabled = true;
11859
+ this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
11764
11860
  this.subscriptions.push(createSubscription(combineLatest([
11765
11861
  this.call.state.callingState$,
11766
11862
  this.call.state.ownCapabilities$,
@@ -11914,7 +12010,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
11914
12010
  return this.call.stopPublish(TrackType.AUDIO, stopTracks);
11915
12011
  }
11916
12012
  async startSpeakingWhileMutedDetection(deviceId) {
11917
- const startPromise = (async () => {
12013
+ await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
11918
12014
  await this.stopSpeakingWhileMutedDetection();
11919
12015
  if (isReactNative()) {
11920
12016
  this.rnSpeechDetector = new RNSpeechDetector();
@@ -11922,7 +12018,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
11922
12018
  const unsubscribe = this.rnSpeechDetector?.onSpeakingDetectedStateChange((event) => {
11923
12019
  this.state.setSpeakingWhileMuted(event.isSoundDetected);
11924
12020
  });
11925
- return () => {
12021
+ this.soundDetectorCleanup = () => {
11926
12022
  unsubscribe();
11927
12023
  this.rnSpeechDetector?.stop();
11928
12024
  this.rnSpeechDetector = undefined;
@@ -11933,24 +12029,21 @@ class MicrophoneManager extends InputMediaDeviceManager {
11933
12029
  const stream = await this.getStream({
11934
12030
  deviceId,
11935
12031
  });
11936
- return createSoundDetector(stream, (event) => {
12032
+ this.soundDetectorCleanup = createSoundDetector(stream, (event) => {
11937
12033
  this.state.setSpeakingWhileMuted(event.isSoundDetected);
11938
12034
  });
11939
12035
  }
11940
- })();
11941
- this.soundDetectorCleanup = async () => {
11942
- const cleanup = await startPromise;
11943
- await cleanup();
11944
- };
11945
- await startPromise;
12036
+ });
11946
12037
  }
11947
12038
  async stopSpeakingWhileMutedDetection() {
11948
- if (!this.soundDetectorCleanup)
11949
- return;
11950
- const soundDetectorCleanup = this.soundDetectorCleanup;
11951
- this.soundDetectorCleanup = undefined;
11952
- this.state.setSpeakingWhileMuted(false);
11953
- await soundDetectorCleanup();
12039
+ await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
12040
+ if (!this.soundDetectorCleanup)
12041
+ return;
12042
+ const soundDetectorCleanup = this.soundDetectorCleanup;
12043
+ this.soundDetectorCleanup = undefined;
12044
+ this.state.setSpeakingWhileMuted(false);
12045
+ await soundDetectorCleanup();
12046
+ });
11954
12047
  }
11955
12048
  }
11956
12049
 
@@ -13712,7 +13805,7 @@ class Call {
13712
13805
  }
13713
13806
  async initCamera(options) {
13714
13807
  // Wait for any in progress camera operation
13715
- await this.camera.statusChangePromise;
13808
+ await this.camera.statusChangeSettled();
13716
13809
  if (this.state.localParticipant?.videoStream ||
13717
13810
  !this.permissionsContext.hasPermission('send-video')) {
13718
13811
  return;
@@ -13749,7 +13842,7 @@ class Call {
13749
13842
  }
13750
13843
  async initMic(options) {
13751
13844
  // Wait for any in progress mic operation
13752
- await this.microphone.statusChangePromise;
13845
+ await this.microphone.statusChangeSettled();
13753
13846
  if (this.state.localParticipant?.audioStream ||
13754
13847
  !this.permissionsContext.hasPermission('send-audio')) {
13755
13848
  return;
@@ -15290,7 +15383,7 @@ class StreamClient {
15290
15383
  });
15291
15384
  };
15292
15385
  this.getUserAgent = () => {
15293
- const version = "1.3.0" ;
15386
+ const version = "1.3.1" ;
15294
15387
  return (this.userAgent ||
15295
15388
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
15296
15389
  };