@stream-io/video-client 1.45.0 → 1.46.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 +20 -0
- package/dist/index.browser.es.js +186 -49
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +186 -49
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +186 -49
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +5 -1
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -0
- package/dist/src/helpers/DynascaleManager.d.ts +20 -0
- package/dist/src/helpers/RNSpeechDetector.d.ts +4 -2
- package/dist/src/types.d.ts +37 -5
- package/package.json +1 -1
- package/src/Call.ts +92 -40
- package/src/devices/MicrophoneManager.ts +7 -1
- package/src/devices/SpeakerManager.ts +1 -0
- package/src/events/__tests__/participant.test.ts +41 -0
- package/src/events/call.ts +3 -0
- package/src/events/participant.ts +1 -0
- package/src/gen/video/sfu/event/events.ts +5 -0
- package/src/helpers/DynascaleManager.ts +72 -1
- package/src/helpers/RNSpeechDetector.ts +52 -12
- package/src/helpers/__tests__/DynascaleManager.test.ts +120 -0
- package/src/helpers/__tests__/RNSpeechDetector.test.ts +52 -0
- package/src/store/stateStore.ts +1 -1
- package/src/types.ts +48 -5
package/dist/index.cjs.js
CHANGED
|
@@ -3285,6 +3285,7 @@ class ParticipantJoined$Type extends runtime.MessageType {
|
|
|
3285
3285
|
super('stream.video.sfu.event.ParticipantJoined', [
|
|
3286
3286
|
{ no: 1, name: 'call_cid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
3287
3287
|
{ no: 2, name: 'participant', kind: 'message', T: () => Participant },
|
|
3288
|
+
{ no: 3, name: 'is_pinned', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
3288
3289
|
]);
|
|
3289
3290
|
}
|
|
3290
3291
|
}
|
|
@@ -4804,7 +4805,7 @@ class StreamVideoWriteableStateStore {
|
|
|
4804
4805
|
* The currently connected user.
|
|
4805
4806
|
*/
|
|
4806
4807
|
get connectedUser() {
|
|
4807
|
-
return
|
|
4808
|
+
return this.connectedUserSubject.getValue();
|
|
4808
4809
|
}
|
|
4809
4810
|
/**
|
|
4810
4811
|
* A list of {@link Call} objects created/tracked by this client.
|
|
@@ -6303,7 +6304,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6303
6304
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6304
6305
|
};
|
|
6305
6306
|
|
|
6306
|
-
const version = "1.
|
|
6307
|
+
const version = "1.46.1";
|
|
6307
6308
|
const [major, minor, patch] = version.split('.');
|
|
6308
6309
|
let sdkInfo = {
|
|
6309
6310
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -9002,6 +9003,7 @@ const watchCallRejected = (call) => {
|
|
|
9002
9003
|
else {
|
|
9003
9004
|
if (rejectedBy[eventCall.created_by.id]) {
|
|
9004
9005
|
call.logger.info('call creator rejected, leaving call');
|
|
9006
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9005
9007
|
await call.leave({ message: 'ring: creator rejected' });
|
|
9006
9008
|
}
|
|
9007
9009
|
}
|
|
@@ -9012,6 +9014,7 @@ const watchCallRejected = (call) => {
|
|
|
9012
9014
|
*/
|
|
9013
9015
|
const watchCallEnded = (call) => {
|
|
9014
9016
|
return function onCallEnded() {
|
|
9017
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9015
9018
|
const { callingState } = call.state;
|
|
9016
9019
|
if (callingState !== exports.CallingState.IDLE &&
|
|
9017
9020
|
callingState !== exports.CallingState.LEFT) {
|
|
@@ -9043,6 +9046,7 @@ const watchSfuCallEnded = (call) => {
|
|
|
9043
9046
|
// update the call state to reflect the call has ended.
|
|
9044
9047
|
call.state.setEndedAt(new Date());
|
|
9045
9048
|
const reason = CallEndedReason[e.reason];
|
|
9049
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9046
9050
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9047
9051
|
}
|
|
9048
9052
|
catch (err) {
|
|
@@ -9238,6 +9242,7 @@ const watchParticipantJoined = (state) => {
|
|
|
9238
9242
|
// already announced participants.
|
|
9239
9243
|
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
9240
9244
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
9245
|
+
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
9241
9246
|
viewportVisibilityState: {
|
|
9242
9247
|
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
9243
9248
|
screenShareTrack: exports.VisibilityState.UNKNOWN,
|
|
@@ -9631,6 +9636,31 @@ class DynascaleManager {
|
|
|
9631
9636
|
this.logger = videoLoggerSystem.getLogger('DynascaleManager');
|
|
9632
9637
|
this.useWebAudio = false;
|
|
9633
9638
|
this.pendingSubscriptionsUpdate = null;
|
|
9639
|
+
/**
|
|
9640
|
+
* Audio elements that were blocked by the browser's autoplay policy.
|
|
9641
|
+
* These can be retried by calling `resumeAudio()` from a user gesture.
|
|
9642
|
+
*/
|
|
9643
|
+
this.blockedAudioElementsSubject = new rxjs.BehaviorSubject(new Set());
|
|
9644
|
+
/**
|
|
9645
|
+
* Whether the browser's autoplay policy is blocking audio playback.
|
|
9646
|
+
* Will be `true` when the browser blocks autoplay (e.g., no prior user interaction).
|
|
9647
|
+
* Use `resumeAudio()` within a user gesture to unblock.
|
|
9648
|
+
*/
|
|
9649
|
+
this.autoplayBlocked$ = this.blockedAudioElementsSubject.pipe(rxjs.map((elements) => elements.size > 0), rxjs.distinctUntilChanged());
|
|
9650
|
+
this.addBlockedAudioElement = (audioElement) => {
|
|
9651
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9652
|
+
const next = new Set(elements);
|
|
9653
|
+
next.add(audioElement);
|
|
9654
|
+
return next;
|
|
9655
|
+
});
|
|
9656
|
+
};
|
|
9657
|
+
this.removeBlockedAudioElement = (audioElement) => {
|
|
9658
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9659
|
+
const nextElements = new Set(elements);
|
|
9660
|
+
nextElements.delete(audioElement);
|
|
9661
|
+
return nextElements;
|
|
9662
|
+
});
|
|
9663
|
+
};
|
|
9634
9664
|
this.videoTrackSubscriptionOverridesSubject = new rxjs.BehaviorSubject({});
|
|
9635
9665
|
this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
|
|
9636
9666
|
this.incomingVideoSettings$ = this.videoTrackSubscriptionOverrides$.pipe(rxjs.map((overrides) => {
|
|
@@ -9662,6 +9692,7 @@ class DynascaleManager {
|
|
|
9662
9692
|
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
9663
9693
|
}
|
|
9664
9694
|
this.audioBindingsWatchdog?.dispose();
|
|
9695
|
+
setCurrentValue(this.blockedAudioElementsSubject, new Set());
|
|
9665
9696
|
const context = this.audioContext;
|
|
9666
9697
|
if (context && context.state !== 'closed') {
|
|
9667
9698
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -9959,8 +9990,10 @@ class DynascaleManager {
|
|
|
9959
9990
|
return;
|
|
9960
9991
|
setTimeout(() => {
|
|
9961
9992
|
audioElement.srcObject = source ?? null;
|
|
9962
|
-
if (!source)
|
|
9993
|
+
if (!source) {
|
|
9994
|
+
this.removeBlockedAudioElement(audioElement);
|
|
9963
9995
|
return;
|
|
9996
|
+
}
|
|
9964
9997
|
// Safari has a special quirk that prevents playing audio until the user
|
|
9965
9998
|
// interacts with the page or focuses on the tab where the call happens.
|
|
9966
9999
|
// This is a workaround for the issue where:
|
|
@@ -9984,6 +10017,10 @@ class DynascaleManager {
|
|
|
9984
10017
|
audioElement.muted = false;
|
|
9985
10018
|
audioElement.play().catch((e) => {
|
|
9986
10019
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
10020
|
+
if (e.name === 'NotAllowedError') {
|
|
10021
|
+
this.tracer.trace('audioPlaybackBlocked', null);
|
|
10022
|
+
this.addBlockedAudioElement(audioElement);
|
|
10023
|
+
}
|
|
9987
10024
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
9988
10025
|
});
|
|
9989
10026
|
}
|
|
@@ -10010,6 +10047,7 @@ class DynascaleManager {
|
|
|
10010
10047
|
audioElement.autoplay = true;
|
|
10011
10048
|
return () => {
|
|
10012
10049
|
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
10050
|
+
this.removeBlockedAudioElement(audioElement);
|
|
10013
10051
|
sinkIdSubscription?.unsubscribe();
|
|
10014
10052
|
volumeSubscription.unsubscribe();
|
|
10015
10053
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -10018,6 +10056,28 @@ class DynascaleManager {
|
|
|
10018
10056
|
gainNode?.disconnect();
|
|
10019
10057
|
};
|
|
10020
10058
|
};
|
|
10059
|
+
/**
|
|
10060
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
10061
|
+
* Must be called from within a user gesture (e.g., click handler).
|
|
10062
|
+
*
|
|
10063
|
+
* @returns a promise that resolves when all blocked elements have been retried.
|
|
10064
|
+
*/
|
|
10065
|
+
this.resumeAudio = async () => {
|
|
10066
|
+
this.tracer.trace('resumeAudio', null);
|
|
10067
|
+
const blocked = new Set();
|
|
10068
|
+
await Promise.all(Array.from(getCurrentValue(this.blockedAudioElementsSubject), async (el) => {
|
|
10069
|
+
try {
|
|
10070
|
+
if (el.srcObject) {
|
|
10071
|
+
await el.play();
|
|
10072
|
+
}
|
|
10073
|
+
}
|
|
10074
|
+
catch {
|
|
10075
|
+
this.logger.warn(`Can't resume audio for element: `, el);
|
|
10076
|
+
blocked.add(el);
|
|
10077
|
+
}
|
|
10078
|
+
}));
|
|
10079
|
+
setCurrentValue(this.blockedAudioElementsSubject, blocked);
|
|
10080
|
+
};
|
|
10021
10081
|
this.getOrCreateAudioContext = () => {
|
|
10022
10082
|
if (!this.useWebAudio)
|
|
10023
10083
|
return;
|
|
@@ -11887,31 +11947,43 @@ class RNSpeechDetector {
|
|
|
11887
11947
|
constructor(externalAudioStream) {
|
|
11888
11948
|
this.pc1 = new RTCPeerConnection({});
|
|
11889
11949
|
this.pc2 = new RTCPeerConnection({});
|
|
11950
|
+
this.isStopped = false;
|
|
11890
11951
|
this.externalAudioStream = externalAudioStream;
|
|
11891
11952
|
}
|
|
11892
11953
|
/**
|
|
11893
11954
|
* Starts the speech detection.
|
|
11894
11955
|
*/
|
|
11895
11956
|
async start(onSoundDetectedStateChanged) {
|
|
11957
|
+
let detachListeners;
|
|
11958
|
+
let unsubscribe;
|
|
11896
11959
|
try {
|
|
11960
|
+
this.isStopped = false;
|
|
11897
11961
|
const audioStream = this.externalAudioStream != null
|
|
11898
11962
|
? this.externalAudioStream
|
|
11899
11963
|
: await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
11900
11964
|
this.audioStream = audioStream;
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
}
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
}
|
|
11907
|
-
|
|
11965
|
+
const onPc1IceCandidate = (e) => {
|
|
11966
|
+
this.forwardIceCandidate(this.pc2, e.candidate);
|
|
11967
|
+
};
|
|
11968
|
+
const onPc2IceCandidate = (e) => {
|
|
11969
|
+
this.forwardIceCandidate(this.pc1, e.candidate);
|
|
11970
|
+
};
|
|
11971
|
+
const onTrackPc2 = (e) => {
|
|
11908
11972
|
e.streams[0].getTracks().forEach((track) => {
|
|
11909
11973
|
// In RN, the remote track is automatically added to the audio output device
|
|
11910
11974
|
// so we need to mute it to avoid hearing the audio back
|
|
11911
11975
|
// @ts-expect-error _setVolume is a private method in react-native-webrtc
|
|
11912
11976
|
track._setVolume(0);
|
|
11913
11977
|
});
|
|
11914
|
-
}
|
|
11978
|
+
};
|
|
11979
|
+
this.pc1.addEventListener('icecandidate', onPc1IceCandidate);
|
|
11980
|
+
this.pc2.addEventListener('icecandidate', onPc2IceCandidate);
|
|
11981
|
+
this.pc2.addEventListener('track', onTrackPc2);
|
|
11982
|
+
detachListeners = () => {
|
|
11983
|
+
this.pc1.removeEventListener('icecandidate', onPc1IceCandidate);
|
|
11984
|
+
this.pc2.removeEventListener('icecandidate', onPc2IceCandidate);
|
|
11985
|
+
this.pc2.removeEventListener('track', onTrackPc2);
|
|
11986
|
+
};
|
|
11915
11987
|
audioStream
|
|
11916
11988
|
.getTracks()
|
|
11917
11989
|
.forEach((track) => this.pc1.addTrack(track, audioStream));
|
|
@@ -11921,13 +11993,17 @@ class RNSpeechDetector {
|
|
|
11921
11993
|
const answer = await this.pc2.createAnswer();
|
|
11922
11994
|
await this.pc1.setRemoteDescription(answer);
|
|
11923
11995
|
await this.pc2.setLocalDescription(answer);
|
|
11924
|
-
|
|
11996
|
+
unsubscribe = this.onSpeakingDetectedStateChange(onSoundDetectedStateChanged);
|
|
11925
11997
|
return () => {
|
|
11926
|
-
|
|
11998
|
+
detachListeners?.();
|
|
11999
|
+
unsubscribe?.();
|
|
11927
12000
|
this.stop();
|
|
11928
12001
|
};
|
|
11929
12002
|
}
|
|
11930
12003
|
catch (error) {
|
|
12004
|
+
detachListeners?.();
|
|
12005
|
+
unsubscribe?.();
|
|
12006
|
+
this.stop();
|
|
11931
12007
|
const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
|
|
11932
12008
|
logger.error('error handling permissions: ', error);
|
|
11933
12009
|
return () => { };
|
|
@@ -11937,6 +12013,9 @@ class RNSpeechDetector {
|
|
|
11937
12013
|
* Stops the speech detection and releases all allocated resources.
|
|
11938
12014
|
*/
|
|
11939
12015
|
stop() {
|
|
12016
|
+
if (this.isStopped)
|
|
12017
|
+
return;
|
|
12018
|
+
this.isStopped = true;
|
|
11940
12019
|
this.pc1.close();
|
|
11941
12020
|
this.pc2.close();
|
|
11942
12021
|
if (this.externalAudioStream != null) {
|
|
@@ -12036,6 +12115,18 @@ class RNSpeechDetector {
|
|
|
12036
12115
|
this.audioStream.release();
|
|
12037
12116
|
}
|
|
12038
12117
|
}
|
|
12118
|
+
forwardIceCandidate(destination, candidate) {
|
|
12119
|
+
if (this.isStopped ||
|
|
12120
|
+
!candidate ||
|
|
12121
|
+
destination.signalingState === 'closed') {
|
|
12122
|
+
return;
|
|
12123
|
+
}
|
|
12124
|
+
destination.addIceCandidate(candidate).catch(() => {
|
|
12125
|
+
// silently ignore the error
|
|
12126
|
+
const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
|
|
12127
|
+
logger.info('cannot add ice candidate - ignoring');
|
|
12128
|
+
});
|
|
12129
|
+
}
|
|
12039
12130
|
}
|
|
12040
12131
|
|
|
12041
12132
|
class MicrophoneManager extends AudioDeviceManager {
|
|
@@ -12131,6 +12222,7 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12131
12222
|
const deviceId = this.state.selectedDevice;
|
|
12132
12223
|
const devices = await rxjs.firstValueFrom(this.listDevices());
|
|
12133
12224
|
const label = devices.find((d) => d.deviceId === deviceId)?.label;
|
|
12225
|
+
let lastCapturesAudio;
|
|
12134
12226
|
this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
|
|
12135
12227
|
noAudioThresholdMs: this.silenceThresholdMs,
|
|
12136
12228
|
emitIntervalMs: this.silenceThresholdMs,
|
|
@@ -12142,7 +12234,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12142
12234
|
deviceId,
|
|
12143
12235
|
label,
|
|
12144
12236
|
};
|
|
12145
|
-
|
|
12237
|
+
if (capturesAudio !== lastCapturesAudio) {
|
|
12238
|
+
lastCapturesAudio = capturesAudio;
|
|
12239
|
+
this.call.tracer.trace('mic.capture_report', event);
|
|
12240
|
+
}
|
|
12146
12241
|
this.call.streamClient.dispatchEvent(event);
|
|
12147
12242
|
},
|
|
12148
12243
|
});
|
|
@@ -12664,6 +12759,7 @@ class SpeakerManager {
|
|
|
12664
12759
|
this.defaultDevice = defaultDevice;
|
|
12665
12760
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
12666
12761
|
defaultDevice,
|
|
12762
|
+
isRingingTypeCall: this.call.ringing,
|
|
12667
12763
|
});
|
|
12668
12764
|
}
|
|
12669
12765
|
}
|
|
@@ -12863,6 +12959,7 @@ class Call {
|
|
|
12863
12959
|
const currentUserId = this.currentUserId;
|
|
12864
12960
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
12865
12961
|
this.logger.info('Leaving call because of being blocked');
|
|
12962
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
|
|
12866
12963
|
await this.leave({ message: 'user blocked' }).catch((err) => {
|
|
12867
12964
|
this.logger.error('Error leaving call after being blocked', err);
|
|
12868
12965
|
});
|
|
@@ -12899,6 +12996,7 @@ class Call {
|
|
|
12899
12996
|
const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === exports.CallingState.RINGING;
|
|
12900
12997
|
if ((isAcceptedElsewhere || isRejectedByMe) &&
|
|
12901
12998
|
!hasPending(this.joinLeaveConcurrencyTag)) {
|
|
12999
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
|
|
12902
13000
|
this.leave().catch(() => {
|
|
12903
13001
|
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
12904
13002
|
});
|
|
@@ -12910,6 +13008,9 @@ class Call {
|
|
|
12910
13008
|
const receiver_id = this.clientStore.connectedUser?.id;
|
|
12911
13009
|
const ended_at = callSession?.ended_at;
|
|
12912
13010
|
const created_by_id = this.state.createdBy?.id;
|
|
13011
|
+
if (this.currentUserId && created_by_id === this.currentUserId) {
|
|
13012
|
+
globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
|
|
13013
|
+
}
|
|
12913
13014
|
const rejected_by = callSession?.rejected_by;
|
|
12914
13015
|
const accepted_by = callSession?.accepted_by;
|
|
12915
13016
|
let leaveCallIdle = false;
|
|
@@ -13048,7 +13149,16 @@ class Call {
|
|
|
13048
13149
|
}
|
|
13049
13150
|
if (callingState === exports.CallingState.RINGING && reject !== false) {
|
|
13050
13151
|
if (reject) {
|
|
13051
|
-
|
|
13152
|
+
const reasonToEndCallReason = {
|
|
13153
|
+
timeout: 'missed',
|
|
13154
|
+
cancel: 'canceled',
|
|
13155
|
+
busy: 'busy',
|
|
13156
|
+
decline: 'rejected',
|
|
13157
|
+
};
|
|
13158
|
+
const rejectReason = reason ?? 'decline';
|
|
13159
|
+
const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
|
|
13160
|
+
await this.reject(rejectReason);
|
|
13161
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
13052
13162
|
}
|
|
13053
13163
|
else {
|
|
13054
13164
|
// if reject was undefined, we still have to cancel the call automatically
|
|
@@ -13056,9 +13166,11 @@ class Call {
|
|
|
13056
13166
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
13057
13167
|
if (this.isCreatedByMe && !hasOtherParticipants) {
|
|
13058
13168
|
await this.reject('cancel');
|
|
13169
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
13059
13170
|
}
|
|
13060
13171
|
}
|
|
13061
13172
|
}
|
|
13173
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
13062
13174
|
this.statsReporter?.stop();
|
|
13063
13175
|
this.statsReporter = undefined;
|
|
13064
13176
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -13085,7 +13197,9 @@ class Call {
|
|
|
13085
13197
|
this.ringingSubject.next(false);
|
|
13086
13198
|
this.cancelAutoDrop();
|
|
13087
13199
|
this.clientStore.unregisterCall(this);
|
|
13088
|
-
globalThis.streamRNVideoSDK?.callManager.stop(
|
|
13200
|
+
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
13201
|
+
isRingingTypeCall: this.ringing,
|
|
13202
|
+
});
|
|
13089
13203
|
this.camera.dispose();
|
|
13090
13204
|
this.microphone.dispose();
|
|
13091
13205
|
this.screenShare.dispose();
|
|
@@ -13251,11 +13365,19 @@ class Call {
|
|
|
13251
13365
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
13252
13366
|
*/
|
|
13253
13367
|
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
13254
|
-
await this.setup();
|
|
13255
13368
|
const callingState = this.state.callingState;
|
|
13256
13369
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
13257
13370
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
13258
13371
|
}
|
|
13372
|
+
if (data?.ring) {
|
|
13373
|
+
this.ringingSubject.next(true);
|
|
13374
|
+
}
|
|
13375
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
13376
|
+
if (callingX) {
|
|
13377
|
+
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
13378
|
+
await callingX.joinCall(this, this.clientStore.calls);
|
|
13379
|
+
}
|
|
13380
|
+
await this.setup();
|
|
13259
13381
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
13260
13382
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
13261
13383
|
// we will count the number of join failures per SFU.
|
|
@@ -13264,38 +13386,44 @@ class Call {
|
|
|
13264
13386
|
const sfuJoinFailures = new Map();
|
|
13265
13387
|
const joinData = data;
|
|
13266
13388
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
catch (err) {
|
|
13276
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
13277
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
13278
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
13279
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
13280
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
13281
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
13282
|
-
throw err;
|
|
13283
|
-
}
|
|
13284
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
13285
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
13286
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
13287
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
13288
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
13289
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
13290
|
-
if (switchSfu || failures >= 2) {
|
|
13291
|
-
joinData.migrating_from = sfuId;
|
|
13292
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
13389
|
+
try {
|
|
13390
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
13391
|
+
try {
|
|
13392
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
13393
|
+
await this.doJoin(data);
|
|
13394
|
+
delete joinData.migrating_from;
|
|
13395
|
+
delete joinData.migrating_from_list;
|
|
13396
|
+
break;
|
|
13293
13397
|
}
|
|
13294
|
-
|
|
13295
|
-
|
|
13398
|
+
catch (err) {
|
|
13399
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
13400
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
13401
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
13402
|
+
// if the error is unrecoverable, we should not retry as that signals
|
|
13403
|
+
// that connectivity is good, but the coordinator doesn't allow the user
|
|
13404
|
+
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
13405
|
+
throw err;
|
|
13406
|
+
}
|
|
13407
|
+
// immediately switch to a different SFU in case of recoverable join error
|
|
13408
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
13409
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
13410
|
+
const sfuId = this.credentials?.server.edge_name || '';
|
|
13411
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
13412
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
13413
|
+
if (switchSfu || failures >= 2) {
|
|
13414
|
+
joinData.migrating_from = sfuId;
|
|
13415
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
13416
|
+
}
|
|
13417
|
+
if (attempt === maxJoinRetries - 1) {
|
|
13418
|
+
throw err;
|
|
13419
|
+
}
|
|
13296
13420
|
}
|
|
13421
|
+
await sleep(retryInterval(attempt));
|
|
13297
13422
|
}
|
|
13298
|
-
|
|
13423
|
+
}
|
|
13424
|
+
catch (error) {
|
|
13425
|
+
callingX?.endCall(this, 'error');
|
|
13426
|
+
throw error;
|
|
13299
13427
|
}
|
|
13300
13428
|
};
|
|
13301
13429
|
/**
|
|
@@ -13442,7 +13570,9 @@ class Call {
|
|
|
13442
13570
|
// re-apply them on later reconnections or server-side data fetches
|
|
13443
13571
|
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
13444
13572
|
await this.applyDeviceConfig(this.state.settings, true, false);
|
|
13445
|
-
globalThis.streamRNVideoSDK?.callManager.start(
|
|
13573
|
+
globalThis.streamRNVideoSDK?.callManager.start({
|
|
13574
|
+
isRingingTypeCall: this.ringing,
|
|
13575
|
+
});
|
|
13446
13576
|
this.deviceSettingsAppliedOnce = true;
|
|
13447
13577
|
}
|
|
13448
13578
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -13870,6 +14000,7 @@ class Call {
|
|
|
13870
14000
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13871
14001
|
return;
|
|
13872
14002
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
14003
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
|
|
13873
14004
|
this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
|
|
13874
14005
|
this.logger.warn(`Can't leave call after disconnect request`, err);
|
|
13875
14006
|
});
|
|
@@ -14774,6 +14905,12 @@ class Call {
|
|
|
14774
14905
|
unbind();
|
|
14775
14906
|
};
|
|
14776
14907
|
};
|
|
14908
|
+
/**
|
|
14909
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
14910
|
+
*/
|
|
14911
|
+
this.resumeAudio = () => {
|
|
14912
|
+
return this.dynascaleManager.resumeAudio();
|
|
14913
|
+
};
|
|
14777
14914
|
/**
|
|
14778
14915
|
* Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
|
|
14779
14916
|
*
|
|
@@ -14891,7 +15028,7 @@ class Call {
|
|
|
14891
15028
|
* A flag indicating whether the call was created by the current user.
|
|
14892
15029
|
*/
|
|
14893
15030
|
get isCreatedByMe() {
|
|
14894
|
-
return this.state.createdBy?.id === this.currentUserId;
|
|
15031
|
+
return (this.currentUserId && this.state.createdBy?.id === this.currentUserId);
|
|
14895
15032
|
}
|
|
14896
15033
|
}
|
|
14897
15034
|
|
|
@@ -16015,7 +16152,7 @@ class StreamClient {
|
|
|
16015
16152
|
this.getUserAgent = () => {
|
|
16016
16153
|
if (!this.cachedUserAgent) {
|
|
16017
16154
|
const { clientAppIdentifier = {} } = this.options;
|
|
16018
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
16155
|
+
const { sdkName = 'js', sdkVersion = "1.46.1", ...extras } = clientAppIdentifier;
|
|
16019
16156
|
this.cachedUserAgent = [
|
|
16020
16157
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16021
16158
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|