@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 +14 -0
- package/dist/index.browser.es.js +155 -62
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +155 -62
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +155 -62
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -7
- package/dist/src/devices/MicrophoneManager.d.ts +1 -0
- package/dist/src/helpers/concurrency.d.ts +30 -0
- package/dist/src/store/rxUtils.d.ts +9 -0
- package/package.json +1 -1
- package/src/Call.ts +2 -2
- package/src/devices/CameraManager.ts +2 -2
- package/src/devices/InputMediaDeviceManager.ts +32 -49
- package/src/devices/MicrophoneManager.ts +31 -35
- package/src/devices/__tests__/MicrophoneManager.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -2
- package/src/helpers/__tests__/concurrency.test.ts +269 -0
- package/src/helpers/concurrency.ts +125 -0
- package/src/store/rxUtils.ts +22 -0
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
|
|
package/dist/index.browser.es.js
CHANGED
|
@@ -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
|
-
|
|
10956
|
-
|
|
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.
|
|
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
|
-
|
|
10984
|
-
|
|
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.
|
|
11098
|
+
if (!signal.aborted) {
|
|
11099
|
+
this.state.setPendingStatus(this.state.status);
|
|
11100
|
+
}
|
|
10995
11101
|
}
|
|
10996
|
-
};
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
11634
|
+
if (this.state.optimisticStatus === 'enabled') {
|
|
11540
11635
|
try {
|
|
11541
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
15387
|
+
const version = "1.3.1" ;
|
|
15295
15388
|
return (this.userAgent ||
|
|
15296
15389
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
15297
15390
|
};
|