@stream-io/video-client 1.24.0 → 1.25.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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +335 -127
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +334 -126
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +335 -127
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/StreamSfuClient.d.ts +12 -4
  9. package/dist/src/StreamVideoClient.d.ts +3 -1
  10. package/dist/src/coordinator/connection/errors.d.ts +1 -0
  11. package/dist/src/gen/video/sfu/models/models.d.ts +4 -0
  12. package/dist/src/rtc/BasePeerConnection.d.ts +23 -4
  13. package/dist/src/rtc/NegotiationError.d.ts +15 -0
  14. package/dist/src/rtc/Publisher.d.ts +2 -2
  15. package/dist/src/rtc/helpers/sdp.d.ts +7 -0
  16. package/dist/src/types.d.ts +11 -0
  17. package/package.json +1 -1
  18. package/src/Call.ts +66 -38
  19. package/src/StreamSfuClient.ts +17 -7
  20. package/src/StreamVideoClient.ts +17 -7
  21. package/src/coordinator/connection/connection.ts +2 -1
  22. package/src/coordinator/connection/errors.ts +31 -0
  23. package/src/devices/ScreenShareManager.ts +12 -2
  24. package/src/devices/devices.ts +23 -12
  25. package/src/events/__tests__/internal.test.ts +1 -0
  26. package/src/gen/google/protobuf/struct.ts +2 -2
  27. package/src/gen/google/protobuf/timestamp.ts +1 -1
  28. package/src/gen/video/sfu/event/events.ts +15 -15
  29. package/src/gen/video/sfu/models/models.ts +9 -5
  30. package/src/gen/video/sfu/signal_rpc/signal.client.ts +1 -1
  31. package/src/gen/video/sfu/signal_rpc/signal.ts +6 -6
  32. package/src/rtc/BasePeerConnection.ts +132 -46
  33. package/src/rtc/NegotiationError.ts +21 -0
  34. package/src/rtc/Publisher.ts +12 -9
  35. package/src/rtc/Subscriber.ts +8 -2
  36. package/src/rtc/__tests__/Publisher.test.ts +160 -17
  37. package/src/rtc/__tests__/Subscriber.test.ts +31 -14
  38. package/src/rtc/helpers/__tests__/sdp.stereo.test.ts +120 -0
  39. package/src/rtc/helpers/sdp.ts +43 -1
  40. package/src/types.ts +12 -0
@@ -6,7 +6,7 @@ export { AxiosError } from 'axios';
6
6
  import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transport';
7
7
  import { ReplaySubject, combineLatest, BehaviorSubject, shareReplay, map, distinctUntilChanged, takeWhile, distinctUntilKeyChanged, fromEventPattern, startWith, concatMap, merge, from, fromEvent, debounceTime, pairwise, of } from 'rxjs';
8
8
  import { UAParser } from 'ua-parser-js';
9
- import { parse } from 'sdp-transform';
9
+ import { parse, write } from 'sdp-transform';
10
10
 
11
11
  /* tslint:disable */
12
12
  /**
@@ -420,7 +420,7 @@ class ErrorFromResponse extends Error {
420
420
  }
421
421
  }
422
422
 
423
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
423
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
424
424
  // @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
425
425
  // tslint:disable
426
426
  //
@@ -653,7 +653,7 @@ class ListValue$Type extends MessageType {
653
653
  no: 1,
654
654
  name: 'values',
655
655
  kind: 'message',
656
- repeat: 1 /*RepeatType.PACKED*/,
656
+ repeat: 2 /*RepeatType.UNPACKED*/,
657
657
  T: () => Value,
658
658
  },
659
659
  ]);
@@ -685,7 +685,7 @@ class ListValue$Type extends MessageType {
685
685
  */
686
686
  const ListValue = new ListValue$Type();
687
687
 
688
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
688
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
689
689
  // @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
690
690
  // tslint:disable
691
691
  //
@@ -821,7 +821,7 @@ class Timestamp$Type extends MessageType {
821
821
  */
822
822
  const Timestamp = new Timestamp$Type();
823
823
 
824
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
824
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
825
825
  // @generated from protobuf file "video/sfu/models/models.proto" (package "stream.video.sfu.models", syntax proto3)
826
826
  // tslint:disable
827
827
  /**
@@ -963,6 +963,10 @@ var ErrorCode;
963
963
  * @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_MEDIA_TRANSPORT_FAILURE = 205;
964
964
  */
965
965
  ErrorCode[ErrorCode["PARTICIPANT_MEDIA_TRANSPORT_FAILURE"] = 205] = "PARTICIPANT_MEDIA_TRANSPORT_FAILURE";
966
+ /**
967
+ * @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_SIGNAL_LOST = 206;
968
+ */
969
+ ErrorCode[ErrorCode["PARTICIPANT_SIGNAL_LOST"] = 206] = "PARTICIPANT_SIGNAL_LOST";
966
970
  /**
967
971
  * @generated from protobuf enum value: ERROR_CODE_CALL_NOT_FOUND = 300;
968
972
  */
@@ -1243,7 +1247,7 @@ class CallState$Type extends MessageType {
1243
1247
  no: 1,
1244
1248
  name: 'participants',
1245
1249
  kind: 'message',
1246
- repeat: 1 /*RepeatType.PACKED*/,
1250
+ repeat: 2 /*RepeatType.UNPACKED*/,
1247
1251
  T: () => Participant,
1248
1252
  },
1249
1253
  { no: 2, name: 'started_at', kind: 'message', T: () => Timestamp },
@@ -1257,7 +1261,7 @@ class CallState$Type extends MessageType {
1257
1261
  no: 4,
1258
1262
  name: 'pins',
1259
1263
  kind: 'message',
1260
- repeat: 1 /*RepeatType.PACKED*/,
1264
+ repeat: 2 /*RepeatType.UNPACKED*/,
1261
1265
  T: () => Pin,
1262
1266
  },
1263
1267
  ]);
@@ -1435,7 +1439,7 @@ class SubscribeOption$Type extends MessageType {
1435
1439
  no: 2,
1436
1440
  name: 'codecs',
1437
1441
  kind: 'message',
1438
- repeat: 1 /*RepeatType.PACKED*/,
1442
+ repeat: 2 /*RepeatType.UNPACKED*/,
1439
1443
  T: () => Codec,
1440
1444
  },
