@stream-io/video-client 1.50.0 → 1.51.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/index.browser.es.js +288 -58
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +288 -58
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +288 -58
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +1 -0
  9. package/dist/src/devices/CameraManager.d.ts +1 -0
  10. package/dist/src/devices/DeviceManager.d.ts +20 -0
  11. package/dist/src/devices/VirtualDevice.d.ts +59 -0
  12. package/dist/src/devices/devicePersistence.d.ts +1 -1
  13. package/dist/src/devices/index.d.ts +1 -0
  14. package/dist/src/rtc/BasePeerConnection.d.ts +7 -2
  15. package/dist/src/rtc/Publisher.d.ts +21 -3
  16. package/dist/src/rtc/TransceiverCache.d.ts +5 -1
  17. package/dist/src/rtc/helpers/degradationPreference.d.ts +1 -0
  18. package/dist/src/rtc/types.d.ts +2 -0
  19. package/package.json +2 -2
  20. package/src/Call.ts +22 -11
  21. package/src/devices/CameraManager.ts +9 -2
  22. package/src/devices/DeviceManager.ts +148 -8
  23. package/src/devices/DeviceManagerState.ts +4 -1
  24. package/src/devices/VirtualDevice.ts +69 -0
  25. package/src/devices/__tests__/CameraManager.test.ts +19 -0
  26. package/src/devices/__tests__/DeviceManager.test.ts +121 -1
  27. package/src/devices/devicePersistence.ts +2 -1
  28. package/src/devices/index.ts +1 -0
  29. package/src/rtc/BasePeerConnection.ts +15 -3
  30. package/src/rtc/Publisher.ts +140 -41
  31. package/src/rtc/TransceiverCache.ts +10 -3
  32. package/src/rtc/__tests__/Call.reconnect.test.ts +121 -2
  33. package/src/rtc/__tests__/Publisher.test.ts +659 -112
  34. package/src/rtc/__tests__/Subscriber.test.ts +2 -2
  35. package/src/rtc/helpers/__tests__/degradationPreference.test.ts +33 -1
  36. package/src/rtc/helpers/degradationPreference.ts +18 -0
  37. package/src/rtc/types.ts +2 -0
package/dist/index.cjs.js CHANGED
@@ -6379,7 +6379,7 @@ const getSdkVersion = (sdk) => {
6379
6379
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6380
6380
  };
6381
6381
 
6382
- const version = "1.50.0";
6382
+ const version = "1.51.0";
6383
6383
  const [major, minor, patch] = version.split('.');
