@stream-io/video-client 1.45.0 → 1.46.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
@@ -3285,6 +3285,7 @@ class ParticipantJoined$Type extends runtime.MessageType {
3285
3285
  super('stream.video.sfu.event.ParticipantJoined', [
3286
3286
  { no: 1, name: 'call_cid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
3287
3287
  { no: 2, name: 'participant', kind: 'message', T: () => Participant },
3288
+ { no: 3, name: 'is_pinned', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
3288
3289
  ]);
3289
3290
  }
3290
3291
  }
@@ -4804,7 +4805,7 @@ class StreamVideoWriteableStateStore {
4804
4805
  * The currently connected user.
4805
4806
  */
4806
4807
  get connectedUser() {
4807
- return getCurrentValue(this.connectedUserSubject);
4808
+ return this.connectedUserSubject.getValue();
4808
4809
  }
4809
4810
  /**
4810
4811
  * A list of {@link Call} objects created/tracked by this client.
@@ -6303,7 +6304,7 @@ const getSdkVersion = (sdk) => {
6303
6304
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6304
6305
  };
6305
6306
 
6306
- const version = "1.45.0";
6307
+ const version = "1.46.0";
6307
6308
  const [major, minor, patch] = version.split('.');
6308
6309
  let sdkInfo = {
6309
6310
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -9002,6 +9003,7 @@ const watchCallRejected = (call) => {
9002
9003
  else {
9003
9004
  if (rejectedBy[eventCall.created_by.id]) {
9004
9005
  call.logger.info('call creator rejected, leaving call');
9006
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9005
9007
  await call.leave({ message: 'ring: creator rejected' });
9006
9008
  }
9007
9009
  }
@@ -9012,6 +9014,7 @@ const watchCallRejected = (call) => {
9012
9014
  */
9013
9015
  const watchCallEnded = (call) => {
9014
9016
  return function onCallEnded() {
9017
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9015
9018
  const { callingState } = call.state;
9016
9019
  if (callingState !== exports.CallingState.IDLE &&
9017
9020
  callingState !== exports.CallingState.LEFT) {
@@ -9043,6 +9046,7 @@ const watchSfuCallEnded = (call) => {
9043
9046
  // update the call state to reflect the call has ended.
9044
9047
  call.state.setEndedAt(new Date());
9045
9048
  const reason = CallEndedReason[e.reason];
9049
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9046
9050
  await call.leave({ message: `callEnded received: ${reason}` });
9047
9051
  }
9048
9052
  catch (err) {
@@ -9238,6 +9242,7 @@ const watchParticipantJoined = (state) => {
9238
9242
  // already announced participants.
9239
9243
  const orphanedTracks = reconcileOrphanedTracks(state, participant);
9240
9244
  state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
9245
+ ...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
9241
9246
  viewportVisibilityState: {
9242
9247
  videoTrack: exports.VisibilityState.UNKNOWN,
9243
9248
  screenShareTrack: exports.VisibilityState.UNKNOWN,
@@ -9631,6 +9636,31 @@ class DynascaleManager {
9631
9636
  this.logger = videoLoggerSystem.getLogger('DynascaleManager');
9632
9637
  this.useWebAudio = false;
9633
9638
  this.pendingSubscriptionsUpdate = null;
9639
+ /**
9640
+ * Audio elements that were blocked by the browser's autoplay policy.
9641
+ * These can be retried by calling `resumeAudio()` from a user gesture.
9642
+ */
9643
+ this.blockedAudioElementsSubject = new rxjs.BehaviorSubject(new Set());
9644
+ /**
9645
+ * Whether the browser's autoplay policy is blocking audio playback.
9646
+ * Will be `true` when the browser blocks autoplay (e.g., no prior user interaction).
9647
+ * Use `resumeAudio()` within a user gesture to unblock.
9648
+ */
9649
+ this.autoplayBlocked$ = this.blockedAudioElementsSubject.pipe(rxjs.map((elements) => elements.size > 0), rxjs.distinctUntilChanged());
9650
+ this.addBlockedAudioElement = (audioElement) => {
9651
+ setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
9652
+ const next = new Set(elements);
9653
+ next.add(audioElement);
9654
+ return next;
9655
+ });
9656
+ };
9657
+ this.removeBlockedAudioElement = (audioElement) => {
9658
+ setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
9659
+ const nextElements = new Set(elements);
9660
+ nextElements.delete(audioElement);
9661
+ return nextElements;
9662
+ });
9663
+ };
9634
9664
  this.videoTrackSubscriptionOverridesSubject = new rxjs.BehaviorSubject({});
9635
9665
  this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
9636
9666
  this.incomingVideoSettings$ = this.videoTrackSubscriptionOverrides$.pipe(rxjs.map((overrides) => {
@@ -9662,6 +9692,7 @@ class DynascaleManager {
9662
9692
  clearTimeout(this.pendingSubscriptionsUpdate);
9663
9693
  }
9664
9694
  this.audioBindingsWatchdog?.dispose();
9695
+ setCurrentValue(this.blockedAudioElementsSubject, new Set());
9665
9696
  const context = this.audioContext;
9666
9697
  if (context && context.state !== 'closed') {
9667
9698
  document.removeEventListener('click', this.resumeAudioContext);
@@ -9959,8 +9990,10 @@ class DynascaleManager {
9959
9990
  return;
9960
9991
  setTimeout(() => {
9961
9992
  audioElement.srcObject = source ?? null;
9962
- if (!source)
9993
+ if (!source) {
9994
+ this.removeBlockedAudioElement(audioElement);
9963
9995
  return;
9996
+ }
9964
9997
  // Safari has a special quirk that prevents playing audio until the user
9965
9998
  // interacts with the page or focuses on the tab where the call happens.
9966
9999
  // This is a workaround for the issue where:
@@ -9984,6 +10017,10 @@ class DynascaleManager {
9984
10017
  audioElement.muted = false;
9985
10018
  audioElement.play().catch((e) => {
9986
10019
  this.tracer.trace('audioPlaybackError', e.message);
10020
+ if (e.name === 'NotAllowedError') {
10021
+ this.tracer.trace('audioPlaybackBlocked', null);
10022
+ this.addBlockedAudioElement(audioElement);
10023
+ }
9987
10024
  this.logger.warn(`Failed to play audio stream`, e);
9988
10025
  });
9989
10026
  }
@@ -10010,6 +10047,7 @@ class DynascaleManager {
10010
10047
  audioElement.autoplay = true;
10011
10048
  return () => {
10012
10049
  this.audioBindingsWatchdog?.unregister(sessionId, trackType);
10050
+ this.removeBlockedAudioElement(audioElement);
10013
10051
  sinkIdSubscription?.unsubscribe();
10014
10052
  volumeSubscription.unsubscribe();
10015
10053
  updateMediaStreamSubscription.unsubscribe();
@@ -10018,6 +10056,28 @@ class DynascaleManager {
10018
10056
  gainNode?.disconnect();
10019
10057
  };
10020
10058
  };
10059
+ /**
10060
+ * Plays all audio elements blocked by the browser's autoplay policy.
10061
+ * Must be called from within a user gesture (e.g., click handler).
10062
+ *
10063
+ * @returns a promise that resolves when all blocked elements have been retried.
10064
+ */
10065
+ this.resumeAudio = async () => {
10066
+ this.tracer.trace('resumeAudio', null);
10067
+ const blocked = new Set();
10068
+ await Promise.all(Array.from(getCurrentValue(this.blockedAudioElementsSubject), async (el) => {
10069
+ try {
10070
+ if (el.srcObject) {
10071
+ await el.play();
10072
+ }
10073
+ }
10074
+ catch {
10075
+ this.logger.warn(`Can't resume audio for element: `, el);
10076
+ blocked.add(el);
10077
+ }
10078
+ }));
10079
+ setCurrentValue(this.blockedAudioElementsSubject, blocked);
10080
+ };
10021
10081
  this.getOrCreateAudioContext = () => {
10022
10082
  if (!this.useWebAudio)
10023
10083
  return;
@@ -11898,11 +11958,15 @@ class RNSpeechDetector {
11898
11958
  ? this.externalAudioStream
11899
11959
  : await navigator.mediaDevices.getUserMedia({ audio: true });
11900
11960
  this.audioStream = audioStream;
11901
- this.pc1.addEventListener('icecandidate', async (e) => {
11902
- await this.pc2.addIceCandidate(e.candidate);
11961
+ this.pc1.addEventListener('icecandidate', (e) => {
11962
+ this.pc2.addIceCandidate(e.candidate).catch(() => {
11963
+ // do nothing
11964
+ });
11903
11965
  });
11904
11966
  this.pc2.addEventListener('icecandidate', async (e) => {
11905
- await this.pc1.addIceCandidate(e.candidate);
11967
+ this.pc1.addIceCandidate(e.candidate).catch(() => {
11968
+ // do nothing
11969
+ });
11906
11970
  });
11907
11971
  this.pc2.addEventListener('track', (e) => {
11908
11972
  e.streams[0].getTracks().forEach((track) => {
@@ -12131,6 +12195,7 @@ class MicrophoneManager extends AudioDeviceManager {
12131
12195
  const deviceId = this.state.selectedDevice;
12132
12196
  const devices = await rxjs.firstValueFrom(this.listDevices());
12133
12197
  const label = devices.find((d) => d.deviceId === deviceId)?.label;
12198
+ let lastCapturesAudio;
12134
12199
  this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
12135
12200
  noAudioThresholdMs: this.silenceThresholdMs,
12136
12201
  emitIntervalMs: this.silenceThresholdMs,
@@ -12142,7 +12207,10 @@ class MicrophoneManager extends AudioDeviceManager {
12142
12207
  deviceId,
12143
12208
  label,
12144
12209
  };
12145
- this.call.tracer.trace('mic.capture_report', event);
12210
+ if (capturesAudio !== lastCapturesAudio) {
12211
+ lastCapturesAudio = capturesAudio;
12212
+ this.call.tracer.trace('mic.capture_report', event);
12213
+ }
12146
12214
  this.call.streamClient.dispatchEvent(event);
12147
12215
  },
12148
12216
  });
@@ -12664,6 +12732,7 @@ class SpeakerManager {
12664
12732
  this.defaultDevice = defaultDevice;
12665
12733
  globalThis.streamRNVideoSDK?.callManager.setup({
12666
12734
  defaultDevice,
12735
+ isRingingTypeCall: this.call.ringing,
12667
12736
  });
12668
12737
  }
12669
12738
  }
@@ -12863,6 +12932,7 @@ class Call {
12863
12932
  const currentUserId = this.currentUserId;
12864
12933
  if (currentUserId && blockedUserIds.includes(currentUserId)) {
12865
12934
  this.logger.info('Leaving call because of being blocked');
12935
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
12866
12936
  await this.leave({ message: 'user blocked' }).catch((err) => {
12867
12937
  this.logger.error('Error leaving call after being blocked', err);
12868
12938
  });
@@ -12899,6 +12969,7 @@ class Call {
12899
12969
  const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === exports.CallingState.RINGING;
12900
12970
  if ((isAcceptedElsewhere || isRejectedByMe) &&
12901
12971
  !hasPending(this.joinLeaveConcurrencyTag)) {
12972
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
12902
12973
  this.leave().catch(() => {
12903
12974
  this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
12904
12975
  });
@@ -12910,6 +12981,9 @@ class Call {
12910
12981
  const receiver_id = this.clientStore.connectedUser?.id;
12911
12982
  const ended_at = callSession?.ended_at;
12912
12983
  const created_by_id = this.state.createdBy?.id;
12984
+ if (this.currentUserId && created_by_id === this.currentUserId) {
12985
+ globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
12986
+ }
12913
12987
  const rejected_by = callSession?.rejected_by;
12914
12988
  const accepted_by = callSession?.accepted_by;
12915
12989
  let leaveCallIdle = false;
@@ -13048,7 +13122,16 @@ class Call {
13048
13122
  }
13049
13123
  if (callingState === exports.CallingState.RINGING && reject !== false) {
13050
13124
  if (reject) {
13051
- await this.reject(reason ?? 'decline');
13125
+ const reasonToEndCallReason = {
13126
+ timeout: 'missed',
13127
+ cancel: 'canceled',
13128
+ busy: 'busy',
13129
+ decline: 'rejected',
13130
+ };
13131
+ const rejectReason = reason ?? 'decline';
13132
+ const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
13133
+ await this.reject(rejectReason);
13134
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
13052
13135
  }
13053
13136
  else {
13054
13137
  // if reject was undefined, we still have to cancel the call automatically
@@ -13056,9 +13139,11 @@ class Call {
13056
13139
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
13057
13140
  if (this.isCreatedByMe && !hasOtherParticipants) {
13058
13141
  await this.reject('cancel');
13142
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
13059
13143
  }
13060
13144
  }
13061
13145
  }
13146
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this);
13062
13147
  this.statsReporter?.stop();
13063
13148
  this.statsReporter = undefined;
13064
13149
  const leaveReason = message ?? reason ?? 'user is leaving the call';
@@ -13085,7 +13170,9 @@ class Call {
13085
13170
  this.ringingSubject.next(false);
13086
13171
  this.cancelAutoDrop();
13087
13172
  this.clientStore.unregisterCall(this);
13088
- globalThis.streamRNVideoSDK?.callManager.stop();
13173
+ globalThis.streamRNVideoSDK?.callManager.stop({
13174
+ isRingingTypeCall: this.ringing,
13175
+ });
13089
13176
  this.camera.dispose();
13090
13177
  this.microphone.dispose();
13091
13178
  this.screenShare.dispose();
@@ -13251,11 +13338,19 @@ class Call {
13251
13338
  * @returns a promise which resolves once the call join-flow has finished.
13252
13339
  */
13253
13340
  this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
13254
- await this.setup();
13255
13341
  const callingState = this.state.callingState;
13256
13342
  if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
13257
13343
  throw new Error(`Illegal State: call.join() shall be called only once`);
13258
13344
  }
13345
+ if (data?.ring) {
13346
+ this.ringingSubject.next(true);
13347
+ }
13348
+ const callingX = globalThis.streamRNVideoSDK?.callingX;
13349
+ if (callingX) {
13350
+ // for Android/iOS, we need to start the call in the callingx library as soon as possible
13351
+ await callingX.joinCall(this, this.clientStore.calls);
13352
+ }
13353
+ await this.setup();
13259
13354
  this.joinResponseTimeout = joinResponseTimeout;
13260
13355
  this.rpcRequestTimeout = rpcRequestTimeout;
13261
13356
  // we will count the number of join failures per SFU.
@@ -13264,38 +13359,44 @@ class Call {
13264
13359
  const sfuJoinFailures = new Map();
13265
13360
  const joinData = data;
13266
13361
  maxJoinRetries = Math.max(maxJoinRetries, 1);
13267
- for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
13268
- try {
13269
- this.logger.trace(`Joining call (${attempt})`, this.cid);
13270
- await this.doJoin(data);
13271
- delete joinData.migrating_from;
13272
- delete joinData.migrating_from_list;
13273
- break;
13274
- }
13275
- catch (err) {
13276
- this.logger.warn(`Failed to join call (${attempt})`, this.cid);
13277
- if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
13278
- (err instanceof SfuJoinError && err.unrecoverable)) {
13279
- // if the error is unrecoverable, we should not retry as that signals
13280
- // that connectivity is good, but the coordinator doesn't allow the user
13281
- // to join the call due to some reason (e.g., ended call, expired token...)
13282
- throw err;
13283
- }
13284
- // immediately switch to a different SFU in case of recoverable join error
13285
- const switchSfu = err instanceof SfuJoinError &&
13286
- SfuJoinError.isJoinErrorCode(err.errorEvent);
13287
- const sfuId = this.credentials?.server.edge_name || '';
13288
- const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
13289
- sfuJoinFailures.set(sfuId, failures);
13290
- if (switchSfu || failures >= 2) {
13291
- joinData.migrating_from = sfuId;
13292
- joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
13362
+ try {
13363
+ for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
13364
+ try {
13365
+ this.logger.trace(`Joining call (${attempt})`, this.cid);
13366
+ await this.doJoin(data);
13367
+ delete joinData.migrating_from;
13368
+ delete joinData.migrating_from_list;
13369
+ break;
13293
13370
  }
13294
- if (attempt === maxJoinRetries - 1) {
13295
- throw err;
13371
+ catch (err) {
13372
+ this.logger.warn(`Failed to join call (${attempt})`, this.cid);
13373
+ if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
13374
+ (err instanceof SfuJoinError && err.unrecoverable)) {
13375
+ // if the error is unrecoverable, we should not retry as that signals
13376
+ // that connectivity is good, but the coordinator doesn't allow the user
13377
+ // to join the call due to some reason (e.g., ended call, expired token...)
13378
+ throw err;
13379
+ }
13380
+ // immediately switch to a different SFU in case of recoverable join error
13381
+ const switchSfu = err instanceof SfuJoinError &&
13382
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
13383
+ const sfuId = this.credentials?.server.edge_name || '';
13384
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
13385
+ sfuJoinFailures.set(sfuId, failures);
13386
+ if (switchSfu || failures >= 2) {
13387
+ joinData.migrating_from = sfuId;
13388
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
13389
+ }
13390
+ if (attempt === maxJoinRetries - 1) {
13391
+ throw err;
13392
+ }
13296
13393
  }
13394
+ await sleep(retryInterval(attempt));
13297
13395
  }
13298
- await sleep(retryInterval(attempt));
13396
+ }
13397
+ catch (error) {
13398
+ callingX?.endCall(this, 'error');
13399
+ throw error;
13299
13400
  }
13300
13401
  };
13301
13402
  /**
@@ -13442,7 +13543,9 @@ class Call {
13442
13543
  // re-apply them on later reconnections or server-side data fetches
13443
13544
  if (!this.deviceSettingsAppliedOnce && this.state.settings) {
13444
13545
  await this.applyDeviceConfig(this.state.settings, true, false);
13445
- globalThis.streamRNVideoSDK?.callManager.start();
13546
+ globalThis.streamRNVideoSDK?.callManager.start({
13547
+ isRingingTypeCall: this.ringing,
13548
+ });
13446
13549
  this.deviceSettingsAppliedOnce = true;
13447
13550
  }
13448
13551
  // We shouldn't persist the `ring` and `notify` state after joining the call
@@ -13870,6 +13973,7 @@ class Call {
13870
13973
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
13871
13974
  return;
13872
13975
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
13976
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
13873
13977
  this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
13874
13978
  this.logger.warn(`Can't leave call after disconnect request`, err);
13875
13979
  });
@@ -14774,6 +14878,12 @@ class Call {
14774
14878
  unbind();
14775
14879
  };
14776
14880
  };
14881
+ /**
14882
+ * Plays all audio elements blocked by the browser's autoplay policy.
14883
+ */
14884
+ this.resumeAudio = () => {
14885
+ return this.dynascaleManager.resumeAudio();
14886
+ };
14777
14887
  /**
14778
14888
  * Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
14779
14889
  *
@@ -14891,7 +15001,7 @@ class Call {
14891
15001
  * A flag indicating whether the call was created by the current user.
14892
15002
  */
14893
15003
  get isCreatedByMe() {
14894
- return this.state.createdBy?.id === this.currentUserId;
15004
+ return (this.currentUserId && this.state.createdBy?.id === this.currentUserId);
14895
15005
  }
14896
15006
  }
14897
15007
 
@@ -16015,7 +16125,7 @@ class StreamClient {
16015
16125
  this.getUserAgent = () => {
16016
16126
  if (!this.cachedUserAgent) {
16017
16127
  const { clientAppIdentifier = {} } = this.options;
16018
- const { sdkName = 'js', sdkVersion = "1.45.0", ...extras } = clientAppIdentifier;
16128
+ const { sdkName = 'js', sdkVersion = "1.46.0", ...extras } = clientAppIdentifier;
16019
16129
  this.cachedUserAgent = [
16020
16130
  `stream-video-${sdkName}-v${sdkVersion}`,
16021
16131
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),