@stream-io/video-client 1.44.6-beta.0 → 1.46.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,28 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [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)
6
+
7
+ ### Features
8
+
9
+ - callkit/telecom integration ([#2028](https://github.com/GetStream/stream-video-js/issues/2028)) ([d579acd](https://github.com/GetStream/stream-video-js/commit/d579acd1975fb4945e40452b27e372694c737628))
10
+ - **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))
11
+
12
+ ### Bug Fixes
13
+
14
+ - **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))
15
+ - **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))
16
+
17
+ ## [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)
18
+
19
+ ### Features
20
+
21
+ - **client:** Disconnected device event ([#2178](https://github.com/GetStream/stream-video-js/issues/2178)) ([5017ca0](https://github.com/GetStream/stream-video-js/commit/5017ca0fd53f5d203167d55227cb7fddc055705a))
22
+
23
+ ### Bug Fixes
24
+
25
+ - **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))
26
+
5
27
  ## [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
28
 
7
29
  ### Bug Fixes
@@ -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
  }
@@ -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.44.6-beta.0";
6287
+ const version = "1.46.0";
6287
6288
  const [major, minor, patch] = version.split('.');
6288
6289
  let sdkInfo = {
6289
6290
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -9221,6 +9222,7 @@ const watchParticipantJoined = (state) => {
9221
9222
  // already announced participants.
9222
9223
  const orphanedTracks = reconcileOrphanedTracks(state, participant);
9223
9224
  state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
9225
+ ...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
9224
9226
  viewportVisibilityState: {
9225
9227
  videoTrack: VisibilityState.UNKNOWN,
9226
9228
  screenShareTrack: VisibilityState.UNKNOWN,
@@ -9552,11 +9554,14 @@ class AudioBindingsWatchdog {
9552
9554
  for (const p of this.state.participants) {
9553
9555
  if (p.isLocalParticipant)
9554
9556
  continue;
9555
- const { audioStream, screenShareAudioStream, sessionId, userId } = p;
9556
- if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
9557
+ const { audioStream, screenShareAudioStream, sessionId, userId, publishedTracks, } = p;
9558
+ if (audioStream &&
9559
+ publishedTracks.includes(TrackType.AUDIO) &&
9560
+ !this.bindings.has(toBindingKey(sessionId))) {
9557
9561
  danglingUserIds.push(userId);
9558
9562
  }
9559
9563
  if (screenShareAudioStream &&
9564
+ publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
9560
9565
  !this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
9561
9566
  danglingUserIds.push(userId);
9562
9567
  }
@@ -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;
@@ -11190,6 +11250,7 @@ class DeviceManager {
11190
11250
  isDeviceReplaced = true;
11191
11251
  }
11192
11252
  if (isDeviceDisconnected) {
11253
+ this.dispatchDeviceDisconnectedEvent(prevDevice);
11193
11254
  await this.disable();
11194
11255
  await this.select(undefined);
11195
11256
  }
@@ -11199,7 +11260,7 @@ class DeviceManager {
11199
11260
  await this.enable();
11200
11261
  this.isTrackStoppedDueToTrackEnd = false;
11201
11262
  }
11202
- else {
11263
+ else if (!hasPending(this.statusChangeConcurrencyTag)) {
11203
11264
  await this.applySettingsToStream();
11204
11265
  }
11205
11266
  }
@@ -11213,6 +11274,20 @@ class DeviceManager {
11213
11274
  const kind = this.mediaDeviceKind;
11214
11275
  return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
11215
11276
  }
11277
+ dispatchDeviceDisconnectedEvent(device) {
11278
+ const event = {
11279
+ type: 'device.disconnected',
11280
+ call_cid: this.call.cid,
11281
+ status: this.isTrackStoppedDueToTrackEnd
11282
+ ? this.state.prevStatus
11283
+ : this.state.status,
11284
+ deviceId: device.deviceId,
11285
+ label: device.label,
11286
+ kind: device.kind,
11287
+ };
11288
+ this.call.tracer.trace('device.disconnected', event);
11289
+ this.call.streamClient.dispatchEvent(event);
11290
+ }
11216
11291
  persistPreference(selectedDevice, status) {
11217
11292
  const deviceKind = this.mediaDeviceKind;
11218
11293
  const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
@@ -11863,11 +11938,15 @@ class RNSpeechDetector {
11863
11938
  ? this.externalAudioStream
11864
11939
  : await navigator.mediaDevices.getUserMedia({ audio: true });
11865
11940
  this.audioStream = audioStream;
11866
- this.pc1.addEventListener('icecandidate', async (e) => {
11867
- await this.pc2.addIceCandidate(e.candidate);
11941
+ this.pc1.addEventListener('icecandidate', (e) => {
11942
+ this.pc2.addIceCandidate(e.candidate).catch(() => {
11943
+ // do nothing
11944
+ });
11868
11945
  });
11869
11946
  this.pc2.addEventListener('icecandidate', async (e) => {
11870
- await this.pc1.addIceCandidate(e.candidate);
11947
+ this.pc1.addIceCandidate(e.candidate).catch(() => {
11948
+ // do nothing
11949
+ });
11871
11950
  });
11872
11951
  this.pc2.addEventListener('track', (e) => {
11873
11952
  e.streams[0].getTracks().forEach((track) => {
@@ -12096,6 +12175,7 @@ class MicrophoneManager extends AudioDeviceManager {
12096
12175
  const deviceId = this.state.selectedDevice;
12097
12176
  const devices = await firstValueFrom(this.listDevices());
12098
12177
  const label = devices.find((d) => d.deviceId === deviceId)?.label;
12178
+ let lastCapturesAudio;
12099
12179
  this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
12100
12180
  noAudioThresholdMs: this.silenceThresholdMs,
12101
12181
  emitIntervalMs: this.silenceThresholdMs,
@@ -12107,7 +12187,10 @@ class MicrophoneManager extends AudioDeviceManager {
12107
12187
  deviceId,
12108
12188
  label,
12109
12189
  };
12110
- this.call.tracer.trace('mic.capture_report', event);
12190
+ if (capturesAudio !== lastCapturesAudio) {
12191
+ lastCapturesAudio = capturesAudio;
12192
+ this.call.tracer.trace('mic.capture_report', event);
12193
+ }
12111
12194
  this.call.streamClient.dispatchEvent(event);
12112
12195
  },
12113
12196
  });
@@ -13027,16 +13110,16 @@ class Call {
13027
13110
  };
13028
13111
  const rejectReason = reason ?? 'decline';
13029
13112
  const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
13030
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
13031
13113
  await this.reject(rejectReason);
13114
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
13032
13115
  }
13033
13116
  else {
13034
13117
  // if reject was undefined, we still have to cancel the call automatically
13035
13118
  // when I am the creator and everyone else left the call
13036
13119
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
13037
13120
  if (this.isCreatedByMe && !hasOtherParticipants) {
13038
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
13039
13121
  await this.reject('cancel');
13122
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
13040
13123
  }
13041
13124
  }
13042
13125
  }
@@ -14775,6 +14858,12 @@ class Call {
14775
14858
  unbind();
14776
14859
  };
14777
14860
  };
14861
+ /**
14862
+ * Plays all audio elements blocked by the browser's autoplay policy.
14863
+ */
14864
+ this.resumeAudio = () => {
14865
+ return this.dynascaleManager.resumeAudio();
14866
+ };
14778
14867
  /**
14779
14868
  * Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
14780
14869
  *
@@ -16018,7 +16107,7 @@ class StreamClient {
16018
16107
  this.getUserAgent = () => {
16019
16108
  if (!this.cachedUserAgent) {
16020
16109
  const { clientAppIdentifier = {} } = this.options;
16021
- const { sdkName = 'js', sdkVersion = "1.44.6-beta.0", ...extras } = clientAppIdentifier;
16110
+ const { sdkName = 'js', sdkVersion = "1.46.0", ...extras } = clientAppIdentifier;
16022
16111
  this.cachedUserAgent = [
16023
16112
  `stream-video-${sdkName}-v${sdkVersion}`,
16024
16113
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),