@stream-io/video-client 1.3.0 → 1.4.0

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
@@ -6751,6 +6751,101 @@ const muteTypeToTrackType = (muteType) => {
6751
6751
  }
6752
6752
  };
6753
6753
 
6754
+ /**
6755
+ * Runs async functions serially. Useful for wrapping async actions that
6756
+ * should never run simultaneously: if marked with the same tag, functions
6757
+ * will run one after another.
6758
+ *
6759
+ * @param tag Async functions with the same tag will run serially. Async functions
6760
+ * with different tags can run in parallel.
6761
+ * @param cb Async function to run.
6762
+ * @returns Promise that resolves when async functions returns.
6763
+ */
6764
+ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
6765
+ /**
6766
+ * Runs async functions serially, and cancels all other actions with the same tag
6767
+ * when a new action is scheduled. Useful for wrapping async actions that override
6768
+ * each other (e.g. enabling and disabling camera).
6769
+ *
6770
+ * If an async function hasn't started yet and was canceled, it will never run.
6771
+ * If an async function is already running and was canceled, it will be notified
6772
+ * via an abort signal passed as an argument.
6773
+ *
6774
+ * @param tag Async functions with the same tag will run serially and are canceled
6775
+ * when a new action with the same tag is scheduled.
6776
+ * @param cb Async function to run. Receives AbortSignal as the only argument.
6777
+ * @returns Promise that resolves when async functions returns. If the function didn't
6778
+ * start and was canceled, will resolve with 'canceled'. If the function started to run,
6779
+ * it's up to the function to decide how to react to cancelation.
6780
+ */
6781
+ const withCancellation = createRunner(wrapWithCancellation);
6782
+ const pendingPromises = new Map();
6783
+ async function settled(tag) {
6784
+ await pendingPromises.get(tag)?.promise;
6785
+ }
6786
+ /**
6787
+ * Implements common functionality of running async functions serially, by chaining
6788
+ * their promises one after another.
6789
+ *
6790
+ * Before running, async function is "wrapped" using the provided wrapper. This wrapper
6791
+ * can add additional steps to run before or after the function.
6792
+ *
6793
+ * When async function is scheduled to run, the previous function is notified
6794
+ * by calling the associated onContinued callback. This behavior of this callback
6795
+ * is defined by the wrapper.
6796
+ */
6797
+ function createRunner(wrapper) {
6798
+ return function run(tag, cb) {
6799
+ const { cb: wrapped, onContinued } = wrapper(tag, cb);
6800
+ const pending = pendingPromises.get(tag);
6801
+ pending?.onContinued();
6802
+ const promise = pending
6803
+ ? pending.promise.then(wrapped, wrapped)
6804
+ : wrapped();
6805
+ pendingPromises.set(tag, { promise, onContinued });
6806
+ return promise;
6807
+ };
6808
+ }
6809
+ /**
6810
+ * Wraps an async function with an additional step run after the function:
6811
+ * if the function is the last in the queue, it cleans up the whole chain
6812
+ * of promises after finishing.
6813
+ */
6814
+ function wrapWithContinuationTracking(tag, cb) {
6815
+ let hasContinuation = false;
6816
+ const wrapped = () => cb().finally(() => {
6817
+ if (!hasContinuation) {
6818
+ pendingPromises.delete(tag);
6819
+ }
6820
+ });
6821
+ const onContinued = () => (hasContinuation = true);
6822
+ return { cb: wrapped, onContinued };
6823
+ }
6824
+ /**
6825
+ * Wraps an async function with additional functionalilty:
6826
+ * 1. Associates an abort signal with every function, that is passed to it
6827
+ * as an argument. When a new function is scheduled to run after the current
6828
+ * one, current signal is aborted.
6829
+ * 2. If current function didn't start and was aborted, in will never start.
6830
+ * 3. If the function is the last in the queue, it cleans up the whole chain
6831
+ * of promises after finishing.
6832
+ */
6833
+ function wrapWithCancellation(tag, cb) {
6834
+ const ac = new AbortController();
6835
+ const wrapped = () => {
6836
+ if (ac.signal.aborted) {
6837
+ return Promise.resolve('canceled');
6838
+ }
6839
+ return cb(ac.signal).finally(() => {
6840
+ if (!ac.signal.aborted) {
6841
+ pendingPromises.delete(tag);
6842
+ }
6843
+ });
6844
+ };
6845
+ const onContinued = () => ac.abort();
6846
+ return { cb: wrapped, onContinued };
6847
+ }
6848
+
6754
6849
  /**
6755
6850
  * Checks if the provided update is a function patch.
6756
6851
  *
@@ -6808,9 +6903,27 @@ const createSubscription = (observable, handler) => {
6808
6903
  subscription.unsubscribe();
6809
6904
  };
6810
6905
  };
6906
+ /**
6907
+ * Creates a subscription and returns a function to unsubscribe. Makes sure that
6908
+ * only one async handler runs at the same time. If updates come in quicker than
6909
+ * it takes for the current handler to finish, other handlers will wait.
6910
+ *
6911
+ * @param observable the observable to subscribe to.
6912
+ * @param handler the async handler to call when the observable emits a value.
6913
+ */
6914
+ const createSafeAsyncSubscription = (observable, handler) => {
6915
+ const tag = Symbol();
6916
+ const subscription = observable.subscribe((value) => {
6917
+ withoutConcurrency(tag, () => handler(value));
6918
+ });
6919
+ return () => {
6920
+ subscription.unsubscribe();
6921
+ };
6922
+ };
6811
6923
 
