@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/dist/index.cjs.js CHANGED
@@ -6303,7 +6303,7 @@ const getSdkVersion = (sdk) => {
6303
6303
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6304
6304
  };
6305
6305
 
6306
- const version = "1.44.3";
6306
+ const version = "1.44.5";
6307
6307
  const [major, minor, patch] = version.split('.');
6308
6308
  let sdkInfo = {
6309
6309
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -9511,6 +9511,96 @@ class ViewportTracker {
9511
9511
  }
9512
9512
  }
9513
9513
 
9514
+ const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
9515
+ /**
9516
+ * Tracks audio element bindings and periodically warns about
9517
+ * remote participants whose audio streams have no bound element.
9518
+ */
9519
+ class AudioBindingsWatchdog {
9520
+ constructor(state, tracer) {
9521
+ this.state = state;
9522
+ this.tracer = tracer;
9523
+ this.bindings = new Map();
9524
+ this.enabled = true;
9525
+ this.logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
9526
+ /**
9527
+ * Registers an audio element binding for the given session and track type.
9528
+ * Warns if a different element is already bound to the same key.
9529
+ */
9530
+ this.register = (audioElement, sessionId, trackType) => {
9531
+ const key = toBindingKey(sessionId, trackType);
9532
+ const existing = this.bindings.get(key);
9533
+ if (existing && existing !== audioElement) {
9534
+ this.logger.warn(`Audio element already bound to ${sessionId} and ${trackType}`);
9535
+ this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
9536
+ }
9537
+ this.bindings.set(key, audioElement);
9538
+ };
9539
+ /**
9540
+ * Removes the audio element binding for the given session and track type.
9541
+ */
9542
+ this.unregister = (sessionId, trackType) => {
9543
+ this.bindings.delete(toBindingKey(sessionId, trackType));
9544
+ };
9545
+ /**
9546
+ * Enables or disables the watchdog.
9547
+ * When disabled, the periodic check stops but bindings are still tracked.
9548
+ */
9549
+ this.setEnabled = (enabled) => {
9550
+ this.enabled = enabled;
9551
+ if (enabled) {
9552
+ this.start();
9553
+ }
9554
+ else {
9555
+ this.stop();
9556
+ }
9557
+ };
9558
+ /**
9559
+ * Stops the watchdog and unsubscribes from callingState changes.
9560
+ */
9561
+ this.dispose = () => {
9562
+ this.stop();
9563
+ this.unsubscribeCallingState();
9564
+ };
9565
+ this.start = () => {
9566
+ clearInterval(this.watchdogInterval);
9567
+ this.watchdogInterval = setInterval(() => {
9568
+ const danglingUserIds = [];
9569
+ for (const p of this.state.participants) {
9570
+ if (p.isLocalParticipant)
9571
+ continue;
9572
+ const { audioStream, screenShareAudioStream, sessionId, userId } = p;
9573
+ if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
9574
+ danglingUserIds.push(userId);
9575
+ }
9576
+ if (screenShareAudioStream &&
9577
+ !this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
9578
+ danglingUserIds.push(userId);
9579
+ }
9580
+ }
9581
+ if (danglingUserIds.length > 0) {
9582
+ const key = 'audioBinding.danglingWarning';
9583
+ this.tracer.traceOnce(key, key, danglingUserIds);
9584
+ this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
9585
+ }
9586
+ }, 3000);
9587
+ };
9588
+ this.stop = () => {
9589
+ clearInterval(this.watchdogInterval);
9590
+ };
9591
+ this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
9592
+ if (!this.enabled)
9593
+ return;
9594
+ if (callingState !== exports.CallingState.JOINED) {
9595
+ this.stop();
9596
+ }
9597
+ else {
9598
+ this.start();
9599
+ }
9600
+ });
9601
+ }
9602
+ }
9603
+
9514
9604
  const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
9515
9605
  videoTrack: exports.VisibilityState.UNKNOWN,
9516
9606
  screenShareTrack: exports.VisibilityState.UNKNOWN,
