@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.es.js CHANGED
@@ -6360,7 +6360,7 @@ const getSdkVersion = (sdk) => {
6360
6360
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6361
6361
  };
6362
6362
 
6363
- const version = "1.50.0";
6363
+ const version = "1.51.0";
6364
6364
  const [major, minor, patch] = version.split('.');
6365
6365
  let sdkInfo = {
6366
6366
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -7479,7 +7479,7 @@ class BasePeerConnection {
7479
7479
  this.on = (event, fn) => {
7480
7480
  const getTag = () => this.tag;
7481
7481
  this.subscriptions.push(this.dispatcher.on(event, getTag, (e) => {
7482
- const lockKey = `pc.${this.lock}.${event}`;
7482
+ const lockKey = this.eventLockKey(event);
7483
7483
  withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
7484
7484
  if (this.isDisposed)
7485
7485
  return;
@@ -7487,6 +7487,13 @@ class BasePeerConnection {
7487
7487
  });
7488
7488
  }));
7489
7489
  };
7490
+ /**
7491
+ * Returns the per-event `withoutConcurrency` tag used to serialize the
7492
+ * dispatcher handler for `event` on this peer connection.
7493
+ */
7494
+ this.eventLockKey = (event) => {
7495
+ return `pc.${this.lock}.${event}`;
7496
+ };
7490
7497
  /**
7491
7498
  * Appends the trickled ICE candidates to the `RTCPeerConnection`.
7492
7499
  */
@@ -7740,7 +7747,7 @@ class BasePeerConnection {
7740
7747
  /**
7741
7748
  * Disposes the `RTCPeerConnection` instance.
7742
7749
  */
7743
- dispose() {
7750
+ async dispose() {
7744
7751
  clearTimeout(this.iceRestartTimeout);
7745
7752
  this.iceRestartTimeout = undefined;
7746
7753
  clearTimeout(this.preConnectStuckTimeout);
@@ -7762,6 +7769,7 @@ class BasePeerConnection {
7762
7769
  pc.removeEventListener('signalingstatechange', this.onSignalingChange);
7763
7770
  pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
7764
7771
  pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
7772
+ pc.removeEventListener('connectionstatechange', this.onConnectionStateChange);
7765
7773
  this.unsubscribeIceTrickle?.();
7766
7774
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
7767
7775
  this.subscriptions = [];
@@ -7789,8 +7797,14 @@ class TransceiverCache {
7789
7797
  * Gets the transceiver for the given publish option.
7790
7798
  */
7791
7799
  this.get = (publishOption) => {
7792
- return this.cache.find((bundle) => bundle.publishOption.id === publishOption.id &&
7793
- bundle.publishOption.trackType === publishOption.trackType);
7800
+ return this.getBy(publishOption.id, publishOption.trackType);
7801
+ };
7802
+ /**
7803
+ * Gets the transceiver for the given publish option id and track type.
7804
+ */
7805
+ this.getBy = (publishOptionId, trackType) => {
7806
+ return this.cache.find((bundle) => bundle.publishOption.id === publishOptionId &&
7807
+ bundle.publishOption.trackType === trackType);
7794
7808
  };
7795
7809
  /**
7796
7810
  * Updates the cached bundle with the given patch.
@@ -8075,6 +8089,21 @@ const toRTCDegradationPreference = (preference) => {
8075
8089
  ensureExhausted(preference, 'Unknown degradation preference');
8076
8090
  }
8077
8091
  };
8092
+ const fromRTCDegradationPreference = (preference) => {
8093
+ switch (preference) {
8094
+ case 'balanced':
8095
+ return DegradationPreference.BALANCED;
8096
+ case 'maintain-framerate':
8097
+ return DegradationPreference.MAINTAIN_FRAMERATE;
8098
+ case 'maintain-resolution':
8099
+ return DegradationPreference.MAINTAIN_RESOLUTION;
8100
+ // @ts-expect-error not in the typedefs yet
8101
+ case 'maintain-framerate-and-resolution':
8102
+ return DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION;
8103
+ default:
8104
+ return DegradationPreference.UNSPECIFIED;
8105
+ }
8106
+ };
8078
8107
 
8079
8108
  /**
8080
8109
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -8109,13 +8138,13 @@ class Publisher extends BasePeerConnection {
8109
8138
  // create a clone of the track as otherwise the same trackId will
8110
8139
  // appear in the SDP in multiple transceivers
8111
8140
  const trackToPublish = this.cloneTrack(track);
8112
- const { transceiver } = this.transceiverCache.get(publishOption) || {};
8113
- if (!transceiver) {
8141
+ const bundle = this.transceiverCache.get(publishOption);
8142
+ if (!bundle) {
8114
8143
  await this.addTransceiver(trackToPublish, publishOption, options);
8115
8144
  }
8116
8145
  else {
8117
- const previousTrack = transceiver.sender.track;
8118
- await this.updateTransceiver(transceiver, trackToPublish, trackType, options);
8146
+ const previousTrack = bundle.transceiver.sender.track;
8147
+ await this.updateTransceiver(bundle, trackToPublish, options);
8119
8148
  if (!isReactNative()) {
8120
8149
  this.stopTrack(previousTrack);
8121
8150
  }
@@ -8150,13 +8179,20 @@ class Publisher extends BasePeerConnection {
8150
8179
  /**
8151
8180
  * Updates the transceiver with the given track and track type.
8152
8181
  */
8153
- this.updateTransceiver = async (transceiver, track, trackType, options = {}) => {
8182
+ this.updateTransceiver = async (bundle, track, options = {}) => {
8183
+ const { transceiver, publishOption } = bundle;
8184
+ const trackType = publishOption.trackType;
8154
8185
  const sender = transceiver.sender;
8155
8186
  if (sender.track)
8156
8187
  this.trackIdToTrackType.delete(sender.track.id);
8157
8188
  await sender.replaceTrack(track);
8158
- if (track)
8189
+ if (track) {
8159
8190
  this.trackIdToTrackType.set(track.id, trackType);
8191
+ if (isFirefox() && bundle.videoSender) {
8192
+ // restore the encoding config from the cache, if any
8193
+ await this.changePublishQuality(bundle.videoSender, bundle);
8194
+ }
8195
+ }
8160
8196
  if (isAudioTrackType(trackType)) {
8161
8197
  await this.updateAudioPublishOptions(trackType, options);
8162
8198
  }
@@ -8216,7 +8252,7 @@ class Publisher extends BasePeerConnection {
8216
8252
  continue;
8217
8253
  // it is safe to stop the track here, it is a clone
8218
8254
  this.stopTrack(transceiver.sender.track);
8219
- await this.updateTransceiver(transceiver, null, publishOption.trackType);
8255
+ await this.updateTransceiver(item, null);
8220
8256
  }
8221
8257
  };
8222
8258
  /**
@@ -8273,33 +8309,38 @@ class Publisher extends BasePeerConnection {
8273
8309
  /**
8274
8310
  * Stops the cloned track that is being published to the SFU.
8275
8311
  */
8276
- this.stopTracks = (...trackTypes) => {
8277
- for (const item of this.transceiverCache.items()) {
8278
- const { publishOption, transceiver } = item;
8279
- if (!trackTypes.includes(publishOption.trackType))
8280
- continue;
8281
- this.stopTrack(transceiver.sender.track);
8282
- }
8312
+ this.stopTracks = async (...trackTypes) => {
8313
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8314
+ for (const item of this.transceiverCache.items()) {
8315
+ const { publishOption, transceiver } = item;
8316
+ if (!trackTypes.includes(publishOption.trackType))
8317
+ continue;
8318
+ const track = transceiver.sender.track;
8319
+ await this.silenceSenderOnFirefox(item);
8320
+ this.stopTrack(track);
8321
+ }
8322
+ });
8283
8323
  };
8284
8324
  /**
8285
8325
  * Stops all the cloned tracks that are being published to the SFU.
8286
8326
  */
8287
- this.stopAllTracks = () => {
8288
- for (const { transceiver } of this.transceiverCache.items()) {
8289
- this.stopTrack(transceiver.sender.track);
8290
- }
8291
- for (const track of this.clonedTracks) {
8292
- this.stopTrack(track);
8293
- }
8327
+ this.stopAllTracks = async () => {
8328
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8329
+ for (const item of this.transceiverCache.items()) {
8330
+ const track = item.transceiver.sender.track;
8331
+ await this.silenceSenderOnFirefox(item);
8332
+ this.stopTrack(track);
8333
+ }
8334
+ for (const track of this.clonedTracks) {
8335
+ this.stopTrack(track);
8336
+ }
8337
+ });
8294
8338
  };
8295
- this.changePublishQuality = async (videoSender) => {
8296
- const { trackType, layers, publishOptionId } = videoSender;
8297
- const enabledLayers = layers.filter((l) => l.active);
8339
+ this.changePublishQuality = async (videoSender, bundle) => {
8340
+ const enabledLayers = videoSender.layers.filter((l) => l.active);
8298
8341
  const tag = 'Update publish quality:';
8299
8342
  this.logger.info(`${tag} requested layers by SFU:`, enabledLayers);
8300
- const transceiverId = this.transceiverCache.find((t) => t.publishOption.id === publishOptionId &&
8301
- t.publishOption.trackType === trackType);
8302
- const sender = transceiverId?.transceiver.sender;
8343
+ const sender = bundle?.transceiver.sender;
8303
8344
  if (!sender) {
8304
8345
  return this.logger.warn(`${tag} no video sender found.`);
8305
8346
  }
@@ -8307,7 +8348,7 @@ class Publisher extends BasePeerConnection {
8307
8348
  if (params.encodings.length === 0) {
8308
8349
  return this.logger.warn(`${tag} there are no encodings set.`);
8309
8350
  }
8310
- const codecInUse = transceiverId?.publishOption.codec?.name;
8351
+ const codecInUse = bundle?.publishOption.codec?.name;
8311
8352
  const usesSvcCodec = codecInUse && isSvcCodec(codecInUse);
8312
8353
  let changed = false;
8313
8354
  for (const encoder of params.encodings) {
@@ -8507,6 +8548,72 @@ class Publisher extends BasePeerConnection {
8507
8548
  track.stop();
8508
8549
  this.clonedTracks.delete(track);
8509
8550
  };
8551
+ /**
8552
+ * Silences a Firefox sender on the wire during unpublish.
8553
+ *
8554
+ * Firefox keeps emitting RTP after track.stop(), but the right lever
8555
+ * differs by track type:
8556
+ * - audio: `replaceTrack(null)` is the only reliable silencer;
8557
+ * `setParameters({encodings:[...active:false]})` does NOT stop
8558
+ * the Opus encoder.
8559
+ * - video: `setParameters({encodings:[...active:false]})` pauses
8560
+ * the encoder; `replaceTrack(null)` does NOT reliably stop the
8561
+ * video encoder. The prior active=true configuration is captured
8562
+ * onto `bundle.videoSender` so `updateTransceiver` can restore
8563
+ * it on the next publish.
8564
+ *
8565
+ * No-op on non-Firefox browsers and during teardown.
8566
+ */
8567
+ this.silenceSenderOnFirefox = async (bundle) => {
8568
+ if (this.isDisposed || !isFirefox())
8569
+ return;
8570
+ const { transceiver, publishOption } = bundle;
8571
+ if (isAudioTrackType(publishOption.trackType)) {
8572
+ await transceiver.sender.replaceTrack(null).catch((err) => {
8573
+ this.logger.warn('Failed to clear audio sender track', err);
8574
+ });
8575
+ return;
8576
+ }
8577
+ await this.disableAllEncodings(bundle);
8578
+ };
8579
+ this.disableAllEncodings = async (bundle) => {
8580
+ const { transceiver, publishOption } = bundle;
8581
+ const sender = transceiver.sender;
8582
+ const params = sender.getParameters();
8583
+ if (!params.encodings || params.encodings.length === 0)
8584
+ return;
8585
+ if (!bundle.videoSender) {
8586
+ this.transceiverCache.update(publishOption, {
8587
+ videoSender: {
8588
+ trackType: publishOption.trackType,
8589
+ publishOptionId: publishOption.id,
8590
+ codec: publishOption.codec,
8591
+ degradationPreference: fromRTCDegradationPreference(params.degradationPreference),
8592
+ layers: params.encodings.map((e) => ({
8593
+ name: e.rid ?? 'q',
8594
+ active: e.active ?? true,
8595
+ maxBitrate: e.maxBitrate ?? 0,
8596
+ scaleResolutionDownBy: e.scaleResolutionDownBy ?? 0,
8597
+ maxFramerate: e.maxFramerate ?? 0,
8598
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
8599
+ scalabilityMode: e.scalabilityMode ?? '',
8600
+ })),
8601
+ },
8602
+ });
8603
+ }
8604
+ let changed = false;
8605
+ for (const encoding of params.encodings) {
8606
+ if (encoding.active !== false) {
8607
+ encoding.active = false;
8608
+ changed = true;
8609
+ }
8610
+ }
8611
+ if (!changed)
8612
+ return;
8613
+ await sender.setParameters(params).catch((err) => {
8614
+ this.logger.error('Failed to disable video sender encodings:', err);
8615
+ });
8616
+ };
8510
8617
  this.publishOptions = publishOptions;
8511
8618
  this.on('iceRestart', (iceRestart) => {
8512
8619
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
@@ -8515,7 +8622,16 @@ class Publisher extends BasePeerConnection {
8515
8622
  });
8516
8623
  this.on('changePublishQuality', async (event) => {
8517
8624
  for (const videoSender of event.videoSenders) {
8518
- await this.changePublishQuality(videoSender);
8625
+ // if not publishing, update the encodingConfigCache and don't modify the state.
8626
+ // we'll apply this config on the next publish/unmute.
8627
+ const { trackType, publishOptionId } = videoSender;
8628
+ const bundle = this.transceiverCache.getBy(publishOptionId, trackType);
8629
+ if (bundle) {
8630
+ this.transceiverCache.update(bundle.publishOption, { videoSender });
8631
+ }
8632
+ if (isFirefox() && !this.isPublishing(trackType))
8633
+ continue;
8634
+ await this.changePublishQuality(videoSender, bundle);
8519
8635
  }
8520
8636
  });
8521
8637
  this.on('changePublishOptions', (event) => {
@@ -8526,9 +8642,14 @@ class Publisher extends BasePeerConnection {
8526
8642
  /**
8527
8643
  * Disposes this Publisher instance.
8528
8644
  */
8529
- dispose() {
8530
- super.dispose();
8531
- this.stopAllTracks();
8645
+ async dispose() {
8646
+ await super.dispose();
8647
+ try {
8648
+ await this.stopAllTracks();
8649
+ }
8650
+ catch (err) {
8651
+ this.logger.warn('Failed to stop tracks during dispose', err);
8652
+ }
8532
8653
  this.clonedTracks.clear();
8533
8654
  }
8534
8655
  }
@@ -11312,8 +11433,8 @@ const normalize = (options) => {
11312
11433
  : false,
11313
11434
  };
11314
11435
  };
11315
- const createSyntheticDevice = (deviceId, kind) => {
11316
- return { deviceId, kind, label: '', groupId: '' };
11436
+ const createSyntheticDevice = (deviceId, kind, label = '') => {
11437
+ return { deviceId, kind, label, groupId: '' };
11317
11438
  };
11318
11439
  const readPreferences = (storageKey) => {
11319
11440
  try {
@@ -11367,6 +11488,8 @@ class DeviceManager {
11367
11488
  this.areSubscriptionsSetUp = false;
11368
11489
  this.isTrackStoppedDueToTrackEnd = false;
11369
11490
  this.filters = [];
11491
+ this.virtualDevicesSubject = new BehaviorSubject([]);
11492
+ this.virtualDeviceConcurrencyTag = Symbol('virtualDeviceConcurrencyTag');
11370
11493
  this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
11371
11494
  this.filterRegistrationConcurrencyTag = Symbol('filterRegistrationConcurrencyTag');
11372
11495
  /**
@@ -11379,6 +11502,7 @@ class DeviceManager {
11379
11502
  this.subscriptions.forEach((s) => s());
11380
11503
  this.subscriptions = [];
11381
11504
  this.areSubscriptionsSetUp = false;
11505
+ this.virtualDevicesSubject.next([]);
11382
11506
  };
11383
11507
  this.runCurrentStreamCleanups = () => {
11384
11508
  this.currentStreamCleanups.forEach((c) => c());
@@ -11437,7 +11561,93 @@ class DeviceManager {
11437
11561
  * @returns an Observable that will be updated if a device is connected or disconnected
11438
11562
  */
11439
11563
  listDevices() {
11440
- return this.getDevices();
11564
+ return combineLatest([this.getDevices(), this.virtualDevicesSubject]).pipe(map(([real, virtual]) => [
11565
+ ...real,
11566
+ ...virtual.map((d) => createSyntheticDevice(d.deviceId, d.kind, d.label)),
11567
+ ]));
11568
+ }
11569
+ /**
11570
+ * Registers a virtual camera or microphone backed by a caller-supplied
11571
+ * stream factory. The device appears in `listDevices()` and can be selected
11572
+ * via `select()` like any real device.
11573
+ *
11574
+ * Web only. React Native is not supported.
11575
+ *
11576
+ * Only supported for camera and microphone managers; calling on any other
11577
+ * manager throws.
11578
+ */
11579
+ registerVirtualDevice(virtualDevice) {
11580
+ if (isReactNative()) {
11581
+ throw new Error('Virtual devices are not supported on React Native.');
11582
+ }
11583
+ if (this.trackType !== TrackType.AUDIO &&
11584
+ this.trackType !== TrackType.VIDEO) {
11585
+ throw new Error('Virtual devices are only supported for camera and microphone.');
11586
+ }
11587
+ const deviceId = `stream-virtual:${generateUUIDv4()}`;
11588
+ const entry = {
11589
+ deviceId,
11590
+ kind: this.mediaDeviceKind,
11591
+ ...virtualDevice,
11592
+ };
11593
+ setCurrentValue(this.virtualDevicesSubject, (current) => [
11594
+ ...current,
11595
+ entry,
11596
+ ]);
11597
+ return {
11598
+ deviceId: entry.deviceId,
11599
+ unregister: async () => {
11600
+ await withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11601
+ setCurrentValue(this.virtualDevicesSubject, (current) => current.filter((d) => d !== entry));
11602
+ if (this.activeVirtualSession?.deviceId === deviceId) {
11603
+ await this.stopActiveVirtualSession();
11604
+ }
11605
+ });
11606
+ if (this.state.selectedDevice === deviceId) {
11607
+ await this.statusChangeSettled();
11608
+ await this.disable({ forceStop: true });
11609
+ await this.select(undefined);
11610
+ }
11611
+ },
11612
+ };
11613
+ }
11614
+ sanitizeVirtualStream(stream) {
11615
+ stream.getTracks().forEach((track) => {
11616
+ const originalGetSettings = track.getSettings.bind(track);
11617
+ track.getSettings = () => {
11618
+ const settings = originalGetSettings();
11619
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11620
+ const { deviceId, ...rest } = settings;
11621
+ return rest;
11622
+ };
11623
+ });
11624
+ return stream;
11625
+ }
11626
+ findVirtualDevice(deviceId) {
11627
+ if (!deviceId)
11628
+ return undefined;
11629
+ return getCurrentValue(this.virtualDevicesSubject).find((d) => d.deviceId === deviceId);
11630
+ }
11631
+ async stopActiveVirtualSession() {
11632
+ const session = this.activeVirtualSession;
11633
+ this.activeVirtualSession = undefined;
11634
+ await session?.stop?.();
11635
+ }
11636
+ async getSelectedStream(constraints) {
11637
+ const deviceId = this.state.selectedDevice;
11638
+ if (!deviceId?.startsWith('stream-virtual')) {
11639
+ return this.getStream(constraints);
11640
+ }
11641
+ return withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11642
+ const virtualDevice = this.findVirtualDevice(deviceId);
11643
+ if (!virtualDevice) {
11644
+ throw new Error(`Virtual device is not registered: ${deviceId}`);
11645
+ }
11646
+ await this.stopActiveVirtualSession();
11647
+ const { stream, stop } = await virtualDevice.getUserMedia(constraints);
11648
+ this.activeVirtualSession = { deviceId, stop };
11649
+ return this.sanitizeVirtualStream(stream);
11650
+ });
11441
11651
  }
11442
11652
  /**
11443
11653
  * Returns `true` when this device is in enabled state.
@@ -11597,6 +11807,9 @@ class DeviceManager {
11597
11807
  }
11598
11808
  });
11599
11809
  }
11810
+ getResolvedConstraints(constraints) {
11811
+ return constraints;
11812
+ }
11600
11813
  publishStream(stream, options) {
11601
11814
  return this.call.publish(stream, this.trackType, options);
11602
11815
  }
@@ -11617,6 +11830,7 @@ class DeviceManager {
11617
11830
  this.muteLocalStream(stopTracks);
11618
11831
  const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
11619
11832
  if (allEnded) {
11833
+ await this.stopActiveVirtualSession();
11620
11834
  // @ts-expect-error release() is present in react-native-webrtc
11621
11835
  if (typeof mediaStream.release === 'function') {
11622
11836
  // @ts-expect-error called to dispose the stream in RN
@@ -11672,12 +11886,12 @@ class DeviceManager {
11672
11886
  // before chainWith below registers new ones for the new chain.
11673
11887
  this.runCurrentStreamCleanups();
11674
11888
  const defaultConstraints = this.state.defaultConstraints;
11675
- const constraints = {
11889
+ const constraints = this.getResolvedConstraints({
11676
11890
  ...defaultConstraints,
11677
11891
  deviceId: this.state.selectedDevice
11678
11892
  ? { exact: this.state.selectedDevice }
11679
11893
  : undefined,
11680
- };
11894
+ });
11681
11895
  /**
11682
11896
  * Chains two media streams together.
11683
11897
  *
@@ -11734,7 +11948,7 @@ class DeviceManager {
11734
11948
  };
11735
11949
  // the rootStream represents the stream coming from the actual device
11736
11950
  // e.g. camera or microphone stream
11737
- rootStreamPromise = this.getStream(constraints);
11951
+ rootStreamPromise = this.getSelectedStream(constraints);
11738
11952
  // we publish the last MediaStream of the chain
11739
11953
  stream = await this.filters.reduce((parent, entry) => parent
11740
11954
  .then((inputStream) => {
@@ -12051,7 +12265,10 @@ class DeviceManagerState {
12051
12265
  setCurrentValue(this.mediaStreamSubject, stream);
12052
12266
  setCurrentValue(this.rootMediaStreamSubject, rootStream);
12053
12267
  if (rootStream) {
12054
- this.setDevice(this.getDeviceIdFromStream(rootStream));
12268
+ const derived = this.getDeviceIdFromStream(rootStream);
12269
+ if (derived) {
12270
+ this.setDevice(derived);
12271
+ }
12055
12272
  }
12056
12273
  }
12057
12274
  /**
@@ -12264,7 +12481,7 @@ class CameraManager extends DeviceManager {
12264
12481
  getDevices() {
12265
12482
  return getVideoDevices(this.call.tracer);
12266
12483
  }
12267
- getStream(constraints) {
12484
+ getResolvedConstraints(constraints) {
12268
12485
  constraints.width = this.targetResolution.width;
12269
12486
  constraints.height = this.targetResolution.height;
12270
12487
  // We can't set both device id and facing mode
@@ -12275,6 +12492,9 @@ class CameraManager extends DeviceManager {
12275
12492
  constraints.facingMode =
12276
12493
  this.state.direction === 'front' ? 'user' : 'environment';
12277
12494
  }
12495
+ return constraints;
12496
+ }
12497
+ getStream(constraints) {
12278
12498
  return getVideoStream(constraints, this.call.tracer);
12279
12499
  }
12280
12500
  }
@@ -13602,9 +13822,10 @@ class Call {
13602
13822
  this.sfuStatsReporter?.flush();
13603
13823
  this.sfuStatsReporter?.stop();
13604
13824
  this.sfuStatsReporter = undefined;
13605
- this.subscriber?.dispose();
13825
+ this.lastStatsOptions = undefined;
13826
+ await this.subscriber?.dispose();
13606
13827
  this.subscriber = undefined;
13607
- this.publisher?.dispose();
13828
+ await this.publisher?.dispose();
13608
13829
  this.publisher = undefined;
13609
13830
  await this.sfuClient?.leaveAndClose(leaveReason);
13610
13831
  this.sfuClient = undefined;
@@ -13880,15 +14101,17 @@ class Call {
13880
14101
  const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
13881
14102
  const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
13882
14103
  const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
13883
- let statsOptions = this.sfuStatsReporter?.options;
14104
+ let statsOptions = this.lastStatsOptions;
13884
14105
  if (!this.credentials ||
13885
14106
  !statsOptions ||
13886
14107
  performingRejoin ||
13887
- performingMigration) {
14108
+ performingMigration ||
14109
+ data?.migrating_from) {
13888
14110
  try {
13889
14111
  const joinResponse = await this.doJoinRequest(data);
13890
14112
  this.credentials = joinResponse.credentials;
13891
14113
  statsOptions = joinResponse.stats_options;
14114
+ this.lastStatsOptions = statsOptions;
13892
14115
  }
13893
14116
  catch (error) {
13894
14117
  // prevent triggering reconnect flow if the state is OFFLINE
@@ -13995,7 +14218,7 @@ class Call {
13995
14218
  }
13996
14219
  else {
13997
14220
  const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
13998
- this.initPublisherAndSubscriber({
14221
+ await this.initPublisherAndSubscriber({
13999
14222
  sfuClient,
14000
14223
  connectionConfig,
14001
14224
  clientDetails,
@@ -14140,11 +14363,11 @@ class Call {
14140
14363
  * Initializes the Publisher and Subscriber Peer Connections.
14141
14364
  * @internal
14142
14365
  */
14143
- this.initPublisherAndSubscriber = (opts) => {
14366
+ this.initPublisherAndSubscriber = async (opts) => {
14144
14367
  const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
14145
14368
  const { enable_rtc_stats: enableTracing } = statsOptions;
14146
14369
  if (closePreviousInstances && this.subscriber) {
14147
- this.subscriber.dispose();
14370
+ await this.subscriber.dispose();
14148
14371
  }
14149
14372
  const basePeerConnectionOptions = {
14150
14373
  sfuClient,
@@ -14173,7 +14396,7 @@ class Call {
14173
14396
  const isAnonymous = this.streamClient.user?.type === 'anonymous';
14174
14397
  if (!isAnonymous) {
14175
14398
  if (closePreviousInstances && this.publisher) {
14176
- this.publisher.dispose();
14399
+ await this.publisher.dispose();
14177
14400
  }
14178
14401
  this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
14179
14402
  }
@@ -14276,10 +14499,17 @@ class Call {
14276
14499
  * `ICE_NEVER_CONNECTED` increments the unsupported-network counter).
14277
14500
  */
14278
14501
  this.reconnect = async (strategy, reason) => {
14279
- if (this.state.callingState === CallingState.RECONNECTING ||
14502
+ if (this.state.callingState === CallingState.JOINING ||
14503
+ this.state.callingState === CallingState.RECONNECTING ||
14280
14504
  this.state.callingState === CallingState.MIGRATING ||
14281
14505
  this.state.callingState === CallingState.RECONNECTING_FAILED)
14282
14506
  return;
14507
+ // Drop redundant reconnect calls. If a reconnect is already queued or
14508
+ // running for this Call, that entry will resolve whatever broke;
14509
+ // queueing more entries just replays the full REJOIN cycle (one extra
14510
+ // `POST /join` per entry) once the call is already healthy again.
14511
+ if (hasPending(this.reconnectConcurrencyTag))
14512
+ return;
14283
14513
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
14284
14514
  const reconnectStartTime = Date.now();
14285
14515
  this.reconnectStrategy = strategy;
@@ -14484,8 +14714,8 @@ class Call {
14484
14714
  this.state.setCallingState(CallingState.JOINED);
14485
14715
  }
14486
14716
  finally {
14487
- currentSubscriber?.dispose();
14488
- currentPublisher?.dispose();
14717
+ await currentSubscriber?.dispose();
14718
+ await currentPublisher?.dispose();
14489
14719
  // and close the previous SFU client, without specifying close code
14490
14720
  currentSfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'Migrating away');
14491
14721
  }
@@ -14674,7 +14904,7 @@ class Call {
14674
14904
  this.stopPublish = async (...trackTypes) => {
14675
14905
  if (!this.sfuClient || !this.publisher)
14676
14906
  return;
14677
- this.publisher.stopTracks(...trackTypes);
14907
+ await this.publisher.stopTracks(...trackTypes);
14678
14908
  await this.updateLocalStreamState(undefined, ...trackTypes);
14679
14909
  };
14680
14910
  /**
@@ -16787,7 +17017,7 @@ class StreamClient {
16787
17017
  this.getUserAgent = () => {
16788
17018
  if (!this.cachedUserAgent) {
16789
17019
  const { clientAppIdentifier = {} } = this.options;
16790
- const { sdkName = 'js', sdkVersion = "1.50.0", ...extras } = clientAppIdentifier;
17020
+ const { sdkName = 'js', sdkVersion = "1.51.0", ...extras } = clientAppIdentifier;
16791
17021
  this.cachedUserAgent = [
16792
17022
  `stream-video-${sdkName}-v${sdkVersion}`,
16793
17023
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),