@stream-io/video-client 1.44.6-beta.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/index.browser.es.js +58 -79
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +58 -79
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +58 -79
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +1 -1
- package/dist/src/coordinator/connection/types.d.ts +22 -1
- package/dist/src/devices/DeviceManager.d.ts +1 -0
- package/dist/src/types.d.ts +5 -37
- package/package.json +1 -1
- package/src/Call.ts +40 -85
- package/src/coordinator/connection/types.ts +23 -0
- package/src/devices/DeviceManager.ts +20 -1
- package/src/devices/SpeakerManager.ts +0 -1
- package/src/devices/__tests__/DeviceManager.test.ts +8 -0
- package/src/devices/__tests__/mocks.ts +4 -0
- package/src/events/call.ts +0 -3
- package/src/helpers/AudioBindingsWatchdog.ts +14 -2
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +27 -1
- package/src/store/stateStore.ts +1 -1
- package/src/types.ts +5 -48
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [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
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **client:** Disconnected device event ([#2178](https://github.com/GetStream/stream-video-js/issues/2178)) ([5017ca0](https://github.com/GetStream/stream-video-js/commit/5017ca0fd53f5d203167d55227cb7fddc055705a))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
- **client:** warn about dangling audio bindings only for published audio tracks ([#2183](https://github.com/GetStream/stream-video-js/issues/2183)) ([ff47662](https://github.com/GetStream/stream-video-js/commit/ff47662484cd666cf321b61d9b49dd4eb161192f))
|
|
14
|
+
|
|
5
15
|
## [1.44.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.44.4...@stream-io/video-client-1.44.5) (2026-03-27)
|
|
6
16
|
|
|
7
17
|
### Bug Fixes
|
package/dist/index.browser.es.js
CHANGED
|
@@ -4784,7 +4784,7 @@ class StreamVideoWriteableStateStore {
|
|
|
4784
4784
|
* The currently connected user.
|
|
4785
4785
|
*/
|
|
4786
4786
|
get connectedUser() {
|
|
4787
|
-
return this.connectedUserSubject
|
|
4787
|
+
return getCurrentValue(this.connectedUserSubject);
|
|
4788
4788
|
}
|
|
4789
4789
|
/**
|
|
4790
4790
|
* A list of {@link Call} objects created/tracked by this client.
|
|
@@ -6283,7 +6283,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6283
6283
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6284
6284
|
};
|
|
6285
6285
|
|
|
6286
|
-
const version = "1.
|
|
6286
|
+
const version = "1.45.0";
|
|
6287
6287
|
const [major, minor, patch] = version.split('.');
|
|
6288
6288
|
let sdkInfo = {
|
|
6289
6289
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -8982,7 +8982,6 @@ const watchCallRejected = (call) => {
|
|
|
8982
8982
|
else {
|
|
8983
8983
|
if (rejectedBy[eventCall.created_by.id]) {
|
|
8984
8984
|
call.logger.info('call creator rejected, leaving call');
|
|
8985
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
8986
8985
|
await call.leave({ message: 'ring: creator rejected' });
|
|
8987
8986
|
}
|
|
8988
8987
|
}
|
|
@@ -8993,7 +8992,6 @@ const watchCallRejected = (call) => {
|
|
|
8993
8992
|
*/
|
|
8994
8993
|
const watchCallEnded = (call) => {
|
|
8995
8994
|
return function onCallEnded() {
|
|
8996
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
8997
8995
|
const { callingState } = call.state;
|
|
8998
8996
|
if (callingState !== CallingState.IDLE &&
|
|
8999
8997
|
callingState !== CallingState.LEFT) {
|
|
@@ -9025,7 +9023,6 @@ const watchSfuCallEnded = (call) => {
|
|
|
9025
9023
|
// update the call state to reflect the call has ended.
|
|
9026
9024
|
call.state.setEndedAt(new Date());
|
|
9027
9025
|
const reason = CallEndedReason[e.reason];
|
|
9028
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9029
9026
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9030
9027
|
}
|
|
9031
9028
|
catch (err) {
|
|
@@ -9552,11 +9549,14 @@ class AudioBindingsWatchdog {
|
|
|
9552
9549
|
for (const p of this.state.participants) {
|
|
9553
9550
|
if (p.isLocalParticipant)
|
|
9554
9551
|
continue;
|
|
9555
|
-
const { audioStream, screenShareAudioStream, sessionId, userId } = p;
|
|
9556
|
-
if (audioStream &&
|
|
9552
|
+
const { audioStream, screenShareAudioStream, sessionId, userId, publishedTracks, } = p;
|
|
9553
|
+
if (audioStream &&
|
|
9554
|
+
publishedTracks.includes(TrackType.AUDIO) &&
|
|
9555
|
+
!this.bindings.has(toBindingKey(sessionId))) {
|
|
9557
9556
|
danglingUserIds.push(userId);
|
|
9558
9557
|
}
|
|
9559
9558
|
if (screenShareAudioStream &&
|
|
9559
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
9560
9560
|
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
9561
9561
|
danglingUserIds.push(userId);
|
|
9562
9562
|
}
|
|
@@ -11190,6 +11190,7 @@ class DeviceManager {
|
|
|
11190
11190
|
isDeviceReplaced = true;
|
|
11191
11191
|
}
|
|
11192
11192
|
if (isDeviceDisconnected) {
|
|
11193
|
+
this.dispatchDeviceDisconnectedEvent(prevDevice);
|
|
11193
11194
|
await this.disable();
|
|
11194
11195
|
await this.select(undefined);
|
|
11195
11196
|
}
|
|
@@ -11199,7 +11200,7 @@ class DeviceManager {
|
|
|
11199
11200
|
await this.enable();
|
|
11200
11201
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
11201
11202
|
}
|
|
11202
|
-
else {
|
|
11203
|
+
else if (!hasPending(this.statusChangeConcurrencyTag)) {
|
|
11203
11204
|
await this.applySettingsToStream();
|
|
11204
11205
|
}
|
|
11205
11206
|
}
|
|
@@ -11213,6 +11214,20 @@ class DeviceManager {
|
|
|
11213
11214
|
const kind = this.mediaDeviceKind;
|
|
11214
11215
|
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
11215
11216
|
}
|
|
11217
|
+
dispatchDeviceDisconnectedEvent(device) {
|
|
11218
|
+
const event = {
|
|
11219
|
+
type: 'device.disconnected',
|
|
11220
|
+
call_cid: this.call.cid,
|
|
11221
|
+
status: this.isTrackStoppedDueToTrackEnd
|
|
11222
|
+
? this.state.prevStatus
|
|
11223
|
+
: this.state.status,
|
|
11224
|
+
deviceId: device.deviceId,
|
|
11225
|
+
label: device.label,
|
|
11226
|
+
kind: device.kind,
|
|
11227
|
+
};
|
|
11228
|
+
this.call.tracer.trace('device.disconnected', event);
|
|
11229
|
+
this.call.streamClient.dispatchEvent(event);
|
|
11230
|
+
}
|
|
11216
11231
|
persistPreference(selectedDevice, status) {
|
|
11217
11232
|
const deviceKind = this.mediaDeviceKind;
|
|
11218
11233
|
const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
|
|
@@ -12629,7 +12644,6 @@ class SpeakerManager {
|
|
|
12629
12644
|
this.defaultDevice = defaultDevice;
|
|
12630
12645
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
12631
12646
|
defaultDevice,
|
|
12632
|
-
isRingingTypeCall: this.call.ringing,
|
|
12633
12647
|
});
|
|
12634
12648
|
}
|
|
12635
12649
|
}
|
|
@@ -12829,7 +12843,6 @@ class Call {
|
|
|
12829
12843
|
const currentUserId = this.currentUserId;
|
|
12830
12844
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
12831
12845
|
this.logger.info('Leaving call because of being blocked');
|
|
12832
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
|
|
12833
12846
|
await this.leave({ message: 'user blocked' }).catch((err) => {
|
|
12834
12847
|
this.logger.error('Error leaving call after being blocked', err);
|
|
12835
12848
|
});
|
|
@@ -12866,7 +12879,6 @@ class Call {
|
|
|
12866
12879
|
const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === CallingState.RINGING;
|
|
12867
12880
|
if ((isAcceptedElsewhere || isRejectedByMe) &&
|
|
12868
12881
|
!hasPending(this.joinLeaveConcurrencyTag)) {
|
|
12869
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
|
|
12870
12882
|
this.leave().catch(() => {
|
|
12871
12883
|
this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
|
|
12872
12884
|
});
|
|
@@ -12878,9 +12890,6 @@ class Call {
|
|
|
12878
12890
|
const receiver_id = this.clientStore.connectedUser?.id;
|
|
12879
12891
|
const ended_at = callSession?.ended_at;
|
|
12880
12892
|
const created_by_id = this.state.createdBy?.id;
|
|
12881
|
-
if (this.currentUserId && created_by_id === this.currentUserId) {
|
|
12882
|
-
globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
|
|
12883
|
-
}
|
|
12884
12893
|
const rejected_by = callSession?.rejected_by;
|
|
12885
12894
|
const accepted_by = callSession?.accepted_by;
|
|
12886
12895
|
let leaveCallIdle = false;
|
|
@@ -13019,28 +13028,17 @@ class Call {
|
|
|
13019
13028
|
}
|
|
13020
13029
|
if (callingState === CallingState.RINGING && reject !== false) {
|
|
13021
13030
|
if (reject) {
|
|
13022
|
-
|
|
13023
|
-
timeout: 'missed',
|
|
13024
|
-
cancel: 'canceled',
|
|
13025
|
-
busy: 'busy',
|
|
13026
|
-
decline: 'rejected',
|
|
13027
|
-
};
|
|
13028
|
-
const rejectReason = reason ?? 'decline';
|
|
13029
|
-
const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
|
|
13030
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
13031
|
-
await this.reject(rejectReason);
|
|
13031
|
+
await this.reject(reason ?? 'decline');
|
|
13032
13032
|
}
|
|
13033
13033
|
else {
|
|
13034
13034
|
// if reject was undefined, we still have to cancel the call automatically
|
|
13035
13035
|
// when I am the creator and everyone else left the call
|
|
13036
13036
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
13037
13037
|
if (this.isCreatedByMe && !hasOtherParticipants) {
|
|
13038
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
13039
13038
|
await this.reject('cancel');
|
|
13040
13039
|
}
|
|
13041
13040
|
}
|
|
13042
13041
|
}
|
|
13043
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
13044
13042
|
this.statsReporter?.stop();
|
|
13045
13043
|
this.statsReporter = undefined;
|
|
13046
13044
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -13067,9 +13065,7 @@ class Call {
|
|
|
13067
13065
|
this.ringingSubject.next(false);
|
|
13068
13066
|
this.cancelAutoDrop();
|
|
13069
13067
|
this.clientStore.unregisterCall(this);
|
|
13070
|
-
globalThis.streamRNVideoSDK?.callManager.stop(
|
|
13071
|
-
isRingingTypeCall: this.ringing,
|
|
13072
|
-
});
|
|
13068
|
+
globalThis.streamRNVideoSDK?.callManager.stop();
|
|
13073
13069
|
this.camera.dispose();
|
|
13074
13070
|
this.microphone.dispose();
|
|
13075
13071
|
this.screenShare.dispose();
|
|
@@ -13235,19 +13231,11 @@ class Call {
|
|
|
13235
13231
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
13236
13232
|
*/
|
|
13237
13233
|
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
13234
|
+
await this.setup();
|
|
13238
13235
|
const callingState = this.state.callingState;
|
|
13239
13236
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
13240
13237
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
13241
13238
|
}
|
|
13242
|
-
if (data?.ring) {
|
|
13243
|
-
this.ringingSubject.next(true);
|
|
13244
|
-
}
|
|
13245
|
-
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
13246
|
-
if (callingX) {
|
|
13247
|
-
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
13248
|
-
await callingX.joinCall(this, this.clientStore.calls);
|
|
13249
|
-
}
|
|
13250
|
-
await this.setup();
|
|
13251
13239
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
13252
13240
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
13253
13241
|
// we will count the number of join failures per SFU.
|
|
@@ -13256,44 +13244,38 @@ class Call {
|
|
|
13256
13244
|
const sfuJoinFailures = new Map();
|
|
13257
13245
|
const joinData = data;
|
|
13258
13246
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13266
|
-
|
|
13247
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
13248
|
+
try {
|
|
13249
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
13250
|
+
await this.doJoin(data);
|
|
13251
|
+
delete joinData.migrating_from;
|
|
13252
|
+
delete joinData.migrating_from_list;
|
|
13253
|
+
break;
|
|
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;
|
|
13267
13263
|
}
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
13281
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
13282
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
13283
|
-
if (switchSfu || failures >= 2) {
|
|
13284
|
-
joinData.migrating_from = sfuId;
|
|
13285
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
13286
|
-
}
|
|
13287
|
-
if (attempt === maxJoinRetries - 1) {
|
|
13288
|
-
throw err;
|
|
13289
|
-
}
|
|
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());
|
|
13273
|
+
}
|
|
13274
|
+
if (attempt === maxJoinRetries - 1) {
|
|
13275
|
+
throw err;
|
|
13290
13276
|
}
|
|
13291
|
-
await sleep(retryInterval(attempt));
|
|
13292
13277
|
}
|
|
13293
|
-
|
|
13294
|
-
catch (error) {
|
|
13295
|
-
callingX?.endCall(this, 'error');
|
|
13296
|
-
throw error;
|
|
13278
|
+
await sleep(retryInterval(attempt));
|
|
13297
13279
|
}
|
|
13298
13280
|
};
|
|
13299
13281
|
/**
|
|
@@ -13440,9 +13422,7 @@ class Call {
|
|
|
13440
13422
|
// re-apply them on later reconnections or server-side data fetches
|
|
13441
13423
|
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
13442
13424
|
await this.applyDeviceConfig(this.state.settings, true, false);
|
|
13443
|
-
globalThis.streamRNVideoSDK?.callManager.start(
|
|
13444
|
-
isRingingTypeCall: this.ringing,
|
|
13445
|
-
});
|
|
13425
|
+
globalThis.streamRNVideoSDK?.callManager.start();
|
|
13446
13426
|
this.deviceSettingsAppliedOnce = true;
|
|
13447
13427
|
}
|
|
13448
13428
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -13870,7 +13850,6 @@ class Call {
|
|
|
13870
13850
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13871
13851
|
return;
|
|
13872
13852
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
13873
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
|
|
13874
13853
|
this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
|
|
13875
13854
|
this.logger.warn(`Can't leave call after disconnect request`, err);
|
|
13876
13855
|
});
|
|
@@ -14892,7 +14871,7 @@ class Call {
|
|
|
14892
14871
|
* A flag indicating whether the call was created by the current user.
|
|
14893
14872
|
*/
|
|
14894
14873
|
get isCreatedByMe() {
|
|
14895
|
-
return
|
|
14874
|
+
return this.state.createdBy?.id === this.currentUserId;
|
|
14896
14875
|
}
|
|
14897
14876
|
}
|
|
14898
14877
|
|
|
@@ -16018,7 +15997,7 @@ class StreamClient {
|
|
|
16018
15997
|
this.getUserAgent = () => {
|
|
16019
15998
|
if (!this.cachedUserAgent) {
|
|
16020
15999
|
const { clientAppIdentifier = {} } = this.options;
|
|
16021
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
16000
|
+
const { sdkName = 'js', sdkVersion = "1.45.0", ...extras } = clientAppIdentifier;
|
|
16022
16001
|
this.cachedUserAgent = [
|
|
16023
16002
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16024
16003
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|