@stream-io/video-client 1.31.0 → 1.33.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 (73) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.browser.es.js +350 -83
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +351 -84
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +350 -83
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +3 -2
  9. package/dist/src/StreamVideoClient.d.ts +2 -0
  10. package/dist/src/coordinator/connection/types.d.ts +4 -0
  11. package/dist/src/devices/AudioDeviceManager.d.ts +25 -0
  12. package/dist/src/devices/AudioDeviceManagerState.d.ts +24 -0
  13. package/dist/src/devices/CameraManager.d.ts +2 -2
  14. package/dist/src/devices/CameraManagerState.d.ts +3 -4
  15. package/dist/src/devices/{InputMediaDeviceManager.d.ts → DeviceManager.d.ts} +6 -6
  16. package/dist/src/devices/{InputMediaDeviceManagerState.d.ts → DeviceManagerState.d.ts} +4 -4
  17. package/dist/src/devices/MicrophoneManager.d.ts +5 -3
  18. package/dist/src/devices/MicrophoneManagerState.d.ts +6 -10
  19. package/dist/src/devices/ScreenShareManager.d.ts +4 -2
  20. package/dist/src/devices/ScreenShareState.d.ts +6 -2
  21. package/dist/src/devices/SpeakerState.d.ts +4 -4
  22. package/dist/src/devices/index.d.ts +2 -2
  23. package/dist/src/gen/coordinator/index.d.ts +169 -2
  24. package/dist/src/gen/video/sfu/event/events.d.ts +8 -0
  25. package/dist/src/gen/video/sfu/models/models.d.ts +43 -0
  26. package/dist/src/rtc/BasePeerConnection.d.ts +2 -12
  27. package/dist/src/rtc/Publisher.d.ts +9 -6
  28. package/dist/src/rtc/Subscriber.d.ts +2 -1
  29. package/dist/src/rtc/TransceiverCache.d.ts +10 -11
  30. package/dist/src/rtc/index.d.ts +1 -1
  31. package/dist/src/rtc/{videoLayers.d.ts → layers.d.ts} +7 -1
  32. package/dist/src/rtc/types.d.ts +31 -0
  33. package/package.json +3 -2
  34. package/src/Call.ts +19 -12
  35. package/src/StreamVideoClient.ts +42 -3
  36. package/src/__tests__/Call.publishing.test.ts +14 -3
  37. package/src/__tests__/StreamVideoClient.api.test.ts +1 -1
  38. package/src/coordinator/connection/types.ts +5 -0
  39. package/src/devices/AudioDeviceManager.ts +61 -0
  40. package/src/devices/AudioDeviceManagerState.ts +44 -0
  41. package/src/devices/CameraManager.ts +4 -4
  42. package/src/devices/CameraManagerState.ts +9 -8
  43. package/src/devices/{InputMediaDeviceManager.ts → DeviceManager.ts} +11 -8
  44. package/src/devices/{InputMediaDeviceManagerState.ts → DeviceManagerState.ts} +7 -4
  45. package/src/devices/MicrophoneManager.ts +26 -6
  46. package/src/devices/MicrophoneManagerState.ts +18 -19
  47. package/src/devices/ScreenShareManager.ts +23 -4
  48. package/src/devices/ScreenShareState.ts +11 -3
  49. package/src/devices/SpeakerState.ts +6 -14
  50. package/src/devices/__tests__/CameraManager.test.ts +1 -0
  51. package/src/devices/__tests__/{InputMediaDeviceManager.test.ts → DeviceManager.test.ts} +4 -4
  52. package/src/devices/__tests__/{InputMediaDeviceManagerFilters.test.ts → DeviceManagerFilters.test.ts} +4 -4
  53. package/src/devices/__tests__/{InputMediaDeviceManagerState.test.ts → DeviceManagerState.test.ts} +2 -2
  54. package/src/devices/__tests__/MicrophoneManager.test.ts +41 -1
  55. package/src/devices/__tests__/NoiseCancellationStub.ts +3 -1
  56. package/src/devices/__tests__/ScreenShareManager.test.ts +5 -1
  57. package/src/devices/index.ts +2 -2
  58. package/src/events/__tests__/internal.test.ts +25 -11
  59. package/src/gen/coordinator/index.ts +169 -2
  60. package/src/gen/video/sfu/event/events.ts +14 -0
  61. package/src/gen/video/sfu/models/models.ts +65 -0
  62. package/src/rtc/BasePeerConnection.ts +1 -16
  63. package/src/rtc/Publisher.ts +74 -31
  64. package/src/rtc/Subscriber.ts +2 -4
  65. package/src/rtc/TransceiverCache.ts +23 -27
  66. package/src/rtc/__tests__/Publisher.test.ts +61 -29
  67. package/src/rtc/__tests__/{videoLayers.test.ts → layers.test.ts} +76 -1
  68. package/src/rtc/index.ts +2 -1
  69. package/src/rtc/{videoLayers.ts → layers.ts} +28 -7
  70. package/src/rtc/types.ts +44 -0
  71. package/src/sorting/presets.ts +2 -2
  72. package/src/store/CallState.ts +36 -10
  73. package/src/store/__tests__/CallState.test.ts +20 -2
package/dist/index.cjs.js CHANGED
@@ -115,6 +115,7 @@ const OwnCapability = {
115
115
  REMOVE_CALL_MEMBER: 'remove-call-member',
116
116
  SCREENSHARE: 'screenshare',
117
117
  SEND_AUDIO: 'send-audio',
118
+ SEND_CLOSED_CAPTIONS_CALL: 'send-closed-captions-call',
118
119
  SEND_VIDEO: 'send-video',
119
120
  START_BROADCAST_CALL: 'start-broadcast-call',
120
121
  START_CLOSED_CAPTIONS_CALL: 'start-closed-captions-call',
@@ -960,6 +961,24 @@ var ParticipantSource;
960
961
  */
961
962
  ParticipantSource[ParticipantSource["SRT"] = 5] = "SRT";
962
963
  })(ParticipantSource || (ParticipantSource = {}));
