@stream-io/video-client 1.42.3 → 1.43.0-beta.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.browser.es.js +109 -53
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +110 -54
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +109 -53
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/MicrophoneManager.d.ts +2 -0
- package/dist/src/helpers/no-audio-detector.d.ts +1 -7
- package/dist/src/types.d.ts +36 -5
- package/package.json +3 -3
- package/src/Call.ts +78 -39
- package/src/devices/MicrophoneManager.ts +21 -5
- package/src/devices/SpeakerManager.ts +1 -0
- package/src/devices/__tests__/MicrophoneManager.test.ts +52 -0
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +26 -1
- package/src/devices/__tests__/web-audio.mocks.ts +6 -2
- package/src/events/call.ts +3 -0
- package/src/helpers/__tests__/no-audio-detector.test.ts +54 -28
- package/src/helpers/no-audio-detector.ts +25 -20
- package/src/types.ts +47 -5
package/dist/index.browser.es.js
CHANGED
|
@@ -6231,7 +6231,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6231
6231
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6232
6232
|
};
|
|
6233
6233
|
|
|
6234
|
-
const version = "1.
|
|
6234
|
+
const version = "1.43.0-beta.0";
|
|
6235
6235
|
const [major, minor, patch] = version.split('.');
|
|
6236
6236
|
let sdkInfo = {
|
|
6237
6237
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -8924,6 +8924,7 @@ const watchCallRejected = (call) => {
|
|
|
8924
8924
|
else {
|
|
8925
8925
|
if (rejectedBy[eventCall.created_by.id]) {
|
|
8926
8926
|
call.logger.info('call creator rejected, leaving call');
|
|
8927
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
8927
8928
|
await call.leave({ message: 'ring: creator rejected' });
|
|
8928
8929
|
}
|
|
8929
8930
|
}
|
|
@@ -8937,6 +8938,7 @@ const watchCallEnded = (call) => {
|
|
|
8937
8938
|
const { callingState } = call.state;
|
|
8938
8939
|
if (callingState !== CallingState.IDLE &&
|
|
8939
8940
|
callingState !== CallingState.LEFT) {
|
|
8941
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
8940
8942
|
call
|
|
8941
8943
|
.leave({ message: 'call.ended event received', reject: false })
|
|
8942
8944
|
.catch((err) => {
|
|
@@ -8965,6 +8967,7 @@ const watchSfuCallEnded = (call) => {
|
|
|
8965
8967
|
// update the call state to reflect the call has ended.
|
|
8966
8968
|
call.state.setEndedAt(new Date());
|
|
8967
8969
|
const reason = CallEndedReason[e.reason];
|
|
8970
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
8968
8971
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
8969
8972
|
}
|
|
8970
8973
|
catch (err) {
|
|
@@ -11480,12 +11483,21 @@ const createSoundDetector = (audioStream, onSoundDetectedStateChanged, options =
|
|
|
11480
11483
|
};
|
|
11481
11484
|
|
|
11482
11485
|
/**
|
|
11483
|
-
* Analyzes
|
|
11486
|
+
* Analyzes time-domain waveform data to determine if audio is being captured.
|
|
11487
|
+
* Uses the waveform RMS around the 128 midpoint for robust silence detection.
|
|
11484
11488
|
*/
|
|
11485
|
-
const hasAudio = (analyser
|
|
11486
|
-
const data = new Uint8Array(analyser.
|
|
11487
|
-
analyser.
|
|
11488
|
-
|
|
11489
|
+
const hasAudio = (analyser) => {
|
|
11490
|
+
const data = new Uint8Array(analyser.fftSize);
|
|
11491
|
+
analyser.getByteTimeDomainData(data);
|
|
11492
|
+
let squareSum = 0;
|
|
11493
|
+
for (const sample of data) {
|
|
11494
|
+
const centered = sample - 128;
|
|
11495
|
+
// Ignore tiny quantization/jitter around midpoint (e.g. 127/128 samples).
|
|
11496
|
+
const signal = Math.abs(centered) <= 1 ? 0 : centered;
|
|
11497
|
+
squareSum += signal * signal;
|
|
11498
|
+
}
|
|
11499
|
+
const rms = Math.sqrt(squareSum / data.length);
|
|
11500
|
+
return rms > 0;
|
|
11489
11501
|
};
|
|
11490
11502
|
/** Helper for "no event" transitions */
|
|
11491
11503
|
const noEmit = (nextState) => ({
|
|
@@ -11499,9 +11511,9 @@ const emit = (capturesAudio, nextState) => ({ shouldEmit: true, nextState, captu
|
|
|
11499
11511
|
*/
|
|
11500
11512
|
const transitionState = (state, audioDetected, options) => {
|
|
11501
11513
|
if (audioDetected) {
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11514
|
+
// Any observed audio means the microphone is capturing.
|
|
11515
|
+
// Emit recovery/success and let the caller stop the detector.
|
|
11516
|
+
return emit(true, { kind: 'IDLE' });
|
|
11505
11517
|
}
|
|
11506
11518
|
const { noAudioThresholdMs, emitIntervalMs } = options;
|
|
11507
11519
|
const now = Date.now();
|
|
@@ -11542,16 +11554,17 @@ const createAudioAnalyzer = (audioStream, fftSize) => {
|
|
|
11542
11554
|
* @returns a cleanup function which once invoked stops the no-audio detector.
|
|
11543
11555
|
*/
|
|
11544
11556
|
const createNoAudioDetector = (audioStream, options) => {
|
|
11545
|
-
const { detectionFrequencyInMs = 350,
|
|
11557
|
+
const { detectionFrequencyInMs = 350, fftSize = 512, onCaptureStatusChange, } = options;
|
|
11546
11558
|
let state = { kind: 'IDLE' };
|
|
11547
11559
|
const { audioContext, analyser } = createAudioAnalyzer(audioStream, fftSize);
|
|
11548
11560
|
const detectionIntervalId = setInterval(() => {
|
|
11549
|
-
const [
|
|
11550
|
-
if (
|
|
11561
|
+
const [track] = audioStream.getAudioTracks();
|
|
11562
|
+
if (track && !track.enabled) {
|
|
11551
11563
|
state = { kind: 'IDLE' };
|
|
11552
11564
|
return;
|
|
11553
11565
|
}
|
|
11554
|
-
|
|
11566
|
+
// Missing or ended track is treated as no-audio to surface abrupt capture loss.
|
|
11567
|
+
const audioDetected = track?.readyState === 'live' && hasAudio(analyser);
|
|
11555
11568
|
const transition = transitionState(state, audioDetected, options);
|
|
11556
11569
|
state = transition.nextState;
|
|
11557
11570
|
if (!transition.shouldEmit)
|
|
@@ -12024,6 +12037,9 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12024
12037
|
}
|
|
12025
12038
|
async startSpeakingWhileMutedDetection(deviceId) {
|
|
12026
12039
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
12040
|
+
if (this.soundDetectorCleanup && this.soundDetectorDeviceId === deviceId)
|
|
12041
|
+
return;
|
|
12042
|
+
await this.teardownSpeakingWhileMutedDetection();
|
|
12027
12043
|
if (isReactNative()) {
|
|
12028
12044
|
this.rnSpeechDetector = new RNSpeechDetector();
|
|
12029
12045
|
const unsubscribe = await this.rnSpeechDetector.start((event) => {
|
|
@@ -12044,16 +12060,23 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12044
12060
|
this.state.setSpeakingWhileMuted(event.isSoundDetected);
|
|
12045
12061
|
});
|
|
12046
12062
|
}
|
|
12063
|
+
this.soundDetectorDeviceId = deviceId;
|
|
12047
12064
|
});
|
|
12048
12065
|
}
|
|
12049
12066
|
async stopSpeakingWhileMutedDetection() {
|
|
12050
12067
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12068
|
+
return this.teardownSpeakingWhileMutedDetection();
|
|
12069
|
+
});
|
|
12070
|
+
}
|
|
12071
|
+
async teardownSpeakingWhileMutedDetection() {
|
|
12072
|
+
const soundDetectorCleanup = this.soundDetectorCleanup;
|
|
12073
|
+
this.soundDetectorCleanup = undefined;
|
|
12074
|
+
this.soundDetectorDeviceId = undefined;
|
|
12075
|
+
this.state.setSpeakingWhileMuted(false);
|
|
12076
|
+
if (!soundDetectorCleanup)
|
|
12077
|
+
return;
|
|
12078
|
+
await soundDetectorCleanup().catch((err) => {
|
|
12079
|
+
this.logger.warn('Failed to stop speaking while muted detector', err);
|
|
12057
12080
|
});
|
|
12058
12081
|
}
|
|
12059
12082
|
async hasPermission(permissionState) {
|
|
@@ -12322,6 +12345,7 @@ class SpeakerManager {
|
|
|
12322
12345
|
this.defaultDevice = defaultDevice;
|
|
12323
12346
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
12324
12347
|
defaultDevice,
|
|
12348
|
+
isRingingTypeCall: this.call.ringing,
|
|
12325
12349
|
});
|
|
12326
12350
|
}
|
|
12327
12351
|
}
|
|
@@ -12512,6 +12536,7 @@ class Call {
|
|
|
12512
12536
|
const currentUserId = this.currentUserId;
|
|
12513
12537
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
12514
12538
|
this.logger.info('Leaving call because of being blocked');
|
|
12539
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
|
|
12515
12540
|
await this.leave({ message: 'user blocked' }).catch((err) => {
|
|
12516
12541
|
this.logger.error('Error leaving call after being blocked', err);
|
|
12517
12542
|
});
|
|
@@ -12548,6 +12573,7 @@ class Call {
|
|
|
12548
12573
|
const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === CallingState.RINGING;
|
|
12549
12574
|
if ((isAcceptedElsewhere || isRejectedByMe) &&
|
|
12550
12575
|
!hasPending(this.joinLeaveConcurrencyTag)) {
|
|
12576
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
|
|
12551
12577
|
this.leave().catch(() => {
|
|
12552
12578
|
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
12553
12579
|
});
|
|
@@ -12697,17 +12723,28 @@ class Call {
|
|
|
12697
12723
|
}
|
|
12698
12724
|
if (callingState === CallingState.RINGING && reject !== false) {
|
|
12699
12725
|
if (reject) {
|
|
12700
|
-
|
|
12726
|
+
const reasonToEndCallReason = {
|
|
12727
|
+
timeout: 'missed',
|
|
12728
|
+
cancel: 'canceled',
|
|
12729
|
+
busy: 'busy',
|
|
12730
|
+
decline: 'rejected',
|
|
12731
|
+
};
|
|
12732
|
+
const rejectReason = reason ?? 'decline';
|
|
12733
|
+
const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
|
|
12734
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
12735
|
+
await this.reject(rejectReason);
|
|
12701
12736
|
}
|
|
12702
12737
|
else {
|
|
12703
12738
|
// if reject was undefined, we still have to cancel the call automatically
|
|
12704
12739
|
// when I am the creator and everyone else left the call
|
|
12705
12740
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
12706
12741
|
if (this.isCreatedByMe && !hasOtherParticipants) {
|
|
12742
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
12707
12743
|
await this.reject('cancel');
|
|
12708
12744
|
}
|
|
12709
12745
|
}
|
|
12710
12746
|
}
|
|
12747
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
12711
12748
|
this.statsReporter?.stop();
|
|
12712
12749
|
this.statsReporter = undefined;
|
|
12713
12750
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -12734,7 +12771,9 @@ class Call {
|
|
|
12734
12771
|
this.ringingSubject.next(false);
|
|
12735
12772
|
this.cancelAutoDrop();
|
|
12736
12773
|
this.clientStore.unregisterCall(this);
|
|
12737
|
-
globalThis.streamRNVideoSDK?.callManager.stop(
|
|
12774
|
+
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
12775
|
+
isRingingTypeCall: this.ringing,
|
|
12776
|
+
});
|
|
12738
12777
|
this.camera.dispose();
|
|
12739
12778
|
this.microphone.dispose();
|
|
12740
12779
|
this.screenShare.dispose();
|
|
@@ -12887,11 +12926,19 @@ class Call {
|
|
|
12887
12926
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
12888
12927
|
*/
|
|
12889
12928
|
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
12890
|
-
await this.setup();
|
|
12891
12929
|
const callingState = this.state.callingState;
|
|
12892
12930
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
12893
12931
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
12894
12932
|
}
|
|
12933
|
+
if (data?.ring) {
|
|
12934
|
+
this.ringingSubject.next(true);
|
|
12935
|
+
}
|
|
12936
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
12937
|
+
if (callingX) {
|
|
12938
|
+
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
12939
|
+
await callingX.startCall(this);
|
|
12940
|
+
}
|
|
12941
|
+
await this.setup();
|
|
12895
12942
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
12896
12943
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
12897
12944
|
// we will count the number of join failures per SFU.
|
|
@@ -12900,38 +12947,44 @@ class Call {
|
|
|
12900
12947
|
const sfuJoinFailures = new Map();
|
|
12901
12948
|
const joinData = data;
|
|
12902
12949
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
catch (err) {
|
|
12912
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
12913
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
12914
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
12915
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
12916
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
12917
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
12918
|
-
throw err;
|
|
12919
|
-
}
|
|
12920
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
12921
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
12922
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
12923
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
12924
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
12925
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
12926
|
-
if (switchSfu || failures >= 2) {
|
|
12927
|
-
joinData.migrating_from = sfuId;
|
|
12928
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
12950
|
+
try {
|
|
12951
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
12952
|
+
try {
|
|
12953
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
12954
|
+
await this.doJoin(data);
|
|
12955
|
+
delete joinData.migrating_from;
|
|
12956
|
+
delete joinData.migrating_from_list;
|
|
12957
|
+
break;
|
|
12929
12958
|
}
|
|
12930
|
-
|
|
12931
|
-
|
|
12959
|
+
catch (err) {
|
|
12960
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
12961
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
12962
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
12963
|
+
// if the error is unrecoverable, we should not retry as that signals
|
|
12964
|
+
// that connectivity is good, but the coordinator doesn't allow the user
|
|
12965
|
+
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
12966
|
+
throw err;
|
|
12967
|
+
}
|
|
12968
|
+
// immediately switch to a different SFU in case of recoverable join error
|
|
12969
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
12970
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
12971
|
+
const sfuId = this.credentials?.server.edge_name || '';
|
|
12972
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
12973
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
12974
|
+
if (switchSfu || failures >= 2) {
|
|
12975
|
+
joinData.migrating_from = sfuId;
|
|
12976
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
12977
|
+
}
|
|
12978
|
+
if (attempt === maxJoinRetries - 1) {
|
|
12979
|
+
throw err;
|
|
12980
|
+
}
|
|
12932
12981
|
}
|
|
12982
|
+
await sleep(retryInterval(attempt));
|
|
12933
12983
|
}
|
|
12934
|
-
|
|
12984
|
+
}
|
|
12985
|
+
catch (error) {
|
|
12986
|
+
callingX?.endCall(this, 'error');
|
|
12987
|
+
throw error;
|
|
12935
12988
|
}
|
|
12936
12989
|
};
|
|
12937
12990
|
/**
|
|
@@ -13078,7 +13131,9 @@ class Call {
|
|
|
13078
13131
|
// re-apply them on later reconnections or server-side data fetches
|
|
13079
13132
|
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
13080
13133
|
await this.applyDeviceConfig(this.state.settings, true);
|
|
13081
|
-
globalThis.streamRNVideoSDK?.callManager.start(
|
|
13134
|
+
globalThis.streamRNVideoSDK?.callManager.start({
|
|
13135
|
+
isRingingTypeCall: this.ringing,
|
|
13136
|
+
});
|
|
13082
13137
|
this.deviceSettingsAppliedOnce = true;
|
|
13083
13138
|
}
|
|
13084
13139
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -13506,6 +13561,7 @@ class Call {
|
|
|
13506
13561
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13507
13562
|
return;
|
|
13508
13563
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
13564
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
|
|
13509
13565
|
this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
|
|
13510
13566
|
this.logger.warn(`Can't leave call after disconnect request`, err);
|
|
13511
13567
|
});
|
|
@@ -15601,7 +15657,7 @@ class StreamClient {
|
|
|
15601
15657
|
this.getUserAgent = () => {
|
|
15602
15658
|
if (!this.cachedUserAgent) {
|
|
15603
15659
|
const { clientAppIdentifier = {} } = this.options;
|
|
15604
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
15660
|
+
const { sdkName = 'js', sdkVersion = "1.43.0-beta.0", ...extras } = clientAppIdentifier;
|
|
15605
15661
|
this.cachedUserAgent = [
|
|
15606
15662
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
15607
15663
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|