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