964
+ /**
965
+ * @generated from protobuf enum stream.video.sfu.models.AudioBitrateProfile
966
+ */
967
+ var AudioBitrateProfile;
968
+ (function (AudioBitrateProfile) {
969
+ /**
970
+ * @generated from protobuf enum value: AUDIO_BITRATE_PROFILE_VOICE_STANDARD_UNSPECIFIED = 0;
971
+ */
972
+ AudioBitrateProfile[AudioBitrateProfile["VOICE_STANDARD_UNSPECIFIED"] = 0] = "VOICE_STANDARD_UNSPECIFIED";
973
+ /**
974
+ * @generated from protobuf enum value: AUDIO_BITRATE_PROFILE_VOICE_HIGH_QUALITY = 1;
975
+ */
976
+ AudioBitrateProfile[AudioBitrateProfile["VOICE_HIGH_QUALITY"] = 1] = "VOICE_HIGH_QUALITY";
977
+ /**
978
+ * @generated from protobuf enum value: AUDIO_BITRATE_PROFILE_MUSIC_HIGH_QUALITY = 2;
979
+ */
980
+ AudioBitrateProfile[AudioBitrateProfile["MUSIC_HIGH_QUALITY"] = 2] = "MUSIC_HIGH_QUALITY";
981
+ })(AudioBitrateProfile || (AudioBitrateProfile = {}));
963
982
  /**
964
983
  * @generated from protobuf enum stream.video.sfu.models.ErrorCode
965
984
  */
@@ -1573,6 +1592,13 @@ class PublishOption$Type extends runtime.MessageType {
1573
1592
  kind: 'scalar',
1574
1593
  T: 8 /*ScalarType.BOOL*/,
1575
1594
  },
1595
+ {
1596
+ no: 10,
1597
+ name: 'audio_bitrate_profiles',
1598
+ kind: 'message',
1599
+ repeat: 2 /*RepeatType.UNPACKED*/,
1600
+ T: () => AudioBitrate,
1601
+ },
1576
1602
  ]);
1577
1603
  }
1578
1604
  }
