@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.es.js CHANGED
@@ -6284,7 +6284,7 @@ const getSdkVersion = (sdk) => {
6284
6284
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6285
6285
  };
6286
6286
 
6287
- const version = "1.44.3";
6287
+ const version = "1.44.5";
6288
6288
  const [major, minor, patch] = version.split('.');
6289
6289
  let sdkInfo = {
6290
6290
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -9492,6 +9492,96 @@ class ViewportTracker {
9492
9492
  }
9493
9493
  }
9494
9494
 
9495
+ const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
9496
+ /**
9497
+ * Tracks audio element bindings and periodically warns about
9498
+ * remote participants whose audio streams have no bound element.
9499
+ */
9500
+ class AudioBindingsWatchdog {
9501
+ constructor(state, tracer) {
9502
+ this.state = state;
9503
+ this.tracer = tracer;
9504
+ this.bindings = new Map();
9505
+ this.enabled = true;
9506
+ this.logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
9507
+ /**
9508
+ * Registers an audio element binding for the given session and track type.
9509
+ * Warns if a different element is already bound to the same key.
9510
+ */
9511
+ this.register = (audioElement, sessionId, trackType) => {
9512
+ const key = toBindingKey(sessionId, trackType);
9513
+ const existing = this.bindings.get(key);
9514
+ if (existing && existing !== audioElement) {
9515
+ this.logger.warn(`Audio element already bound to ${sessionId} and ${trackType}`);
9516
+ this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
9517
+ }
9518
+ this.bindings.set(key, audioElement);
9519
+ };
9520
+ /**
9521
+ * Removes the audio element binding for the given session and track type.
9522
+ */
9523
+ this.unregister = (sessionId, trackType) => {
9524
+ this.bindings.delete(toBindingKey(sessionId, trackType));
9525
+ };
9526
+ /**
9527
+ * Enables or disables the watchdog.
9528
+ * When disabled, the periodic check stops but bindings are still tracked.
9529
+ */
9530
+ this.setEnabled = (enabled) => {
9531
+ this.enabled = enabled;
9532
+ if (enabled) {
9533
+ this.start();
9534
+ }
9535
+ else {
9536
+ this.stop();
9537
+ }
9538
+ };
9539
+ /**
9540
+ * Stops the watchdog and unsubscribes from callingState changes.
9541
+ */
9542
+ this.dispose = () => {
9543
+ this.stop();
9544
+ this.unsubscribeCallingState();
9545
+ };
9546
+ this.start = () => {
9547
+ clearInterval(this.watchdogInterval);
9548
+ this.watchdogInterval = setInterval(() => {
9549
+ const danglingUserIds = [];
9550
+ for (const p of this.state.participants) {
9551
+ if (p.isLocalParticipant)
9552
+ continue;
9553
+ const { audioStream, screenShareAudioStream, sessionId, userId } = p;
9554
+ if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
9555
+ danglingUserIds.push(userId);
9556
+ }
9557
+ if (screenShareAudioStream &&
9558
+ !this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
9559
+ danglingUserIds.push(userId);
9560
+ }
9561
+ }
9562
+ if (danglingUserIds.length > 0) {
9563
+ const key = 'audioBinding.danglingWarning';
9564
+ this.tracer.traceOnce(key, key, danglingUserIds);
9565
+ this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
9566
+ }
9567
+ }, 3000);
9568
+ };
9569
+ this.stop = () => {
9570
+ clearInterval(this.watchdogInterval);
9571
+ };
9572
+ this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
9573
+ if (!this.enabled)
9574
+ return;
9575
+ if (callingState !== CallingState.JOINED) {
9576
+ this.stop();
9577
+ }
9578
+ else {
9579
+ this.start();
9580
+ }
9581
+ });
9582
+ }
9583
+ }
9584
+
9495
9585
  const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
9496
9586
  videoTrack: VisibilityState.UNKNOWN,
9497
9587
  screenShareTrack: VisibilityState.UNKNOWN,