1441
1445
  ]);
@@ -1568,7 +1572,7 @@ class TrackInfo$Type extends MessageType {
1568
1572
  no: 5,
1569
1573
  name: 'layers',
1570
1574
  kind: 'message',
1571
- repeat: 1 /*RepeatType.PACKED*/,
1575
+ repeat: 2 /*RepeatType.UNPACKED*/,
1572
1576
  T: () => VideoLayer,
1573
1577
  },
1574
1578
  { no: 6, name: 'mid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
@@ -1935,7 +1939,7 @@ var models = /*#__PURE__*/Object.freeze({
1935
1939
  get WebsocketReconnectStrategy () { return WebsocketReconnectStrategy; }
1936
1940
  });
1937
1941
 
1938
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
1942
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
1939
1943
  // @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
1940
1944
  // tslint:disable
1941
1945
  // @generated message type with reflection information, may provide speed optimized methods
@@ -2103,14 +2107,14 @@ class SendStatsRequest$Type extends MessageType {
2103
2107
  no: 16,
2104
2108
  name: 'encode_stats',
2105
2109
  kind: 'message',
2106
- repeat: 1 /*RepeatType.PACKED*/,
2110
+ repeat: 2 /*RepeatType.UNPACKED*/,
2107
2111
  T: () => PerformanceStats,
2108
2112
  },
2109
2113
  {
2110
2114
  no: 17,
2111
2115
  name: 'decode_stats',
2112
2116
  kind: 'message',
2113
- repeat: 1 /*RepeatType.PACKED*/,
2117
+ repeat: 2 /*RepeatType.UNPACKED*/,
2114
2118
  T: () => PerformanceStats,
2115
2119
  },
2116
2120
  {
@@ -2177,7 +2181,7 @@ class UpdateMuteStatesRequest$Type extends MessageType {
2177
2181
  no: 3,
2178
2182
  name: 'mute_states',
2179
2183
  kind: 'message',
2180
- repeat: 1 /*RepeatType.PACKED*/,
2184
+ repeat: 2 /*RepeatType.UNPACKED*/,
2181
2185
  T: () => TrackMuteState,
2182
2186
  },
2183
2187
  ]);
@@ -2254,7 +2258,7 @@ class UpdateSubscriptionsRequest$Type extends MessageType {
2254
2258
  no: 3,
2255
2259
  name: 'tracks',
2256
2260
  kind: 'message',
2257
- repeat: 1 /*RepeatType.PACKED*/,
2261
+ repeat: 2 /*RepeatType.UNPACKED*/,
2258
2262
  T: () => TrackSubscriptionDetails,
2259
2263
  },
2260
2264
  ]);
@@ -2353,7 +2357,7 @@ class SetPublisherRequest$Type extends MessageType {
2353
2357
  no: 3,
2354
2358
  name: 'tracks',
2355
2359
  kind: 'message',
2356
- repeat: 1 /*RepeatType.PACKED*/,
2360
+ repeat: 2 /*RepeatType.UNPACKED*/,
2357
2361
  T: () => TrackInfo,
2358
2362
  },
2359
2363
  ]);
@@ -2433,7 +2437,7 @@ const SignalServer = new ServiceType('stream.video.sfu.signal.SignalServer', [
2433
2437
  },
2434
2438
  ]);
2435
2439
 
2436
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2440
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2437
2441
  // @generated from protobuf file "video/sfu/event/events.proto" (package "stream.video.sfu.event", syntax proto3)
2438
2442
  // tslint:disable
2439
2443
  // @generated message type with reflection information, may provide speed optimized methods
@@ -2609,7 +2613,7 @@ class ChangePublishOptions$Type extends MessageType {
2609
2613
  no: 1,
2610
2614
  name: 'publish_options',
2611
2615
  kind: 'message',
2612
- repeat: 1 /*RepeatType.PACKED*/,
2616
+ repeat: 2 /*RepeatType.UNPACKED*/,
2613
2617
  T: () => PublishOption,
2614
2618
  },
2615
2619
  { no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
@@ -2648,7 +2652,7 @@ class PinsChanged$Type extends MessageType {
2648
2652
  no: 1,
2649
2653
  name: 'pins',
2650
2654
  kind: 'message',
2651
- repeat: 1 /*RepeatType.PACKED*/,
2655
+ repeat: 2 /*RepeatType.UNPACKED*/,
2652
2656
  T: () => Pin,
2653
2657
  },
2654
2658
  ]);
@@ -2891,14 +2895,14 @@ class JoinRequest$Type extends MessageType {
2891
2895
  no: 9,
2892
2896
  name: 'preferred_publish_options',
2893
2897
  kind: 'message',
2894
- repeat: 1 /*RepeatType.PACKED*/,
2898
+ repeat: 2 /*RepeatType.UNPACKED*/,
2895
2899
  T: () => PublishOption,
2896
2900
  },
2897
2901
  {
2898
2902
  no: 10,
2899
2903
  name: 'preferred_subscribe_options',
2900
2904
  kind: 'message',
2901
- repeat: 1 /*RepeatType.PACKED*/,
2905
+ repeat: 2 /*RepeatType.UNPACKED*/,
2902
2906
  T: () => SubscribeOption,
2903
2907
  },
2904
2908
  ]);
@@ -2926,14 +2930,14 @@ class ReconnectDetails$Type extends MessageType {
2926
2930
  no: 3,
2927
2931
  name: 'announced_tracks',
2928
2932
  kind: 'message',
2929
- repeat: 1 /*RepeatType.PACKED*/,
2933
+ repeat: 2 /*RepeatType.UNPACKED*/,
2930
2934
  T: () => TrackInfo,
2931
2935
  },
2932
2936
  {
2933
2937
  no: 4,
2934
2938
  name: 'subscriptions',
2935
2939
  kind: 'message',
2936
- repeat: 1 /*RepeatType.PACKED*/,
2940
+ repeat: 2 /*RepeatType.UNPACKED*/,
2937
2941
  T: () => TrackSubscriptionDetails,
2938
2942
  },
