@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 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
@@ -4784,7 +4784,7 @@ class StreamVideoWriteableStateStore {
4784
4784
  * The currently connected user.
4785
4785
  */
4786
4786
  get connectedUser() {
4787
- return this.connectedUserSubject.getValue();
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.44.6-beta.0";
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 && !this.bindings.has(toBindingKey(sessionId))) {
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
- const reasonToEndCallReason = {
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
- try {
13260
- for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
13261
- try {
13262
- this.logger.trace(`Joining call (${attempt})`, this.cid);
13263
- await this.doJoin(data);
13264
- delete joinData.migrating_from;
13265
- delete joinData.migrating_from_list;
13266
- break;
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
- catch (err) {
13269
- this.logger.warn(`Failed to join call (${attempt})`, this.cid);
13270
- if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
13271
- (err instanceof SfuJoinError && err.unrecoverable)) {
13272
- // if the error is unrecoverable, we should not retry as that signals
13273
- // that connectivity is good, but the coordinator doesn't allow the user
13274
- // to join the call due to some reason (e.g., ended call, expired token...)
13275
- throw err;
13276
- }
13277
- // immediately switch to a different SFU in case of recoverable join error
13278
- const switchSfu = err instanceof SfuJoinError &&
13279
- SfuJoinError.isJoinErrorCode(err.errorEvent);
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 (this.currentUserId && this.state.createdBy?.id === this.currentUserId);
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.44.6-beta.0", ...extras } = clientAppIdentifier;
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}`),