@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.cjs.js CHANGED
@@ -4804,7 +4804,7 @@ class StreamVideoWriteableStateStore {
4804
4804
  * The currently connected user.
4805
4805
  */
4806
4806
  get connectedUser() {
4807
- return getCurrentValue(this.connectedUserSubject);
4807
+ return this.connectedUserSubject.getValue();
4808
4808
  }
4809
4809
  /**
4810
4810
  * A list of {@link Call} objects created/tracked by this client.
@@ -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.6-beta.0";
6307
6307
  const [major, minor, patch] = version.split('.');
6308
6308
  let sdkInfo = {
6309
6309
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -9002,6 +9002,7 @@ const watchCallRejected = (call) => {
9002
9002
  else {
9003
9003
  if (rejectedBy[eventCall.created_by.id]) {
9004
9004
  call.logger.info('call creator rejected, leaving call');
9005
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9005
9006
  await call.leave({ message: 'ring: creator rejected' });
9006
9007
  }
9007
9008
  }
@@ -9012,6 +9013,7 @@ const watchCallRejected = (call) => {
9012
9013
  */
9013
9014
  const watchCallEnded = (call) => {
9014
9015
  return function onCallEnded() {
9016
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9015
9017
  const { callingState } = call.state;
9016
9018
  if (callingState !== exports.CallingState.IDLE &&
9017
9019
  callingState !== exports.CallingState.LEFT) {
@@ -9043,6 +9045,7 @@ const watchSfuCallEnded = (call) => {
9043
9045
  // update the call state to reflect the call has ended.
9044
9046
  call.state.setEndedAt(new Date());
9045
9047
  const reason = CallEndedReason[e.reason];
9048
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9046
9049
  await call.leave({ message: `callEnded received: ${reason}` });
9047
9050
  }
9048
9051
  catch (err) {
@@ -9511,6 +9514,96 @@ class ViewportTracker {
9511
9514
  }
9512
9515
  }
9513
9516
 
9517
+ const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
9518
+ /**
9519
+ * Tracks audio element bindings and periodically warns about
9520
+ * remote participants whose audio streams have no bound element.
9521
+ */
9522
+ class AudioBindingsWatchdog {
9523
+ constructor(state, tracer) {
9524
+ this.state = state;
9525
+ this.tracer = tracer;
9526
+ this.bindings = new Map();
9527
+ this.enabled = true;
9528
+ this.logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
9529
+ /**
9530
+ * Registers an audio element binding for the given session and track type.
9531
+ * Warns if a different element is already bound to the same key.
9532
+ */
9533
+ this.register = (audioElement, sessionId, trackType) => {
9534
+ const key = toBindingKey(sessionId, trackType);
9535
+ const existing = this.bindings.get(key);
9536
+ if (existing && existing !== audioElement) {
9537
+ this.logger.warn(`Audio element already bound to ${sessionId} and ${trackType}`);
9538
+ this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
9539
+ }
9540
+ this.bindings.set(key, audioElement);
9541
+ };
9542
+ /**
9543
+ * Removes the audio element binding for the given session and track type.
9544
+ */
9545
+ this.unregister = (sessionId, trackType) => {
9546
+ this.bindings.delete(toBindingKey(sessionId, trackType));
9547
+ };
9548
+ /**
9549
+ * Enables or disables the watchdog.
9550
+ * When disabled, the periodic check stops but bindings are still tracked.
9551
+ */
9552
+ this.setEnabled = (enabled) => {
9553
+ this.enabled = enabled;
9554
+ if (enabled) {
9555
+ this.start();
9556
+ }
9557
+ else {
9558
+ this.stop();
9559
+ }
9560
+ };
9561
+ /**
9562
+ * Stops the watchdog and unsubscribes from callingState changes.
9563
+ */
9564
+ this.dispose = () => {
9565
+ this.stop();
9566
+ this.unsubscribeCallingState();
9567
+ };
9568
+ this.start = () => {
9569
+ clearInterval(this.watchdogInterval);
9570
+ this.watchdogInterval = setInterval(() => {
9571
+ const danglingUserIds = [];
9572
+ for (const p of this.state.participants) {
9573
+ if (p.isLocalParticipant)
9574
+ continue;
9575
+ const { audioStream, screenShareAudioStream, sessionId, userId } = p;
9576
+ if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
9577
+ danglingUserIds.push(userId);
9578
+ }
9579
+ if (screenShareAudioStream &&
9580
+ !this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
9581
+ danglingUserIds.push(userId);
9582
+ }
9583
+ }
9584
+ if (danglingUserIds.length > 0) {
9585
+ const key = 'audioBinding.danglingWarning';
9586
+ this.tracer.traceOnce(key, key, danglingUserIds);
9587
+ this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
9588
+ }
9589
+ }, 3000);
9590
+ };
9591
+ this.stop = () => {
9592
+ clearInterval(this.watchdogInterval);
9593
+ };
9594
+ this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
9595
+ if (!this.enabled)
9596
+ return;
9597
+ if (callingState !== exports.CallingState.JOINED) {
9598
+ this.stop();
9599
+ }
9600
+ else {
9601
+ this.start();
9602
+ }
9603
+ });
9604
+ }
9605
+ }
9606
+
9514
9607
  const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
9515
9608
  videoTrack: exports.VisibilityState.UNKNOWN,
9516
9609
  screenShareTrack: exports.VisibilityState.UNKNOWN,
@@ -9536,7 +9629,7 @@ class DynascaleManager {
9536
9629
  */
9537
9630
  this.viewportTracker = new ViewportTracker();
9538
9631
  this.logger = videoLoggerSystem.getLogger('DynascaleManager');
9539
- this.useWebAudio = isSafari();
9632
+ this.useWebAudio = false;
9540
9633
  this.pendingSubscriptionsUpdate = null;
9541
9634
  this.videoTrackSubscriptionOverridesSubject = new rxjs.BehaviorSubject({});
9542
9635
  this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
@@ -9568,7 +9661,8 @@ class DynascaleManager {
9568
9661
  if (this.pendingSubscriptionsUpdate) {
9569
9662
  clearTimeout(this.pendingSubscriptionsUpdate);
9570
9663
  }
9571
- const context = this.getOrCreateAudioContext();
9664
+ this.audioBindingsWatchdog?.dispose();
9665
+ const context = this.audioContext;
9572
9666
  if (context && context.state !== 'closed') {
9573
9667
  document.removeEventListener('click', this.resumeAudioContext);
9574
9668
  await context.close();
@@ -9767,12 +9861,13 @@ class DynascaleManager {
9767
9861
  lastDimensions = currentDimensions;
9768
9862
  });
9769
9863
  resizeObserver?.observe(videoElement);
9864
+ const isVideoTrack = trackType === 'videoTrack';
9770
9865
  // element renders and gets bound - track subscription gets
9771
9866
  // triggered first other ones get skipped on initial subscriptions
9772
9867
  const publishedTracksSubscription = boundParticipant.isLocalParticipant
9773
9868
  ? null
9774
9869
  : participant$
9775
- .pipe(rxjs.distinctUntilKeyChanged('publishedTracks'), rxjs.map((p) => trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p)), rxjs.distinctUntilChanged())
9870
+ .pipe(rxjs.distinctUntilKeyChanged('publishedTracks'), rxjs.map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))), rxjs.distinctUntilChanged())
9776
9871
  .subscribe((isPublishing) => {
9777
9872
  if (isPublishing) {
9778
9873
  // the participant just started to publish a track
@@ -9792,10 +9887,11 @@ class DynascaleManager {
9792
9887
  // without prior user interaction:
9793
9888
  // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
9794
9889
  videoElement.muted = true;
9890
+ const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
9795
9891
  const streamSubscription = participant$
9796
- .pipe(rxjs.distinctUntilKeyChanged(trackType === 'videoTrack' ? 'videoStream' : 'screenShareStream'))
9892
+ .pipe(rxjs.distinctUntilKeyChanged(trackKey))
9797
9893
  .subscribe((p) => {
9798
- const source = trackType === 'videoTrack' ? p.videoStream : p.screenShareStream;
9894
+ const source = isVideoTrack ? p.videoStream : p.screenShareStream;
9799
9895
  if (videoElement.srcObject === source)
9800
9896
  return;
9801
9897
  videoElement.srcObject = source ?? null;
@@ -9834,6 +9930,7 @@ class DynascaleManager {
9834
9930
  const participant = this.callState.findParticipantBySessionId(sessionId);
9835
9931
  if (!participant || participant.isLocalParticipant)
9836
9932
  return;
9933
+ this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
9837
9934
  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
9935
  const updateSinkId = (deviceId, audioContext) => {
9839
9936
  if (!deviceId)
@@ -9852,14 +9949,12 @@ class DynascaleManager {
9852
9949
  };
9853
9950
  let sourceNode = undefined;
9854
9951
  let gainNode = undefined;
9952
+ const isAudioTrack = trackType === 'audioTrack';
9953
+ const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
9855
9954
  const updateMediaStreamSubscription = participant$
9856
- .pipe(rxjs.distinctUntilKeyChanged(trackType === 'screenShareAudioTrack'
9857
- ? 'screenShareAudioStream'
9858
- : 'audioStream'))
9955
+ .pipe(rxjs.distinctUntilKeyChanged(trackKey))
9859
9956
  .subscribe((p) => {
9860
- const source = trackType === 'screenShareAudioTrack'
9861
- ? p.screenShareAudioStream
9862
- : p.audioStream;
9957
+ const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
9863
9958
  if (audioElement.srcObject === source)
9864
9959
  return;
9865
9960
  setTimeout(() => {
@@ -9914,6 +10009,7 @@ class DynascaleManager {
9914
10009
  });
9915
10010
  audioElement.autoplay = true;
9916
10011
  return () => {
10012
+ this.audioBindingsWatchdog?.unregister(sessionId, trackType);
9917
10013
  sinkIdSubscription?.unsubscribe();
9918
10014
  volumeSubscription.unsubscribe();
9919
10015
  updateMediaStreamSubscription.unsubscribe();
@@ -9974,6 +10070,9 @@ class DynascaleManager {
9974
10070
  this.callState = callState;
9975
10071
  this.speaker = speaker;
9976
10072
  this.tracer = tracer;
10073
+ if (!isReactNative()) {
10074
+ this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
10075
+ }
9977
10076
  }
9978
10077
  setSfuClient(sfuClient) {
9979
10078
  this.sfuClient = sfuClient;
@@ -10362,9 +10461,6 @@ const getDevices = (permission, kind, tracer) => {
10362
10461
  const checkIfAudioOutputChangeSupported = () => {
10363
10462
  if (typeof document === 'undefined')
10364
10463
  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
10464
  const element = document.createElement('audio');
10369
10465
  return 'setSinkId' in element;
10370
10466
  };
@@ -12553,6 +12649,7 @@ class SpeakerManager {
12553
12649
  this.defaultDevice = defaultDevice;
12554
12650
  globalThis.streamRNVideoSDK?.callManager.setup({
12555
12651
  defaultDevice,
12652
+ isRingingTypeCall: this.call.ringing,
12556
12653
  });
12557
12654
  }
12558
12655
  }
@@ -12752,6 +12849,7 @@ class Call {
12752
12849
  const currentUserId = this.currentUserId;
12753
12850
  if (currentUserId && blockedUserIds.includes(currentUserId)) {
12754
12851
  this.logger.info('Leaving call because of being blocked');
12852
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
12755
12853
  await this.leave({ message: 'user blocked' }).catch((err) => {
12756
12854
  this.logger.error('Error leaving call after being blocked', err);
12757
12855
  });
@@ -12788,6 +12886,7 @@ class Call {
12788
12886
  const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === exports.CallingState.RINGING;
12789
12887
  if ((isAcceptedElsewhere || isRejectedByMe) &&
12790
12888
  !hasPending(this.joinLeaveConcurrencyTag)) {
12889
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
12791
12890
  this.leave().catch(() => {
12792
12891
  this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
12793
12892
  });
@@ -12799,6 +12898,9 @@ class Call {
12799
12898
  const receiver_id = this.clientStore.connectedUser?.id;
12800
12899
  const ended_at = callSession?.ended_at;
12801
12900
  const created_by_id = this.state.createdBy?.id;
12901
+ if (this.currentUserId && created_by_id === this.currentUserId) {
12902
+ globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
12903
+ }
12802
12904
  const rejected_by = callSession?.rejected_by;
12803
12905
  const accepted_by = callSession?.accepted_by;
12804
12906
  let leaveCallIdle = false;
@@ -12937,17 +13039,28 @@ class Call {
12937
13039
  }
12938
13040
  if (callingState === exports.CallingState.RINGING && reject !== false) {
12939
13041
  if (reject) {
12940
- await this.reject(reason ?? 'decline');
13042
+ const reasonToEndCallReason = {
13043
+ timeout: 'missed',
13044
+ cancel: 'canceled',
13045
+ busy: 'busy',
13046
+ decline: 'rejected',
13047
+ };
13048
+ const rejectReason = reason ?? 'decline';
13049
+ const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
13050
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
13051
+ await this.reject(rejectReason);
12941
13052
  }
12942
13053
  else {
12943
13054
  // if reject was undefined, we still have to cancel the call automatically
12944
13055
  // when I am the creator and everyone else left the call
12945
13056
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
12946
13057
  if (this.isCreatedByMe && !hasOtherParticipants) {
13058
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
12947
13059
  await this.reject('cancel');
12948
13060
  }
12949
13061
  }
12950
13062
  }
13063
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this);
12951
13064
  this.statsReporter?.stop();
12952
13065
  this.statsReporter = undefined;
12953
13066
  const leaveReason = message ?? reason ?? 'user is leaving the call';
@@ -12974,7 +13087,9 @@ class Call {
12974
13087
  this.ringingSubject.next(false);
12975
13088
  this.cancelAutoDrop();
12976
13089
  this.clientStore.unregisterCall(this);
12977
- globalThis.streamRNVideoSDK?.callManager.stop();
13090
+ globalThis.streamRNVideoSDK?.callManager.stop({
13091
+ isRingingTypeCall: this.ringing,
13092
+ });
12978
13093
  this.camera.dispose();
12979
13094
  this.microphone.dispose();
12980
13095
  this.screenShare.dispose();
@@ -13140,11 +13255,19 @@ class Call {
13140
13255
  * @returns a promise which resolves once the call join-flow has finished.
13141
13256
  */
13142
13257
  this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
13143
- await this.setup();
13144
13258
  const callingState = this.state.callingState;
13145
13259
  if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
13146
13260
  throw new Error(`Illegal State: call.join() shall be called only once`);
13147
13261
  }
13262
+ if (data?.ring) {
13263
+ this.ringingSubject.next(true);
13264
+ }
13265
+ const callingX = globalThis.streamRNVideoSDK?.callingX;
13266
+ if (callingX) {
13267
+ // for Android/iOS, we need to start the call in the callingx library as soon as possible
13268
+ await callingX.joinCall(this, this.clientStore.calls);
13269
+ }
13270
+ await this.setup();
13148
13271
  this.joinResponseTimeout = joinResponseTimeout;
13149
13272
  this.rpcRequestTimeout = rpcRequestTimeout;
13150
13273
  // we will count the number of join failures per SFU.
@@ -13153,38 +13276,44 @@ class Call {
13153
13276
  const sfuJoinFailures = new Map();
13154
13277
  const joinData = data;
13155
13278
  maxJoinRetries = Math.max(maxJoinRetries, 1);
13156
- for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
13157
- try {
13158
- this.logger.trace(`Joining call (${attempt})`, this.cid);
13159
- await this.doJoin(data);
13160
- delete joinData.migrating_from;
13161
- delete joinData.migrating_from_list;
13162
- break;
13163
- }
13164
- catch (err) {
13165
- this.logger.warn(`Failed to join call (${attempt})`, this.cid);
13166
- if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
13167
- (err instanceof SfuJoinError && err.unrecoverable)) {
13168
- // if the error is unrecoverable, we should not retry as that signals
13169
- // that connectivity is good, but the coordinator doesn't allow the user
13170
- // to join the call due to some reason (e.g., ended call, expired token...)
13171
- throw err;
13172
- }
13173
- // immediately switch to a different SFU in case of recoverable join error
13174
- const switchSfu = err instanceof SfuJoinError &&
13175
- SfuJoinError.isJoinErrorCode(err.errorEvent);
13176
- const sfuId = this.credentials?.server.edge_name || '';
13177
- const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
13178
- sfuJoinFailures.set(sfuId, failures);
13179
- if (switchSfu || failures >= 2) {
13180
- joinData.migrating_from = sfuId;
13181
- joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
13279
+ try {
13280
+ for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
13281
+ try {
13282
+ this.logger.trace(`Joining call (${attempt})`, this.cid);
13283
+ await this.doJoin(data);
13284
+ delete joinData.migrating_from;
13285
+ delete joinData.migrating_from_list;
13286
+ break;
13182
13287
  }
13183
- if (attempt === maxJoinRetries - 1) {
13184
- throw err;
13288
+ catch (err) {
13289
+ this.logger.warn(`Failed to join call (${attempt})`, this.cid);
13290
+ if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
13291
+ (err instanceof SfuJoinError && err.unrecoverable)) {
13292
+ // if the error is unrecoverable, we should not retry as that signals
13293
+ // that connectivity is good, but the coordinator doesn't allow the user
13294
+ // to join the call due to some reason (e.g., ended call, expired token...)
13295
+ throw err;
13296
+ }
13297
+ // immediately switch to a different SFU in case of recoverable join error
13298
+ const switchSfu = err instanceof SfuJoinError &&
13299
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
13300
+ const sfuId = this.credentials?.server.edge_name || '';
13301
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
13302
+ sfuJoinFailures.set(sfuId, failures);
13303
+ if (switchSfu || failures >= 2) {
13304
+ joinData.migrating_from = sfuId;
13305
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
13306
+ }
13307
+ if (attempt === maxJoinRetries - 1) {
13308
+ throw err;
13309
+ }
13185
13310
  }
13311
+ await sleep(retryInterval(attempt));
13186
13312
  }
13187
- await sleep(retryInterval(attempt));
13313
+ }
13314
+ catch (error) {
13315
+ callingX?.endCall(this, 'error');
13316
+ throw error;
13188
13317
  }
13189
13318
  };
13190
13319
  /**
@@ -13331,7 +13460,9 @@ class Call {
13331
13460
  // re-apply them on later reconnections or server-side data fetches
13332
13461
  if (!this.deviceSettingsAppliedOnce && this.state.settings) {
13333
13462
  await this.applyDeviceConfig(this.state.settings, true, false);
13334
- globalThis.streamRNVideoSDK?.callManager.start();
13463
+ globalThis.streamRNVideoSDK?.callManager.start({
13464
+ isRingingTypeCall: this.ringing,
13465
+ });
13335
13466
  this.deviceSettingsAppliedOnce = true;
13336
13467
  }
13337
13468
  // We shouldn't persist the `ring` and `notify` state after joining the call
@@ -13759,6 +13890,7 @@ class Call {
13759
13890
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
13760
13891
  return;
13761
13892
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
13893
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
13762
13894
  this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
13763
13895
  this.logger.warn(`Can't leave call after disconnect request`, err);
13764
13896
  });
@@ -14780,7 +14912,7 @@ class Call {
14780
14912
  * A flag indicating whether the call was created by the current user.
14781
14913
  */
14782
14914
  get isCreatedByMe() {
14783
- return this.state.createdBy?.id === this.currentUserId;
14915
+ return (this.currentUserId && this.state.createdBy?.id === this.currentUserId);
14784
14916
  }
14785
14917
  }
14786
14918
 
@@ -15904,7 +16036,7 @@ class StreamClient {
15904
16036
  this.getUserAgent = () => {
15905
16037
  if (!this.cachedUserAgent) {
15906
16038
  const { clientAppIdentifier = {} } = this.options;
15907
- const { sdkName = 'js', sdkVersion = "1.44.4", ...extras } = clientAppIdentifier;
16039
+ const { sdkName = 'js', sdkVersion = "1.44.6-beta.0", ...extras } = clientAppIdentifier;
15908
16040
  this.cachedUserAgent = [
15909
16041
  `stream-video-${sdkName}-v${sdkVersion}`,
15910
16042
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),