@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/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/gen/coordinator/index.d.ts +6 -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/gen/coordinator/index.ts +7 -0
- package/src/helpers/__tests__/concurrency.test.ts +269 -0
- package/src/helpers/concurrency.ts +125 -0
- package/src/store/rxUtils.ts +22 -0
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
|
-
|
|
10957
|
-
|
|
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.
|
|
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
|
-
|
|
10985
|
-
|
|
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.
|
|
11099
|
+
if (!signal.aborted) {
|
|
11100
|
+
this.state.setPendingStatus(this.state.status);
|
|
11101
|
+
}
|
|
10996
11102
|
}
|
|
10997
|
-
};
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
11635
|
+
if (this.state.optimisticStatus === 'enabled') {
|
|
11541
11636
|
try {
|
|
11542
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
15386
|
+
const version = "1.4.0" ;
|
|
15294
15387
|
return (this.userAgent ||
|
|
15295
15388
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
15296
15389
|
};
|