2939
2943
  {
@@ -2976,14 +2980,14 @@ class Migration$Type extends MessageType {
2976
2980
  no: 2,
2977
2981
  name: 'announced_tracks',
2978
2982
  kind: 'message',
2979
- repeat: 1 /*RepeatType.PACKED*/,
2983
+ repeat: 2 /*RepeatType.UNPACKED*/,
2980
2984
  T: () => TrackInfo,
2981
2985
  },
2982
2986
  {
2983
2987
  no: 3,
2984
2988
  name: 'subscriptions',
2985
2989
  kind: 'message',
2986
- repeat: 1 /*RepeatType.PACKED*/,
2990
+ repeat: 2 /*RepeatType.UNPACKED*/,
2987
2991
  T: () => TrackSubscriptionDetails,
2988
2992
  },
2989
2993
  ]);
@@ -3009,7 +3013,7 @@ class JoinResponse$Type extends MessageType {
3009
3013
  no: 4,
3010
3014
  name: 'publish_options',
3011
3015
  kind: 'message',
3012
- repeat: 1 /*RepeatType.PACKED*/,
3016
+ repeat: 2 /*RepeatType.UNPACKED*/,
3013
3017
  T: () => PublishOption,
3014
3018
  },
3015
3019
  ]);
@@ -3091,7 +3095,7 @@ class ConnectionQualityChanged$Type extends MessageType {
3091
3095
  no: 1,
3092
3096
  name: 'connection_quality_updates',
3093
3097
  kind: 'message',
3094
- repeat: 1 /*RepeatType.PACKED*/,
3098
+ repeat: 2 /*RepeatType.UNPACKED*/,
3095
3099
  T: () => ConnectionQualityInfo,
3096
3100
  },
3097
3101
  ]);
@@ -3160,7 +3164,7 @@ class AudioLevelChanged$Type extends MessageType {
3160
3164
  no: 1,
3161
3165
  name: 'audio_levels',
3162
3166
  kind: 'message',
3163
- repeat: 1 /*RepeatType.PACKED*/,
3167
+ repeat: 2 /*RepeatType.UNPACKED*/,
3164
3168
  T: () => AudioLevel,
3165
3169
  },
3166
3170
  ]);
@@ -3240,7 +3244,7 @@ class VideoSender$Type extends MessageType {
3240
3244
  no: 3,
3241
3245
  name: 'layers',
3242
3246
  kind: 'message',
3243
- repeat: 1 /*RepeatType.PACKED*/,
3247
+ repeat: 2 /*RepeatType.UNPACKED*/,
3244
3248
  T: () => VideoLayerSetting,
3245
3249
  },
3246
3250
  {
@@ -3274,14 +3278,14 @@ class ChangePublishQuality$Type extends MessageType {
3274
3278
  no: 1,
3275
3279
  name: 'audio_senders',
3276
3280
  kind: 'message',
3277
- repeat: 1 /*RepeatType.PACKED*/,
3281
+ repeat: 2 /*RepeatType.UNPACKED*/,
3278
3282
  T: () => AudioSender,
3279
3283
  },
3280
3284
  {
3281
3285
  no: 2,
3282
3286
  name: 'video_senders',
3283
3287
  kind: 'message',
3284
- repeat: 1 /*RepeatType.PACKED*/,
3288
+ repeat: 2 /*RepeatType.UNPACKED*/,
3285
3289
  T: () => VideoSender,
3286
3290
  },
3287
3291
  ]);
@@ -5473,6 +5477,21 @@ class CallState {
5473
5477
  }
5474
5478
  }
5475
5479
 
5480
+ /**
5481
+ * NegotiationError is thrown when there is an error during the negotiation process.
5482
+ * It extends the built-in Error class and includes an SfuError object for more details.
5483
+ */
5484
+ class NegotiationError extends Error {
5485
+ /**
5486
+ * Creates an instance of NegotiationError.
5487
+ */
5488
+ constructor(error) {
5489
+ super(error.message);
5490
+ this.name = 'NegotiationError';
5491
+ this.error = error;
5492
+ }
5493
+ }
5494
+
5476
5495
  /**
5477
5496
  * Flatten the stats report into an array of stats objects.
5478
5497
  *
@@ -5791,7 +5810,7 @@ const aggregate = (stats) => {
5791
5810
  return report;
5792
5811
  };
5793
5812
 
5794
- const version = "1.24.0";
5813
+ const version = "1.25.0";
5795
5814
  const [major, minor, patch] = version.split('.');
5796
5815
  let sdkInfo = {
5797
5816
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6400,12 +6419,38 @@ class BasePeerConnection {
6400
6419
  /**
6401
6420
  * Constructs a new `BasePeerConnection` instance.
6402
6421
  */
