@stream-io/video-client 1.44.4 → 1.44.5

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,12 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [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
+
7
+ ### Bug Fixes
8
+
9
+ - make WebAudio opt-in, add AudioBindingsWatchdog ([#2171](https://github.com/GetStream/stream-video-js/issues/2171)) ([8d00f48](https://github.com/GetStream/stream-video-js/commit/8d00f485a37fec23dca340d32738a3cb1f7f325a))
10
+
5
11
  ## [1.44.4](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.44.3...@stream-io/video-client-1.44.4) (2026-03-20)
6
12
 
7
13
  - trace device permission state transitions ([#2168](https://github.com/GetStream/stream-video-js/issues/2168)) ([e4203a3](https://github.com/GetStream/stream-video-js/commit/e4203a34cad1c90d1bc5612fc379dd1f0f0ebe5d))
@@ -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.4";
6286
+ const version = "1.44.5";
6287
6287
  const [major, minor, patch] = version.split('.');
6288
6288
  let sdkInfo = {
6289
6289
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -9491,6 +9491,96 @@ class ViewportTracker {
9491
9491
  }
9492
9492
  }
9493
9493
 
9494
+ const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
9495
+ /**
9496
+ * Tracks audio element bindings and periodically warns about
9497
+ * remote participants whose audio streams have no bound element.
9498
+ */
9499
+ class AudioBindingsWatchdog {
9500
+ constructor(state, tracer) {
9501
+ this.state = state;
9502
+ this.tracer = tracer;
9503
+ this.bindings = new Map();
9504
+ this.enabled = true;
9505
+ this.logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
9506
+ /**
9507
+ * Registers an audio element binding for the given session and track type.
9508
+ * Warns if a different element is already bound to the same key.
9509
+ */
9510
+ this.register = (audioElement, sessionId, trackType) => {
9511
+ const key = toBindingKey(sessionId, trackType);
9512
+ const existing = this.bindings.get(key);
9513
+ if (existing && existing !== audioElement) {
9514
+ this.logger.warn(`Audio element already bound to ${sessionId} and ${trackType}`);
9515
+ this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
9516
+ }
9517
+ this.bindings.set(key, audioElement);
9518
+ };
9519
+ /**
9520
+ * Removes the audio element binding for the given session and track type.
9521
+ */
9522
+ this.unregister = (sessionId, trackType) => {
9523
+ this.bindings.delete(toBindingKey(sessionId, trackType));
9524
+ };
9525
+ /**
9526
+ * Enables or disables the watchdog.
9527
+ * When disabled, the periodic check stops but bindings are still tracked.
9528
+ */
9529
+ this.setEnabled = (enabled) => {
9530
+ this.enabled = enabled;
9531
+ if (enabled) {
9532
+ this.start();
9533
+ }
9534
+ else {
9535
+ this.stop();
9536
+ }
9537
+ };
9538
+ /**
9539
+ * Stops the watchdog and unsubscribes from callingState changes.
9540
+ */
9541
+ this.dispose = () => {
9542
+ this.stop();
9543
+ this.unsubscribeCallingState();
9544
+ };
9545
+ this.start = () => {
9546
+ clearInterval(this.watchdogInterval);
9547
+ this.watchdogInterval = setInterval(() => {
9548
+ const danglingUserIds = [];
9549
+ for (const p of this.state.participants) {
9550
+ if (p.isLocalParticipant)
9551
+ continue;
9552
+ const { audioStream, screenShareAudioStream, sessionId, userId } = p;
9553
+ if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
9554
+ danglingUserIds.push(userId);
9555
+ }
9556
+ if (screenShareAudioStream &&
9557
+ !this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
9558
+ danglingUserIds.push(userId);
9559
+ }
9560
+ }
9561
+ if (danglingUserIds.length > 0) {
9562
+ const key = 'audioBinding.danglingWarning';
9563
+ this.tracer.traceOnce(key, key, danglingUserIds);
9564
+ this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
9565
+ }
9566
+ }, 3000);
9567
+ };
9568
+ this.stop = () => {
9569
+ clearInterval(this.watchdogInterval);
9570
+ };
9571
+ this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
9572
+ if (!this.enabled)
9573
+ return;
9574
+ if (callingState !== CallingState.JOINED) {
9575
+ this.stop();
9576
+ }
9577
+ else {
9578
+ this.start();
9579
+ }
9580
+ });
9581
+ }
9582
+ }
9583
+
9494
9584
  const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
9495
9585
  videoTrack: VisibilityState.UNKNOWN,
9496
9586
  screenShareTrack: VisibilityState.UNKNOWN,
@@ -9516,7 +9606,7 @@ class DynascaleManager {
9516
9606
  */
9517
9607
  this.viewportTracker = new ViewportTracker();
9518
9608
  this.logger = videoLoggerSystem.getLogger('DynascaleManager');
9519
- this.useWebAudio = isSafari();
9609
+ this.useWebAudio = false;
9520
9610
  this.pendingSubscriptionsUpdate = null;
9521
9611
  this.videoTrackSubscriptionOverridesSubject = new BehaviorSubject({});
9522
9612
  this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
@@ -9548,7 +9638,8 @@ class DynascaleManager {
9548
9638
  if (this.pendingSubscriptionsUpdate) {
9549
9639
  clearTimeout(this.pendingSubscriptionsUpdate);
9550
9640
  }
9551
- const context = this.getOrCreateAudioContext();
9641
+ this.audioBindingsWatchdog?.dispose();
9642
+ const context = this.audioContext;
9552
9643
  if (context && context.state !== 'closed') {
9553
9644
  document.removeEventListener('click', this.resumeAudioContext);
9554
9645
  await context.close();
@@ -9747,12 +9838,13 @@ class DynascaleManager {
9747
9838
  lastDimensions = currentDimensions;
9748
9839
  });
9749
9840
  resizeObserver?.observe(videoElement);
9841
+ const isVideoTrack = trackType === 'videoTrack';
9750
9842
  // element renders and gets bound - track subscription gets
9751
9843
  // triggered first other ones get skipped on initial subscriptions
9752
9844
  const publishedTracksSubscription = boundParticipant.isLocalParticipant
9753
9845
  ? null
9754
9846
  : participant$
9755
- .pipe(distinctUntilKeyChanged('publishedTracks'), map((p) => trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p)), distinctUntilChanged())
9847
+ .pipe(distinctUntilKeyChanged('publishedTracks'), map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))), distinctUntilChanged())
9756
9848
  .subscribe((isPublishing) => {
9757
9849
  if (isPublishing) {
9758
9850
  // the participant just started to publish a track
@@ -9772,10 +9864,11 @@ class DynascaleManager {
9772
9864
  // without prior user interaction:
9773
9865
  // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
9774
9866
  videoElement.muted = true;
9867
+ const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
9775
9868
  const streamSubscription = participant$
9776
- .pipe(distinctUntilKeyChanged(trackType === 'videoTrack' ? 'videoStream' : 'screenShareStream'))
9869
+ .pipe(distinctUntilKeyChanged(trackKey))
9777
9870
  .subscribe((p) => {
9778
- const source = trackType === 'videoTrack' ? p.videoStream : p.screenShareStream;
9871
+ const source = isVideoTrack ? p.videoStream : p.screenShareStream;
9779
9872
  if (videoElement.srcObject === source)
9780
9873
  return;
9781
9874
  videoElement.srcObject = source ?? null;
@@ -9814,6 +9907,7 @@ class DynascaleManager {
9814
9907
  const participant = this.callState.findParticipantBySessionId(sessionId);
9815
9908
  if (!participant || participant.isLocalParticipant)
9816
9909
  return;
9910
+ this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
9817
9911
  const participant$ = this.callState.participants$.pipe(map((ps) => ps.find((p) => p.sessionId === sessionId)), takeWhile((p) => !!p), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
9818
9912
  const updateSinkId = (deviceId, audioContext) => {
9819
9913
  if (!deviceId)
@@ -9832,14 +9926,12 @@ class DynascaleManager {
9832
9926
  };
9833
9927
  let sourceNode = undefined;
9834
9928
  let gainNode = undefined;
9929
+ const isAudioTrack = trackType === 'audioTrack';
9930
+ const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
9835
9931
  const updateMediaStreamSubscription = participant$
9836
- .pipe(distinctUntilKeyChanged(trackType === 'screenShareAudioTrack'
9837
- ? 'screenShareAudioStream'
9838
- : 'audioStream'))
9932
+ .pipe(distinctUntilKeyChanged(trackKey))
9839
9933
  .subscribe((p) => {
9840
- const source = trackType === 'screenShareAudioTrack'
9841
- ? p.screenShareAudioStream
9842
- : p.audioStream;
9934
+ const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
9843
9935
  if (audioElement.srcObject === source)
9844
9936
  return;
9845
9937
  setTimeout(() => {
@@ -9894,6 +9986,7 @@ class DynascaleManager {
9894
9986
  });
9895
9987
  audioElement.autoplay = true;
9896
9988
  return () => {
9989
+ this.audioBindingsWatchdog?.unregister(sessionId, trackType);
9897
9990
  sinkIdSubscription?.unsubscribe();
9898
9991
  volumeSubscription.unsubscribe();
9899
9992
  updateMediaStreamSubscription.unsubscribe();
@@ -9954,6 +10047,9 @@ class DynascaleManager {
9954
10047
  this.callState = callState;
9955
10048
  this.speaker = speaker;
9956
10049
  this.tracer = tracer;
10050
+ if (!isReactNative()) {
10051
+ this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
10052
+ }
9957
10053
  }
9958
10054
  setSfuClient(sfuClient) {
9959
10055
  this.sfuClient = sfuClient;
@@ -10342,9 +10438,6 @@ const getDevices = (permission, kind, tracer) => {
10342
10438
  const checkIfAudioOutputChangeSupported = () => {
10343
10439
  if (typeof document === 'undefined')
10344
10440
  return false;
10345
- // Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
10346
- if (isSafari())
10347
- return 'setSinkId' in AudioContext.prototype;
10348
10441
  const element = document.createElement('audio');
10349
10442
  return 'setSinkId' in element;
10350
10443
  };
@@ -15886,7 +15979,7 @@ class StreamClient {
15886
15979
  this.getUserAgent = () => {
15887
15980
  if (!this.cachedUserAgent) {
15888
15981
  const { clientAppIdentifier = {} } = this.options;
15889
- const { sdkName = 'js', sdkVersion = "1.44.4", ...extras } = clientAppIdentifier;
15982
+ const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
15890
15983
  this.cachedUserAgent = [
15891
15984
  `stream-video-${sdkName}-v${sdkVersion}`,
15892
15985
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),