@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/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.4";
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;
@@ -10343,9 +10439,6 @@ const getDevices = (permission, kind, tracer) => {
10343
10439
  const checkIfAudioOutputChangeSupported = () => {
10344
10440
  if (typeof document === 'undefined')
10345
10441
  return false;
10346
- // Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
10347
- if (isSafari())
10348
- return 'setSinkId' in AudioContext.prototype;
10349
10442
  const element = document.createElement('audio');
10350
10443
  return 'setSinkId' in element;
10351
10444
  };
@@ -15885,7 +15978,7 @@ class StreamClient {
15885
15978
  this.getUserAgent = () => {
15886
15979
  if (!this.cachedUserAgent) {
15887
15980
  const { clientAppIdentifier = {} } = this.options;
15888
- const { sdkName = 'js', sdkVersion = "1.44.4", ...extras } = clientAppIdentifier;
15981
+ const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
15889
15982
  this.cachedUserAgent = [
15890
15983
  `stream-video-${sdkName}-v${sdkVersion}`,
15891
15984
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),