@stream-io/video-client 1.44.4 → 1.44.6-beta.0

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
@@ -4785,7 +4785,7 @@ class StreamVideoWriteableStateStore {
4785
4785
  * The currently connected user.
4786
4786
  */
4787
4787
  get connectedUser() {
4788
- return getCurrentValue(this.connectedUserSubject);
4788
+ return this.connectedUserSubject.getValue();
4789
4789
  }
4790
4790
  /**
4791
4791
  * A list of {@link Call} objects created/tracked by this client.
@@ -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.6-beta.0";
6288
6288
  const [major, minor, patch] = version.split('.');
6289
6289
  let sdkInfo = {
6290
6290
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -8983,6 +8983,7 @@ const watchCallRejected = (call) => {
8983
8983
  else {
8984
8984
  if (rejectedBy[eventCall.created_by.id]) {
8985
8985
  call.logger.info('call creator rejected, leaving call');
8986
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
8986
8987
  await call.leave({ message: 'ring: creator rejected' });
8987
8988
  }
8988
8989
  }
@@ -8993,6 +8994,7 @@ const watchCallRejected = (call) => {
8993
8994
  */
8994
8995
  const watchCallEnded = (call) => {
8995
8996
  return function onCallEnded() {
8997
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
8996
8998
  const { callingState } = call.state;
8997
8999
  if (callingState !== CallingState.IDLE &&
8998
9000
  callingState !== CallingState.LEFT) {
@@ -9024,6 +9026,7 @@ const watchSfuCallEnded = (call) => {
9024
9026
  // update the call state to reflect the call has ended.
9025
9027
  call.state.setEndedAt(new Date());
9026
9028
  const reason = CallEndedReason[e.reason];
9029
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9027
9030
  await call.leave({ message: `callEnded received: ${reason}` });
9028
9031
  }
9029
9032
  catch (err) {
@@ -9492,6 +9495,96 @@ class ViewportTracker {
9492
9495
  }
9493
9496
  }
9494
9497
 
9498
+ const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
9499
+ /**
9500
+ * Tracks audio element bindings and periodically warns about
9501
+ * remote participants whose audio streams have no bound element.
9502
+ */
9503
+ class AudioBindingsWatchdog {
9504
+ constructor(state, tracer) {
9505
+ this.state = state;
9506
+ this.tracer = tracer;
9507
+ this.bindings = new Map();
9508
+ this.enabled = true;
9509
+ this.logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
9510
+ /**
9511
+ * Registers an audio element binding for the given session and track type.
9512
+ * Warns if a different element is already bound to the same key.
9513
+ */
9514
+ this.register = (audioElement, sessionId, trackType) => {
9515
+ const key = toBindingKey(sessionId, trackType);
9516
+ const existing = this.bindings.get(key);
9517
+ if (existing && existing !== audioElement) {
9518
+ this.logger.warn(`Audio element already bound to ${sessionId} and ${trackType}`);
9519
+ this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
9520
+ }
9521
+ this.bindings.set(key, audioElement);
9522
+ };
9523
+ /**
9524
+ * Removes the audio element binding for the given session and track type.
9525
+ */
9526
+ this.unregister = (sessionId, trackType) => {
9527
+ this.bindings.delete(toBindingKey(sessionId, trackType));
9528
+ };
9529
+ /**
9530
+ * Enables or disables the watchdog.
9531
+ * When disabled, the periodic check stops but bindings are still tracked.
9532
+ */
9533
+ this.setEnabled = (enabled) => {
9534
+ this.enabled = enabled;
9535
+ if (enabled) {
9536
+ this.start();
9537
+ }
9538
+ else {
9539
+ this.stop();
9540
+ }
9541
+ };
9542
+ /**
9543
+ * Stops the watchdog and unsubscribes from callingState changes.
9544
+ */
9545
+ this.dispose = () => {
9546
+ this.stop();
9547
+ this.unsubscribeCallingState();
9548
+ };
9549
+ this.start = () => {
9550
+ clearInterval(this.watchdogInterval);
9551
+ this.watchdogInterval = setInterval(() => {
9552
+ const danglingUserIds = [];
9553
+ for (const p of this.state.participants) {
9554
+ if (p.isLocalParticipant)
9555
+ continue;
9556
+ const { audioStream, screenShareAudioStream, sessionId, userId } = p;
9557
+ if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
9558
+ danglingUserIds.push(userId);
9559
+ }
9560
+ if (screenShareAudioStream &&
9561
+ !this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
9562
+ danglingUserIds.push(userId);
9563
+ }
9564
+ }
9565
+ if (danglingUserIds.length > 0) {
9566
+ const key = 'audioBinding.danglingWarning';
9567
+ this.tracer.traceOnce(key, key, danglingUserIds);
9568
+ this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
9569
+ }
9570
+ }, 3000);
9571
+ };
9572
+ this.stop = () => {
9573
+ clearInterval(this.watchdogInterval);
9574
+ };
9575
+ this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
9576
+ if (!this.enabled)
9577
+ return;
9578
+ if (callingState !== CallingState.JOINED) {
9579
+ this.stop();
9580
+ }
9581
+ else {
9582
+ this.start();
9583
+ }
9584
+ });
9585
+ }
9586
+ }
9587
+
9495
9588
  const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
9496
9589
  videoTrack: VisibilityState.UNKNOWN,
9497
9590
  screenShareTrack: VisibilityState.UNKNOWN,
@@ -9517,7 +9610,7 @@ class DynascaleManager {
9517
9610
  */
9518
9611
  this.viewportTracker = new ViewportTracker();
9519
9612
  this.logger = videoLoggerSystem.getLogger('DynascaleManager');
9520
- this.useWebAudio = isSafari();
9613
+ this.useWebAudio = false;
9521
9614
  this.pendingSubscriptionsUpdate = null;
9522
9615
  this.videoTrackSubscriptionOverridesSubject = new BehaviorSubject({});
9523
9616
  this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
@@ -9549,7 +9642,8 @@ class DynascaleManager {
9549
9642
  if (this.pendingSubscriptionsUpdate) {
9550
9643
  clearTimeout(this.pendingSubscriptionsUpdate);
9551
9644
  }
9552
- const context = this.getOrCreateAudioContext();
9645
+ this.audioBindingsWatchdog?.dispose();
9646
+ const context = this.audioContext;
9553
9647
  if (context && context.state !== 'closed') {
9554
9648
  document.removeEventListener('click', this.resumeAudioContext);
9555
9649
  await context.close();
@@ -9748,12 +9842,13 @@ class DynascaleManager {
9748
9842
  lastDimensions = currentDimensions;
9749
9843
  });
9750
9844
  resizeObserver?.observe(videoElement);
9845
+ const isVideoTrack = trackType === 'videoTrack';
9751
9846
  // element renders and gets bound - track subscription gets
9752
9847
  // triggered first other ones get skipped on initial subscriptions
9753
9848
  const publishedTracksSubscription = boundParticipant.isLocalParticipant
9754
9849
  ? null
9755
9850
  : participant$
9756
- .pipe(distinctUntilKeyChanged('publishedTracks'), map((p) => trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p)), distinctUntilChanged())
9851
+ .pipe(distinctUntilKeyChanged('publishedTracks'), map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))), distinctUntilChanged())
9757
9852
  .subscribe((isPublishing) => {
9758
9853
  if (isPublishing) {
9759
9854
  // the participant just started to publish a track
@@ -9773,10 +9868,11 @@ class DynascaleManager {
9773
9868
  // without prior user interaction:
9774
9869
  // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
9775
9870
  videoElement.muted = true;
9871
+ const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
9776
9872
  const streamSubscription = participant$
9777
- .pipe(distinctUntilKeyChanged(trackType === 'videoTrack' ? 'videoStream' : 'screenShareStream'))
9873
+ .pipe(distinctUntilKeyChanged(trackKey))
9778
9874
  .subscribe((p) => {
9779
- const source = trackType === 'videoTrack' ? p.videoStream : p.screenShareStream;
9875
+ const source = isVideoTrack ? p.videoStream : p.screenShareStream;
9780
9876
  if (videoElement.srcObject === source)
9781
9877
  return;
9782
9878
  videoElement.srcObject = source ?? null;
@@ -9815,6 +9911,7 @@ class DynascaleManager {
9815
9911
  const participant = this.callState.findParticipantBySessionId(sessionId);
9816
9912
  if (!participant || participant.isLocalParticipant)
9817
9913
  return;
9914
+ this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
9818
9915
  const participant$ = this.callState.participants$.pipe(map((ps) => ps.find((p) => p.sessionId === sessionId)), takeWhile((p) => !!p), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
9819
9916
  const updateSinkId = (deviceId, audioContext) => {
9820
9917
  if (!deviceId)
@@ -9833,14 +9930,12 @@ class DynascaleManager {
9833
9930
  };
9834
9931
  let sourceNode = undefined;
9835
9932
  let gainNode = undefined;
9933
+ const isAudioTrack = trackType === 'audioTrack';
9934
+ const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
9836
9935
  const updateMediaStreamSubscription = participant$
9837
- .pipe(distinctUntilKeyChanged(trackType === 'screenShareAudioTrack'
9838
- ? 'screenShareAudioStream'
9839
- : 'audioStream'))
9936
+ .pipe(distinctUntilKeyChanged(trackKey))
9840
9937
  .subscribe((p) => {
9841
- const source = trackType === 'screenShareAudioTrack'
9842
- ? p.screenShareAudioStream
9843
- : p.audioStream;
9938
+ const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
9844
9939
  if (audioElement.srcObject === source)
9845
9940
  return;
9846
9941
  setTimeout(() => {
@@ -9895,6 +9990,7 @@ class DynascaleManager {
9895
9990
  });
9896
9991
  audioElement.autoplay = true;
9897
9992
  return () => {
9993
+ this.audioBindingsWatchdog?.unregister(sessionId, trackType);
9898
9994
  sinkIdSubscription?.unsubscribe();
9899
9995
  volumeSubscription.unsubscribe();
9900
9996
  updateMediaStreamSubscription.unsubscribe();
@@ -9955,6 +10051,9 @@ class DynascaleManager {
9955
10051
  this.callState = callState;
9956
10052
  this.speaker = speaker;
9957
10053
  this.tracer = tracer;
10054
+ if (!isReactNative()) {
10055
+ this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
10056
+ }
9958
10057
  }
9959
10058
  setSfuClient(sfuClient) {
9960
10059
  this.sfuClient = sfuClient;
@@ -10343,9 +10442,6 @@ const getDevices = (permission, kind, tracer) => {
10343
10442
  const checkIfAudioOutputChangeSupported = () => {
10344
10443
  if (typeof document === 'undefined')
10345
10444
  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
10445
  const element = document.createElement('audio');
10350
10446
  return 'setSinkId' in element;
10351
10447
  };
@@ -12534,6 +12630,7 @@ class SpeakerManager {
12534
12630
  this.defaultDevice = defaultDevice;
12535
12631
  globalThis.streamRNVideoSDK?.callManager.setup({
12536
12632
  defaultDevice,
12633
+ isRingingTypeCall: this.call.ringing,
12537
12634
  });
12538
12635
  }
12539
12636
  }
@@ -12733,6 +12830,7 @@ class Call {
12733
12830
  const currentUserId = this.currentUserId;
12734
12831
  if (currentUserId && blockedUserIds.includes(currentUserId)) {
12735
12832
  this.logger.info('Leaving call because of being blocked');
12833
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
12736
12834
  await this.leave({ message: 'user blocked' }).catch((err) => {
12737
12835
  this.logger.error('Error leaving call after being blocked', err);
12738
12836
  });
@@ -12769,6 +12867,7 @@ class Call {
12769
12867
  const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === CallingState.RINGING;
12770
12868
  if ((isAcceptedElsewhere || isRejectedByMe) &&
12771
12869
  !hasPending(this.joinLeaveConcurrencyTag)) {
12870
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
12772
12871
  this.leave().catch(() => {
12773
12872
  this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
12774
12873
  });
@@ -12780,6 +12879,9 @@ class Call {
12780
12879
  const receiver_id = this.clientStore.connectedUser?.id;
12781
12880
  const ended_at = callSession?.ended_at;
12782
12881
  const created_by_id = this.state.createdBy?.id;
12882
+ if (this.currentUserId && created_by_id === this.currentUserId) {
12883
+ globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
12884
+ }
12783
12885
  const rejected_by = callSession?.rejected_by;
12784
12886
  const accepted_by = callSession?.accepted_by;
12785
12887
  let leaveCallIdle = false;
@@ -12918,17 +13020,28 @@ class Call {
12918
13020
  }
12919
13021
  if (callingState === CallingState.RINGING && reject !== false) {
12920
13022
  if (reject) {
12921
- await this.reject(reason ?? 'decline');
13023
+ const reasonToEndCallReason = {
13024
+ timeout: 'missed',
13025
+ cancel: 'canceled',
13026
+ busy: 'busy',
13027
+ decline: 'rejected',
13028
+ };
13029
+ const rejectReason = reason ?? 'decline';
13030
+ const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
13031
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
13032
+ await this.reject(rejectReason);
12922
13033
  }
12923
13034
  else {
12924
13035
  // if reject was undefined, we still have to cancel the call automatically
12925
13036
  // when I am the creator and everyone else left the call
12926
13037
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
12927
13038
  if (this.isCreatedByMe && !hasOtherParticipants) {
13039
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
12928
13040
  await this.reject('cancel');
12929
13041
  }
12930
13042
  }
12931
13043
  }
13044
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this);
12932
13045
  this.statsReporter?.stop();
12933
13046
  this.statsReporter = undefined;
12934
13047
  const leaveReason = message ?? reason ?? 'user is leaving the call';
@@ -12955,7 +13068,9 @@ class Call {
12955
13068
  this.ringingSubject.next(false);
12956
13069
  this.cancelAutoDrop();
12957
13070
  this.clientStore.unregisterCall(this);
12958
- globalThis.streamRNVideoSDK?.callManager.stop();
13071
+ globalThis.streamRNVideoSDK?.callManager.stop({
13072
+ isRingingTypeCall: this.ringing,
13073
+ });
12959
13074
  this.camera.dispose();
12960
13075
  this.microphone.dispose();
12961
13076
  this.screenShare.dispose();
@@ -13121,11 +13236,19 @@ class Call {
13121
13236
  * @returns a promise which resolves once the call join-flow has finished.
13122
13237
  */
13123
13238
  this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
13124
- await this.setup();
13125
13239
  const callingState = this.state.callingState;
13126
13240
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
13127
13241
  throw new Error(`Illegal State: call.join() shall be called only once`);
13128
13242
  }
13243
+ if (data?.ring) {
13244
+ this.ringingSubject.next(true);
13245
+ }
13246
+ const callingX = globalThis.streamRNVideoSDK?.callingX;
13247
+ if (callingX) {
13248
+ // for Android/iOS, we need to start the call in the callingx library as soon as possible
13249
+ await callingX.joinCall(this, this.clientStore.calls);
13250
+ }
13251
+ await this.setup();
13129
13252
  this.joinResponseTimeout = joinResponseTimeout;
13130
13253
  this.rpcRequestTimeout = rpcRequestTimeout;
13131
13254
  // we will count the number of join failures per SFU.
@@ -13134,38 +13257,44 @@ class Call {
13134
13257
  const sfuJoinFailures = new Map();
13135
13258
  const joinData = data;
13136
13259
  maxJoinRetries = Math.max(maxJoinRetries, 1);
13137
- for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
13138
- try {
13139
- this.logger.trace(`Joining call (${attempt})`, this.cid);
13140
- await this.doJoin(data);
13141
- delete joinData.migrating_from;
13142
- delete joinData.migrating_from_list;
13143
- break;
13144
- }
13145
- catch (err) {
13146
- this.logger.warn(`Failed to join call (${attempt})`, this.cid);
13147
- if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
13148
- (err instanceof SfuJoinError && err.unrecoverable)) {
13149
- // if the error is unrecoverable, we should not retry as that signals
13150
- // that connectivity is good, but the coordinator doesn't allow the user
13151
- // to join the call due to some reason (e.g., ended call, expired token...)
13152
- throw err;
13153
- }
13154
- // immediately switch to a different SFU in case of recoverable join error
13155
- const switchSfu = err instanceof SfuJoinError &&
13156
- SfuJoinError.isJoinErrorCode(err.errorEvent);
13157
- const sfuId = this.credentials?.server.edge_name || '';
13158
- const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
13159
- sfuJoinFailures.set(sfuId, failures);
13160
- if (switchSfu || failures >= 2) {
13161
- joinData.migrating_from = sfuId;
13162
- joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
13260
+ try {
13261
+ for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
13262
+ try {
13263
+ this.logger.trace(`Joining call (${attempt})`, this.cid);
13264
+ await this.doJoin(data);
13265
+ delete joinData.migrating_from;
13266
+ delete joinData.migrating_from_list;
13267
+ break;
13163
13268
  }
13164
- if (attempt === maxJoinRetries - 1) {
13165
- throw err;
13269
+ catch (err) {
13270
+ this.logger.warn(`Failed to join call (${attempt})`, this.cid);
13271
+ if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
13272
+ (err instanceof SfuJoinError && err.unrecoverable)) {
13273
+ // if the error is unrecoverable, we should not retry as that signals
13274
+ // that connectivity is good, but the coordinator doesn't allow the user
13275
+ // to join the call due to some reason (e.g., ended call, expired token...)
13276
+ throw err;
13277
+ }
13278
+ // immediately switch to a different SFU in case of recoverable join error
13279
+ const switchSfu = err instanceof SfuJoinError &&
13280
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
13281
+ const sfuId = this.credentials?.server.edge_name || '';
13282
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
13283
+ sfuJoinFailures.set(sfuId, failures);
13284
+ if (switchSfu || failures >= 2) {
13285
+ joinData.migrating_from = sfuId;
13286
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
13287
+ }
13288
+ if (attempt === maxJoinRetries - 1) {
13289
+ throw err;
13290
+ }
13166
13291
  }
13292
+ await sleep(retryInterval(attempt));
13167
13293
  }
13168
- await sleep(retryInterval(attempt));
13294
+ }
13295
+ catch (error) {
13296
+ callingX?.endCall(this, 'error');
13297
+ throw error;
13169
13298
  }
13170
13299
  };
13171
13300
  /**
@@ -13312,7 +13441,9 @@ class Call {
13312
13441
  // re-apply them on later reconnections or server-side data fetches
13313
13442
  if (!this.deviceSettingsAppliedOnce && this.state.settings) {
13314
13443
  await this.applyDeviceConfig(this.state.settings, true, false);
13315
- globalThis.streamRNVideoSDK?.callManager.start();
13444
+ globalThis.streamRNVideoSDK?.callManager.start({
13445
+ isRingingTypeCall: this.ringing,
13446
+ });
13316
13447
  this.deviceSettingsAppliedOnce = true;
13317
13448
  }
13318
13449
  // We shouldn't persist the `ring` and `notify` state after joining the call
@@ -13740,6 +13871,7 @@ class Call {
13740
13871
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
13741
13872
  return;
13742
13873
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
13874
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
13743
13875
  this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
13744
13876
  this.logger.warn(`Can't leave call after disconnect request`, err);
13745
13877
  });
@@ -14761,7 +14893,7 @@ class Call {
14761
14893
  * A flag indicating whether the call was created by the current user.
14762
14894
  */
14763
14895
  get isCreatedByMe() {
14764
- return this.state.createdBy?.id === this.currentUserId;
14896
+ return (this.currentUserId && this.state.createdBy?.id === this.currentUserId);
14765
14897
  }
14766
14898
  }
14767
14899
 
@@ -15885,7 +16017,7 @@ class StreamClient {
15885
16017
  this.getUserAgent = () => {
15886
16018
  if (!this.cachedUserAgent) {
15887
16019
  const { clientAppIdentifier = {} } = this.options;
15888
- const { sdkName = 'js', sdkVersion = "1.44.4", ...extras } = clientAppIdentifier;
16020
+ const { sdkName = 'js', sdkVersion = "1.44.6-beta.0", ...extras } = clientAppIdentifier;
15889
16021
  this.cachedUserAgent = [
15890
16022
  `stream-video-${sdkName}-v${sdkVersion}`,
15891
16023
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),