6812
6924
  var rxUtils = /*#__PURE__*/Object.freeze({
6813
6925
  __proto__: null,
6926
+ createSafeAsyncSubscription: createSafeAsyncSubscription,
6814
6927
  createSubscription: createSubscription,
6815
6928
  getCurrentValue: getCurrentValue,
6816
6929
  setCurrentValue: setCurrentValue
@@ -10940,6 +11053,7 @@ class InputMediaDeviceManager {
10940
11053
  this.subscriptions = [];
10941
11054
  this.isTrackStoppedDueToTrackEnd = false;
10942
11055
  this.filters = [];
11056
+ this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
10943
11057
  /**
10944
11058
  * Disposes the manager.
10945
11059
  *
@@ -10970,26 +11084,20 @@ class InputMediaDeviceManager {
10970
11084
  */
10971
11085
  async enable() {
10972
11086
  if (this.state.optimisticStatus === 'enabled') {
10973
- await this.statusChangePromise;
10974
11087
  return;
10975
11088
  }
10976
- const signal = this.nextAbortableStatusChangeRequest('enabled');
10977
- const doEnable = async () => {
10978
- if (signal.aborted)
10979
- return;
11089
+ this.state.setPendingStatus('enabled');
11090
+ await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
10980
11091
  try {
10981
11092
  await this.unmuteStream();
10982
11093
  this.state.setStatus('enabled');
10983
11094
  }
10984
11095
  finally {
10985
- if (!signal.aborted)
10986
- this.resetStatusChangeRequest();
11096
+ if (!signal.aborted) {
11097
+ this.state.setPendingStatus(this.state.status);
11098
+ }
10987
11099
  }
10988
- };
10989
- this.statusChangePromise = this.statusChangePromise
10990
- ? this.statusChangePromise.then(doEnable)
10991
- : doEnable();
10992
- await this.statusChangePromise;
11100
+ });
10993
11101
  }
10994
11102
  /**
10995
11103
  * Stops or pauses the stream based on state.disableMode
@@ -10998,27 +11106,27 @@ class InputMediaDeviceManager {
10998
11106
  async disable(forceStop = false) {
10999
11107
  this.state.prevStatus = this.state.status;
11000
11108
  if (!forceStop && this.state.optimisticStatus === 'disabled') {
11001
- await this.statusChangePromise;
11002
11109
  return;
11003
11110
  }
11004
- const stopTracks = forceStop || this.state.disableMode === 'stop-tracks';
11005
- const signal = this.nextAbortableStatusChangeRequest('disabled');
11006
- const doDisable = async () => {
11007
- if (signal.aborted)
11008
- return;
11111
+ this.state.setPendingStatus('disabled');
11112
+ await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
11009
11113
  try {
11114
+ const stopTracks = forceStop || this.state.disableMode === 'stop-tracks';
11010
11115
  await this.muteStream(stopTracks);
11011
11116
  this.state.setStatus('disabled');
11012
11117
  }
11013
11118
  finally {
11014
- if (!signal.aborted)
11015
- this.resetStatusChangeRequest();
11119
+ if (!signal.aborted) {
11120
+ this.state.setPendingStatus(this.state.status);
11121
+ }
11016
11122
  }
11017
- };
11018
- this.statusChangePromise = this.statusChangePromise
11019
- ? this.statusChangePromise.then(doDisable)
11020
- : doDisable();
11021
- await this.statusChangePromise;
11123
+ });
11124
+ }
11125
+ /**
11126
+ * Returns a promise that resolves when all pe
11127
+ */
11128
+ async statusChangeSettled() {
11129
+ await settled(this.statusChangeConcurrencyTag);
11022
11130
  }
11023
11131
  /**
11024
11132
  * If status was previously enabled, it will re-enable the device.
@@ -11035,10 +11143,10 @@ class InputMediaDeviceManager {
11035
11143
  */
11036
11144
  async toggle() {
11037
11145
  if (this.state.optimisticStatus === 'enabled') {
11038
- return this.disable();
11146
+ return await this.disable();
11039
11147
  }
11040
11148
  else {
11041
- return this.enable();
11149
+ return await this.enable();
11042
11150
  }
11043
11151
  }
11044
11152
  /**
@@ -11221,9 +11329,7 @@ class InputMediaDeviceManager {
11221
11329
  this.state.setMediaStream(stream, await rootStream);
11222
11330
  this.getTracks().forEach((track) => {
11223
11331
  track.addEventListener('ended', async () => {
11224
- if (this.statusChangePromise) {
11225
- await this.statusChangePromise;
11226
- }
11332
+ await this.statusChangeSettled();
11227
11333
  if (this.state.status === 'enabled') {
11228
11334
  this.isTrackStoppedDueToTrackEnd = true;
11229
11335
  setTimeout(() => {
@@ -11252,7 +11358,7 @@ class InputMediaDeviceManager {
11252
11358
  try {
11253
11359
  if (!deviceId)
11254
11360
  return;
11255
- await this.statusChangePromise;
11361
+ await this.statusChangeSettled();
11256
11362
  let isDeviceDisconnected = false;
11257
11363
  let isDeviceReplaced = false;
11258
11364
  const currentDevice = this.findDeviceInList(currentDevices, deviceId);
@@ -11289,17 +11395,6 @@ class InputMediaDeviceManager {
11289
11395
  findDeviceInList(devices, deviceId) {
11290
11396
  return devices.find((d) => d.deviceId === deviceId && d.kind === this.mediaDeviceKind);
11291
11397
  }
11292
- nextAbortableStatusChangeRequest(status) {
11293
- this.statusChangeAbortController?.abort();
11294
- this.statusChangeAbortController = new AbortController();
11295
- this.state.setPendingStatus(status);
11296
- return this.statusChangeAbortController.signal;
11297
- }
11298
- resetStatusChangeRequest() {
11299
- this.statusChangePromise = undefined;
11300
- this.statusChangeAbortController = undefined;
11301
- this.state.setPendingStatus(this.state.status);
11302
- }
11303
11398
  }
11304
11399
 
11305
11400
  class InputMediaDeviceManagerState {
@@ -11557,9 +11652,9 @@ class CameraManager extends InputMediaDeviceManager {
11557
11652
  async selectTargetResolution(resolution) {
11558
11653
  this.targetResolution.height = resolution.height;
11559
11654
  this.targetResolution.width = resolution.width;
11560
- if (this.statusChangePromise && this.state.optimisticStatus === 'enabled') {
11655
+ if (this.state.optimisticStatus === 'enabled') {
11561
11656
  try {
11562
- await this.statusChangePromise;
11657
+ await this.statusChangeSettled();
11563
11658
  }
11564
11659
  catch (error) {
11565
11660
  // couldn't enable device, target resolution will be applied the next time user attempts to start the device
@@ -11781,6 +11876,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
11781
11876
  : 'stop-tracks') {
11782
11877
  super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
11783
11878
  this.speakingWhileMutedNotificationEnabled = true;
11879
+ this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
11784
11880
  this.subscriptions.push(createSubscription(rxjs.combineLatest([
11785
11881
  this.call.state.callingState$,
11786
11882
  this.call.state.ownCapabilities$,
@@ -11934,7 +12030,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
11934
12030
  return this.call.stopPublish(TrackType.AUDIO, stopTracks);
11935
12031
  }
11936
12032
  async startSpeakingWhileMutedDetection(deviceId) {
11937
- const startPromise = (async () => {
12033
+ await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
11938
12034
  await this.stopSpeakingWhileMutedDetection();
11939
12035
  if (isReactNative()) {
11940
12036
  this.rnSpeechDetector = new RNSpeechDetector();
@@ -11942,7 +12038,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
11942
12038
  const unsubscribe = this.rnSpeechDetector?.onSpeakingDetectedStateChange((event) => {
11943
12039
  this.state.setSpeakingWhileMuted(event.isSoundDetected);
11944
12040
  });
11945
- return () => {
12041
+ this.soundDetectorCleanup = () => {
11946
12042
  unsubscribe();
11947
12043
  this.rnSpeechDetector?.stop();
11948
12044
  this.rnSpeechDetector = undefined;
@@ -11953,24 +12049,21 @@ class MicrophoneManager extends InputMediaDeviceManager {
11953
12049
  const stream = await this.getStream({
11954
12050
  deviceId,
11955
12051
  });
11956
- return createSoundDetector(stream, (event) => {
12052
+ this.soundDetectorCleanup = createSoundDetector(stream, (event) => {
11957
12053
  this.state.setSpeakingWhileMuted(event.isSoundDetected);
11958
12054
  });
11959
12055
  }
11960
- })();
11961
- this.soundDetectorCleanup = async () => {
11962
- const cleanup = await startPromise;
11963
- await cleanup();
11964
- };
11965
- await startPromise;
12056
+ });
11966
12057
  }
11967
12058
  async stopSpeakingWhileMutedDetection() {
11968
- if (!this.soundDetectorCleanup)
11969
- return;
11970
- const soundDetectorCleanup = this.soundDetectorCleanup;
11971
- this.soundDetectorCleanup = undefined;
11972
- this.state.setSpeakingWhileMuted(false);
11973
- await soundDetectorCleanup();
12059
+ await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
12060
+ if (!this.soundDetectorCleanup)
12061
+ return;
12062
+ const soundDetectorCleanup = this.soundDetectorCleanup;
12063
+ this.soundDetectorCleanup = undefined;
12064
+ this.state.setSpeakingWhileMuted(false);
12065
+ await soundDetectorCleanup();
12066
+ });
11974
12067
  }
11975
12068
  }
11976
12069
 
@@ -13732,7 +13825,7 @@ class Call {
13732
13825
  }
13733
13826
  async initCamera(options) {
13734
13827
  // Wait for any in progress camera operation
13735
- await this.camera.statusChangePromise;
13828
+ await this.camera.statusChangeSettled();
13736
13829
  if (this.state.localParticipant?.videoStream ||
13737
13830
  !this.permissionsContext.hasPermission('send-video')) {
13738
13831
  return;
@@ -13769,7 +13862,7 @@ class Call {
13769
13862
  }
13770
13863
  async initMic(options) {
13771
13864
  // Wait for any in progress mic operation
13772
- await this.microphone.statusChangePromise;
13865
+ await this.microphone.statusChangeSettled();
13773
13866
  if (this.state.localParticipant?.audioStream ||
13774
13867
  !this.permissionsContext.hasPermission('send-audio')) {
13775
13868
  return;
@@ -15310,7 +15403,7 @@ class StreamClient {
15310
15403
  });
15311
15404
  };
15312
15405
  this.getUserAgent = () => {
15313
- const version = "1.3.0" ;
15406
+ const version = "1.4.0" ;
15314
15407
  return (this.userAgent ||
15315
15408
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
15316
15409
  };