@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.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.4";
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;
@@ -10362,9 +10458,6 @@ const getDevices = (permission, kind, tracer) => {
10362
10458
  const checkIfAudioOutputChangeSupported = () => {
10363
10459
  if (typeof document === 'undefined')
10364
10460
  return false;
10365
- // Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
10366
- if (isSafari())
10367
- return 'setSinkId' in AudioContext.prototype;
10368
10461
  const element = document.createElement('audio');
10369
10462
  return 'setSinkId' in element;
10370
10463
  };
@@ -15904,7 +15997,7 @@ class StreamClient {
15904
15997
  this.getUserAgent = () => {
15905
15998
  if (!this.cachedUserAgent) {
15906
15999
  const { clientAppIdentifier = {} } = this.options;
15907
- const { sdkName = 'js', sdkVersion = "1.44.4", ...extras } = clientAppIdentifier;
16000
+ const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
15908
16001
  this.cachedUserAgent = [
15909
16002
  `stream-video-${sdkName}-v${sdkVersion}`,
15910
16003
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),