@@ -9536,7 +9626,7 @@ class DynascaleManager {
9536
9626
  */
9537
9627
  this.viewportTracker = new ViewportTracker();
9538
9628
  this.logger = videoLoggerSystem.getLogger('DynascaleManager');
9539
- this.useWebAudio = isSafari();
9629
+ this.useWebAudio = false;
9540
9630
  this.pendingSubscriptionsUpdate = null;
9541
9631
  this.videoTrackSubscriptionOverridesSubject = new rxjs.BehaviorSubject({});
9542
9632
  this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
@@ -9568,7 +9658,8 @@ class DynascaleManager {
9568
9658
  if (this.pendingSubscriptionsUpdate) {
9569
9659
  clearTimeout(this.pendingSubscriptionsUpdate);
9570
9660
  }
9571
- const context = this.getOrCreateAudioContext();
9661
+ this.audioBindingsWatchdog?.dispose();
9662
+ const context = this.audioContext;
9572
9663
  if (context && context.state !== 'closed') {
9573
9664
  document.removeEventListener('click', this.resumeAudioContext);
9574
9665
  await context.close();
@@ -9767,12 +9858,13 @@ class DynascaleManager {
9767
9858
  lastDimensions = currentDimensions;
9768
9859
  });
9769
9860
  resizeObserver?.observe(videoElement);
9861
+ const isVideoTrack = trackType === 'videoTrack';
9770
9862
  // element renders and gets bound - track subscription gets
9771
9863
  // triggered first other ones get skipped on initial subscriptions
9772
9864
  const publishedTracksSubscription = boundParticipant.isLocalParticipant
9773
9865
  ? null
9774
9866
  : participant$
9775
- .pipe(rxjs.distinctUntilKeyChanged('publishedTracks'), rxjs.map((p) => trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p)), rxjs.distinctUntilChanged())
9867
+ .pipe(rxjs.distinctUntilKeyChanged('publishedTracks'), rxjs.map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))), rxjs.distinctUntilChanged())
9776
9868
  .subscribe((isPublishing) => {
9777
9869
  if (isPublishing) {
9778
9870
  // the participant just started to publish a track
@@ -9792,10 +9884,11 @@ class DynascaleManager {
9792
9884
  // without prior user interaction:
9793
9885
  // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
9794
9886
  videoElement.muted = true;
9887
+ const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
9795
9888
  const streamSubscription = participant$
9796
- .pipe(rxjs.distinctUntilKeyChanged(trackType === 'videoTrack' ? 'videoStream' : 'screenShareStream'))
9889
+ .pipe(rxjs.distinctUntilKeyChanged(trackKey))
9797
9890
  .subscribe((p) => {
9798
- const source = trackType === 'videoTrack' ? p.videoStream : p.screenShareStream;
9891
+ const source = isVideoTrack ? p.videoStream : p.screenShareStream;
9799
9892
  if (videoElement.srcObject === source)
9800
9893
  return;
9801
9894
  videoElement.srcObject = source ?? null;
@@ -9834,6 +9927,7 @@ class DynascaleManager {
9834
9927
  const participant = this.callState.findParticipantBySessionId(sessionId);
9835
9928
  if (!participant || participant.isLocalParticipant)
9836
9929
  return;
9930
+ this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
9837
9931
  const participant$ = this.callState.participants$.pipe(rxjs.map((ps) => ps.find((p) => p.sessionId === sessionId)), rxjs.takeWhile((p) => !!p), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
9838
9932
  const updateSinkId = (deviceId, audioContext) => {
9839
9933
  if (!deviceId)
@@ -9852,14 +9946,12 @@ class DynascaleManager {
9852
9946
  };
9853
9947
  let sourceNode = undefined;
9854
9948
  let gainNode = undefined;
9949
+ const isAudioTrack = trackType === 'audioTrack';
9950
+ const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
9855
9951
  const updateMediaStreamSubscription = participant$
9856
- .pipe(rxjs.distinctUntilKeyChanged(trackType === 'screenShareAudioTrack'
9857
- ? 'screenShareAudioStream'
9858
- : 'audioStream'))
9952
+ .pipe(rxjs.distinctUntilKeyChanged(trackKey))
9859
9953
  .subscribe((p) => {
9860
- const source = trackType === 'screenShareAudioTrack'
9861
- ? p.screenShareAudioStream
9862
- : p.audioStream;
9954
+ const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
9863
9955
  if (audioElement.srcObject === source)
9864
9956
  return;
9865
9957
  setTimeout(() => {
@@ -9914,6 +10006,7 @@ class DynascaleManager {
9914
10006
  });
9915
10007
  audioElement.autoplay = true;
9916
10008
  return () => {
10009
+ this.audioBindingsWatchdog?.unregister(sessionId, trackType);
9917
10010
  sinkIdSubscription?.unsubscribe();
9918
10011
  volumeSubscription.unsubscribe();
9919
10012
  updateMediaStreamSubscription.unsubscribe();
@@ -9974,6 +10067,9 @@ class DynascaleManager {
9974
10067
  this.callState = callState;
9975
10068
  this.speaker = speaker;
9976
10069
  this.tracer = tracer;
10070
+ if (!isReactNative()) {
10071
+ this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
10072
+ }
9977
10073
  }
9978
10074
  setSfuClient(sfuClient) {
9979
10075
  this.sfuClient = sfuClient;
@@ -10318,6 +10414,9 @@ class BrowserPermission {
10318
10414
  }
10319
10415
  setState(state) {
10320
10416
  if (this.state !== state) {
10417
+ const { tracer, queryName } = this.permission;
10418
+ const traceKey = `navigator.mediaDevices.${queryName}.permission`;
10419
+ tracer?.trace(traceKey, { previous: this.state, state });
10321
10420
  this.state = state;
10322
10421
  this.listeners.forEach((listener) => listener(state));
10323
10422
  }
@@ -10359,9 +10458,6 @@ const getDevices = (permission, kind, tracer) => {
10359
10458
  const checkIfAudioOutputChangeSupported = () => {
10360
10459
  if (typeof document === 'undefined')
10361
10460
  return false;
10362
- // Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
10363
- if (isSafari())
10364
- return 'setSinkId' in AudioContext.prototype;
10365
10461
  const element = document.createElement('audio');
10366
10462
  return 'setSinkId' in element;
10367
10463
  };
@@ -10388,17 +10484,19 @@ const videoDeviceConstraints = {
10388
10484
  * Keeps track of the browser permission to use microphone. This permission also
10389
10485
  * affects an ability to enumerate audio devices.
10390
10486
  */
10391
- const getAudioBrowserPermission = lazy(() => new BrowserPermission({
10487
+ const getAudioBrowserPermission = lazy((tracer) => new BrowserPermission({
10392
10488
  constraints: audioDeviceConstraints,
10393
10489
  queryName: 'microphone',
10490
+ tracer,
10394
10491
  }));
10395
10492
  /**
10396
10493
  * Keeps track of the browser permission to use camera. This permission also
10397
10494
  * affects an ability to enumerate video devices.
10398
10495
  */
10399
- const getVideoBrowserPermission = lazy(() => new BrowserPermission({
10496
+ const getVideoBrowserPermission = lazy((tracer) => new BrowserPermission({
10400
10497
  constraints: videoDeviceConstraints,
10401
10498
  queryName: 'camera',
10499
+ tracer,
10402
10500
  }));
10403
10501
  const getDeviceChangeObserver = lazy((tracer) => {
10404
10502
  // 'addEventListener' is not available in React Native, returning
@@ -10414,7 +10512,7 @@ const getDeviceChangeObserver = lazy((tracer) => {
10414
10512
  * the observable errors.
10415
10513
  */
10416
10514
  const getAudioDevices = lazy((tracer) => {
10417
- return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput', tracer)), rxjs.shareReplay(1));
10515
+ return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission(tracer).asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(tracer), 'audioinput', tracer)), rxjs.shareReplay(1));
10418
10516
  });
10419
10517
  /**
10420
10518
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10423,7 +10521,7 @@ const getAudioDevices = lazy((tracer) => {
10423
10521
  * the observable errors.
10424
10522
  */
10425
10523
  const getVideoDevices = lazy((tracer) => {
10426
- return rxjs.merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission().asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput', tracer)), rxjs.shareReplay(1));
10524
+ return rxjs.merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission(tracer).asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getVideoBrowserPermission(tracer), 'videoinput', tracer)), rxjs.shareReplay(1));
10427
10525
  });
10428
10526
  /**
10429
10527
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10432,7 +10530,7 @@ const getVideoDevices = lazy((tracer) => {
10432
10530
  * the observable errors.
10433
10531
  */
10434
10532
  const getAudioOutputDevices = lazy((tracer) => {
10435
- return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput', tracer)), rxjs.shareReplay(1));
10533
+ return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission(tracer).asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(tracer), 'audiooutput', tracer)), rxjs.shareReplay(1));
10436
10534
  });
10437
10535
  let getUserMediaExecId = 0;
10438
10536
  const getStream = async (constraints, tracer) => {
@@ -10498,25 +10596,21 @@ const getAudioStream = async (trackConstraints, tracer) => {
10498
10596
  },
10499
10597
  };
10500
10598
  try {
10501
- await getAudioBrowserPermission().prompt({
10599
+ await getAudioBrowserPermission(tracer).prompt({
10502
10600
  throwOnNotAllowed: true,
10503
10601
  forcePrompt: true,
10504
10602
  });
10505
10603
  return await getStream(constraints, tracer);
10506
10604
  }
10507
10605
  catch (error) {
10606
+ const logger = videoLoggerSystem.getLogger('devices');
10508
10607
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
10509
10608
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10510
10609
  const { deviceId, ...relaxedConstraints } = trackConstraints;
10511
- videoLoggerSystem
10512
- .getLogger('devices')
10513
- .warn('Failed to get audio stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10610
+ logger.warn('Failed to get audio stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10514
10611
  return getAudioStream(relaxedConstraints, tracer);
10515
10612
  }
10516
- videoLoggerSystem.getLogger('devices').error('Failed to get audio stream', {
10517
- error,
10518
- constraints,
10519
- });
10613
+ logger.error('Failed to get audio stream', { error, constraints });
10520
10614
  throw error;
10521
10615
  }
10522
10616
  };
@@ -10536,25 +10630,21 @@ const getVideoStream = async (trackConstraints, tracer) => {
10536
10630
  },
10537
10631
  };
10538
10632
  try {
10539
- await getVideoBrowserPermission().prompt({
10633
+ await getVideoBrowserPermission(tracer).prompt({
10540
10634
  throwOnNotAllowed: true,
10541
10635
  forcePrompt: true,
10542
10636
  });
10543
10637
  return await getStream(constraints, tracer);
10544
10638
  }
10545
10639
  catch (error) {
10640
+ const logger = videoLoggerSystem.getLogger('devices');
10546
10641
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
10547
10642
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10548
10643
  const { deviceId, ...relaxedConstraints } = trackConstraints;
10549
- videoLoggerSystem
10550
- .getLogger('devices')
10551
- .warn('Failed to get video stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10552
- return getVideoStream(relaxedConstraints);
10644
+ logger.warn('Failed to get video stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10645
+ return getVideoStream(relaxedConstraints, tracer);
10553
10646
  }
10554
- videoLoggerSystem.getLogger('devices').error('Failed to get video stream', {
10555
- error,
10556
- constraints,
10557
- });
10647
+ logger.error('Failed to get video stream', { error, constraints });
10558
10648
  throw error;
10559
10649
  }
10560
10650
  };
@@ -11325,8 +11415,8 @@ class DeviceManagerState {
11325
11415
  }
11326
11416
 
11327
11417
  class CameraManagerState extends DeviceManagerState {
11328
- constructor() {
11329
- super('stop-tracks', getVideoBrowserPermission());
11418
+ constructor(tracer) {
11419
+ super('stop-tracks', getVideoBrowserPermission(tracer));
11330
11420
  this.directionSubject = new rxjs.BehaviorSubject(undefined);
11331
11421
  /**
11332
11422
  * Observable that emits the preferred camera direction
@@ -11380,7 +11470,7 @@ class CameraManager extends DeviceManager {
11380
11470
  * @param devicePersistence the device persistence preferences to use.
11381
11471
  */
11382
11472
  constructor(call, devicePersistence) {
11383
- super(call, new CameraManagerState(), TrackType.VIDEO, devicePersistence);
11473
+ super(call, new CameraManagerState(call.tracer), TrackType.VIDEO, devicePersistence);
11384
11474
  this.targetResolution = {
11385
11475
  width: 1280,
11386
11476
  height: 720,
@@ -11591,8 +11681,8 @@ class AudioDeviceManagerState extends DeviceManagerState {
11591
11681
  }
11592
11682
 
11593
11683
  class MicrophoneManagerState extends AudioDeviceManagerState {
11594
- constructor(disableMode) {
11595
- super(disableMode, getAudioBrowserPermission(), AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED);
11684
+ constructor(disableMode, tracer) {
11685
+ super(disableMode, getAudioBrowserPermission(tracer), AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED);
11596
11686
  this.speakingWhileMutedSubject = new rxjs.BehaviorSubject(false);
11597
11687
  /**
11598
11688
  * An Observable that emits `true` if the user's microphone is muted, but they're speaking.
@@ -11932,7 +12022,7 @@ class RNSpeechDetector {
11932
12022
 
11933
12023
  class MicrophoneManager extends AudioDeviceManager {
11934
12024
  constructor(call, devicePersistence, disableMode = 'stop-tracks') {
11935
- super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO, devicePersistence);
12025
+ super(call, new MicrophoneManagerState(disableMode, call.tracer), TrackType.AUDIO, devicePersistence);
11936
12026
  this.speakingWhileMutedNotificationEnabled = true;
11937
12027
  this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
11938
12028
  this.silenceThresholdMs = 5000;
@@ -12034,7 +12124,6 @@ class MicrophoneManager extends AudioDeviceManager {
12034
12124
  deviceId,
12035
12125
  label,
12036
12126
  };
12037
- console.log(event);
12038
12127
  this.call.tracer.trace('mic.capture_report', event);
12039
12128
  this.call.streamClient.dispatchEvent(event);
12040
12129
  },
@@ -15908,7 +15997,7 @@ class StreamClient {
15908
15997
  this.getUserAgent = () => {
15909
15998
  if (!this.cachedUserAgent) {
15910
15999
  const { clientAppIdentifier = {} } = this.options;
15911
- const { sdkName = 'js', sdkVersion = "1.44.3", ...extras } = clientAppIdentifier;
16000
+ const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
15912
16001
  this.cachedUserAgent = [
15913
16002
  `stream-video-${sdkName}-v${sdkVersion}`,
15914
16003
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),