@stream-io/video-client 1.44.3 → 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,20 @@
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
+
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)
12
+
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))
14
+
15
+ ### Bug Fixes
16
+
17
+ - **react:** remove default broken microphone notification from call controls ([#2158](https://github.com/GetStream/stream-video-js/issues/2158)) ([4a95b9c](https://github.com/GetStream/stream-video-js/commit/4a95b9c29e9d2728ae7eea764f07ec8507aa0f5a))
18
+
5
19
  ## [1.44.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.44.2...@stream-io/video-client-1.44.3) (2026-03-06)
6
20
 
7
21
  ### Bug Fixes
@@ -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.3";
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;
@@ -10298,6 +10394,9 @@ class BrowserPermission {
10298
10394
  }
10299
10395
  setState(state) {
10300
10396
  if (this.state !== state) {
10397
+ const { tracer, queryName } = this.permission;
10398
+ const traceKey = `navigator.mediaDevices.${queryName}.permission`;
10399
+ tracer?.trace(traceKey, { previous: this.state, state });
10301
10400
  this.state = state;
10302
10401
  this.listeners.forEach((listener) => listener(state));
10303
10402
  }
@@ -10339,9 +10438,6 @@ const getDevices = (permission, kind, tracer) => {
10339
10438
  const checkIfAudioOutputChangeSupported = () => {
10340
10439
  if (typeof document === 'undefined')
10341
10440
  return false;
10342
- // Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
10343
- if (isSafari())
10344
- return 'setSinkId' in AudioContext.prototype;
10345
10441
  const element = document.createElement('audio');
10346
10442
  return 'setSinkId' in element;
10347
10443
  };
@@ -10368,17 +10464,19 @@ const videoDeviceConstraints = {
10368
10464
  * Keeps track of the browser permission to use microphone. This permission also
10369
10465
  * affects an ability to enumerate audio devices.
10370
10466
  */
10371
- const getAudioBrowserPermission = lazy(() => new BrowserPermission({
10467
+ const getAudioBrowserPermission = lazy((tracer) => new BrowserPermission({
10372
10468
  constraints: audioDeviceConstraints,
10373
10469
  queryName: 'microphone',
10470
+ tracer,
10374
10471
  }));
10375
10472
  /**
10376
10473
  * Keeps track of the browser permission to use camera. This permission also
10377
10474
  * affects an ability to enumerate video devices.
10378
10475
  */
10379
- const getVideoBrowserPermission = lazy(() => new BrowserPermission({
10476
+ const getVideoBrowserPermission = lazy((tracer) => new BrowserPermission({
10380
10477
  constraints: videoDeviceConstraints,
10381
10478
  queryName: 'camera',
10479
+ tracer,
10382
10480
  }));
10383
10481
  const getDeviceChangeObserver = lazy((tracer) => {
10384
10482
  // 'addEventListener' is not available in React Native, returning
@@ -10394,7 +10492,7 @@ const getDeviceChangeObserver = lazy((tracer) => {
10394
10492
  * the observable errors.
10395
10493
  */
10396
10494
  const getAudioDevices = lazy((tracer) => {
10397
- return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput', tracer)), shareReplay(1));
10495
+ return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission(tracer).asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(tracer), 'audioinput', tracer)), shareReplay(1));
10398
10496
  });
10399
10497
  /**
10400
10498
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10403,7 +10501,7 @@ const getAudioDevices = lazy((tracer) => {
10403
10501
  * the observable errors.
10404
10502
  */
10405
10503
  const getVideoDevices = lazy((tracer) => {
10406
- return merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput', tracer)), shareReplay(1));
10504
+ return merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission(tracer).asObservable()).pipe(startWith([]), concatMap(() => getDevices(getVideoBrowserPermission(tracer), 'videoinput', tracer)), shareReplay(1));
10407
10505
  });
10408
10506
  /**
10409
10507
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10412,7 +10510,7 @@ const getVideoDevices = lazy((tracer) => {
10412
10510
  * the observable errors.
10413
10511
  */
10414
10512
  const getAudioOutputDevices = lazy((tracer) => {
10415
- return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput', tracer)), shareReplay(1));
10513
+ return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission(tracer).asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(tracer), 'audiooutput', tracer)), shareReplay(1));
10416
10514
  });
10417
10515
  let getUserMediaExecId = 0;
10418
10516
  const getStream = async (constraints, tracer) => {
@@ -10478,25 +10576,21 @@ const getAudioStream = async (trackConstraints, tracer) => {
10478
10576
  },
10479
10577
  };
10480
10578
  try {
10481
- await getAudioBrowserPermission().prompt({
10579
+ await getAudioBrowserPermission(tracer).prompt({
10482
10580
  throwOnNotAllowed: true,
10483
10581
  forcePrompt: true,
10484
10582
  });
10485
10583
  return await getStream(constraints, tracer);
10486
10584
  }
10487
10585
  catch (error) {
10586
+ const logger = videoLoggerSystem.getLogger('devices');
10488
10587
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
10489
10588
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10490
10589
  const { deviceId, ...relaxedConstraints } = trackConstraints;
10491
- videoLoggerSystem
10492
- .getLogger('devices')
10493
- .warn('Failed to get audio stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10590
+ logger.warn('Failed to get audio stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10494
10591
  return getAudioStream(relaxedConstraints, tracer);
10495
10592
  }
10496
- videoLoggerSystem.getLogger('devices').error('Failed to get audio stream', {
10497
- error,
10498
- constraints,
10499
- });
10593
+ logger.error('Failed to get audio stream', { error, constraints });
10500
10594
  throw error;
10501
10595
  }
10502
10596
  };
@@ -10516,25 +10610,21 @@ const getVideoStream = async (trackConstraints, tracer) => {
10516
10610
  },
10517
10611
  };
10518
10612
  try {
10519
- await getVideoBrowserPermission().prompt({
10613
+ await getVideoBrowserPermission(tracer).prompt({
10520
10614
  throwOnNotAllowed: true,
10521
10615
  forcePrompt: true,
10522
10616
  });
10523
10617
  return await getStream(constraints, tracer);
10524
10618
  }
10525
10619
  catch (error) {
10620
+ const logger = videoLoggerSystem.getLogger('devices');
10526
10621
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
10527
10622
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10528
10623
  const { deviceId, ...relaxedConstraints } = trackConstraints;
10529
- videoLoggerSystem
10530
- .getLogger('devices')
10531
- .warn('Failed to get video stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10532
- return getVideoStream(relaxedConstraints);
10624
+ logger.warn('Failed to get video stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10625
+ return getVideoStream(relaxedConstraints, tracer);
10533
10626
  }
10534
- videoLoggerSystem.getLogger('devices').error('Failed to get video stream', {
10535
- error,
10536
- constraints,
10537
- });
10627
+ logger.error('Failed to get video stream', { error, constraints });
10538
10628
  throw error;
10539
10629
  }
10540
10630
  };
@@ -11305,8 +11395,8 @@ class DeviceManagerState {
11305
11395
  }
11306
11396
 
11307
11397
  class CameraManagerState extends DeviceManagerState {
11308
- constructor() {
11309
- super('stop-tracks', getVideoBrowserPermission());
11398
+ constructor(tracer) {
11399
+ super('stop-tracks', getVideoBrowserPermission(tracer));
11310
11400
  this.directionSubject = new BehaviorSubject(undefined);
11311
11401
  /**
11312
11402
  * Observable that emits the preferred camera direction
@@ -11360,7 +11450,7 @@ class CameraManager extends DeviceManager {
11360
11450
  * @param devicePersistence the device persistence preferences to use.
11361
11451
  */
11362
11452
  constructor(call, devicePersistence) {
11363
- super(call, new CameraManagerState(), TrackType.VIDEO, devicePersistence);
11453
+ super(call, new CameraManagerState(call.tracer), TrackType.VIDEO, devicePersistence);
11364
11454
  this.targetResolution = {
11365
11455
  width: 1280,
11366
11456
  height: 720,
@@ -11571,8 +11661,8 @@ class AudioDeviceManagerState extends DeviceManagerState {
11571
11661
  }
11572
11662
 
11573
11663
  class MicrophoneManagerState extends AudioDeviceManagerState {
11574
- constructor(disableMode) {
11575
- super(disableMode, getAudioBrowserPermission(), AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED);
11664
+ constructor(disableMode, tracer) {
11665
+ super(disableMode, getAudioBrowserPermission(tracer), AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED);
11576
11666
  this.speakingWhileMutedSubject = new BehaviorSubject(false);
11577
11667
  /**
11578
11668
  * An Observable that emits `true` if the user's microphone is muted, but they're speaking.
@@ -11912,7 +12002,7 @@ class RNSpeechDetector {
11912
12002
 
11913
12003
  class MicrophoneManager extends AudioDeviceManager {
11914
12004
  constructor(call, devicePersistence, disableMode = 'stop-tracks') {
11915
- super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO, devicePersistence);
12005
+ super(call, new MicrophoneManagerState(disableMode, call.tracer), TrackType.AUDIO, devicePersistence);
11916
12006
  this.speakingWhileMutedNotificationEnabled = true;
11917
12007
  this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
11918
12008
  this.silenceThresholdMs = 5000;
@@ -12014,7 +12104,6 @@ class MicrophoneManager extends AudioDeviceManager {
12014
12104
  deviceId,
12015
12105
  label,
12016
12106
  };
12017
- console.log(event);
12018
12107
  this.call.tracer.trace('mic.capture_report', event);
12019
12108
  this.call.streamClient.dispatchEvent(event);
12020
12109
  },
@@ -15890,7 +15979,7 @@ class StreamClient {
15890
15979
  this.getUserAgent = () => {
15891
15980
  if (!this.cachedUserAgent) {
15892
15981
  const { clientAppIdentifier = {} } = this.options;
15893
- const { sdkName = 'js', sdkVersion = "1.44.3", ...extras } = clientAppIdentifier;
15982
+ const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
15894
15983
  this.cachedUserAgent = [
15895
15984
  `stream-video-${sdkName}-v${sdkVersion}`,
15896
15985
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),