@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.es.js
CHANGED
|
@@ -3266,6 +3266,7 @@ class ParticipantJoined$Type extends MessageType {
|
|
|
3266
3266
|
super('stream.video.sfu.event.ParticipantJoined', [
|
|
3267
3267
|
{ no: 1, name: 'call_cid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
3268
3268
|
{ no: 2, name: 'participant', kind: 'message', T: () => Participant },
|
|
3269
|
+
{ no: 3, name: 'is_pinned', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
3269
3270
|
]);
|
|
3270
3271
|
}
|
|
3271
3272
|
}
|
|
@@ -4785,7 +4786,7 @@ class StreamVideoWriteableStateStore {
|
|
|
4785
4786
|
* The currently connected user.
|
|
4786
4787
|
*/
|
|
4787
4788
|
get connectedUser() {
|
|
4788
|
-
return
|
|
4789
|
+
return this.connectedUserSubject.getValue();
|
|
4789
4790
|
}
|
|
4790
4791
|
/**
|
|
4791
4792
|
* A list of {@link Call} objects created/tracked by this client.
|
|
@@ -6284,7 +6285,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6284
6285
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6285
6286
|
};
|
|
6286
6287
|
|
|
6287
|
-
const version = "1.
|
|
6288
|
+
const version = "1.46.1";
|
|
6288
6289
|
const [major, minor, patch] = version.split('.');
|
|
6289
6290
|
let sdkInfo = {
|
|
6290
6291
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -8983,6 +8984,7 @@ const watchCallRejected = (call) => {
|
|
|
8983
8984
|
else {
|
|
8984
8985
|
if (rejectedBy[eventCall.created_by.id]) {
|
|
8985
8986
|
call.logger.info('call creator rejected, leaving call');
|
|
8987
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
8986
8988
|
await call.leave({ message: 'ring: creator rejected' });
|
|
8987
8989
|
}
|
|
8988
8990
|
}
|
|
@@ -8993,6 +8995,7 @@ const watchCallRejected = (call) => {
|
|
|
8993
8995
|
*/
|
|
8994
8996
|
const watchCallEnded = (call) => {
|
|
8995
8997
|
return function onCallEnded() {
|
|
8998
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
8996
8999
|
const { callingState } = call.state;
|
|
8997
9000
|
if (callingState !== CallingState.IDLE &&
|
|
8998
9001
|
callingState !== CallingState.LEFT) {
|
|
@@ -9024,6 +9027,7 @@ const watchSfuCallEnded = (call) => {
|
|
|
9024
9027
|
// update the call state to reflect the call has ended.
|
|
9025
9028
|
call.state.setEndedAt(new Date());
|
|
9026
9029
|
const reason = CallEndedReason[e.reason];
|
|
9030
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9027
9031
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9028
9032
|
}
|
|
9029
9033
|
catch (err) {
|
|
@@ -9219,6 +9223,7 @@ const watchParticipantJoined = (state) => {
|
|
|
9219
9223
|
// already announced participants.
|
|
9220
9224
|
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
9221
9225
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
9226
|
+
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
9222
9227
|
viewportVisibilityState: {
|
|
9223
9228
|
videoTrack: VisibilityState.UNKNOWN,
|
|
9224
9229
|
screenShareTrack: VisibilityState.UNKNOWN,
|
|
@@ -9612,6 +9617,31 @@ class DynascaleManager {
|
|
|
9612
9617
|
this.logger = videoLoggerSystem.getLogger('DynascaleManager');
|
|
9613
9618
|
this.useWebAudio = false;
|
|
9614
9619
|
this.pendingSubscriptionsUpdate = null;
|
|
9620
|
+
/**
|
|
9621
|
+
* Audio elements that were blocked by the browser's autoplay policy.
|
|
9622
|
+
* These can be retried by calling `resumeAudio()` from a user gesture.
|
|
9623
|
+
*/
|
|
9624
|
+
this.blockedAudioElementsSubject = new BehaviorSubject(new Set());
|
|
9625
|
+
/**
|
|
9626
|
+
* Whether the browser's autoplay policy is blocking audio playback.
|
|
9627
|
+
* Will be `true` when the browser blocks autoplay (e.g., no prior user interaction).
|
|
9628
|
+
* Use `resumeAudio()` within a user gesture to unblock.
|
|
9629
|
+
*/
|
|
9630
|
+
this.autoplayBlocked$ = this.blockedAudioElementsSubject.pipe(map((elements) => elements.size > 0), distinctUntilChanged());
|
|
9631
|
+
this.addBlockedAudioElement = (audioElement) => {
|
|
9632
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9633
|
+
const next = new Set(elements);
|
|
9634
|
+
next.add(audioElement);
|
|
9635
|
+
return next;
|
|
9636
|
+
});
|
|
9637
|
+
};
|
|
9638
|
+
this.removeBlockedAudioElement = (audioElement) => {
|
|
9639
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9640
|
+
const nextElements = new Set(elements);
|
|
9641
|
+
nextElements.delete(audioElement);
|
|
9642
|
+
return nextElements;
|
|
9643
|
+
});
|
|
9644
|
+
};
|
|
9615
9645
|
this.videoTrackSubscriptionOverridesSubject = new BehaviorSubject({});
|
|
9616
9646
|
this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
|
|
9617
9647
|
this.incomingVideoSettings$ = this.videoTrackSubscriptionOverrides$.pipe(map((overrides) => {
|
|
@@ -9643,6 +9673,7 @@ class DynascaleManager {
|
|
|
9643
9673
|
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
9644
9674
|
}
|
|
9645
9675
|
this.audioBindingsWatchdog?.dispose();
|
|
9676
|
+
setCurrentValue(this.blockedAudioElementsSubject, new Set());
|
|
9646
9677
|
const context = this.audioContext;
|
|
9647
9678
|
if (context && context.state !== 'closed') {
|
|
9648
9679
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -9940,8 +9971,10 @@ class DynascaleManager {
|
|
|
9940
9971
|
return;
|
|
9941
9972
|
setTimeout(() => {
|
|
9942
9973
|
audioElement.srcObject = source ?? null;
|
|
9943
|
-
if (!source)
|
|
9974
|
+
if (!source) {
|
|
9975
|
+
this.removeBlockedAudioElement(audioElement);
|
|
9944
9976
|
return;
|
|
9977
|
+
}
|
|
9945
9978
|
// Safari has a special quirk that prevents playing audio until the user
|
|
9946
9979
|
// interacts with the page or focuses on the tab where the call happens.
|
|
9947
9980
|
// This is a workaround for the issue where:
|
|
@@ -9965,6 +9998,10 @@ class DynascaleManager {
|
|
|
9965
9998
|
audioElement.muted = false;
|
|
9966
9999
|
audioElement.play().catch((e) => {
|
|
9967
10000
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
10001
|
+
if (e.name === 'NotAllowedError') {
|
|
10002
|
+
this.tracer.trace('audioPlaybackBlocked', null);
|
|
10003
|
+
this.addBlockedAudioElement(audioElement);
|
|
10004
|
+
}
|
|
9968
10005
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
9969
10006
|
});
|
|
9970
10007
|
}
|
|
@@ -9991,6 +10028,7 @@ class DynascaleManager {
|
|
|
9991
10028
|
audioElement.autoplay = true;
|
|
9992
10029
|
return () => {
|
|
9993
10030
|
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
10031
|
+
this.removeBlockedAudioElement(audioElement);
|
|
9994
10032
|
sinkIdSubscription?.unsubscribe();
|
|
9995
10033
|
volumeSubscription.unsubscribe();
|
|
9996
10034
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -9999,6 +10037,28 @@ class DynascaleManager {
|
|
|
9999
10037
|
gainNode?.disconnect();
|
|
10000
10038
|
};
|
|
10001
10039
|
};
|
|
10040
|
+
/**
|
|
10041
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
10042
|
+
* Must be called from within a user gesture (e.g., click handler).
|
|
10043
|
+
*
|
|
10044
|
+
* @returns a promise that resolves when all blocked elements have been retried.
|
|
10045
|
+
*/
|
|
10046
|
+
this.resumeAudio = async () => {
|
|
10047
|
+
this.tracer.trace('resumeAudio', null);
|
|
10048
|
+
const blocked = new Set();
|
|
10049
|
+
await Promise.all(Array.from(getCurrentValue(this.blockedAudioElementsSubject), async (el) => {
|
|
10050
|
+
try {
|
|
10051
|
+
if (el.srcObject) {
|
|
10052
|
+
await el.play();
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
10055
|
+
catch {
|
|
10056
|
+
this.logger.warn(`Can't resume audio for element: `, el);
|
|
10057
|
+
blocked.add(el);
|
|
10058
|
+
}
|
|
10059
|
+
}));
|
|
10060
|
+
setCurrentValue(this.blockedAudioElementsSubject, blocked);
|
|
10061
|
+
};
|
|
10002
10062
|
this.getOrCreateAudioContext = () => {
|
|
10003
10063
|
if (!this.useWebAudio)
|
|
10004
10064
|
return;
|
|
@@ -11868,31 +11928,43 @@ class RNSpeechDetector {
|
|
|
11868
11928
|
constructor(externalAudioStream) {
|
|
11869
11929
|
this.pc1 = new RTCPeerConnection({});
|
|
11870
11930
|
this.pc2 = new RTCPeerConnection({});
|
|
11931
|
+
this.isStopped = false;
|
|
11871
11932
|
this.externalAudioStream = externalAudioStream;
|
|
11872
11933
|
}
|
|
11873
11934
|
/**
|
|
11874
11935
|
* Starts the speech detection.
|
|
11875
11936
|
*/
|
|
11876
11937
|
async start(onSoundDetectedStateChanged) {
|
|
11938
|
+
let detachListeners;
|
|
11939
|
+
let unsubscribe;
|
|
11877
11940
|
try {
|
|
11941
|
+
this.isStopped = false;
|
|
11878
11942
|
const audioStream = this.externalAudioStream != null
|
|
11879
11943
|
? this.externalAudioStream
|
|
11880
11944
|
: await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
11881
11945
|
this.audioStream = audioStream;
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
}
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
}
|
|
11888
|
-
|
|
11946
|
+
const onPc1IceCandidate = (e) => {
|
|
11947
|
+
this.forwardIceCandidate(this.pc2, e.candidate);
|
|
11948
|
+
};
|
|
11949
|
+
const onPc2IceCandidate = (e) => {
|
|
11950
|
+
this.forwardIceCandidate(this.pc1, e.candidate);
|
|
11951
|
+
};
|
|
11952
|
+
const onTrackPc2 = (e) => {
|
|
11889
11953
|
e.streams[0].getTracks().forEach((track) => {
|
|
11890
11954
|
// In RN, the remote track is automatically added to the audio output device
|
|
11891
11955
|
// so we need to mute it to avoid hearing the audio back
|
|
11892
11956
|
// @ts-expect-error _setVolume is a private method in react-native-webrtc
|
|
11893
11957
|
track._setVolume(0);
|
|
11894
11958
|
});
|
|
11895
|
-
}
|
|
11959
|
+
};
|
|
11960
|
+
this.pc1.addEventListener('icecandidate', onPc1IceCandidate);
|
|
11961
|
+
this.pc2.addEventListener('icecandidate', onPc2IceCandidate);
|
|
11962
|
+
this.pc2.addEventListener('track', onTrackPc2);
|
|
11963
|
+
detachListeners = () => {
|
|
11964
|
+
this.pc1.removeEventListener('icecandidate', onPc1IceCandidate);
|
|
11965
|
+
this.pc2.removeEventListener('icecandidate', onPc2IceCandidate);
|
|
11966
|
+
this.pc2.removeEventListener('track', onTrackPc2);
|
|
11967
|
+
};
|
|
11896
11968
|
audioStream
|
|
11897
11969
|
.getTracks()
|
|
11898
11970
|
.forEach((track) => this.pc1.addTrack(track, audioStream));
|
|
@@ -11902,13 +11974,17 @@ class RNSpeechDetector {
|
|
|
11902
11974
|
const answer = await this.pc2.createAnswer();
|
|
11903
11975
|
await this.pc1.setRemoteDescription(answer);
|
|
11904
11976
|
await this.pc2.setLocalDescription(answer);
|
|
11905
|
-
|
|
11977
|
+
unsubscribe = this.onSpeakingDetectedStateChange(onSoundDetectedStateChanged);
|
|
11906
11978
|
return () => {
|
|
11907
|
-
|
|
11979
|
+
detachListeners?.();
|
|
11980
|
+
unsubscribe?.();
|
|
11908
11981
|
this.stop();
|
|
11909
11982
|
};
|
|
11910
11983
|
}
|
|
11911
11984
|
catch (error) {
|
|
11985
|
+
detachListeners?.();
|
|
11986
|
+
unsubscribe?.();
|
|
11987
|
+
this.stop();
|
|
11912
11988
|
const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
|
|
11913
11989
|
logger.error('error handling permissions: ', error);
|
|
11914
11990
|
return () => { };
|
|
@@ -11918,6 +11994,9 @@ class RNSpeechDetector {
|
|
|
11918
11994
|
* Stops the speech detection and releases all allocated resources.
|
|
11919
11995
|
*/
|
|
11920
11996
|
stop() {
|
|
11997
|
+
if (this.isStopped)
|
|
11998
|
+
return;
|
|
11999
|
+
this.isStopped = true;
|
|
11921
12000
|
this.pc1.close();
|
|
11922
12001
|
this.pc2.close();
|
|
11923
12002
|
if (this.externalAudioStream != null) {
|
|
@@ -12017,6 +12096,18 @@ class RNSpeechDetector {
|
|
|
12017
12096
|
this.audioStream.release();
|
|
12018
12097
|
}
|
|
12019
12098
|
}
|
|
12099
|
+
forwardIceCandidate(destination, candidate) {
|
|
12100
|
+
if (this.isStopped ||
|
|
12101
|
+
!candidate ||
|
|
12102
|
+
destination.signalingState === 'closed') {
|
|
12103
|
+
return;
|
|
12104
|
+
}
|
|
12105
|
+
destination.addIceCandidate(candidate).catch(() => {
|
|
12106
|
+
// silently ignore the error
|
|
12107
|
+
const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
|
|
12108
|
+
logger.info('cannot add ice candidate - ignoring');
|
|
12109
|
+
});
|
|
12110
|
+
}
|
|
12020
12111
|
}
|
|
12021
12112
|
|
|
12022
12113
|
class MicrophoneManager extends AudioDeviceManager {
|
|
@@ -12112,6 +12203,7 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12112
12203
|
const deviceId = this.state.selectedDevice;
|
|
12113
12204
|
const devices = await firstValueFrom(this.listDevices());
|
|
12114
12205
|
const label = devices.find((d) => d.deviceId === deviceId)?.label;
|
|
12206
|
+
let lastCapturesAudio;
|
|
12115
12207
|
this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
|
|
12116
12208
|
noAudioThresholdMs: this.silenceThresholdMs,
|
|
12117
12209
|
emitIntervalMs: this.silenceThresholdMs,
|
|
@@ -12123,7 +12215,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12123
12215
|
deviceId,
|
|
12124
12216
|
label,
|
|
12125
12217
|
};
|
|
12126
|
-
|
|
12218
|
+
if (capturesAudio !== lastCapturesAudio) {
|
|
12219
|
+
lastCapturesAudio = capturesAudio;
|
|
12220
|
+
this.call.tracer.trace('mic.capture_report', event);
|
|
12221
|
+
}
|
|
12127
12222
|
this.call.streamClient.dispatchEvent(event);
|
|
12128
12223
|
},
|
|
12129
12224
|
});
|
|
@@ -12645,6 +12740,7 @@ class SpeakerManager {
|
|
|
12645
12740
|
this.defaultDevice = defaultDevice;
|
|
12646
12741
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
12647
12742
|
defaultDevice,
|
|
12743
|
+
isRingingTypeCall: this.call.ringing,
|
|
12648
12744
|
});
|
|
12649
12745
|
}
|
|
12650
12746
|
}
|
|
@@ -12844,6 +12940,7 @@ class Call {
|
|
|
12844
12940
|
const currentUserId = this.currentUserId;
|
|
12845
12941
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
12846
12942
|
this.logger.info('Leaving call because of being blocked');
|
|
12943
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
|
|
12847
12944
|
await this.leave({ message: 'user blocked' }).catch((err) => {
|
|
12848
12945
|
this.logger.error('Error leaving call after being blocked', err);
|
|
12849
12946
|
});
|
|
@@ -12880,6 +12977,7 @@ class Call {
|
|
|
12880
12977
|
const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === CallingState.RINGING;
|
|
12881
12978
|
if ((isAcceptedElsewhere || isRejectedByMe) &&
|
|
12882
12979
|
!hasPending(this.joinLeaveConcurrencyTag)) {
|
|
12980
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
|
|
12883
12981
|
this.leave().catch(() => {
|
|
12884
12982
|
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
12885
12983
|
});
|
|
@@ -12891,6 +12989,9 @@ class Call {
|
|
|
12891
12989
|
const receiver_id = this.clientStore.connectedUser?.id;
|
|
12892
12990
|
const ended_at = callSession?.ended_at;
|
|
12893
12991
|
const created_by_id = this.state.createdBy?.id;
|
|
12992
|
+
if (this.currentUserId && created_by_id === this.currentUserId) {
|
|
12993
|
+
globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
|
|
12994
|
+
}
|
|
12894
12995
|
const rejected_by = callSession?.rejected_by;
|
|
12895
12996
|
const accepted_by = callSession?.accepted_by;
|
|
12896
12997
|
let leaveCallIdle = false;
|
|
@@ -13029,7 +13130,16 @@ class Call {
|
|
|
13029
13130
|
}
|
|
13030
13131
|
if (callingState === CallingState.RINGING && reject !== false) {
|
|
13031
13132
|
if (reject) {
|
|
13032
|
-
|
|
13133
|
+
const reasonToEndCallReason = {
|
|
13134
|
+
timeout: 'missed',
|
|
13135
|
+
cancel: 'canceled',
|
|
13136
|
+
busy: 'busy',
|
|
13137
|
+
decline: 'rejected',
|
|
13138
|
+
};
|
|
13139
|
+
const rejectReason = reason ?? 'decline';
|
|
13140
|
+
const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
|
|
13141
|
+
await this.reject(rejectReason);
|
|
13142
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
13033
13143
|
}
|
|
13034
13144
|
else {
|
|
13035
13145
|
// if reject was undefined, we still have to cancel the call automatically
|
|
@@ -13037,9 +13147,11 @@ class Call {
|
|
|
13037
13147
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
13038
13148
|
if (this.isCreatedByMe && !hasOtherParticipants) {
|
|
13039
13149
|
await this.reject('cancel');
|
|
13150
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
13040
13151
|
}
|
|
13041
13152
|
}
|
|
13042
13153
|
}
|
|
13154
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
13043
13155
|
this.statsReporter?.stop();
|
|
13044
13156
|
this.statsReporter = undefined;
|
|
13045
13157
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -13066,7 +13178,9 @@ class Call {
|
|
|
13066
13178
|
this.ringingSubject.next(false);
|
|
13067
13179
|
this.cancelAutoDrop();
|
|
13068
13180
|
this.clientStore.unregisterCall(this);
|
|
13069
|
-
globalThis.streamRNVideoSDK?.callManager.stop(
|
|
13181
|
+
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
13182
|
+
isRingingTypeCall: this.ringing,
|
|
13183
|
+
});
|
|
13070
13184
|
this.camera.dispose();
|
|
13071
13185
|
this.microphone.dispose();
|
|
13072
13186
|
this.screenShare.dispose();
|
|
@@ -13232,11 +13346,19 @@ class Call {
|
|
|
13232
13346
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
13233
13347
|
*/
|
|
13234
13348
|
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
13235
|
-
await this.setup();
|
|
13236
13349
|
const callingState = this.state.callingState;
|
|
13237
13350
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
13238
13351
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
13239
13352
|
}
|
|
13353
|
+
if (data?.ring) {
|
|
13354
|
+
this.ringingSubject.next(true);
|
|
13355
|
+
}
|
|
13356
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
13357
|
+
if (callingX) {
|
|
13358
|
+
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
13359
|
+
await callingX.joinCall(this, this.clientStore.calls);
|
|
13360
|
+
}
|
|
13361
|
+
await this.setup();
|
|
13240
13362
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
13241
13363
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
13242
13364
|
// we will count the number of join failures per SFU.
|
|
@@ -13245,38 +13367,44 @@ class Call {
|
|
|
13245
13367
|
const sfuJoinFailures = new Map();
|
|
13246
13368
|
const joinData = data;
|
|
13247
13369
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
catch (err) {
|
|
13257
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
13258
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
13259
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
13260
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
13261
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
13262
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
13263
|
-
throw err;
|
|
13264
|
-
}
|
|
13265
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
13266
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
13267
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
13268
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
13269
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
13270
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
13271
|
-
if (switchSfu || failures >= 2) {
|
|
13272
|
-
joinData.migrating_from = sfuId;
|
|
13273
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
13370
|
+
try {
|
|
13371
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
13372
|
+
try {
|
|
13373
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
13374
|
+
await this.doJoin(data);
|
|
13375
|
+
delete joinData.migrating_from;
|
|
13376
|
+
delete joinData.migrating_from_list;
|
|
13377
|
+
break;
|
|
13274
13378
|
}
|
|
13275
|
-
|
|
13276
|
-
|
|
13379
|
+
catch (err) {
|
|
13380
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
13381
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
13382
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
13383
|
+
// if the error is unrecoverable, we should not retry as that signals
|
|
13384
|
+
// that connectivity is good, but the coordinator doesn't allow the user
|
|
13385
|
+
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
13386
|
+
throw err;
|
|
13387
|
+
}
|
|
13388
|
+
// immediately switch to a different SFU in case of recoverable join error
|
|
13389
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
13390
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
13391
|
+
const sfuId = this.credentials?.server.edge_name || '';
|
|
13392
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
13393
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
13394
|
+
if (switchSfu || failures >= 2) {
|
|
13395
|
+
joinData.migrating_from = sfuId;
|
|
13396
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
13397
|
+
}
|
|
13398
|
+
if (attempt === maxJoinRetries - 1) {
|
|
13399
|
+
throw err;
|
|
13400
|
+
}
|
|
13277
13401
|
}
|
|
13402
|
+
await sleep(retryInterval(attempt));
|
|
13278
13403
|
}
|
|
13279
|
-
|
|
13404
|
+
}
|
|
13405
|
+
catch (error) {
|
|
13406
|
+
callingX?.endCall(this, 'error');
|
|
13407
|
+
throw error;
|
|
13280
13408
|
}
|
|
13281
13409
|
};
|
|
13282
13410
|
/**
|
|
@@ -13423,7 +13551,9 @@ class Call {
|
|
|
13423
13551
|
// re-apply them on later reconnections or server-side data fetches
|
|
13424
13552
|
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
13425
13553
|
await this.applyDeviceConfig(this.state.settings, true, false);
|
|
13426
|
-
globalThis.streamRNVideoSDK?.callManager.start(
|
|
13554
|
+
globalThis.streamRNVideoSDK?.callManager.start({
|
|
13555
|
+
isRingingTypeCall: this.ringing,
|
|
13556
|
+
});
|
|
13427
13557
|
this.deviceSettingsAppliedOnce = true;
|
|
13428
13558
|
}
|
|
13429
13559
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -13851,6 +13981,7 @@ class Call {
|
|
|
13851
13981
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13852
13982
|
return;
|
|
13853
13983
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
13984
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
|
|
13854
13985
|
this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
|
|
13855
13986
|
this.logger.warn(`Can't leave call after disconnect request`, err);
|
|
13856
13987
|
});
|
|
@@ -14755,6 +14886,12 @@ class Call {
|
|
|
14755
14886
|
unbind();
|
|
14756
14887
|
};
|
|
14757
14888
|
};
|
|
14889
|
+
/**
|
|
14890
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
14891
|
+
*/
|
|
14892
|
+
this.resumeAudio = () => {
|
|
14893
|
+
return this.dynascaleManager.resumeAudio();
|
|
14894
|
+
};
|
|
14758
14895
|
/**
|
|
14759
14896
|
* Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
|
|
14760
14897
|
*
|
|
@@ -14872,7 +15009,7 @@ class Call {
|
|
|
14872
15009
|
* A flag indicating whether the call was created by the current user.
|
|
14873
15010
|
*/
|
|
14874
15011
|
get isCreatedByMe() {
|
|
14875
|
-
return this.state.createdBy?.id === this.currentUserId;
|
|
15012
|
+
return (this.currentUserId && this.state.createdBy?.id === this.currentUserId);
|
|
14876
15013
|
}
|
|
14877
15014
|
}
|
|
14878
15015
|
|
|
@@ -15996,7 +16133,7 @@ class StreamClient {
|
|
|
15996
16133
|
this.getUserAgent = () => {
|
|
15997
16134
|
if (!this.cachedUserAgent) {
|
|
15998
16135
|
const { clientAppIdentifier = {} } = this.options;
|
|
15999
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
16136
|
+
const { sdkName = 'js', sdkVersion = "1.46.1", ...extras } = clientAppIdentifier;
|
|
16000
16137
|
this.cachedUserAgent = [
|
|
16001
16138
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16002
16139
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|