@@ -9517,7 +9607,7 @@ class DynascaleManager {
9517
9607
  */
9518
9608
  this.viewportTracker = new ViewportTracker();
9519
9609
  this.logger = videoLoggerSystem.getLogger('DynascaleManager');
9520
- this.useWebAudio = isSafari();
9610
+ this.useWebAudio = false;
9521
9611
  this.pendingSubscriptionsUpdate = null;
9522
9612
  this.videoTrackSubscriptionOverridesSubject = new BehaviorSubject({});
9523
9613
  this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
@@ -9549,7 +9639,8 @@ class DynascaleManager {
9549
9639
  if (this.pendingSubscriptionsUpdate) {
9550
9640
  clearTimeout(this.pendingSubscriptionsUpdate);
9551
9641
  }
9552
- const context = this.getOrCreateAudioContext();
9642
+ this.audioBindingsWatchdog?.dispose();
9643
+ const context = this.audioContext;
9553
9644
  if (context && context.state !== 'closed') {
9554
9645
  document.removeEventListener('click', this.resumeAudioContext);
9555
9646
  await context.close();
@@ -9748,12 +9839,13 @@ class DynascaleManager {
9748
9839
  lastDimensions = currentDimensions;
9749
9840
  });
9750
9841
  resizeObserver?.observe(videoElement);
9842
+ const isVideoTrack = trackType === 'videoTrack';
9751
9843
  // element renders and gets bound - track subscription gets
9752
9844
  // triggered first other ones get skipped on initial subscriptions
9753
9845
  const publishedTracksSubscription = boundParticipant.isLocalParticipant
9754
9846
  ? null
9755
9847
  : participant$
9756
- .pipe(distinctUntilKeyChanged('publishedTracks'), map((p) => trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p)), distinctUntilChanged())
9848
+ .pipe(distinctUntilKeyChanged('publishedTracks'), map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))), distinctUntilChanged())
9757
9849
  .subscribe((isPublishing) => {
9758
9850
  if (isPublishing) {
9759
9851
  // the participant just started to publish a track
@@ -9773,10 +9865,11 @@ class DynascaleManager {
9773
9865
  // without prior user interaction:
9774
9866
  // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
9775
9867
  videoElement.muted = true;
9868
+ const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
9776
9869
  const streamSubscription = participant$
9777
- .pipe(distinctUntilKeyChanged(trackType === 'videoTrack' ? 'videoStream' : 'screenShareStream'))
9870
+ .pipe(distinctUntilKeyChanged(trackKey))
9778
9871
  .subscribe((p) => {
9779
- const source = trackType === 'videoTrack' ? p.videoStream : p.screenShareStream;
9872
+ const source = isVideoTrack ? p.videoStream : p.screenShareStream;
9780
9873
  if (videoElement.srcObject === source)
9781
9874
  return;
9782
9875
  videoElement.srcObject = source ?? null;
@@ -9815,6 +9908,7 @@ class DynascaleManager {
9815
9908
  const participant = this.callState.findParticipantBySessionId(sessionId);
9816
9909
  if (!participant || participant.isLocalParticipant)
9817
9910
  return;
9911
+ this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
9818
9912
  const participant$ = this.callState.participants$.pipe(map((ps) => ps.find((p) => p.sessionId === sessionId)), takeWhile((p) => !!p), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
9819
9913
  const updateSinkId = (deviceId, audioContext) => {
9820
9914
  if (!deviceId)
@@ -9833,14 +9927,12 @@ class DynascaleManager {
9833
9927
  };
9834
9928
  let sourceNode = undefined;
9835
9929
  let gainNode = undefined;
9930
+ const isAudioTrack = trackType === 'audioTrack';
9931
+ const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
9836
9932
  const updateMediaStreamSubscription = participant$
9837
- .pipe(distinctUntilKeyChanged(trackType === 'screenShareAudioTrack'
9838
- ? 'screenShareAudioStream'
9839
- : 'audioStream'))
9933
+ .pipe(distinctUntilKeyChanged(trackKey))
9840
9934
  .subscribe((p) => {
9841
- const source = trackType === 'screenShareAudioTrack'
9842
- ? p.screenShareAudioStream
9843
- : p.audioStream;
9935
+ const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
9844
9936
  if (audioElement.srcObject === source)
9845
9937
  return;
9846
9938
  setTimeout(() => {
@@ -9895,6 +9987,7 @@ class DynascaleManager {
9895
9987
  });
9896
9988
  audioElement.autoplay = true;
9897
9989
  return () => {
9990
+ this.audioBindingsWatchdog?.unregister(sessionId, trackType);
9898
9991
  sinkIdSubscription?.unsubscribe();
9899
9992
  volumeSubscription.unsubscribe();
9900
9993
  updateMediaStreamSubscription.unsubscribe();
@@ -9955,6 +10048,9 @@ class DynascaleManager {
9955
10048
  this.callState = callState;
9956
10049
  this.speaker = speaker;
9957
10050
  this.tracer = tracer;
10051
+ if (!isReactNative()) {
10052
+ this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
10053
+ }
9958
10054
  }
9959
10055
  setSfuClient(sfuClient) {
9960
10056
  this.sfuClient = sfuClient;
@@ -10299,6 +10395,9 @@ class BrowserPermission {
10299
10395
  }
10300
10396
  setState(state) {
10301
10397
  if (this.state !== state) {
10398
+ const { tracer, queryName } = this.permission;
10399
+ const traceKey = `navigator.mediaDevices.${queryName}.permission`;
10400
+ tracer?.trace(traceKey, { previous: this.state, state });
10302
10401
  this.state = state;
10303
10402
  this.listeners.forEach((listener) => listener(state));
10304
10403
  }
@@ -10340,9 +10439,6 @@ const getDevices = (permission, kind, tracer) => {
10340
10439
  const checkIfAudioOutputChangeSupported = () => {
10341
10440
  if (typeof document === 'undefined')
10342
10441
  return false;
10343
- // Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
10344
- if (isSafari())
10345
- return 'setSinkId' in AudioContext.prototype;
10346
10442
  const element = document.createElement('audio');
10347
10443
  return 'setSinkId' in element;
10348
10444
  };
@@ -10369,17 +10465,19 @@ const videoDeviceConstraints = {
10369
10465
  * Keeps track of the browser permission to use microphone. This permission also
10370
10466
  * affects an ability to enumerate audio devices.
10371
10467
  */
10372
- const getAudioBrowserPermission = lazy(() => new BrowserPermission({
10468
+ const getAudioBrowserPermission = lazy((tracer) => new BrowserPermission({
10373
10469
  constraints: audioDeviceConstraints,
10374
10470
  queryName: 'microphone',
10471
+ tracer,
10375
10472
  }));
10376
10473
  /**
10377
10474
  * Keeps track of the browser permission to use camera. This permission also
10378
10475
  * affects an ability to enumerate video devices.
10379
10476
  */
10380
- const getVideoBrowserPermission = lazy(() => new BrowserPermission({
10477
+ const getVideoBrowserPermission = lazy((tracer) => new BrowserPermission({
10381
10478
  constraints: videoDeviceConstraints,
10382
10479
  queryName: 'camera',
10480
+ tracer,
10383
10481
  }));
10384
10482
  const getDeviceChangeObserver = lazy((tracer) => {
10385
10483
  // 'addEventListener' is not available in React Native, returning
@@ -10395,7 +10493,7 @@ const getDeviceChangeObserver = lazy((tracer) => {
10395
10493
  * the observable errors.
10396
10494
  */
10397
10495
  const getAudioDevices = lazy((tracer) => {
10398
- return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput', tracer)), shareReplay(1));
10496
+ return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission(tracer).asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(tracer), 'audioinput', tracer)), shareReplay(1));
10399
10497
  });
10400
10498
  /**
10401
10499
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10404,7 +10502,7 @@ const getAudioDevices = lazy((tracer) => {
10404
10502
  * the observable errors.
10405
10503
  */
10406
10504
  const getVideoDevices = lazy((tracer) => {
10407
- return merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput', tracer)), shareReplay(1));
10505
+ return merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission(tracer).asObservable()).pipe(startWith([]), concatMap(() => getDevices(getVideoBrowserPermission(tracer), 'videoinput', tracer)), shareReplay(1));
10408
10506
  });
10409
10507
  /**
10410
10508
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10413,7 +10511,7 @@ const getVideoDevices = lazy((tracer) => {
10413
10511
  * the observable errors.
10414
10512
  */
10415
10513
  const getAudioOutputDevices = lazy((tracer) => {
10416
- return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput', tracer)), shareReplay(1));
10514
+ return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission(tracer).asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(tracer), 'audiooutput', tracer)), shareReplay(1));
10417
10515
  });
10418
10516
  let getUserMediaExecId = 0;
10419
10517
  const getStream = async (constraints, tracer) => {
@@ -10479,25 +10577,21 @@ const getAudioStream = async (trackConstraints, tracer) => {
10479
10577
  },
10480
10578
  };
10481
10579
  try {
10482
- await getAudioBrowserPermission().prompt({
10580
+ await getAudioBrowserPermission(tracer).prompt({
10483
10581
  throwOnNotAllowed: true,
10484
10582
  forcePrompt: true,
10485
10583
  });
10486
10584
  return await getStream(constraints, tracer);
10487
10585
  }
10488
10586
  catch (error) {
10587
+ const logger = videoLoggerSystem.getLogger('devices');
10489
10588
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
10490
10589
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10491
10590
  const { deviceId, ...relaxedConstraints } = trackConstraints;
10492
- videoLoggerSystem
10493
- .getLogger('devices')
10494
- .warn('Failed to get audio stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10591
+ logger.warn('Failed to get audio stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10495
10592
  return getAudioStream(relaxedConstraints, tracer);
10496
10593
  }
10497
- videoLoggerSystem.getLogger('devices').error('Failed to get audio stream', {
10498
- error,
10499
- constraints,
10500
- });
10594
+ logger.error('Failed to get audio stream', { error, constraints });
10501
10595
  throw error;
10502
10596
  }
10503
10597
  };
@@ -10517,25 +10611,21 @@ const getVideoStream = async (trackConstraints, tracer) => {
10517
10611
  },
10518
10612
  };
10519
10613
  try {
10520
- await getVideoBrowserPermission().prompt({
10614
+ await getVideoBrowserPermission(tracer).prompt({
10521
10615
  throwOnNotAllowed: true,
10522
10616
  forcePrompt: true,
10523
10617
  });
10524
10618
  return await getStream(constraints, tracer);
10525
10619
  }
10526
10620
  catch (error) {
10621
+ const logger = videoLoggerSystem.getLogger('devices');
10527
10622
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
10528
10623
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
10529
10624
  const { deviceId, ...relaxedConstraints } = trackConstraints;
10530
- videoLoggerSystem
10531
- .getLogger('devices')
10532
- .warn('Failed to get video stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10533
- return getVideoStream(relaxedConstraints);
10625
+ logger.warn('Failed to get video stream, will try again with relaxed constraints', { error, constraints, relaxedConstraints });
10626
+ return getVideoStream(relaxedConstraints, tracer);
10534
10627
  }
10535
- videoLoggerSystem.getLogger('devices').error('Failed to get video stream', {
10536
- error,
10537
- constraints,
10538
- });
10628
+ logger.error('Failed to get video stream', { error, constraints });
10539
10629
  throw error;
10540
10630
  }
10541
10631
  };
@@ -11306,8 +11396,8 @@ class DeviceManagerState {
11306
11396
  }
11307
11397
 
11308
11398
  class CameraManagerState extends DeviceManagerState {
11309
- constructor() {
11310
- super('stop-tracks', getVideoBrowserPermission());
11399
+ constructor(tracer) {
11400
+ super('stop-tracks', getVideoBrowserPermission(tracer));
11311
11401
  this.directionSubject = new BehaviorSubject(undefined);
11312
11402
  /**
11313
11403
  * Observable that emits the preferred camera direction
@@ -11361,7 +11451,7 @@ class CameraManager extends DeviceManager {
11361
11451
  * @param devicePersistence the device persistence preferences to use.
11362
11452
  */
11363
11453
  constructor(call, devicePersistence) {
11364
- super(call, new CameraManagerState(), TrackType.VIDEO, devicePersistence);
11454
+ super(call, new CameraManagerState(call.tracer), TrackType.VIDEO, devicePersistence);
11365
11455
  this.targetResolution = {
11366
11456
  width: 1280,
11367
11457
  height: 720,
@@ -11572,8 +11662,8 @@ class AudioDeviceManagerState extends DeviceManagerState {
11572
11662
  }
11573
11663
 
11574
11664
  class MicrophoneManagerState extends AudioDeviceManagerState {
11575
- constructor(disableMode) {
11576
- super(disableMode, getAudioBrowserPermission(), AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED);
11665
+ constructor(disableMode, tracer) {
11666
+ super(disableMode, getAudioBrowserPermission(tracer), AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED);
11577
11667
  this.speakingWhileMutedSubject = new BehaviorSubject(false);
11578
11668
  /**
11579
11669
  * An Observable that emits `true` if the user's microphone is muted, but they're speaking.
@@ -11913,7 +12003,7 @@ class RNSpeechDetector {
11913
12003
 
11914
12004
  class MicrophoneManager extends AudioDeviceManager {
11915
12005
  constructor(call, devicePersistence, disableMode = 'stop-tracks') {
11916
- super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO, devicePersistence);
12006
+ super(call, new MicrophoneManagerState(disableMode, call.tracer), TrackType.AUDIO, devicePersistence);
11917
12007
  this.speakingWhileMutedNotificationEnabled = true;
11918
12008
  this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
11919
12009
  this.silenceThresholdMs = 5000;
@@ -12015,7 +12105,6 @@ class MicrophoneManager extends AudioDeviceManager {
12015
12105
  deviceId,
12016
12106
  label,
12017
12107
  };
12018
- console.log(event);
12019
12108
  this.call.tracer.trace('mic.capture_report', event);
12020
12109
  this.call.streamClient.dispatchEvent(event);
12021
12110
  },
@@ -15889,7 +15978,7 @@ class StreamClient {
15889
15978
  this.getUserAgent = () => {
15890
15979
  if (!this.cachedUserAgent) {
15891
15980
  const { clientAppIdentifier = {} } = this.options;
15892
- const { sdkName = 'js', sdkVersion = "1.44.3", ...extras } = clientAppIdentifier;
15981
+ const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
15893
15982
  this.cachedUserAgent = [
15894
15983
  `stream-video-${sdkName}-v${sdkVersion}`,
15895
15984
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),