@stream-io/video-client 1.44.6-beta.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
  }
@@ -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.44.6-beta.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,
@@ -9241,6 +9242,7 @@ const watchParticipantJoined = (state) => {
9241
9242
  // already announced participants.
9242
9243
  const orphanedTracks = reconcileOrphanedTracks(state, participant);
9243
9244
  state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
9245
+ ...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
9244
9246
  viewportVisibilityState: {
9245
9247
  videoTrack: exports.VisibilityState.UNKNOWN,
9246
9248
  screenShareTrack: exports.VisibilityState.UNKNOWN,
@@ -9572,11 +9574,14 @@ class AudioBindingsWatchdog {
9572
9574
  for (const p of this.state.participants) {
9573
9575
  if (p.isLocalParticipant)
9574
9576
  continue;
9575
- const { audioStream, screenShareAudioStream, sessionId, userId } = p;
9576
- if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
9577
+ const { audioStream, screenShareAudioStream, sessionId, userId, publishedTracks, } = p;
9578
+ if (audioStream &&
9579
+ publishedTracks.includes(TrackType.AUDIO) &&
9580
+ !this.bindings.has(toBindingKey(sessionId))) {
9577
9581
  danglingUserIds.push(userId);
9578
9582
  }
9579
9583
  if (screenShareAudioStream &&
9584
+ publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
9580
9585
  !this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
9581
9586
  danglingUserIds.push(userId);
9582
9587
  }
@@ -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;
@@ -11210,6 +11270,7 @@ class DeviceManager {
11210
11270
  isDeviceReplaced = true;
11211
11271
  }
11212
11272
  if (isDeviceDisconnected) {
11273
+ this.dispatchDeviceDisconnectedEvent(prevDevice);
11213
11274
  await this.disable();
11214
11275
  await this.select(undefined);
11215
11276
  }
@@ -11219,7 +11280,7 @@ class DeviceManager {
11219
11280
  await this.enable();
11220
11281
  this.isTrackStoppedDueToTrackEnd = false;
11221
11282
  }
11222
- else {
11283
+ else if (!hasPending(this.statusChangeConcurrencyTag)) {
11223
11284
  await this.applySettingsToStream();
11224
11285
  }
11225
11286
  }
@@ -11233,6 +11294,20 @@ class DeviceManager {
11233
11294
  const kind = this.mediaDeviceKind;
11234
11295
  return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
11235
11296
  }
11297
+ dispatchDeviceDisconnectedEvent(device) {
11298
+ const event = {
11299
+ type: 'device.disconnected',
11300
+ call_cid: this.call.cid,
11301
+ status: this.isTrackStoppedDueToTrackEnd
11302
+ ? this.state.prevStatus
11303
+ : this.state.status,
11304
+ deviceId: device.deviceId,
11305
+ label: device.label,
11306
+ kind: device.kind,
11307
+ };
11308
+ this.call.tracer.trace('device.disconnected', event);
11309
+ this.call.streamClient.dispatchEvent(event);
11310
+ }
11236
11311
  persistPreference(selectedDevice, status) {
11237
11312
  const deviceKind = this.mediaDeviceKind;
11238
11313
  const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
@@ -11883,11 +11958,15 @@ class RNSpeechDetector {
11883
11958
  ? this.externalAudioStream
11884
11959
  : await navigator.mediaDevices.getUserMedia({ audio: true });
11885
11960
  this.audioStream = audioStream;
11886
- this.pc1.addEventListener('icecandidate', async (e) => {
11887
- await this.pc2.addIceCandidate(e.candidate);
11961
+ this.pc1.addEventListener('icecandidate', (e) => {
11962
+ this.pc2.addIceCandidate(e.candidate).catch(() => {
11963
+ // do nothing
11964
+ });
11888
11965
  });
11889
11966
  this.pc2.addEventListener('icecandidate', async (e) => {
11890
- await this.pc1.addIceCandidate(e.candidate);
11967
+ this.pc1.addIceCandidate(e.candidate).catch(() => {
11968
+ // do nothing
11969
+ });
11891
11970
  });
11892
11971
  this.pc2.addEventListener('track', (e) => {
11893
11972
  e.streams[0].getTracks().forEach((track) => {
@@ -12116,6 +12195,7 @@ class MicrophoneManager extends AudioDeviceManager {
12116
12195
  const deviceId = this.state.selectedDevice;
12117
12196
  const devices = await rxjs.firstValueFrom(this.listDevices());
12118
12197
  const label = devices.find((d) => d.deviceId === deviceId)?.label;
12198
+ let lastCapturesAudio;
12119
12199
  this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
12120
12200
  noAudioThresholdMs: this.silenceThresholdMs,
12121
12201
  emitIntervalMs: this.silenceThresholdMs,
@@ -12127,7 +12207,10 @@ class MicrophoneManager extends AudioDeviceManager {
12127
12207
  deviceId,
12128
12208
  label,
12129
12209
  };
12130
- 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
+ }
12131
12214
  this.call.streamClient.dispatchEvent(event);
12132
12215
  },
12133
12216
  });
@@ -13047,16 +13130,16 @@ class Call {
13047
13130
  };
13048
13131
  const rejectReason = reason ?? 'decline';
13049
13132
  const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
13050
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
13051
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
13055
13138
  // when I am the creator and everyone else left the call
13056
13139
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
13057
13140
  if (this.isCreatedByMe && !hasOtherParticipants) {
13058
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
13059
13141
  await this.reject('cancel');
13142
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
13060
13143
  }
13061
13144
  }
13062
13145
  }
@@ -14795,6 +14878,12 @@ class Call {
14795
14878
  unbind();
14796
14879
  };
14797
14880
  };
14881
+ /**
14882
+ * Plays all audio elements blocked by the browser's autoplay policy.
14883
+ */
14884
+ this.resumeAudio = () => {
14885
+ return this.dynascaleManager.resumeAudio();
14886
+ };
14798
14887
  /**
14799
14888
  * Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
14800
14889
  *
@@ -16036,7 +16125,7 @@ class StreamClient {
16036
16125
  this.getUserAgent = () => {
16037
16126
  if (!this.cachedUserAgent) {
16038
16127
  const { clientAppIdentifier = {} } = this.options;
16039
- const { sdkName = 'js', sdkVersion = "1.44.6-beta.0", ...extras } = clientAppIdentifier;
16128
+ const { sdkName = 'js', sdkVersion = "1.46.0", ...extras } = clientAppIdentifier;
16040
16129
  this.cachedUserAgent = [
16041
16130
  `stream-video-${sdkName}-v${sdkVersion}`,
16042
16131
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),