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