@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.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
|
-
|
|
10977
|
-
|
|
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.
|
|
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
|
-
|
|
11005
|
-
|
|
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.
|
|
11119
|
+
if (!signal.aborted) {
|
|
11120
|
+
this.state.setPendingStatus(this.state.status);
|
|
11121
|
+
}
|
|
11016
11122
|
}
|
|
11017
|
-
};
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
11655
|
+
if (this.state.optimisticStatus === 'enabled') {
|
|
11561
11656
|
try {
|
|
11562
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
15406
|
+
const version = "1.4.0" ;
|
|
15314
15407
|
return (this.userAgent ||
|
|
15315
15408
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
15316
15409
|
};
|