6403
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, enableTracing, }) {
6422
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, logTag, enableTracing, iceRestartDelay = 2500, }) {
6404
6423
  this.isIceRestarting = false;
6405
6424
  this.isDisposed = false;
6406
6425
  this.trackIdToTrackType = new Map();
6407
6426
  this.subscriptions = [];
6408
6427
  this.lock = Math.random().toString(36).slice(2);
6428
+ this.createPeerConnection = (connectionConfig) => {
6429
+ const pc = new RTCPeerConnection(connectionConfig);
6430
+ pc.addEventListener('icecandidate', this.onIceCandidate);
6431
+ pc.addEventListener('icecandidateerror', this.onIceCandidateError);
6432
+ pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6433
+ pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
6434
+ pc.addEventListener('signalingstatechange', this.onSignalingChange);
6435
+ pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6436
+ return pc;
6437
+ };
6438
+ /**
6439
+ * Attempts to restart ICE on the `RTCPeerConnection`.
6440
+ * This method intentionally doesn't await the `restartIce()` method,
6441
+ * allowing it to run in the background and handle any errors that may occur.
6442
+ */
6443
+ this.tryRestartIce = () => {
6444
+ this.restartIce().catch((e) => {
6445
+ const reason = 'restartICE() failed, initiating reconnect';
6446
+ this.logger('error', reason, e);
6447
+ const strategy = e instanceof NegotiationError &&
6448
+ e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
6449
+ ? WebsocketReconnectStrategy.FAST
6450
+ : WebsocketReconnectStrategy.REJOIN;
6451
+ this.onReconnectionNeeded?.(strategy, reason);
6452
+ });
6453
+ };
6409
6454
  /**
6410
6455
  * Handles events synchronously.
6411
6456
  * Consecutive events are queued and executed one after the other.
@@ -6458,6 +6503,18 @@ class BasePeerConnection {
6458
6503
  this.getTrackType = (trackId) => {
6459
6504
  return this.trackIdToTrackType.get(trackId);
6460
6505
  };
6506
+ /**
6507
+ * Checks if the `RTCPeerConnection` is healthy.
6508
+ * It checks the ICE connection state and the peer connection state.
6509
+ * If either state is `failed`, `disconnected`, or `closed`,
6510
+ * it returns `false`, otherwise it returns `true`.
6511
+ */
6512
+ this.isHealthy = () => {
6513
+ const failedStates = new Set(['failed', 'closed']);
6514
+ const iceState = this.pc.iceConnectionState;
6515
+ const connectionState = this.pc.connectionState;
6516
+ return !failedStates.has(iceState) && !failedStates.has(connectionState);
6517
+ };
6461
6518
  /**
6462
6519
  * Handles the ICECandidate event and
6463
6520
  * Initiates an ICE Trickle process with the SFU.
@@ -6496,9 +6553,7 @@ class BasePeerConnection {
6496
6553
  this.onConnectionStateChange = async () => {
6497
6554
  const state = this.pc.connectionState;
6498
6555
  this.logger('debug', `Connection state changed`, state);
6499
- if (!this.tracer)
6500
- return;
6501
- if (state === 'connected' || state === 'failed') {
6556
+ if (this.tracer && (state === 'connected' || state === 'failed')) {
6502
6557
  try {
6503
6558
  const stats = await this.stats.get();
6504
6559
  this.tracer.trace('getstats', stats.delta);
@@ -6507,6 +6562,12 @@ class BasePeerConnection {
6507
6562
  this.tracer.trace('getstatsOnFailure', err.toString());
6508
6563
  }
6509
6564
  }
6565
+ // we can't recover from a failed connection state (contrary to ICE)
6566
+ if (state === 'failed') {
6567
+ this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, 'Connection failed');
6568
+ return;
6569
+ }
6570
+ this.handleConnectionStateUpdate(state);
6510
6571
  };
6511
6572
  /**
6512
6573
  * Handles the ICE connection state change event.
@@ -6514,34 +6575,53 @@ class BasePeerConnection {
6514
6575
  this.onIceConnectionStateChange = () => {
6515
6576
  const state = this.pc.iceConnectionState;
6516
6577
  this.logger('debug', `ICE connection state changed`, state);
6517
- if (this.state.callingState === CallingState.OFFLINE)
6578
+ this.handleConnectionStateUpdate(state);
6579
+ };
6580
+ this.handleConnectionStateUpdate = (state) => {
6581
+ const { callingState } = this.state;
6582
+ if (callingState === CallingState.OFFLINE)
6518
6583
  return;
6519
- if (this.state.callingState === CallingState.RECONNECTING)
6584
+ if (callingState === CallingState.RECONNECTING)
6520
6585
  return;
6521
6586
  // do nothing when ICE is restarting
6522
6587
  if (this.isIceRestarting)
6523
6588
  return;
6524
- if (state === 'failed') {
6525
- this.onUnrecoverableError?.('ICE connection failed');
6526
- }
6527
- else if (state === 'disconnected') {
6528
- this.logger('debug', `Attempting to restart ICE`);
6529
- this.restartIce().catch((e) => {
6530
- const reason = `ICE restart failed`;
6531
- this.logger('error', reason, e);
6532
- this.onUnrecoverableError?.(`${reason}: ${e}`);
6533
- });
6589
+ switch (state) {
6590
+ case 'failed':
6591
+ // in the `failed` state, we try to restart ICE immediately
6592
+ this.logger('info', 'restartICE due to failed connection');
6593
+ this.tryRestartIce();
6594
+ break;
6595
+ case 'disconnected':
6596
+ // in the `disconnected` state, we schedule a restartICE() after a delay
6597
+ // as the browser might recover the connection in the meantime
6598
+ this.logger('info', 'disconnected connection, scheduling restartICE');
6599
+ clearTimeout(this.iceRestartTimeout);
6600
+ this.iceRestartTimeout = setTimeout(() => {
6601
+ const currentState = this.pc.iceConnectionState;
6602
+ if (currentState === 'disconnected' || currentState === 'failed') {
6603
+ this.tryRestartIce();
6604
+ }
6605
+ }, this.iceRestartDelay);
6606
+ break;
6607
+ case 'connected':
6608
+ // in the `connected` state, we clear the ice restart timeout if it exists
6609
+ if (this.iceRestartTimeout) {
6610
+ this.logger('info', 'connected connection, canceling restartICE');
6611
+ clearTimeout(this.iceRestartTimeout);
6612
+ this.iceRestartTimeout = undefined;
6613
+ }
6614
+ break;
6534
6615
  }
6535
6616
  };
6536
6617
  /**
6537
6618
  * Handles the ICE candidate error event.
6538
6619
  */
6539
6620
  this.onIceCandidateError = (e) => {
6540
- const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
6541
- `${e.errorCode}: ${e.errorText}`;
6542
- const iceState = this.pc.iceConnectionState;
6543
- const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
6544
- this.logger(logLevel, `ICE Candidate error`, errorMessage);
6621
+ const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent
6622
+ ? `${e.errorCode}: ${e.errorText}`
6623
+ : e;
6624
+ this.logger('debug', 'ICE Candidate error', errorMessage);
6545
6625
  };
6546
6626
  /**
6547
6627
  * Handles the ICE gathering state change event.
@@ -6559,18 +6639,13 @@ class BasePeerConnection {
6559
6639
  this.sfuClient = sfuClient;
6560
6640
  this.state = state;
6561
6641
  this.dispatcher = dispatcher;
6562
- this.onUnrecoverableError = onUnrecoverableError;
6642
+ this.iceRestartDelay = iceRestartDelay;
6643
+ this.onReconnectionNeeded = onReconnectionNeeded;
6563
6644
  this.logger = getLogger([
6564
6645
  peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
6565
6646
  logTag,
6566
6647
  ]);
6567
- this.pc = new RTCPeerConnection(connectionConfig);
6568
- this.pc.addEventListener('icecandidate', this.onIceCandidate);
6569
- this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
6570
- this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6571
- this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
6572
- this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
6573
- this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6648
+ this.pc = this.createPeerConnection(connectionConfig);
6574
6649
  this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
6575
6650
  if (enableTracing) {
6576
6651
  const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
@@ -6586,7 +6661,9 @@ class BasePeerConnection {
6586
6661
  * Disposes the `RTCPeerConnection` instance.
6587
6662
  */
6588
6663
  dispose() {
6589
- this.onUnrecoverableError = undefined;
6664
+ clearTimeout(this.iceRestartTimeout);
6665
+ this.iceRestartTimeout = undefined;
6666
+ this.onReconnectionNeeded = undefined;
6590
6667
  this.isDisposed = true;
6591
6668
  this.detachEventHandlers();
6592
6669
  this.pc.close();
@@ -6596,11 +6673,12 @@ class BasePeerConnection {
6596
6673
  * Detaches the event handlers from the `RTCPeerConnection`.
6597
6674
  */
6598
6675
  detachEventHandlers() {
6599
- this.pc.removeEventListener('icecandidate', this.onIceCandidate);
6600
- this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
6601
- this.pc.removeEventListener('signalingstatechange', this.onSignalingChange);
6602
- this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6603
- this.pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
6676
+ const pc = this.pc;
6677
+ pc.removeEventListener('icecandidate', this.onIceCandidate);
6678
+ pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
6679
+ pc.removeEventListener('signalingstatechange', this.onSignalingChange);
6680
+ pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6681
+ pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
6604
6682
  this.unsubscribeIceTrickle?.();
6605
6683
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
6606
6684
  }
@@ -6910,6 +6988,45 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
6910
6988
  return '';
6911
6989
  return String(transceiverInitIndex);
6912
6990
  };
6991
+ /**
6992
+ * Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
6993
+ *
6994
+ * @param offerSdp the offer SDP containing the stereo configuration.
6995
+ * @param answerSdp the answer SDP to be modified.
6996
+ */
6997
+ const enableStereo = (offerSdp, answerSdp) => {
6998
+ const offeredStereoMids = new Set();
6999
+ const parsedOfferSdp = parse(offerSdp);
7000
+ for (const media of parsedOfferSdp.media) {
7001
+ if (media.type !== 'audio')
7002
+ continue;
7003
+ const opus = media.rtp.find((r) => r.codec === 'opus');
7004
+ if (!opus)
7005
+ continue;
7006
+ for (const fmtp of media.fmtp) {
7007
+ if (fmtp.payload === opus.payload && fmtp.config.includes('stereo=1')) {
7008
+ offeredStereoMids.add(media.mid);
7009
+ }
7010
+ }
7011
+ }
7012
+ // No stereo offered, return the original answerSdp
7013
+ if (offeredStereoMids.size === 0)
7014
+ return answerSdp;
7015
+ const parsedAnswerSdp = parse(answerSdp);
7016
+ for (const media of parsedAnswerSdp.media) {
7017
+ if (media.type !== 'audio' || !offeredStereoMids.has(media.mid))
7018
+ continue;
7019
+ const opus = media.rtp.find((r) => r.codec === 'opus');
7020
+ if (!opus)
7021
+ continue;
7022
+ for (const fmtp of media.fmtp) {
7023
+ if (fmtp.payload === opus.payload && !fmtp.config.includes('stereo=1')) {
7024
+ fmtp.config += ';stereo=1';
7025
+ }
7026
+ }
7027
+ }
7028
+ return write(parsedAnswerSdp);
7029
+ };
6913
7030
 
6914
7031
  /**
6915
7032
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -7023,11 +7140,11 @@ class Publisher extends BasePeerConnection {
7023
7140
  /**
7024
7141
  * Returns true if the given track type is currently being published to the SFU.
7025
7142
  *
7026
- * @param trackType the track type to check.
7143
+ * @param trackType the track type to check. If omitted, checks if any track is being published.
7027
7144
  */
7028
7145
  this.isPublishing = (trackType) => {
7029
7146
  for (const item of this.transceiverCache.items()) {
7030
- if (item.publishOption.trackType !== trackType)
7147
+ if (trackType && item.publishOption.trackType !== trackType)
7031
7148
  continue;
7032
7149
  const track = item.transceiver.sender.track;
7033
7150
  if (!track)
@@ -7150,10 +7267,17 @@ class Publisher extends BasePeerConnection {
7150
7267
  const { sdp = '' } = offer;
7151
7268
  const { response } = await this.sfuClient.setPublisher({ sdp, tracks });
7152
7269
  if (response.error)
7153
- throw new Error(response.error.message);
7270
+ throw new NegotiationError(response.error);
7154
7271
  const { sdp: answerSdp } = response;
7155
7272
  await this.pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
7156
7273
  }
7274
+ catch (err) {
7275
+ // negotiation failed, rollback to the previous state
7276
+ if (this.pc.signalingState === 'have-local-offer') {
7277
+ await this.pc.setLocalDescription({ type: 'rollback' });
7278
+ }
7279
+ throw err;
7280
+ }
7157
7281
  finally {
7158
7282
  this.isIceRestarting = false;
7159
7283
  }
@@ -7245,11 +7369,7 @@ class Publisher extends BasePeerConnection {
7245
7369
  this.on('iceRestart', (iceRestart) => {
7246
7370
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
7247
7371
  return;
7248
- this.restartIce().catch((err) => {
7249
- const reason = `ICE restart failed`;
7250
- this.logger('warn', reason, err);
7251
- this.onUnrecoverableError?.(`${reason}: ${err}`);
7252
- });
7372
+ this.tryRestartIce();
7253
7373
  });
7254
7374
  this.on('changePublishQuality', async (event) => {
7255
7375
  for (const videoSender of event.videoSenders) {
@@ -7297,11 +7417,13 @@ class Subscriber extends BasePeerConnection {
7297
7417
  return;
7298
7418
  }
7299
7419
  const previousIsIceRestarting = this.isIceRestarting;
7420
+ this.isIceRestarting = true;
7300
7421
  try {
7301
- this.isIceRestarting = true;
7302
- await this.sfuClient.iceRestart({
7422
+ const { response } = await this.sfuClient.iceRestart({
7303
7423
  peerType: PeerType.SUBSCRIBER,
7304
7424
  });
7425
+ if (response.error)
7426
+ throw new NegotiationError(response.error);
7305
7427
  }
7306
7428
  catch (e) {
7307
7429
  // restore the previous state, as our intent for restarting ICE failed
@@ -7370,6 +7492,9 @@ class Subscriber extends BasePeerConnection {
7370
7492
  });
7371
7493
  this.addTrickledIceCandidates();
7372
7494
  const answer = await this.pc.createAnswer();
7495
+ if (answer.sdp) {
7496
+ answer.sdp = enableStereo(subscriberOffer.sdp, answer.sdp);
7497
+ }
7373
7498
  await this.pc.setLocalDescription(answer);
7374
7499
  await this.sfuClient.sendAnswer({
7375
7500
  peerType: PeerType.SUBSCRIBER,
@@ -7659,11 +7784,15 @@ class StreamSfuClient {
7659
7784
  */
7660
7785
  this.isLeaving = false;
7661
7786
  /**
7662
- * Flag to indicate if the client is in the process of closing the connection.
7787
+ * Flag to indicate if the client is in the process of clean closing the connection.
7788
+ * When set to `true`, the client will not attempt to reconnect
7789
+ * and will close the WebSocket connection gracefully.
7790
+ * Otherwise, it will close the connection with an error code and
7791
+ * trigger a reconnection attempt.
7663
7792
  */
7664
- this.isClosing = false;
7665
- this.pingIntervalInMs = 10 * 1000;
7666
- this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
7793
+ this.isClosingClean = false;
7794
+ this.pingIntervalInMs = 5 * 1000;
7795
+ this.unhealthyTimeoutInMs = 15 * 1000;
7667
7796
  /**
7668
7797
  * Promise that resolves when the JoinResponse is received.
7669
7798
  * Rejects after a certain threshold if the response is not received.
@@ -7707,7 +7836,7 @@ class StreamSfuClient {
7707
7836
  // Normally, this shouldn't have any effect, because WS should never emit 'close'
7708
7837
  // before emitting 'open'. However, strager things have happened, and we don't
7709
7838
  // want to leave signalReady in pending state.
7710
- reject(new Error('SFU WS closed unexpectedly'));
7839
+ reject(new Error(`SFU WS closed or connection can't be established`));
7711
7840
  });
7712
7841
  }),
7713
7842
  new Promise((resolve, reject) => {
@@ -7725,7 +7854,7 @@ class StreamSfuClient {
7725
7854
  this.onSignalClose?.(`${e.code} ${e.reason}`);
7726
7855
  };
7727
7856
  this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
7728
- this.isClosing = true;
7857
+ this.isClosingClean = code !== StreamSfuClient.ERROR_CONNECTION_UNHEALTHY;
7729
7858
  if (this.signalWs.readyState === WebSocket.OPEN) {
7730
7859
  this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
7731
7860
  this.signalWs.close(code, `js-client: ${reason}`);
@@ -7965,7 +8094,11 @@ StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
7965
8094
  * Here, we don't use 1000 (normal closure) because we don't want the
7966
8095
  * SFU to clean up the resources associated with the current participant.
7967
8096
  */
7968
- StreamSfuClient.DISPOSE_OLD_SOCKET = 4002;
8097
+ StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
8098
+ /**
8099
+ * The close code used when the client fails to join the call (on the SFU).
8100
+ */
8101
+ StreamSfuClient.JOIN_FAILED = 4101;
7969
8102
 
7970
8103
  /**
7971
8104
  * Event handler that watched the delivery of `call.accepted`.
@@ -9479,21 +9612,30 @@ let getDisplayMediaExecId = 0;
9479
9612
  const getScreenShareStream = async (options, tracer) => {
9480
9613
  const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
9481
9614
  try {
9482
- tracer?.trace(tag, options);
9483
- const stream = await navigator.mediaDevices.getDisplayMedia({
9484
- video: true,
9485
- audio: {
9486
- channelCount: {
9487
- ideal: 2,
9488
- },
9489
- echoCancellation: false,
9490
- autoGainControl: false,
9491
- noiseSuppression: false,
9492
- },
9615
+ const constraints = {
9493
9616
  // @ts-expect-error - not present in types yet
9494
9617
  systemAudio: 'include',
9495
9618
  ...options,
9496
- });
9619
+ video: typeof options?.video === 'boolean'
9620
+ ? options.video // must be 'true'
9621
+ : {
9622
+ width: { max: 2560 },
9623
+ height: { max: 1440 },
9624
+ frameRate: { ideal: 30 },
9625
+ ...options?.video,
9626
+ },
9627
+ audio: typeof options?.audio === 'boolean'
9628
+ ? options.audio
9629
+ : {
9630
+ channelCount: { ideal: 2 },
9631
+ echoCancellation: false,
9632
+ autoGainControl: false,
9633
+ noiseSuppression: false,
9634
+ ...options?.audio,
9635
+ },
9636
+ };
9637
+ tracer?.trace(tag, constraints);
9638
+ const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
9497
9639
  tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
9498
9640
  return stream;
9499
9641
  }
@@ -10841,11 +10983,18 @@ class ScreenShareManager extends InputMediaDeviceManager {
10841
10983
  getDevices() {
10842
10984
  return of([]); // there are no devices to be listed for Screen Share
10843
10985
  }
10844
- getStream(constraints) {
10986
+ async getStream(constraints) {
10845
10987
  if (!this.state.audioEnabled) {
10846
10988
  constraints.audio = false;
10847
10989
  }
10848
- return getScreenShareStream(constraints, this.call.tracer);
10990
+ const stream = await getScreenShareStream(constraints, this.call.tracer);
10991
+ const [track] = stream.getVideoTracks();
10992
+ const { contentHint } = this.state.settings || {};
10993
+ if (typeof contentHint !== 'undefined' && track && 'contentHint' in track) {
10994
+ this.call.tracer.trace('navigator.mediaDevices.getDisplayMedia.contentHint', contentHint);
10995
+ track.contentHint = contentHint;
10996
+ }
10997
+ return stream;
10849
10998
  }
10850
10999
  async stopPublishStream() {
10851
11000
  return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
@@ -11240,10 +11389,13 @@ class Call {
11240
11389
  * Leave the call and stop the media streams that were published by the call.
11241
11390
  */
11242
11391
  this.leave = async ({ reject, reason, message } = {}) => {
11392
+ if (this.state.callingState === CallingState.LEFT) {
11393
+ throw new Error('Cannot leave call that has already been left.');
11394
+ }
11243
11395
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
11244
11396
  const callingState = this.state.callingState;
11245
11397
  if (callingState === CallingState.LEFT) {
11246
- throw new Error('Cannot leave call that has already been left.');
11398
+ return;
11247
11399
  }
11248
11400
  if (callingState === CallingState.JOINING) {
11249
11401
  const waitUntilCallJoined = () => {
@@ -11350,7 +11502,6 @@ class Call {
11350
11502
  this.state.setMembers(response.members);
11351
11503
  this.state.setOwnCapabilities(response.own_capabilities);
11352
11504
  if (params?.ring) {
11353
- // the call response can indicate where the call is still ringing or not
11354
11505
  this.ringingSubject.next(true);
11355
11506
  }
11356
11507
  if (this.streamClient._hasConnectionID()) {
@@ -11372,7 +11523,6 @@ class Call {
11372
11523
  this.state.setMembers(response.members);
11373
11524
  this.state.setOwnCapabilities(response.own_capabilities);
11374
11525
  if (data?.ring) {
11375
- // the call response can indicate where the call is still ringing or not
11376
11526
  this.ringingSubject.next(true);
11377
11527
  }
11378
11528
  if (this.streamClient._hasConnectionID()) {
@@ -11552,7 +11702,7 @@ class Call {
11552
11702
  }
11553
11703
  catch (error) {
11554
11704
  this.logger('warn', 'Join SFU request failed', error);
11555
- sfuClient.close(StreamSfuClient.ERROR_CONNECTION_UNHEALTHY, 'Join request failed, connection considered unhealthy');
11705
+ sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
11556
11706
  // restore the previous call state if the join-flow fails
11557
11707
  this.state.setCallingState(callingState);
11558
11708
  throw error;
@@ -11695,7 +11845,7 @@ class Call {
11695
11845
  }
11696
11846
  if (this.publisher) {
11697
11847
  this.publisher.setSfuClient(nextSfuClient);
11698
- if (includePublisher) {
11848
+ if (includePublisher && this.publisher.isPublishing()) {
11699
11849
  await this.publisher.restartIce();
11700
11850
  }
11701
11851
  }
@@ -11717,9 +11867,10 @@ class Call {
11717
11867
  connectionConfig,
11718
11868
  logTag: String(this.sfuClientTag),
11719
11869
  enableTracing,
11720
- onUnrecoverableError: (reason) => {
11721
- this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
11722
- this.logger('warn', `[Reconnect] Error reconnecting after a subscriber error: ${reason}`, err);
11870
+ onReconnectionNeeded: (kind, reason) => {
11871
+ this.reconnect(kind, reason).catch((err) => {
11872
+ const message = `[Reconnect] Error reconnecting after a subscriber error: ${reason}`;
11873
+ this.logger('warn', message, err);
11723
11874
  });
11724
11875
  },
11725
11876
  });
@@ -11738,9 +11889,10 @@ class Call {
11738
11889
  publishOptions,
11739
11890
  logTag: String(this.sfuClientTag),
11740
11891
  enableTracing,
11741
- onUnrecoverableError: (reason) => {
11742
- this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
11743
- this.logger('warn', `[Reconnect] Error reconnecting after a publisher error: ${reason}`, err);
11892
+ onReconnectionNeeded: (kind, reason) => {
11893
+ this.reconnect(kind, reason).catch((err) => {
11894
+ const message = `[Reconnect] Error reconnecting after a publisher error: ${reason}`;
11895
+ this.logger('warn', message, err);
11744
11896
  });
11745
11897
  },
11746
11898
  });
@@ -11824,9 +11976,12 @@ class Call {
11824
11976
  callingState === CallingState.LEFT)
11825
11977
  return;
11826
11978
  // normal close, no need to reconnect
11827
- if (sfuClient.isLeaving || sfuClient.isClosing)
11979
+ if (sfuClient.isLeaving || sfuClient.isClosingClean)
11828
11980
  return;
11829
- this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
11981
+ const strategy = this.publisher?.isHealthy() && this.subscriber?.isHealthy()
11982
+ ? WebsocketReconnectStrategy.FAST
11983
+ : WebsocketReconnectStrategy.REJOIN;
11984
+ this.reconnect(strategy, reason).catch((err) => {
11830
11985
  this.logger('warn', '[Reconnect] Error reconnecting', err);
11831
11986
  });
11832
11987
  };
@@ -11847,10 +12002,12 @@ class Call {
11847
12002
  const reconnectStartTime = Date.now();
11848
12003
  this.reconnectStrategy = strategy;
11849
12004
  this.reconnectReason = reason;
12005
+ let attempt = 0;
11850
12006
  do {
11851
- if (this.disconnectionTimeoutSeconds > 0 &&
11852
- (Date.now() - reconnectStartTime) / 1000 >
11853
- this.disconnectionTimeoutSeconds) {
12007
+ const reconnectingTime = Date.now() - reconnectStartTime;
12008
+ const shouldGiveUpReconnecting = this.disconnectionTimeoutSeconds > 0 &&
12009
+ reconnectingTime / 1000 > this.disconnectionTimeoutSeconds;
12010
+ if (shouldGiveUpReconnecting) {
11854
12011
  this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
11855
12012
  this.state.setCallingState(CallingState.RECONNECTING_FAILED);
11856
12013
  return;
@@ -11859,7 +12016,7 @@ class Call {
11859
12016
  if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
11860
12017
  this.reconnectAttempts++;
11861
12018
  }
11862
- const current = WebsocketReconnectStrategy[this.reconnectStrategy];
12019
+ const currentStrategy = WebsocketReconnectStrategy[this.reconnectStrategy];
11863
12020
  try {
11864
12021
  // wait until the network is available
11865
12022
  await this.networkAvailableTask?.promise;
@@ -11867,7 +12024,7 @@ class Call {
11867
12024
  switch (this.reconnectStrategy) {
11868
12025
  case WebsocketReconnectStrategy.UNSPECIFIED:
11869
12026
  case WebsocketReconnectStrategy.DISCONNECT:
11870
- this.logger('debug', `[Reconnect] No-op strategy ${current}`);
12027
+ this.logger('debug', `[Reconnect] No-op strategy ${currentStrategy}`);
11871
12028
  break;
11872
12029
  case WebsocketReconnectStrategy.FAST:
11873
12030
  await this.reconnectFast();
@@ -11886,7 +12043,7 @@ class Call {
11886
12043
  }
11887
12044
  catch (error) {
11888
12045
  if (this.state.callingState === CallingState.OFFLINE) {
11889
- this.logger('trace', `[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
12046
+ this.logger('debug', `[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
11890
12047
  break;
11891
12048
  // we don't need to handle the error if the call is offline
11892
12049
  // network change event will trigger the reconnection
@@ -11896,9 +12053,24 @@ class Call {
11896
12053
  this.state.setCallingState(CallingState.RECONNECTING_FAILED);
11897
12054
  return;
11898
12055
  }
11899
- this.logger('warn', `[Reconnect] ${current} (${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
11900
12056
  await sleep(500);
11901
- this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
12057
+ const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
12058
+ const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
12059
+ this.fastReconnectDeadlineSeconds;
12060
+ // don't immediately switch to the REJOIN strategy, but instead attempt
12061
+ // to reconnect with the FAST strategy for a few times before switching.
12062
+ // in some cases, we immediately switch to the REJOIN strategy.
12063
+ const shouldRejoin = mustPerformRejoin || // if we are past the fast reconnect deadline
12064
+ wasMigrating || // if we were migrating, but the migration failed
12065
+ attempt >= 3 || // after 3 failed attempts
12066
+ !(this.publisher?.isHealthy() ?? true) || // if the publisher is not healthy
12067
+ !(this.subscriber?.isHealthy() ?? true); // if the subscriber is not healthy
12068
+ attempt++;
12069
+ const nextStrategy = shouldRejoin
12070
+ ? WebsocketReconnectStrategy.REJOIN
12071
+ : WebsocketReconnectStrategy.FAST;
12072
+ this.reconnectStrategy = nextStrategy;
12073
+ this.logger('info', `[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
11902
12074
  }
11903
12075
  } while (this.state.callingState !== CallingState.JOINED &&
11904
12076
  this.state.callingState !== CallingState.RECONNECTING_FAILED &&
@@ -12913,6 +13085,38 @@ class Call {
12913
13085
 
12914
13086
  var https = null;
12915
13087
 
13088
+ const APIErrorCodes = {
13089
+ [-1]: 'InternalSystemError',
13090
+ 2: 'AccessKeyError',
13091
+ 3: 'AuthenticationFailedError',
13092
+ 4: 'InputError',
13093
+ 5: 'AuthenticationError',
13094
+ 6: 'DuplicateUsernameError',
13095
+ 9: 'RateLimitError',
13096
+ 16: 'DoesNotExistError',
13097
+ 17: 'NotAllowedError',
13098
+ 18: 'EventNotSupportedError',
13099
+ 19: 'ChannelFeatureNotSupportedError',
13100
+ 20: 'MessageTooLongError',
13101
+ 21: 'MultipleNestingLevelError',
13102
+ 22: 'PayloadTooBigError',
13103
+ 23: 'RequestTimeoutError',
13104
+ 24: 'MaxHeaderSizeExceededError',
13105
+ 40: 'AuthErrorTokenExpired',
13106
+ 41: 'AuthErrorTokenNotValidYet',
13107
+ 42: 'AuthErrorTokenUsedBeforeIssuedAt',
13108
+ 43: 'AuthErrorTokenSignatureInvalid',
13109
+ 44: 'CustomCommandEndpointMissingError',
13110
+ 45: 'CustomCommandEndpointCallError',
13111
+ 46: 'ConnectionIDNotFoundError',
13112
+ 60: 'CoolDownError',
13113
+ 69: 'ErrWrongRegion',
13114
+ 70: 'ErrQueryChannelPermissions',
13115
+ 71: 'ErrTooManyConnections',
13116
+ 73: 'MessageModerationFailedError',
13117
+ 99: 'AppSuspendedError',
13118
+ };
13119
+
12916
13120
  /**
12917
13121
  * StableWSConnection - A WS connection that reconnects upon failure.
12918
13122
  * - the browser will sometimes report that you're online or offline
@@ -13139,7 +13343,7 @@ class StableWSConnection {
13139
13343
  message = error.message;
13140
13344
  statusCode = error.StatusCode;
13141
13345
  }
13142
- const msg = `WS failed with code: ${code} and reason: ${message}`;
13346
+ const msg = `WS failed with code: ${code}: ${APIErrorCodes[code] || code} and reason: ${message}`;
13143
13347
  this._log(msg, { event }, 'warn');
13144
13348
  const error = new Error(msg);
13145
13349
  error.code = code;
@@ -14017,7 +14221,7 @@ class StreamClient {
14017
14221
  this.getUserAgent = () => {
14018
14222
  if (!this.cachedUserAgent) {
14019
14223
  const { clientAppIdentifier = {} } = this.options;
14020
- const { sdkName = 'js', sdkVersion = "1.24.0", ...extras } = clientAppIdentifier;
14224
+ const { sdkName = 'js', sdkVersion = "1.25.0", ...extras } = clientAppIdentifier;
14021
14225
  this.cachedUserAgent = [
14022
14226
  `stream-video-${sdkName}-v${sdkVersion}`,
14023
14227
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -14367,13 +14571,17 @@ class StreamVideoClient {
14367
14571
  * @param type the type of the call.
14368
14572
  * @param id the id of the call.
14369
14573
  */
14370
- this.call = (type, id) => {
14371
- return new Call({
14372
- streamClient: this.streamClient,
14373
- id: id,
14374
- type: type,
14375
- clientStore: this.writeableStateStore,
14376
- });
14574
+ this.call = (type, id, options = {}) => {
14575
+ const call = options.reuseInstance
14576
+ ? this.writeableStateStore.findCall(type, id)
14577
+ : undefined;
14578
+ return (call ??
14579
+ new Call({
14580
+ streamClient: this.streamClient,
14581
+ id: id,
14582
+ type: type,
14583
+ clientStore: this.writeableStateStore,
14584
+ }));
14377
14585
  };
14378
14586
  /**
14379
14587
  * Creates a new guest user with the given data.