6384
6384
  let sdkInfo = {
6385
6385
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -7498,7 +7498,7 @@ class BasePeerConnection {
7498
7498
  this.on = (event, fn) => {
7499
7499
  const getTag = () => this.tag;
7500
7500
  this.subscriptions.push(this.dispatcher.on(event, getTag, (e) => {
7501
- const lockKey = `pc.${this.lock}.${event}`;
7501
+ const lockKey = this.eventLockKey(event);
7502
7502
  withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
7503
7503
  if (this.isDisposed)
7504
7504
  return;
@@ -7506,6 +7506,13 @@ class BasePeerConnection {
7506
7506
  });
7507
7507
  }));
7508
7508
  };
7509
+ /**
7510
+ * Returns the per-event `withoutConcurrency` tag used to serialize the
7511
+ * dispatcher handler for `event` on this peer connection.
7512
+ */
7513
+ this.eventLockKey = (event) => {
7514
+ return `pc.${this.lock}.${event}`;
7515
+ };
7509
7516
  /**
7510
7517
  * Appends the trickled ICE candidates to the `RTCPeerConnection`.
7511
7518
  */
@@ -7759,7 +7766,7 @@ class BasePeerConnection {
7759
7766
  /**
7760
7767
  * Disposes the `RTCPeerConnection` instance.
7761
7768
  */
7762
- dispose() {
7769
+ async dispose() {
7763
7770
  clearTimeout(this.iceRestartTimeout);
7764
7771
  this.iceRestartTimeout = undefined;
7765
7772
  clearTimeout(this.preConnectStuckTimeout);
@@ -7781,6 +7788,7 @@ class BasePeerConnection {
7781
7788
  pc.removeEventListener('signalingstatechange', this.onSignalingChange);
7782
7789
  pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
7783
7790
  pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
7791
+ pc.removeEventListener('connectionstatechange', this.onConnectionStateChange);
7784
7792
  this.unsubscribeIceTrickle?.();
7785
7793
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
7786
7794
  this.subscriptions = [];
@@ -7808,8 +7816,14 @@ class TransceiverCache {
7808
7816
  * Gets the transceiver for the given publish option.
7809
7817
  */
7810
7818
  this.get = (publishOption) => {
7811
- return this.cache.find((bundle) => bundle.publishOption.id === publishOption.id &&
7812
- bundle.publishOption.trackType === publishOption.trackType);
7819
+ return this.getBy(publishOption.id, publishOption.trackType);
7820
+ };
7821
+ /**
7822
+ * Gets the transceiver for the given publish option id and track type.
7823
+ */
7824
+ this.getBy = (publishOptionId, trackType) => {
7825
+ return this.cache.find((bundle) => bundle.publishOption.id === publishOptionId &&
7826
+ bundle.publishOption.trackType === trackType);
7813
7827
  };
7814
7828
  /**
7815
7829
  * Updates the cached bundle with the given patch.
@@ -8094,6 +8108,21 @@ const toRTCDegradationPreference = (preference) => {
8094
8108
  ensureExhausted(preference, 'Unknown degradation preference');
8095
8109
  }
8096
8110
  };
8111
+ const fromRTCDegradationPreference = (preference) => {
8112
+ switch (preference) {
8113
+ case 'balanced':
8114
+ return DegradationPreference.BALANCED;
8115
+ case 'maintain-framerate':
8116
+ return DegradationPreference.MAINTAIN_FRAMERATE;
8117
+ case 'maintain-resolution':
8118
+ return DegradationPreference.MAINTAIN_RESOLUTION;
8119
+ // @ts-expect-error not in the typedefs yet
8120
+ case 'maintain-framerate-and-resolution':
8121
+ return DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION;
8122
+ default:
8123
+ return DegradationPreference.UNSPECIFIED;
8124
+ }
8125
+ };
8097
8126
 
8098
8127
  /**
8099
8128
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -8128,13 +8157,13 @@ class Publisher extends BasePeerConnection {
8128
8157
  // create a clone of the track as otherwise the same trackId will
8129
8158
  // appear in the SDP in multiple transceivers
8130
8159
  const trackToPublish = this.cloneTrack(track);
8131
- const { transceiver } = this.transceiverCache.get(publishOption) || {};
8132
- if (!transceiver) {
8160
+ const bundle = this.transceiverCache.get(publishOption);
8161
+ if (!bundle) {
8133
8162
  await this.addTransceiver(trackToPublish, publishOption, options);
8134
8163
  }
8135
8164
  else {
8136
- const previousTrack = transceiver.sender.track;
8137
- await this.updateTransceiver(transceiver, trackToPublish, trackType, options);
8165
+ const previousTrack = bundle.transceiver.sender.track;
8166
+ await this.updateTransceiver(bundle, trackToPublish, options);
8138
8167
  if (!isReactNative()) {
8139
8168
  this.stopTrack(previousTrack);
8140
8169
  }
@@ -8169,13 +8198,20 @@ class Publisher extends BasePeerConnection {
8169
8198
  /**
8170
8199
  * Updates the transceiver with the given track and track type.
8171
8200
  */
8172
- this.updateTransceiver = async (transceiver, track, trackType, options = {}) => {
8201
+ this.updateTransceiver = async (bundle, track, options = {}) => {
8202
+ const { transceiver, publishOption } = bundle;
8203
+ const trackType = publishOption.trackType;
8173
8204
  const sender = transceiver.sender;
8174
8205
  if (sender.track)
8175
8206
  this.trackIdToTrackType.delete(sender.track.id);
8176
8207
  await sender.replaceTrack(track);
8177
- if (track)
8208
+ if (track) {
8178
8209
  this.trackIdToTrackType.set(track.id, trackType);
8210
+ if (isFirefox() && bundle.videoSender) {
8211
+ // restore the encoding config from the cache, if any
8212
+ await this.changePublishQuality(bundle.videoSender, bundle);
8213
+ }
8214
+ }
8179
8215
  if (isAudioTrackType(trackType)) {
8180
8216
  await this.updateAudioPublishOptions(trackType, options);
8181
8217
  }
@@ -8235,7 +8271,7 @@ class Publisher extends BasePeerConnection {
8235
8271
  continue;
8236
8272
  // it is safe to stop the track here, it is a clone
8237
8273
  this.stopTrack(transceiver.sender.track);
8238
- await this.updateTransceiver(transceiver, null, publishOption.trackType);
8274
+ await this.updateTransceiver(item, null);
8239
8275
  }
8240
8276
  };
8241
8277
  /**
@@ -8292,33 +8328,38 @@ class Publisher extends BasePeerConnection {
8292
8328
  /**
8293
8329
  * Stops the cloned track that is being published to the SFU.
8294
8330
  */
8295
- this.stopTracks = (...trackTypes) => {
8296
- for (const item of this.transceiverCache.items()) {
8297
- const { publishOption, transceiver } = item;
8298
- if (!trackTypes.includes(publishOption.trackType))
8299
- continue;
8300
- this.stopTrack(transceiver.sender.track);
8301
- }
8331
+ this.stopTracks = async (...trackTypes) => {
8332
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8333
+ for (const item of this.transceiverCache.items()) {
8334
+ const { publishOption, transceiver } = item;
8335
+ if (!trackTypes.includes(publishOption.trackType))
8336
+ continue;
8337
+ const track = transceiver.sender.track;
8338
+ await this.silenceSenderOnFirefox(item);
8339
+ this.stopTrack(track);
8340
+ }
8341
+ });
8302
8342
  };
8303
8343
  /**
8304
8344
  * Stops all the cloned tracks that are being published to the SFU.
8305
8345
  */
8306
- this.stopAllTracks = () => {
8307
- for (const { transceiver } of this.transceiverCache.items()) {
8308
- this.stopTrack(transceiver.sender.track);
8309
- }
8310
- for (const track of this.clonedTracks) {
8311
- this.stopTrack(track);
8312
- }
8346
+ this.stopAllTracks = async () => {
8347
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8348
+ for (const item of this.transceiverCache.items()) {
8349
+ const track = item.transceiver.sender.track;
8350
+ await this.silenceSenderOnFirefox(item);
8351
+ this.stopTrack(track);
8352
+ }
8353
+ for (const track of this.clonedTracks) {
8354
+ this.stopTrack(track);
8355
+ }
8356
+ });
8313
8357
  };
8314
- this.changePublishQuality = async (videoSender) => {
8315
- const { trackType, layers, publishOptionId } = videoSender;
8316
- const enabledLayers = layers.filter((l) => l.active);
8358
+ this.changePublishQuality = async (videoSender, bundle) => {
8359
+ const enabledLayers = videoSender.layers.filter((l) => l.active);
8317
8360
  const tag = 'Update publish quality:';
8318
8361
  this.logger.info(`${tag} requested layers by SFU:`, enabledLayers);
8319
- const transceiverId = this.transceiverCache.find((t) => t.publishOption.id === publishOptionId &&
8320
- t.publishOption.trackType === trackType);
8321
- const sender = transceiverId?.transceiver.sender;
8362
+ const sender = bundle?.transceiver.sender;
8322
8363
  if (!sender) {
8323
8364
  return this.logger.warn(`${tag} no video sender found.`);
8324
8365
  }
@@ -8326,7 +8367,7 @@ class Publisher extends BasePeerConnection {
8326
8367
  if (params.encodings.length === 0) {
8327
8368
  return this.logger.warn(`${tag} there are no encodings set.`);
8328
8369
  }
8329
- const codecInUse = transceiverId?.publishOption.codec?.name;
8370
+ const codecInUse = bundle?.publishOption.codec?.name;
8330
8371
  const usesSvcCodec = codecInUse && isSvcCodec(codecInUse);
8331
8372
  let changed = false;
8332
8373
  for (const encoder of params.encodings) {
@@ -8526,6 +8567,72 @@ class Publisher extends BasePeerConnection {
8526
8567
  track.stop();
8527
8568
  this.clonedTracks.delete(track);
8528
8569
  };
8570
+ /**
8571
+ * Silences a Firefox sender on the wire during unpublish.
8572
+ *
8573
+ * Firefox keeps emitting RTP after track.stop(), but the right lever
8574
+ * differs by track type:
8575
+ * - audio: `replaceTrack(null)` is the only reliable silencer;
8576
+ * `setParameters({encodings:[...active:false]})` does NOT stop
8577
+ * the Opus encoder.
8578
+ * - video: `setParameters({encodings:[...active:false]})` pauses
8579
+ * the encoder; `replaceTrack(null)` does NOT reliably stop the
8580
+ * video encoder. The prior active=true configuration is captured
8581
+ * onto `bundle.videoSender` so `updateTransceiver` can restore
8582
+ * it on the next publish.
8583
+ *
8584
+ * No-op on non-Firefox browsers and during teardown.
8585
+ */
8586
+ this.silenceSenderOnFirefox = async (bundle) => {
8587
+ if (this.isDisposed || !isFirefox())
8588
+ return;
8589
+ const { transceiver, publishOption } = bundle;
8590
+ if (isAudioTrackType(publishOption.trackType)) {
8591
+ await transceiver.sender.replaceTrack(null).catch((err) => {
8592
+ this.logger.warn('Failed to clear audio sender track', err);
8593
+ });
8594
+ return;
8595
+ }
8596
+ await this.disableAllEncodings(bundle);
8597
+ };
8598
+ this.disableAllEncodings = async (bundle) => {
8599
+ const { transceiver, publishOption } = bundle;
8600
+ const sender = transceiver.sender;
8601
+ const params = sender.getParameters();
8602
+ if (!params.encodings || params.encodings.length === 0)
8603
+ return;
8604
+ if (!bundle.videoSender) {
8605
+ this.transceiverCache.update(publishOption, {
8606
+ videoSender: {
8607
+ trackType: publishOption.trackType,
8608
+ publishOptionId: publishOption.id,
8609
+ codec: publishOption.codec,
8610
+ degradationPreference: fromRTCDegradationPreference(params.degradationPreference),
8611
+ layers: params.encodings.map((e) => ({
8612
+ name: e.rid ?? 'q',
8613
+ active: e.active ?? true,
8614
+ maxBitrate: e.maxBitrate ?? 0,
8615
+ scaleResolutionDownBy: e.scaleResolutionDownBy ?? 0,
8616
+ maxFramerate: e.maxFramerate ?? 0,
8617
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
8618
+ scalabilityMode: e.scalabilityMode ?? '',
8619
+ })),
8620
+ },
8621
+ });
8622
+ }
8623
+ let changed = false;
8624
+ for (const encoding of params.encodings) {
8625
+ if (encoding.active !== false) {
8626
+ encoding.active = false;
8627
+ changed = true;
8628
+ }
8629
+ }
8630
+ if (!changed)
8631
+ return;
8632
+ await sender.setParameters(params).catch((err) => {
8633
+ this.logger.error('Failed to disable video sender encodings:', err);
8634
+ });
8635
+ };
8529
8636
  this.publishOptions = publishOptions;
8530
8637
  this.on('iceRestart', (iceRestart) => {
8531
8638
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
@@ -8534,7 +8641,16 @@ class Publisher extends BasePeerConnection {
8534
8641
  });
8535
8642
  this.on('changePublishQuality', async (event) => {
8536
8643
  for (const videoSender of event.videoSenders) {
8537
- await this.changePublishQuality(videoSender);
8644
+ // if not publishing, update the encodingConfigCache and don't modify the state.
8645
+ // we'll apply this config on the next publish/unmute.
8646
+ const { trackType, publishOptionId } = videoSender;
8647
+ const bundle = this.transceiverCache.getBy(publishOptionId, trackType);
8648
+ if (bundle) {
8649
+ this.transceiverCache.update(bundle.publishOption, { videoSender });
8650
+ }
8651
+ if (isFirefox() && !this.isPublishing(trackType))
8652
+ continue;
8653
+ await this.changePublishQuality(videoSender, bundle);
8538
8654
  }
8539
8655
  });
8540
8656
  this.on('changePublishOptions', (event) => {
@@ -8545,9 +8661,14 @@ class Publisher extends BasePeerConnection {
8545
8661
  /**
8546
8662
  * Disposes this Publisher instance.
8547
8663
  */
8548
- dispose() {
8549
- super.dispose();
8550
- this.stopAllTracks();
8664
+ async dispose() {
8665
+ await super.dispose();
8666
+ try {
8667
+ await this.stopAllTracks();
8668
+ }
8669
+ catch (err) {
8670
+ this.logger.warn('Failed to stop tracks during dispose', err);
8671
+ }
8551
8672
  this.clonedTracks.clear();
8552
8673
  }
8553
8674
  }
@@ -11331,8 +11452,8 @@ const normalize = (options) => {
11331
11452
  : false,
11332
11453
  };
11333
11454
  };
11334
- const createSyntheticDevice = (deviceId, kind) => {
11335
- return { deviceId, kind, label: '', groupId: '' };
11455
+ const createSyntheticDevice = (deviceId, kind, label = '') => {
11456
+ return { deviceId, kind, label, groupId: '' };
11336
11457
  };
11337
11458
  const readPreferences = (storageKey) => {
11338
11459
  try {
@@ -11386,6 +11507,8 @@ class DeviceManager {
11386
11507
  this.areSubscriptionsSetUp = false;
11387
11508
  this.isTrackStoppedDueToTrackEnd = false;
11388
11509
  this.filters = [];
11510
+ this.virtualDevicesSubject = new rxjs.BehaviorSubject([]);
11511
+ this.virtualDeviceConcurrencyTag = Symbol('virtualDeviceConcurrencyTag');
11389
11512
  this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
11390
11513
  this.filterRegistrationConcurrencyTag = Symbol('filterRegistrationConcurrencyTag');
11391
11514
  /**
@@ -11398,6 +11521,7 @@ class DeviceManager {
11398
11521
  this.subscriptions.forEach((s) => s());
11399
11522
  this.subscriptions = [];
11400
11523
  this.areSubscriptionsSetUp = false;
11524
+ this.virtualDevicesSubject.next([]);
11401
11525
  };
11402
11526
  this.runCurrentStreamCleanups = () => {
11403
11527
  this.currentStreamCleanups.forEach((c) => c());
@@ -11456,7 +11580,93 @@ class DeviceManager {
11456
11580
  * @returns an Observable that will be updated if a device is connected or disconnected
11457
11581
  */
11458
11582
  listDevices() {
11459
- return this.getDevices();
11583
+ return rxjs.combineLatest([this.getDevices(), this.virtualDevicesSubject]).pipe(rxjs.map(([real, virtual]) => [
11584
+ ...real,
11585
+ ...virtual.map((d) => createSyntheticDevice(d.deviceId, d.kind, d.label)),
11586
+ ]));
11587
+ }
11588
+ /**
11589
+ * Registers a virtual camera or microphone backed by a caller-supplied
11590
+ * stream factory. The device appears in `listDevices()` and can be selected
11591
+ * via `select()` like any real device.
11592
+ *
11593
+ * Web only. React Native is not supported.
11594
+ *
11595
+ * Only supported for camera and microphone managers; calling on any other
11596
+ * manager throws.
11597
+ */
11598
+ registerVirtualDevice(virtualDevice) {
11599
+ if (isReactNative()) {
11600
+ throw new Error('Virtual devices are not supported on React Native.');
11601
+ }
11602
+ if (this.trackType !== TrackType.AUDIO &&
11603
+ this.trackType !== TrackType.VIDEO) {
11604
+ throw new Error('Virtual devices are only supported for camera and microphone.');
11605
+ }
11606
+ const deviceId = `stream-virtual:${generateUUIDv4()}`;
11607
+ const entry = {
11608
+ deviceId,
11609
+ kind: this.mediaDeviceKind,
11610
+ ...virtualDevice,
11611
+ };
11612
+ setCurrentValue(this.virtualDevicesSubject, (current) => [
11613
+ ...current,
11614
+ entry,
11615
+ ]);
11616
+ return {
11617
+ deviceId: entry.deviceId,
11618
+ unregister: async () => {
11619
+ await withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11620
+ setCurrentValue(this.virtualDevicesSubject, (current) => current.filter((d) => d !== entry));
11621
+ if (this.activeVirtualSession?.deviceId === deviceId) {
11622
+ await this.stopActiveVirtualSession();
11623
+ }
11624
+ });
11625
+ if (this.state.selectedDevice === deviceId) {
11626
+ await this.statusChangeSettled();
11627
+ await this.disable({ forceStop: true });
11628
+ await this.select(undefined);
11629
+ }
11630
+ },
11631
+ };
11632
+ }
11633
+ sanitizeVirtualStream(stream) {
11634
+ stream.getTracks().forEach((track) => {
11635
+ const originalGetSettings = track.getSettings.bind(track);
11636
+ track.getSettings = () => {
11637
+ const settings = originalGetSettings();
11638
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11639
+ const { deviceId, ...rest } = settings;
11640
+ return rest;
11641
+ };
11642
+ });
11643
+ return stream;
11644
+ }
11645
+ findVirtualDevice(deviceId) {
11646
+ if (!deviceId)
11647
+ return undefined;
11648
+ return getCurrentValue(this.virtualDevicesSubject).find((d) => d.deviceId === deviceId);
11649
+ }
11650
+ async stopActiveVirtualSession() {
11651
+ const session = this.activeVirtualSession;
11652
+ this.activeVirtualSession = undefined;
11653
+ await session?.stop?.();
11654
+ }
11655
+ async getSelectedStream(constraints) {
11656
+ const deviceId = this.state.selectedDevice;
11657
+ if (!deviceId?.startsWith('stream-virtual')) {
11658
+ return this.getStream(constraints);
11659
+ }
11660
+ return withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11661
+ const virtualDevice = this.findVirtualDevice(deviceId);
11662
+ if (!virtualDevice) {
11663
+ throw new Error(`Virtual device is not registered: ${deviceId}`);
11664
+ }
11665
+ await this.stopActiveVirtualSession();
11666
+ const { stream, stop } = await virtualDevice.getUserMedia(constraints);
11667
+ this.activeVirtualSession = { deviceId, stop };
11668
+ return this.sanitizeVirtualStream(stream);
11669
+ });
11460
11670
  }
11461
11671
  /**
11462
11672
  * Returns `true` when this device is in enabled state.
@@ -11616,6 +11826,9 @@ class DeviceManager {
11616
11826
  }
11617
11827
  });
11618
11828
  }
11829
+ getResolvedConstraints(constraints) {
11830
+ return constraints;
11831
+ }
11619
11832
  publishStream(stream, options) {
11620
11833
  return this.call.publish(stream, this.trackType, options);
11621
11834
  }
@@ -11636,6 +11849,7 @@ class DeviceManager {
11636
11849
  this.muteLocalStream(stopTracks);
11637
11850
  const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
11638
11851
  if (allEnded) {
11852
+ await this.stopActiveVirtualSession();
11639
11853
  // @ts-expect-error release() is present in react-native-webrtc
11640
11854
  if (typeof mediaStream.release === 'function') {
11641
11855
  // @ts-expect-error called to dispose the stream in RN
@@ -11691,12 +11905,12 @@ class DeviceManager {
11691
11905
  // before chainWith below registers new ones for the new chain.
11692
11906
  this.runCurrentStreamCleanups();
11693
11907
  const defaultConstraints = this.state.defaultConstraints;
11694
- const constraints = {
11908
+ const constraints = this.getResolvedConstraints({
11695
11909
  ...defaultConstraints,
11696
11910
  deviceId: this.state.selectedDevice
11697
11911
  ? { exact: this.state.selectedDevice }
11698
11912
  : undefined,
11699
- };
11913
+ });
11700
11914
  /**
11701
11915
  * Chains two media streams together.
11702
11916
  *
@@ -11753,7 +11967,7 @@ class DeviceManager {
11753
11967
  };
11754
11968
  // the rootStream represents the stream coming from the actual device
11755
11969
  // e.g. camera or microphone stream
11756
- rootStreamPromise = this.getStream(constraints);
11970
+ rootStreamPromise = this.getSelectedStream(constraints);
11757
11971
  // we publish the last MediaStream of the chain
11758
11972
  stream = await this.filters.reduce((parent, entry) => parent
11759
11973
  .then((inputStream) => {
@@ -12070,7 +12284,10 @@ class DeviceManagerState {
12070
12284
  setCurrentValue(this.mediaStreamSubject, stream);
12071
12285
  setCurrentValue(this.rootMediaStreamSubject, rootStream);
12072
12286
  if (rootStream) {
12073
- this.setDevice(this.getDeviceIdFromStream(rootStream));
12287
+ const derived = this.getDeviceIdFromStream(rootStream);
12288
+ if (derived) {
12289
+ this.setDevice(derived);
12290
+ }
12074
12291
  }
12075
12292
  }
12076
12293
  /**
@@ -12283,7 +12500,7 @@ class CameraManager extends DeviceManager {
12283
12500
  getDevices() {
12284
12501
  return getVideoDevices(this.call.tracer);
12285
12502
  }
12286
- getStream(constraints) {
12503
+ getResolvedConstraints(constraints) {
12287
12504
  constraints.width = this.targetResolution.width;
12288
12505
  constraints.height = this.targetResolution.height;
12289
12506
  // We can't set both device id and facing mode
@@ -12294,6 +12511,9 @@ class CameraManager extends DeviceManager {
12294
12511
  constraints.facingMode =
12295
12512
  this.state.direction === 'front' ? 'user' : 'environment';
12296
12513
  }
12514
+ return constraints;
12515
+ }
12516
+ getStream(constraints) {
12297
12517
  return getVideoStream(constraints, this.call.tracer);
12298
12518
  }
12299
12519
  }
@@ -13621,9 +13841,10 @@ class Call {
13621
13841
  this.sfuStatsReporter?.flush();
13622
13842
  this.sfuStatsReporter?.stop();
13623
13843
  this.sfuStatsReporter = undefined;
13624
- this.subscriber?.dispose();
13844
+ this.lastStatsOptions = undefined;
13845
+ await this.subscriber?.dispose();
13625
13846
  this.subscriber = undefined;
13626
- this.publisher?.dispose();
13847
+ await this.publisher?.dispose();
13627
13848
  this.publisher = undefined;
13628
13849
  await this.sfuClient?.leaveAndClose(leaveReason);
13629
13850
  this.sfuClient = undefined;
@@ -13899,15 +14120,17 @@ class Call {
13899
14120
  const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
13900
14121
  const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
13901
14122
  const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
13902
- let statsOptions = this.sfuStatsReporter?.options;
14123
+ let statsOptions = this.lastStatsOptions;
13903
14124
  if (!this.credentials ||
13904
14125
  !statsOptions ||
13905
14126
  performingRejoin ||
13906
- performingMigration) {
14127
+ performingMigration ||
14128
+ data?.migrating_from) {
13907
14129
  try {
13908
14130
  const joinResponse = await this.doJoinRequest(data);
13909
14131
  this.credentials = joinResponse.credentials;
13910
14132
  statsOptions = joinResponse.stats_options;
14133
+ this.lastStatsOptions = statsOptions;
13911
14134
  }
13912
14135
  catch (error) {
13913
14136
  // prevent triggering reconnect flow if the state is OFFLINE
@@ -14014,7 +14237,7 @@ class Call {
14014
14237
  }
14015
14238
  else {
14016
14239
  const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
14017
- this.initPublisherAndSubscriber({
14240
+ await this.initPublisherAndSubscriber({
14018
14241
  sfuClient,
14019
14242
  connectionConfig,
14020
14243
  clientDetails,
@@ -14159,11 +14382,11 @@ class Call {
14159
14382
  * Initializes the Publisher and Subscriber Peer Connections.
14160
14383
  * @internal
14161
14384
  */
14162
- this.initPublisherAndSubscriber = (opts) => {
14385
+ this.initPublisherAndSubscriber = async (opts) => {
14163
14386
  const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
14164
14387
  const { enable_rtc_stats: enableTracing } = statsOptions;
14165
14388
  if (closePreviousInstances && this.subscriber) {
14166
- this.subscriber.dispose();
14389
+ await this.subscriber.dispose();
14167
14390
  }
14168
14391
  const basePeerConnectionOptions = {
14169
14392
  sfuClient,
@@ -14192,7 +14415,7 @@ class Call {
14192
14415
  const isAnonymous = this.streamClient.user?.type === 'anonymous';
14193
14416
  if (!isAnonymous) {
14194
14417
  if (closePreviousInstances && this.publisher) {
14195
- this.publisher.dispose();
14418
+ await this.publisher.dispose();
14196
14419
  }
14197
14420
  this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
14198
14421
  }
@@ -14295,10 +14518,17 @@ class Call {
14295
14518
  * `ICE_NEVER_CONNECTED` increments the unsupported-network counter).
14296
14519
  */
14297
14520
  this.reconnect = async (strategy, reason) => {
14298
- if (this.state.callingState === exports.CallingState.RECONNECTING ||
14521
+ if (this.state.callingState === exports.CallingState.JOINING ||
14522
+ this.state.callingState === exports.CallingState.RECONNECTING ||
14299
14523
  this.state.callingState === exports.CallingState.MIGRATING ||
14300
14524
  this.state.callingState === exports.CallingState.RECONNECTING_FAILED)
14301
14525
  return;
14526
+ // Drop redundant reconnect calls. If a reconnect is already queued or
14527
+ // running for this Call, that entry will resolve whatever broke;
14528
+ // queueing more entries just replays the full REJOIN cycle (one extra
14529
+ // `POST /join` per entry) once the call is already healthy again.
14530
+ if (hasPending(this.reconnectConcurrencyTag))
14531
+ return;
14302
14532
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
14303
14533
  const reconnectStartTime = Date.now();
14304
14534
  this.reconnectStrategy = strategy;
@@ -14503,8 +14733,8 @@ class Call {
14503
14733
  this.state.setCallingState(exports.CallingState.JOINED);
14504
14734
  }
14505
14735
  finally {
14506
- currentSubscriber?.dispose();
14507
- currentPublisher?.dispose();
14736
+ await currentSubscriber?.dispose();
14737
+ await currentPublisher?.dispose();
14508
14738
  // and close the previous SFU client, without specifying close code
14509
14739
  currentSfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'Migrating away');
14510
14740
  }
@@ -14693,7 +14923,7 @@ class Call {
14693
14923
  this.stopPublish = async (...trackTypes) => {
14694
14924
  if (!this.sfuClient || !this.publisher)
14695
14925
  return;
14696
- this.publisher.stopTracks(...trackTypes);
14926
+ await this.publisher.stopTracks(...trackTypes);
14697
14927
  await this.updateLocalStreamState(undefined, ...trackTypes);
14698
14928
  };
14699
14929
  /**
@@ -16806,7 +17036,7 @@ class StreamClient {
16806
17036
  this.getUserAgent = () => {
16807
17037
  if (!this.cachedUserAgent) {
16808
17038
  const { clientAppIdentifier = {} } = this.options;
16809
- const { sdkName = 'js', sdkVersion = "1.50.0", ...extras } = clientAppIdentifier;
17039
+ const { sdkName = 'js', sdkVersion = "1.51.0", ...extras } = clientAppIdentifier;
16810
17040
  this.cachedUserAgent = [
16811
17041
  `stream-video-${sdkName}-v${sdkVersion}`,
16812
17042
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),