@@ -1636,6 +1662,28 @@ let ICETrickle$Type$1 = class ICETrickle$Type extends runtime.MessageType {
1636
1662
  */
1637
1663
  const ICETrickle$1 = new ICETrickle$Type$1();
1638
1664
  // @generated message type with reflection information, may provide speed optimized methods
1665
+ class AudioBitrate$Type extends runtime.MessageType {
1666
+ constructor() {
1667
+ super('stream.video.sfu.models.AudioBitrate', [
1668
+ {
1669
+ no: 1,
1670
+ name: 'profile',
1671
+ kind: 'enum',
1672
+ T: () => [
1673
+ 'stream.video.sfu.models.AudioBitrateProfile',
1674
+ AudioBitrateProfile,
1675
+ 'AUDIO_BITRATE_PROFILE_',
1676
+ ],
1677
+ },
1678
+ { no: 2, name: 'bitrate', kind: 'scalar', T: 5 /*ScalarType.INT32*/ },
1679
+ ]);
1680
+ }
1681
+ }
1682
+ /**
1683
+ * @generated MessageType for protobuf message stream.video.sfu.models.AudioBitrate
1684
+ */
1685
+ const AudioBitrate = new AudioBitrate$Type();
1686
+ // @generated message type with reflection information, may provide speed optimized methods
1639
1687
  class TrackInfo$Type extends runtime.MessageType {
1640
1688
  constructor() {
1641
1689
  super('stream.video.sfu.models.TrackInfo', [
@@ -1986,6 +2034,8 @@ var models = /*#__PURE__*/Object.freeze({
1986
2034
  get AndroidThermalState () { return AndroidThermalState; },
1987
2035
  AppleState: AppleState,
1988
2036
  get AppleThermalState () { return AppleThermalState; },
2037
+ AudioBitrate: AudioBitrate,
2038
+ get AudioBitrateProfile () { return AudioBitrateProfile; },
1989
2039
  Browser: Browser,
1990
2040
  Call: Call$1,
1991
2041
  get CallEndedReason () { return CallEndedReason; },
@@ -2951,6 +3001,12 @@ class JoinRequest$Type extends runtime.MessageType {
2951
3001
  super('stream.video.sfu.event.JoinRequest', [
2952
3002
  { no: 1, name: 'token', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2953
3003
  { no: 2, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
3004
+ {
3005
+ no: 13,
3006
+ name: 'unified_session_id',
3007
+ kind: 'scalar',
3008
+ T: 9 /*ScalarType.STRING*/,
3009
+ },
2954
3010
  {
2955
3011
  no: 3,
2956
3012
  name: 'subscriber_sdp',
@@ -4654,11 +4710,11 @@ const ifInvisibleOrUnknownBy = conditional((a, b) => a.viewportVisibilityState?.
4654
4710
  /**
4655
4711
  * The default sorting preset.
4656
4712
  */
4657
- const defaultSortPreset = combineComparators(pinned, screenSharing, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
4713
+ const defaultSortPreset = combineComparators(screenSharing, pinned, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
4658
4714
  /**
4659
4715
  * The sorting preset for speaker layout.
4660
4716
  */
4661
- const speakerLayoutSortPreset = combineComparators(pinned, screenSharing, dominantSpeaker, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
4717
+ const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
4662
4718
  /**
4663
4719
  * The sorting preset for layouts that don't render all participants but
4664
4720
  * instead, render them in pages.
@@ -5005,14 +5061,32 @@ class CallState {
5005
5061
  * @param pins the latest pins from the server.
5006
5062
  */
5007
5063
  this.setServerSidePins = (pins) => {
5008
- const pinsLookup = pins.reduce((lookup, pin) => {
5009
- lookup[pin.sessionId] = Date.now();
5064
+ const now = Date.now();
5065
+ const unknownSymbol = Symbol('unknown');
5066
+ // generate a lookup table of pinnedAt timestamps by userId and sessionId
5067
+ // if there are multiple pins for the same userId, then we set the pinnedAt
5068
+ // to `unknown` (for that userId lookup) so that we don't apply any pin for that participant
5069
+ // this is to avoid conflicts during reconstruction of the pin state after reconnections
5070
+ // as sessionIds can change
5071
+ const pinnedAtByIdentifier = pins.reduce((lookup, pin, index) => {
5072
+ var _a;
5073
+ const pinnedAt = now + (pins.length - index);
5074
+ if (lookup[pin.userId]) {
5075
+ lookup[pin.userId] = unknownSymbol;
5076
+ }
5077
+ else {
5078
+ lookup[pin.userId] = pinnedAt;
5079
+ }
5080
+ lookup[_a = pin.sessionId] ?? (lookup[_a] = pinnedAt);
5010
5081
  return lookup;
5011
5082
  }, {});
5012
5083
  return this.setParticipants((participants) => participants.map((participant) => {
5013
- const serverSidePinnedAt = pinsLookup[participant.sessionId];
5084
+ // first check by sessionId as that is 100% correct, then by attempt reconstruction by userId
5085
+ const serverSidePinnedAt = pinnedAtByIdentifier[participant.sessionId] ??
5086
+ pinnedAtByIdentifier[participant.userId];
5014
5087
  // the participant is newly pinned
5015
- if (serverSidePinnedAt) {
5088
+ if (typeof serverSidePinnedAt === 'number' &&
5089
+ typeof participant.pin?.pinnedAt !== 'number') {
5016
5090
  return {
5017
5091
  ...participant,
5018
5092
  pin: {
@@ -5023,7 +5097,8 @@ class CallState {
5023
5097
  }
5024
5098
  // the participant is no longer pinned server side
5025
5099
  // we need to reset the pin
5026
- if (participant.pin && !participant.pin.isLocalPin) {
5100
+ if (typeof serverSidePinnedAt !== 'number' &&
5101
+ participant.pin?.isLocalPin === false) {
5027
5102
  return {
5028
5103
  ...participant,
5029
5104
  pin: undefined,
@@ -5760,7 +5835,7 @@ const getSdkVersion = (sdk) => {
5760
5835
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
5761
5836
  };
5762
5837
 
5763
- const version = "1.31.0";
5838
+ const version = "1.33.0";
5764
5839
  const [major, minor, patch] = version.split('.');
5765
5840
  let sdkInfo = {
5766
5841
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6958,15 +7033,24 @@ class TransceiverCache {
6958
7033
  /**
6959
7034
  * Adds a transceiver to the cache.
6960
7035
  */
6961
- this.add = (publishOption, transceiver) => {
6962
- this.cache.push({ publishOption, transceiver });
6963
- this.transceiverOrder.push(transceiver);
7036
+ this.add = (bundle) => {
7037
+ this.cache.push(bundle);
7038
+ this.transceiverOrder.push(bundle.transceiver);
6964
7039
  };
6965
7040
  /**
6966
7041
  * Gets the transceiver for the given publish option.
6967
7042
  */
6968
7043
  this.get = (publishOption) => {
6969
- return this.findTransceiver(publishOption)?.transceiver;
7044
+ return this.cache.find((bundle) => bundle.publishOption.id === publishOption.id &&
7045
+ bundle.publishOption.trackType === publishOption.trackType);
7046
+ };
7047
+ /**
7048
+ * Updates the cached bundle with the given patch.
7049
+ */
7050
+ this.update = (publishOption, patch) => {
7051
+ const bundle = this.get(publishOption);
7052
+ if (bundle)
7053
+ Object.assign(bundle, patch);
6970
7054
  };
6971
7055
  /**
6972
7056
  * Checks if the cache has the given publish option.
@@ -7012,10 +7096,6 @@ class TransceiverCache {
7012
7096
  this.layers.push({ publishOption, layers });
7013
7097
  }
7014
7098
  };
7015
- this.findTransceiver = (publishOption) => {
7016
- return this.cache.find((item) => item.publishOption.id === publishOption.id &&
7017
- item.publishOption.trackType === publishOption.trackType);
7018
- };
7019
7099
  this.findLayer = (publishOption) => {
7020
7100
  return this.layers.find((item) => item.publishOption.id === publishOption.id &&
7021
7101
  item.publishOption.trackType === publishOption.trackType);
@@ -7073,10 +7153,20 @@ const toTrackType = (trackType) => {
7073
7153
  };
7074
7154
  const isAudioTrackType = (trackType) => trackType === TrackType.AUDIO || trackType === TrackType.SCREEN_SHARE_AUDIO;
7075
7155
 
7076
- const defaultBitratePerRid = {
7077
- q: 300000,
7078
- h: 750000,
7079
- f: 1250000,
7156
+ /**
7157
+ * Prepares the audio layer for the given track.
7158
+ * Based on the provided audio bitrate profile, we apply the appropriate bitrate.
7159
+ */
7160
+ const computeAudioLayers = (publishOption, options) => {
7161
+ const { audioBitrateProfile } = options;
7162
+ const profileConfig = publishOption.audioBitrateProfiles?.find((config) => config.profile === audioBitrateProfile);
7163
+ const maxBitrate = profileConfig?.bitrate ||
7164
+ {
7165
+ [AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED]: 64000,
7166
+ [AudioBitrateProfile.VOICE_HIGH_QUALITY]: 128000,
7167
+ [AudioBitrateProfile.MUSIC_HIGH_QUALITY]: 128000,
7168
+ }[audioBitrateProfile || AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED];
7169
+ return [{ maxBitrate }];
7080
7170
  };
7081
7171
  /**
7082
7172
  * In SVC, we need to send only one video encoding (layer).
@@ -7089,7 +7179,7 @@ const toSvcEncodings = (layers) => {
7089
7179
  if (!layers)
7090
7180
  return;
7091
7181
  // we take the highest quality layer, and we assign it to `q` encoder.
7092
- const withRid = (rid) => (l) => l.rid === rid;
7182
+ const withRid = (rid) => (layer) => layer.rid === rid;
7093
7183
  const highestLayer = layers.find(withRid('f')) ||
7094
7184
  layers.find(withRid('h')) ||
7095
7185
  layers.find(withRid('q'));
@@ -7143,7 +7233,8 @@ const computeVideoLayers = (videoTrack, publishOption) => {
7143
7233
  rid,
7144
7234
  width: Math.round(width / downscaleFactor),
7145
7235
  height: Math.round(height / downscaleFactor),
7146
- maxBitrate: Math.round(maxBitrate / bitrateFactor) || defaultBitratePerRid[rid],
7236
+ maxBitrate: Math.round(maxBitrate / bitrateFactor) ||
7237
+ { q: 300000, h: 750000, f: 1250000 }[rid],
7147
7238
  maxFramerate: fps,
7148
7239
  };
7149
7240
  if (svcCodec) {
@@ -7309,8 +7400,9 @@ class Publisher extends BasePeerConnection {
7309
7400
  *
7310
7401
  * @param track the track to publish.
7311
7402
  * @param trackType the track type to publish.
7403
+ * @param options the publish options to use.
7312
7404
  */
7313
- this.publish = async (track, trackType) => {
7405
+ this.publish = async (track, trackType, options = {}) => {
7314
7406
  if (!this.publishOptions.some((o) => o.trackType === trackType)) {
7315
7407
  throw new Error(`No publish options found for ${TrackType[trackType]}`);
7316
7408
  }
@@ -7320,13 +7412,13 @@ class Publisher extends BasePeerConnection {
7320
7412
  // create a clone of the track as otherwise the same trackId will
7321
7413
  // appear in the SDP in multiple transceivers
7322
7414
  const trackToPublish = this.cloneTrack(track);
7323
- const transceiver = this.transceiverCache.get(publishOption);
7415
+ const { transceiver } = this.transceiverCache.get(publishOption) || {};
7324
7416
  if (!transceiver) {
7325
- await this.addTransceiver(trackToPublish, publishOption);
7417
+ await this.addTransceiver(trackToPublish, publishOption, options);
7326
7418
  }
7327
7419
  else {
7328
7420
  const previousTrack = transceiver.sender.track;
7329
- await this.updateTransceiver(transceiver, trackToPublish, trackType);
7421
+ await this.updateTransceiver(transceiver, trackToPublish, trackType, options);
7330
7422
  if (!isReactNative()) {
7331
7423
  this.stopTrack(previousTrack);
7332
7424
  }
@@ -7336,11 +7428,13 @@ class Publisher extends BasePeerConnection {
7336
7428
  /**
7337
7429
  * Adds a new transceiver carrying the given track to the peer connection.
7338
7430
  */
7339
- this.addTransceiver = async (track, publishOption) => {
7340
- const videoEncodings = computeVideoLayers(track, publishOption);
7431
+ this.addTransceiver = async (track, publishOption, options) => {
7432
+ const encodings = isAudioTrackType(publishOption.trackType)
7433
+ ? computeAudioLayers(publishOption, options)
7434
+ : computeVideoLayers(track, publishOption);
7341
7435
  const sendEncodings = isSvcCodec(publishOption.codec?.name)
7342
- ? toSvcEncodings(videoEncodings)
7343
- : videoEncodings;
7436
+ ? toSvcEncodings(encodings)
7437
+ : encodings;
7344
7438
  const transceiver = this.pc.addTransceiver(track, {
7345
7439
  direction: 'sendonly',
7346
7440
  sendEncodings,
@@ -7350,20 +7444,49 @@ class Publisher extends BasePeerConnection {
7350
7444
  await transceiver.sender.setParameters(params);
7351
7445
  const trackType = publishOption.trackType;
7352
7446
  this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
7353
- this.transceiverCache.add(publishOption, transceiver);
7447
+ this.transceiverCache.add({ publishOption, transceiver, options });
7354
7448
  this.trackIdToTrackType.set(track.id, trackType);
7355
7449
  await this.negotiate();
7356
7450
  };
7357
7451
  /**
7358
7452
  * Updates the transceiver with the given track and track type.
7359
7453
  */
7360
- this.updateTransceiver = async (transceiver, track, trackType) => {
7454
+ this.updateTransceiver = async (transceiver, track, trackType, options = {}) => {
7361
7455
  const sender = transceiver.sender;
7362
7456
  if (sender.track)
7363
7457
  this.trackIdToTrackType.delete(sender.track.id);
7364
7458
  await sender.replaceTrack(track);
7365
7459
  if (track)
7366
7460
  this.trackIdToTrackType.set(track.id, trackType);
7461
+ if (isAudioTrackType(trackType)) {
7462
+ await this.updateAudioPublishOptions(trackType, options);
7463
+ }
7464
+ };
7465
+ /**
7466
+ * Updates the publish options for the given track type.
7467
+ */
7468
+ this.updateAudioPublishOptions = async (trackType, options) => {
7469
+ for (const publishOption of this.publishOptions) {
7470
+ if (publishOption.trackType !== trackType)
7471
+ continue;
7472
+ const bundle = this.transceiverCache.get(publishOption);
7473
+ if (!bundle)
7474
+ continue;
7475
+ const { transceiver, options: current } = bundle;
7476
+ if (current.audioBitrateProfile !== options.audioBitrateProfile) {
7477
+ const encodings = computeAudioLayers(publishOption, options);
7478
+ if (encodings && encodings.length > 0) {
7479
+ const params = transceiver.sender.getParameters();
7480
+ const [currentEncoding] = params.encodings;
7481
+ const [targetEncoding] = encodings;
7482
+ if (currentEncoding.maxBitrate !== targetEncoding.maxBitrate) {
7483
+ currentEncoding.maxBitrate = targetEncoding.maxBitrate;
7484
+ }
7485
+ await transceiver.sender.setParameters(params);
7486
+ }
7487
+ }
7488
+ this.transceiverCache.update(publishOption, { options });
7489
+ }
7367
7490
  };
7368
7491
  /**
7369
7492
  * Synchronizes the current Publisher state with the provided publish options.
@@ -7378,12 +7501,12 @@ class Publisher extends BasePeerConnection {
7378
7501
  continue;
7379
7502
  const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
7380
7503
  i.publishOption.trackType === trackType);
7381
- if (!item || !item.transceiver)
7504
+ if (!item)
7382
7505
  continue;
7383
7506
  // take the track from the existing transceiver for the same track type,
7384
7507
  // clone it and publish it with the new publish options
7385
7508
  const track = this.cloneTrack(item.transceiver.sender.track);
7386
- await this.addTransceiver(track, publishOption);
7509
+ await this.addTransceiver(track, publishOption, item.options);
7387
7510
  }
7388
7511
  // stop publishing with options not required anymore -> [vp9]
7389
7512
  for (const item of this.transceiverCache.items()) {
@@ -7563,11 +7686,9 @@ class Publisher extends BasePeerConnection {
7563
7686
  this.getAnnouncedTracks = (sdp) => {
7564
7687
  const trackInfos = [];
7565
7688
  for (const bundle of this.transceiverCache.items()) {
7566
- const { transceiver, publishOption } = bundle;
7567
- const track = transceiver.sender.track;
7568
- if (!track)
7689
+ if (!bundle.transceiver.sender.track)
7569
7690
  continue;
7570
- trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
7691
+ trackInfos.push(this.toTrackInfo(bundle, sdp));
7571
7692
  }
7572
7693
  return trackInfos;
7573
7694
  };
@@ -7580,17 +7701,18 @@ class Publisher extends BasePeerConnection {
7580
7701
  const sdp = this.pc.localDescription?.sdp;
7581
7702
  const trackInfos = [];
7582
7703
  for (const publishOption of this.publishOptions) {
7583
- const transceiver = this.transceiverCache.get(publishOption);
7584
- if (!transceiver || !transceiver.sender.track)
7704
+ const bundle = this.transceiverCache.get(publishOption);
7705
+ if (!bundle || !bundle.transceiver.sender.track)
7585
7706
  continue;
7586
- trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
7707
+ trackInfos.push(this.toTrackInfo(bundle, sdp));
7587
7708
  }
7588
7709
  return trackInfos;
7589
7710
  };
7590
7711
  /**
7591
7712
  * Converts the given transceiver to a `TrackInfo` object.
7592
7713
  */
7593
- this.toTrackInfo = (transceiver, publishOption, sdp) => {
7714
+ this.toTrackInfo = (bundle, sdp) => {
7715
+ const { transceiver, publishOption } = bundle;
7594
7716
  const track = transceiver.sender.track;
7595
7717
  const isTrackLive = track.readyState === 'live';
7596
7718
  const layers = isTrackLive
@@ -7598,15 +7720,16 @@ class Publisher extends BasePeerConnection {
7598
7720
  : this.transceiverCache.getLayers(publishOption);
7599
7721
  this.transceiverCache.setLayers(publishOption, layers);
7600
7722
  const isAudioTrack = isAudioTrackType(publishOption.trackType);
7601
- const isStereo = isAudioTrack && track.getSettings().channelCount === 2;
7602
7723
  const transceiverIndex = this.transceiverCache.indexOf(transceiver);
7603
7724
  const audioSettings = this.state.settings?.audio;
7725
+ const stereo = publishOption.trackType === TrackType.SCREEN_SHARE_AUDIO ||
7726
+ (isAudioTrack && !!audioSettings?.hifi_audio_enabled);
7604
7727
  return {
7605
7728
  trackId: track.id,
7606
7729
  layers: toVideoLayers(layers),
7607
7730
  trackType: publishOption.trackType,
7608
7731
  mid: extractMid(transceiver, transceiverIndex, sdp),
7609
- stereo: isStereo,
7732
+ stereo,
7610
7733
  dtx: isAudioTrack && !!audioSettings?.opus_dtx_enabled,
7611
7734
  red: isAudioTrack && !!audioSettings?.redundant_coding_enabled,
7612
7735
  muted: !isTrackLive,
@@ -9890,7 +10013,7 @@ function resolveDeviceId(deviceId, kind) {
9890
10013
  */
9891
10014
  const isMobile = () => /Mobi/i.test(navigator.userAgent);
9892
10015
 
9893
- class InputMediaDeviceManager {
10016
+ class DeviceManager {
9894
10017
  constructor(call, state, trackType) {
9895
10018
  /**
9896
10019
  * if true, stops the media stream when call is left
@@ -10097,8 +10220,8 @@ class InputMediaDeviceManager {
10097
10220
  }
10098
10221
  });
10099
10222
  }
10100
- publishStream(stream) {
10101
- return this.call.publish(stream, this.trackType);
10223
+ publishStream(stream, options) {
10224
+ return this.call.publish(stream, this.trackType, options);
10102
10225
  }
10103
10226
  stopPublishStream() {
10104
10227
  return this.call.stopPublish(this.trackType);
@@ -10334,16 +10457,15 @@ class InputMediaDeviceManager {
10334
10457
  }
10335
10458
  }
10336
10459
 
10337
- class InputMediaDeviceManagerState {
10460
+ class DeviceManagerState {
10338
10461
  /**
10339
- * Constructs new InputMediaDeviceManagerState instance.
10462
+ * Constructs a new InputMediaDeviceManagerState instance.
10340
10463
  *
10341
10464
  * @param disableMode the disable mode to use.
10342
10465
  * @param permission the BrowserPermission to use for querying.
10343
10466
  * `undefined` means no permission is required.
10344
10467
  */
10345
- constructor(disableMode = 'stop-tracks', permission) {
10346
- this.disableMode = disableMode;
10468
+ constructor(disableMode, permission) {
10347
10469
  this.statusSubject = new rxjs.BehaviorSubject(undefined);
10348
10470
  this.optimisticStatusSubject = new rxjs.BehaviorSubject(undefined);
10349
10471
  this.mediaStreamSubject = new rxjs.BehaviorSubject(undefined);
@@ -10374,6 +10496,7 @@ class InputMediaDeviceManagerState {
10374
10496
  * The default constraints for the device.
10375
10497
  */
10376
10498
  this.defaultConstraints$ = this.defaultConstraintsSubject.asObservable();
10499
+ this.disableMode = disableMode;
10377
10500
  this.hasBrowserPermission$ = permission
10378
10501
  ? permission.asObservable().pipe(rxjs.shareReplay(1))
10379
10502
  : rxjs.of(true);
@@ -10460,10 +10583,15 @@ class InputMediaDeviceManagerState {
10460
10583
  }
10461
10584
  }
10462
10585
 
10463
- class CameraManagerState extends InputMediaDeviceManagerState {
10586
+ class CameraManagerState extends DeviceManagerState {
10464
10587
  constructor() {
10465
10588
  super('stop-tracks', getVideoBrowserPermission());
10466
10589
  this.directionSubject = new rxjs.BehaviorSubject(undefined);
10590
+ /**
10591
+ * Observable that emits the preferred camera direction
10592
+ * front - means the camera facing the user
10593
+ * back - means the camera facing the environment
10594
+ */
10467
10595
  this.direction$ = this.directionSubject
10468
10596
  .asObservable()
10469
10597
  .pipe(rxjs.distinctUntilChanged());
@@ -10503,7 +10631,7 @@ class CameraManagerState extends InputMediaDeviceManagerState {
10503
10631
  }
10504
10632
  }
10505
10633
 
10506
- class CameraManager extends InputMediaDeviceManager {
10634
+ class CameraManager extends DeviceManager {
10507
10635
  /**
10508
10636
  * Constructs a new CameraManager.
10509
10637
  *
@@ -10649,18 +10777,87 @@ class CameraManager extends InputMediaDeviceManager {
10649
10777
  }
10650
10778
  }
10651
10779
 
10652
- class MicrophoneManagerState extends InputMediaDeviceManagerState {
10780
+ /**
10781
+ * Base class for High Fidelity enabled Device Managers.
10782
+ */
10783
+ class AudioDeviceManager extends DeviceManager {
10784
+ /**
10785
+ * Sets the audio bitrate profile and stereo mode.
10786
+ */
10787
+ async setAudioBitrateProfile(profile) {
10788
+ if (!this.call.state.settings?.audio.hifi_audio_enabled) {
10789
+ throw new Error('High Fidelity audio is not enabled for this call');
10790
+ }
10791
+ this.doSetAudioBitrateProfile(profile);
10792
+ this.state.setAudioBitrateProfile(profile);
10793
+ if (this.enabled) {
10794
+ await this.applySettingsToStream();
10795
+ }
10796
+ }
10797
+ /**
10798
+ * Overrides the default `publishStream` method to inject the audio bitrate profile.
10799
+ */
10800
+ publishStream(stream, options) {
10801
+ return super.publishStream(stream, {
10802
+ audioBitrateProfile: this.state.audioBitrateProfile,
10803
+ ...options,
10804
+ });
10805
+ }
10806
+ }
10807
+ /**
10808
+ * Prepares a new MediaTrackConstraints set based on the provided arguments.
10809
+ */
10810
+ const createAudioConstraints = (profile) => {
10811
+ const stereo = profile === AudioBitrateProfile.MUSIC_HIGH_QUALITY;
10812
+ return {
10813
+ echoCancellation: !stereo,
10814
+ noiseSuppression: !stereo,
10815
+ autoGainControl: !stereo,
10816
+ channelCount: { ideal: stereo ? 2 : 1 },
10817
+ };
10818
+ };
10819
+
10820
+ /**
10821
+ * Base state class for High Fidelity enabled device managers.
10822
+ */
10823
+ class AudioDeviceManagerState extends DeviceManagerState {
10824
+ /**
10825
+ * Constructs a new AudioDeviceManagerState instance.
10826
+ */
10827
+ constructor(disableMode, permission, profile) {
10828
+ super(disableMode, permission);
10829
+ this.audioBitrateProfileSubject = new rxjs.BehaviorSubject(profile);
10830
+ this.audioBitrateProfile$ = this.audioBitrateProfileSubject
10831
+ .asObservable()
10832
+ .pipe(rxjs.distinctUntilChanged());
10833
+ }
10834
+ /**
10835
+ * Returns the current audio bitrate profile.
10836
+ */
10837
+ get audioBitrateProfile() {
10838
+ return getCurrentValue(this.audioBitrateProfile$);
10839
+ }
10840
+ /**
10841
+ * Sets the audio bitrate profile and stereo mode.
10842
+ */
10843
+ setAudioBitrateProfile(profile) {
10844
+ setCurrentValue(this.audioBitrateProfileSubject, profile);
10845
+ }
10846
+ }
10847
+
10848
+ class MicrophoneManagerState extends AudioDeviceManagerState {
10653
10849
  constructor(disableMode) {
10654
- super(disableMode, getAudioBrowserPermission());
10850
+ super(disableMode, getAudioBrowserPermission(), AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED);
10655
10851
  this.speakingWhileMutedSubject = new rxjs.BehaviorSubject(false);
10852
+ /**
10853
+ * An Observable that emits `true` if the user's microphone is muted, but they're speaking.
10854
+ */
10656
10855
  this.speakingWhileMuted$ = this.speakingWhileMutedSubject
10657
10856
  .asObservable()
10658
10857
  .pipe(rxjs.distinctUntilChanged());
10659
10858
  }
10660
10859
  /**
10661
- * `true` if the user's microphone is muted but they'are speaking.
10662
- *
10663
- * This feature is not available in the React Native SDK.
10860
+ * `true` if the user's microphone is muted but they're speaking.
10664
10861
  */
10665
10862
  get speakingWhileMuted() {
10666
10863
  return getCurrentValue(this.speakingWhileMuted$);
@@ -10893,7 +11090,7 @@ class RNSpeechDetector {
10893
11090
  }
10894
11091
  }
10895
11092
 
10896
- class MicrophoneManager extends InputMediaDeviceManager {
11093
+ class MicrophoneManager extends AudioDeviceManager {
10897
11094
  constructor(call, disableMode = 'stop-tracks') {
10898
11095
  super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
10899
11096
  this.speakingWhileMutedNotificationEnabled = true;
@@ -11089,6 +11286,21 @@ class MicrophoneManager extends InputMediaDeviceManager {
11089
11286
  getStream(constraints) {
11090
11287
  return getAudioStream(constraints, this.call.tracer);
11091
11288
  }
11289
+ doSetAudioBitrateProfile(profile) {
11290
+ this.setDefaultConstraints({
11291
+ ...this.state.defaultConstraints,
11292
+ ...createAudioConstraints(profile),
11293
+ });
11294
+ if (this.noiseCancellation) {
11295
+ const disableAudioProcessing = profile === AudioBitrateProfile.MUSIC_HIGH_QUALITY;
11296
+ if (disableAudioProcessing) {
11297
+ this.noiseCancellation.disable(); // disable for high quality music mode
11298
+ }
11299
+ else {
11300
+ this.noiseCancellation.enable(); // restore it for other modes if available
11301
+ }
11302
+ }
11303
+ }
11092
11304
  async startSpeakingWhileMutedDetection(deviceId) {
11093
11305
  await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
11094
11306
  await this.stopSpeakingWhileMutedDetection();
@@ -11125,9 +11337,12 @@ class MicrophoneManager extends InputMediaDeviceManager {
11125
11337
  }
11126
11338
  }
11127
11339
 
11128
- class ScreenShareState extends InputMediaDeviceManagerState {
11340
+ class ScreenShareState extends AudioDeviceManagerState {
11341
+ /**
11342
+ * Constructs a new ScreenShareState instance.
11343
+ */
11129
11344
  constructor() {
11130
- super(...arguments);
11345
+ super('stop-tracks', undefined, AudioBitrateProfile.MUSIC_HIGH_QUALITY);
11131
11346
  this.audioEnabledSubject = new rxjs.BehaviorSubject(true);
11132
11347
  this.settingsSubject = new rxjs.BehaviorSubject(undefined);
11133
11348
  /**
@@ -11176,7 +11391,7 @@ class ScreenShareState extends InputMediaDeviceManagerState {
11176
11391
  }
11177
11392
  }
11178
11393
 
11179
- class ScreenShareManager extends InputMediaDeviceManager {
11394
+ class ScreenShareManager extends AudioDeviceManager {
11180
11395
  constructor(call) {
11181
11396
  super(call, new ScreenShareState(), TrackType.SCREEN_SHARE);
11182
11397
  }
@@ -11186,6 +11401,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
11186
11401
  const maybeTargetResolution = settings?.screensharing.target_resolution;
11187
11402
  if (maybeTargetResolution) {
11188
11403
  this.setDefaultConstraints({
11404
+ ...this.state.defaultConstraints,
11189
11405
  video: {
11190
11406
  width: maybeTargetResolution.width,
11191
11407
  height: maybeTargetResolution.height,
@@ -11242,6 +11458,19 @@ class ScreenShareManager extends InputMediaDeviceManager {
11242
11458
  }
11243
11459
  return stream;
11244
11460
  }
11461
+ doSetAudioBitrateProfile(profile) {
11462
+ const { defaultConstraints } = this.state;
11463
+ const baseAudioConstraints = typeof defaultConstraints?.audio !== 'boolean'
11464
+ ? defaultConstraints?.audio
11465
+ : null;
11466
+ this.setDefaultConstraints({
11467
+ ...defaultConstraints,
11468
+ audio: {
11469
+ ...baseAudioConstraints,
11470
+ ...createAudioConstraints(profile),
11471
+ },
11472
+ });
11473
+ }
11245
11474
  async stopPublishStream() {
11246
11475
  return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
11247
11476
  }
@@ -11255,19 +11484,27 @@ class ScreenShareManager extends InputMediaDeviceManager {
11255
11484
 
11256
11485
  class SpeakerState {
11257
11486
  constructor(tracer) {
11487
+ this.tracer = tracer;
11258
11488
  this.selectedDeviceSubject = new rxjs.BehaviorSubject('');
11259
11489
  this.volumeSubject = new rxjs.BehaviorSubject(1);
11260
11490
  /**
11261
11491
  * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
11262
11492
  */
11263
11493
  this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
11264
- this.tracer = tracer;
11494
+ /**
11495
+ * An Observable that emits the currently selected device
11496
+ *
11497
+ * Note: this feature is not supported in React Native
11498
+ */
11265
11499
  this.selectedDevice$ = this.selectedDeviceSubject
11266
11500
  .asObservable()
11267
11501
  .pipe(rxjs.distinctUntilChanged());
11268
- this.volume$ = this.volumeSubject
11269
- .asObservable()
11270
- .pipe(rxjs.distinctUntilChanged());
11502
+ /**
11503
+ * An Observable that emits the currently selected volume
11504
+ *
11505
+ * Note: this feature is not supported in React Native
11506
+ */
11507
+ this.volume$ = this.volumeSubject.asObservable().pipe(rxjs.distinctUntilChanged());
11271
11508
  }
11272
11509
  /**
11273
11510
  * The currently selected device
@@ -11871,7 +12108,6 @@ class Call {
11871
12108
  if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
11872
12109
  throw new Error(`Illegal State: call.join() shall be called only once`);
11873
12110
  }
11874
- this.state.setCallingState(exports.CallingState.JOINING);
11875
12111
  // we will count the number of join failures per SFU.
11876
12112
  // once the number of failures reaches 2, we will piggyback on the `migrating_from`
11877
12113
  // field to force the coordinator to provide us another SFU
@@ -11900,8 +12136,6 @@ class Call {
11900
12136
  joinData.migrating_from = sfuId;
11901
12137
  }
11902
12138
  if (attempt === maxJoinRetries - 1) {
11903
- // restore the previous call state if the join-flow fails
11904
- this.state.setCallingState(callingState);
11905
12139
  throw err;
11906
12140
  }
11907
12141
  }
@@ -11961,6 +12195,7 @@ class Call {
11961
12195
  })
11962
12196
  : previousSfuClient;
11963
12197
  this.sfuClient = sfuClient;
12198
+ this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
11964
12199
  this.dynascaleManager.setSfuClient(sfuClient);
11965
12200
  const clientDetails = await getClientDetails();
11966
12201
  // we don't need to send JoinRequest if we are re-using an existing healthy SFU client
@@ -11984,6 +12219,7 @@ class Call {
11984
12219
  : [];
11985
12220
  try {
11986
12221
  const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
12222
+ unifiedSessionId: this.unifiedSessionId,
11987
12223
  subscriberSdp,
11988
12224
  publisherSdp,
11989
12225
  clientDetails,
@@ -12029,6 +12265,7 @@ class Call {
12029
12265
  statsOptions,
12030
12266
  publishOptions: this.currentPublishOptions || [],
12031
12267
  closePreviousInstances: !performingMigration,
12268
+ unifiedSessionId: this.unifiedSessionId,
12032
12269
  });
12033
12270
  }
12034
12271
  // make sure we only track connection timing if we are not calling this method as part of a reconnection flow
@@ -12155,7 +12392,7 @@ class Call {
12155
12392
  * @internal
12156
12393
  */
12157
12394
  this.initPublisherAndSubscriber = (opts) => {
12158
- const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, } = opts;
12395
+ const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
12159
12396
  const { enable_rtc_stats: enableTracing } = statsOptions;
12160
12397
  if (closePreviousInstances && this.subscriber) {
12161
12398
  this.subscriber.dispose();
@@ -12210,7 +12447,6 @@ class Call {
12210
12447
  this.tracer.setEnabled(enableTracing);
12211
12448
  this.sfuStatsReporter?.stop();
12212
12449
  if (statsOptions?.reporting_interval_ms > 0) {
12213
- this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
12214
12450
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
12215
12451
  clientDetails,
12216
12452
  options: statsOptions,
@@ -12220,7 +12456,7 @@ class Call {
12220
12456
  camera: this.camera,
12221
12457
  state: this.state,
12222
12458
  tracer: this.tracer,
12223
- unifiedSessionId: this.unifiedSessionId,
12459
+ unifiedSessionId,
12224
12460
  });
12225
12461
  this.sfuStatsReporter.start();
12226
12462
  }
@@ -12593,10 +12829,11 @@ class Call {
12593
12829
  *
12594
12830
  * @param mediaStream the media stream to publish.
12595
12831
  * @param trackType the type of the track to announce.
12832
+ * @param options the publish options.
12596
12833
  */
12597
- this.publish = async (mediaStream, trackType) => {
12834
+ this.publish = async (mediaStream, trackType, options) => {
12598
12835
  if (!this.sfuClient)
12599
- throw new Error(`Call not joined yet.`);
12836
+ throw new Error(`Call is not joined yet`);
12600
12837
  // joining is in progress, and we should wait until the client is ready
12601
12838
  await this.sfuClient.joinTask;
12602
12839
  if (!this.permissionsContext.canPublish(trackType)) {
@@ -12614,14 +12851,15 @@ class Call {
12614
12851
  throw new Error(`Can't publish ended tracks.`);
12615
12852
  }
12616
12853
  pushToIfMissing(this.trackPublishOrder, trackType);
12617
- await this.publisher.publish(track, trackType);
12854
+ await this.publisher.publish(track, trackType, options);
12618
12855
  const trackTypes = [trackType];
12619
12856
  if (trackType === TrackType.SCREEN_SHARE) {
12620
12857
  const [audioTrack] = mediaStream.getAudioTracks();
12621
12858
  if (audioTrack) {
12622
- pushToIfMissing(this.trackPublishOrder, TrackType.SCREEN_SHARE_AUDIO);
12623
- await this.publisher.publish(audioTrack, TrackType.SCREEN_SHARE_AUDIO);
12624
- trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
12859
+ const screenShareAudio = TrackType.SCREEN_SHARE_AUDIO;
12860
+ pushToIfMissing(this.trackPublishOrder, screenShareAudio);
12861
+ await this.publisher.publish(audioTrack, screenShareAudio, options);
12862
+ trackTypes.push(screenShareAudio);
12625
12863
  }
12626
12864
  }
12627
12865
  if (track.kind === 'video') {
@@ -14540,7 +14778,7 @@ class StreamClient {
14540
14778
  this.getUserAgent = () => {
14541
14779
  if (!this.cachedUserAgent) {
14542
14780
  const { clientAppIdentifier = {} } = this.options;
14543
- const { sdkName = 'js', sdkVersion = "1.31.0", ...extras } = clientAppIdentifier;
14781
+ const { sdkName = 'js', sdkVersion = "1.33.0", ...extras } = clientAppIdentifier;
14544
14782
  this.cachedUserAgent = [
14545
14783
  `stream-video-${sdkName}-v${sdkVersion}`,
14546
14784
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -14729,6 +14967,7 @@ class StreamVideoClient {
14729
14967
  this.effectsRegistered = false;
14730
14968
  this.eventHandlersToUnregister = [];
14731
14969
  this.connectionConcurrencyTag = Symbol('connectionConcurrencyTag');
14970
+ this.rejectCallWhenBusy = false;
14732
14971
  this.registerClientInstance = (apiKey, user) => {
14733
14972
  const instanceKey = getInstanceKey(apiKey, user);
14734
14973
  if (StreamVideoClient._instances.has(instanceKey)) {
@@ -14774,7 +15013,16 @@ class StreamVideoClient {
14774
15013
  let call = this.writeableStateStore.findCall(e.call.type, e.call.id);
14775
15014
  if (call) {
14776
15015
  if (ringing) {
14777
- await call.updateFromRingingEvent(e);
15016
+ if (this.shouldRejectCall(call.cid)) {
15017
+ this.logger('info', `Leaving call with busy reject reason ${call.cid} because user is busy`);
15018
+ // remove the instance from the state store
15019
+ await call.leave();
15020
+ // explicitly reject the call with busy reason as calling state was not ringing before and leave would not call it therefore
15021
+ await call.reject('busy');
15022
+ }
15023
+ else {
15024
+ await call.updateFromRingingEvent(e);
15025
+ }
14778
15026
  }
14779
15027
  else {
14780
15028
  call.state.updateFromCallResponse(e.call);
@@ -14789,11 +15037,19 @@ class StreamVideoClient {
14789
15037
  clientStore: this.writeableStateStore,
14790
15038
  ringing,
14791
15039
  });
14792
- call.state.updateFromCallResponse(e.call);
14793
15040
  if (ringing) {
14794
- await call.get();
15041
+ if (this.shouldRejectCall(call.cid)) {
15042
+ this.logger('info', `Rejecting call ${call.cid} because user is busy`);
15043
+ // call is not in the state store yet, so just reject api is enough
15044
+ await call.reject('busy');
15045
+ }
15046
+ else {
15047
+ await call.updateFromRingingEvent(e);
15048
+ await call.get();
15049
+ }
14795
15050
  }
14796
15051
  else {
15052
+ call.state.updateFromCallResponse(e.call);
14797
15053
  this.writeableStateStore.registerCall(call);
14798
15054
  this.logger('info', `New call created and registered: ${call.cid}`);
14799
15055
  }
@@ -15060,6 +15316,16 @@ class StreamVideoClient {
15060
15316
  this.connectAnonymousUser = async (user, tokenOrProvider) => {
15061
15317
  return withoutConcurrency(this.connectionConcurrencyTag, () => this.streamClient.connectAnonymousUser(user, tokenOrProvider));
15062
15318
  };
15319
+ this.shouldRejectCall = (currentCallId) => {
15320
+ if (!this.rejectCallWhenBusy)
15321
+ return false;
15322
+ const hasOngoingRingingCall = this.state.calls.some((c) => c.cid !== currentCallId &&
15323
+ c.ringing &&
15324
+ c.state.callingState !== exports.CallingState.IDLE &&
15325
+ c.state.callingState !== exports.CallingState.LEFT &&
15326
+ c.state.callingState !== exports.CallingState.RECONNECTING_FAILED);
15327
+ return hasOngoingRingingCall;
15328
+ };
15063
15329
  const apiKey = typeof apiKeyOrArgs === 'string' ? apiKeyOrArgs : apiKeyOrArgs.apiKey;
15064
15330
  const clientOptions = typeof apiKeyOrArgs === 'string' ? opts : apiKeyOrArgs.options;
15065
15331
  if (clientOptions?.enableTimerWorker)
@@ -15067,6 +15333,7 @@ class StreamVideoClient {
15067
15333
  const rootLogger = clientOptions?.logger || logToConsole;
15068
15334
  setLogger(rootLogger, clientOptions?.logLevel || 'warn');
15069
15335
  this.logger = getLogger(['client']);
15336
+ this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
15070
15337
  this.streamClient = createCoordinatorClient(apiKey, clientOptions);
15071
15338
  this.writeableStateStore = new StreamVideoWriteableStateStore();
15072
15339
  this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
@@ -15122,6 +15389,8 @@ exports.CallTypes = CallTypes;
15122
15389
  exports.CameraManager = CameraManager;
15123
15390
  exports.CameraManagerState = CameraManagerState;
15124
15391
  exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnum;
15392
+ exports.DeviceManager = DeviceManager;
15393
+ exports.DeviceManagerState = DeviceManagerState;
15125
15394
  exports.DynascaleManager = DynascaleManager;
15126
15395
  exports.ErrorFromResponse = ErrorFromResponse;
15127
15396
  exports.FrameRecordingSettingsRequestModeEnum = FrameRecordingSettingsRequestModeEnum;
@@ -15129,8 +15398,6 @@ exports.FrameRecordingSettingsRequestQualityEnum = FrameRecordingSettingsRequest
15129
15398
  exports.FrameRecordingSettingsResponseModeEnum = FrameRecordingSettingsResponseModeEnum;
15130
15399
  exports.IngressAudioEncodingOptionsRequestChannelsEnum = IngressAudioEncodingOptionsRequestChannelsEnum;
15131
15400
  exports.IngressVideoLayerRequestCodecEnum = IngressVideoLayerRequestCodecEnum;
15132
- exports.InputMediaDeviceManager = InputMediaDeviceManager;
15133
- exports.InputMediaDeviceManagerState = InputMediaDeviceManagerState;
15134
15401
  exports.LayoutSettingsRequestNameEnum = LayoutSettingsRequestNameEnum;
15135
15402
  exports.MicrophoneManager = MicrophoneManager;
15136
15403
  exports.MicrophoneManagerState = MicrophoneManagerState;