@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
package/dist/index.cjs.js CHANGED
@@ -422,7 +422,7 @@ class ErrorFromResponse extends Error {
422
422
  }
423
423
  }
424
424
 
425
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
425
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
426
426
  // @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
427
427
  // tslint:disable
428
428
  //
@@ -655,7 +655,7 @@ class ListValue$Type extends runtime.MessageType {
655
655
  no: 1,
656
656
  name: 'values',
657
657
  kind: 'message',
658
- repeat: 1 /*RepeatType.PACKED*/,
658
+ repeat: 2 /*RepeatType.UNPACKED*/,
659
659
  T: () => Value,
660
660
  },
661
661
  ]);
@@ -687,7 +687,7 @@ class ListValue$Type extends runtime.MessageType {
687
687
  */
688
688
  const ListValue = new ListValue$Type();
689
689
 
690
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
690
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
691
691
  // @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
692
692
  // tslint:disable
693
693
  //
@@ -823,7 +823,7 @@ class Timestamp$Type extends runtime.MessageType {
823
823
  */
824
824
  const Timestamp = new Timestamp$Type();
825
825
 
826
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
826
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
827
827
  // @generated from protobuf file "video/sfu/models/models.proto" (package "stream.video.sfu.models", syntax proto3)
828
828
  // tslint:disable
829
829
  /**
@@ -965,6 +965,10 @@ var ErrorCode;
965
965
  * @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_MEDIA_TRANSPORT_FAILURE = 205;
966
966
  */
967
967
  ErrorCode[ErrorCode["PARTICIPANT_MEDIA_TRANSPORT_FAILURE"] = 205] = "PARTICIPANT_MEDIA_TRANSPORT_FAILURE";
968
+ /**
969
+ * @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_SIGNAL_LOST = 206;
970
+ */
971
+ ErrorCode[ErrorCode["PARTICIPANT_SIGNAL_LOST"] = 206] = "PARTICIPANT_SIGNAL_LOST";
968
972
  /**
969
973
  * @generated from protobuf enum value: ERROR_CODE_CALL_NOT_FOUND = 300;
970
974
  */
@@ -1245,7 +1249,7 @@ class CallState$Type extends runtime.MessageType {
1245
1249
  no: 1,
1246
1250
  name: 'participants',
1247
1251
  kind: 'message',
1248
- repeat: 1 /*RepeatType.PACKED*/,
1252
+ repeat: 2 /*RepeatType.UNPACKED*/,
1249
1253
  T: () => Participant,
1250
1254
  },
1251
1255
  { no: 2, name: 'started_at', kind: 'message', T: () => Timestamp },
@@ -1259,7 +1263,7 @@ class CallState$Type extends runtime.MessageType {
1259
1263
  no: 4,
1260
1264
  name: 'pins',
1261
1265
  kind: 'message',
1262
- repeat: 1 /*RepeatType.PACKED*/,
1266
+ repeat: 2 /*RepeatType.UNPACKED*/,
1263
1267
  T: () => Pin,
1264
1268
  },
1265
1269
  ]);
@@ -1437,7 +1441,7 @@ class SubscribeOption$Type extends runtime.MessageType {
1437
1441
  no: 2,
1438
1442
  name: 'codecs',
1439
1443
  kind: 'message',
1440
- repeat: 1 /*RepeatType.PACKED*/,
1444
+ repeat: 2 /*RepeatType.UNPACKED*/,
1441
1445
  T: () => Codec,
1442
1446
  },
1443
1447
  ]);
@@ -1570,7 +1574,7 @@ class TrackInfo$Type extends runtime.MessageType {
1570
1574
  no: 5,
1571
1575
  name: 'layers',
1572
1576
  kind: 'message',
1573
- repeat: 1 /*RepeatType.PACKED*/,
1577
+ repeat: 2 /*RepeatType.UNPACKED*/,
1574
1578
  T: () => VideoLayer,
1575
1579
  },
1576
1580
  { no: 6, name: 'mid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
@@ -1937,7 +1941,7 @@ var models = /*#__PURE__*/Object.freeze({
1937
1941
  get WebsocketReconnectStrategy () { return WebsocketReconnectStrategy; }
1938
1942
  });
1939
1943
 
1940
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
1944
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
1941
1945
  // @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
1942
1946
  // tslint:disable
1943
1947
  // @generated message type with reflection information, may provide speed optimized methods
@@ -2105,14 +2109,14 @@ class SendStatsRequest$Type extends runtime.MessageType {
2105
2109
  no: 16,
2106
2110
  name: 'encode_stats',
2107
2111
  kind: 'message',
2108
- repeat: 1 /*RepeatType.PACKED*/,
2112
+ repeat: 2 /*RepeatType.UNPACKED*/,
2109
2113
  T: () => PerformanceStats,
2110
2114
  },
2111
2115
  {
2112
2116
  no: 17,
2113
2117
  name: 'decode_stats',
2114
2118
  kind: 'message',
2115
- repeat: 1 /*RepeatType.PACKED*/,
2119
+ repeat: 2 /*RepeatType.UNPACKED*/,
2116
2120
  T: () => PerformanceStats,
2117
2121
  },
2118
2122
  {
@@ -2179,7 +2183,7 @@ class UpdateMuteStatesRequest$Type extends runtime.MessageType {
2179
2183
  no: 3,
2180
2184
  name: 'mute_states',
2181
2185
  kind: 'message',
2182
- repeat: 1 /*RepeatType.PACKED*/,
2186
+ repeat: 2 /*RepeatType.UNPACKED*/,
2183
2187
  T: () => TrackMuteState,
2184
2188
  },
2185
2189
  ]);
@@ -2256,7 +2260,7 @@ class UpdateSubscriptionsRequest$Type extends runtime.MessageType {
2256
2260
  no: 3,
2257
2261
  name: 'tracks',
2258
2262
  kind: 'message',
2259
- repeat: 1 /*RepeatType.PACKED*/,
2263
+ repeat: 2 /*RepeatType.UNPACKED*/,
2260
2264
  T: () => TrackSubscriptionDetails,
2261
2265
  },
2262
2266
  ]);
@@ -2355,7 +2359,7 @@ class SetPublisherRequest$Type extends runtime.MessageType {
2355
2359
  no: 3,
2356
2360
  name: 'tracks',
2357
2361
  kind: 'message',
2358
- repeat: 1 /*RepeatType.PACKED*/,
2362
+ repeat: 2 /*RepeatType.UNPACKED*/,
2359
2363
  T: () => TrackInfo,
2360
2364
  },
2361
2365
  ]);
@@ -2435,7 +2439,7 @@ const SignalServer = new runtimeRpc.ServiceType('stream.video.sfu.signal.SignalS
2435
2439
  },
2436
2440
  ]);
2437
2441
 
2438
- // @generated by protobuf-ts 2.9.6 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2442
+ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2439
2443
  // @generated from protobuf file "video/sfu/event/events.proto" (package "stream.video.sfu.event", syntax proto3)
2440
2444
  // tslint:disable
2441
2445
  // @generated message type with reflection information, may provide speed optimized methods
@@ -2611,7 +2615,7 @@ class ChangePublishOptions$Type extends runtime.MessageType {
2611
2615
  no: 1,
2612
2616
  name: 'publish_options',
2613
2617
  kind: 'message',
2614
- repeat: 1 /*RepeatType.PACKED*/,
2618
+ repeat: 2 /*RepeatType.UNPACKED*/,
2615
2619
  T: () => PublishOption,
2616
2620
  },
2617
2621
  { no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
@@ -2650,7 +2654,7 @@ class PinsChanged$Type extends runtime.MessageType {
2650
2654
  no: 1,
2651
2655
  name: 'pins',
2652
2656
  kind: 'message',
2653
- repeat: 1 /*RepeatType.PACKED*/,
2657
+ repeat: 2 /*RepeatType.UNPACKED*/,
2654
2658
  T: () => Pin,
2655
2659
  },
2656
2660
  ]);
@@ -2893,14 +2897,14 @@ class JoinRequest$Type extends runtime.MessageType {
2893
2897
  no: 9,
2894
2898
  name: 'preferred_publish_options',
2895
2899
  kind: 'message',
2896
- repeat: 1 /*RepeatType.PACKED*/,
2900
+ repeat: 2 /*RepeatType.UNPACKED*/,
2897
2901
  T: () => PublishOption,
2898
2902
  },
2899
2903
  {
2900
2904
  no: 10,
2901
2905
  name: 'preferred_subscribe_options',
2902
2906
  kind: 'message',
2903
- repeat: 1 /*RepeatType.PACKED*/,
2907
+ repeat: 2 /*RepeatType.UNPACKED*/,
2904
2908
  T: () => SubscribeOption,
2905
2909
  },
2906
2910
  ]);
@@ -2928,14 +2932,14 @@ class ReconnectDetails$Type extends runtime.MessageType {
2928
2932
  no: 3,
2929
2933
  name: 'announced_tracks',
2930
2934
  kind: 'message',
2931
- repeat: 1 /*RepeatType.PACKED*/,
2935
+ repeat: 2 /*RepeatType.UNPACKED*/,
2932
2936
  T: () => TrackInfo,
2933
2937
  },
2934
2938
  {
2935
2939
  no: 4,
2936
2940
  name: 'subscriptions',
2937
2941
  kind: 'message',
2938
- repeat: 1 /*RepeatType.PACKED*/,
2942
+ repeat: 2 /*RepeatType.UNPACKED*/,
2939
2943
  T: () => TrackSubscriptionDetails,
2940
2944
  },
2941
2945
  {
@@ -2978,14 +2982,14 @@ class Migration$Type extends runtime.MessageType {
2978
2982
  no: 2,
2979
2983
  name: 'announced_tracks',
2980
2984
  kind: 'message',
2981
- repeat: 1 /*RepeatType.PACKED*/,
2985
+ repeat: 2 /*RepeatType.UNPACKED*/,
2982
2986
  T: () => TrackInfo,
2983
2987
  },
2984
2988
  {
2985
2989
  no: 3,
2986
2990
  name: 'subscriptions',
2987
2991
  kind: 'message',
2988
- repeat: 1 /*RepeatType.PACKED*/,
2992
+ repeat: 2 /*RepeatType.UNPACKED*/,
2989
2993
  T: () => TrackSubscriptionDetails,
2990
2994
  },
2991
2995
  ]);
@@ -3011,7 +3015,7 @@ class JoinResponse$Type extends runtime.MessageType {
3011
3015
  no: 4,
3012
3016
  name: 'publish_options',
3013
3017
  kind: 'message',
3014
- repeat: 1 /*RepeatType.PACKED*/,
3018
+ repeat: 2 /*RepeatType.UNPACKED*/,
3015
3019
  T: () => PublishOption,
3016
3020
  },
3017
3021
  ]);
@@ -3093,7 +3097,7 @@ class ConnectionQualityChanged$Type extends runtime.MessageType {
3093
3097
  no: 1,
3094
3098
  name: 'connection_quality_updates',
3095
3099
  kind: 'message',
3096
- repeat: 1 /*RepeatType.PACKED*/,
3100
+ repeat: 2 /*RepeatType.UNPACKED*/,
3097
3101
  T: () => ConnectionQualityInfo,
3098
3102
  },
3099
3103
  ]);
@@ -3162,7 +3166,7 @@ class AudioLevelChanged$Type extends runtime.MessageType {
3162
3166
  no: 1,
3163
3167
  name: 'audio_levels',
3164
3168
  kind: 'message',
3165
- repeat: 1 /*RepeatType.PACKED*/,
3169
+ repeat: 2 /*RepeatType.UNPACKED*/,
3166
3170
  T: () => AudioLevel,
3167
3171
  },
3168
3172
  ]);
@@ -3242,7 +3246,7 @@ class VideoSender$Type extends runtime.MessageType {
3242
3246
  no: 3,
3243
3247
  name: 'layers',
3244
3248
  kind: 'message',
3245
- repeat: 1 /*RepeatType.PACKED*/,
3249
+ repeat: 2 /*RepeatType.UNPACKED*/,
3246
3250
  T: () => VideoLayerSetting,
3247
3251
  },
3248
3252
  {
@@ -3276,14 +3280,14 @@ class ChangePublishQuality$Type extends runtime.MessageType {
3276
3280
  no: 1,
3277
3281
  name: 'audio_senders',
3278
3282
  kind: 'message',
3279
- repeat: 1 /*RepeatType.PACKED*/,
3283
+ repeat: 2 /*RepeatType.UNPACKED*/,
3280
3284
  T: () => AudioSender,
3281
3285
  },
3282
3286
  {
3283
3287
  no: 2,
3284
3288
  name: 'video_senders',
3285
3289
  kind: 'message',
3286
- repeat: 1 /*RepeatType.PACKED*/,
3290
+ repeat: 2 /*RepeatType.UNPACKED*/,
3287
3291
  T: () => VideoSender,
3288
3292
  },
3289
3293
  ]);
@@ -5475,6 +5479,21 @@ class CallState {
5475
5479
  }
5476
5480
  }
5477
5481
 
5482
+ /**
5483
+ * NegotiationError is thrown when there is an error during the negotiation process.
5484
+ * It extends the built-in Error class and includes an SfuError object for more details.
5485
+ */
5486
+ class NegotiationError extends Error {
5487
+ /**
5488
+ * Creates an instance of NegotiationError.
5489
+ */
5490
+ constructor(error) {
5491
+ super(error.message);
5492
+ this.name = 'NegotiationError';
5493
+ this.error = error;
5494
+ }
5495
+ }
5496
+
5478
5497
  /**
5479
5498
  * Flatten the stats report into an array of stats objects.
5480
5499
  *
@@ -5793,7 +5812,7 @@ const aggregate = (stats) => {
5793
5812
  return report;
5794
5813
  };
5795
5814
 
5796
- const version = "1.24.0";
5815
+ const version = "1.25.0";
5797
5816
  const [major, minor, patch] = version.split('.');
5798
5817
  let sdkInfo = {
5799
5818
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6402,12 +6421,38 @@ class BasePeerConnection {
6402
6421
  /**
6403
6422
  * Constructs a new `BasePeerConnection` instance.
6404
6423
  */
6405
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, enableTracing, }) {
6424
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, logTag, enableTracing, iceRestartDelay = 2500, }) {
6406
6425
  this.isIceRestarting = false;
6407
6426
  this.isDisposed = false;
6408
6427
  this.trackIdToTrackType = new Map();
6409
6428
  this.subscriptions = [];
6410
6429
  this.lock = Math.random().toString(36).slice(2);
6430
+ this.createPeerConnection = (connectionConfig) => {
6431
+ const pc = new RTCPeerConnection(connectionConfig);
6432
+ pc.addEventListener('icecandidate', this.onIceCandidate);
6433
+ pc.addEventListener('icecandidateerror', this.onIceCandidateError);
6434
+ pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6435
+ pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
6436
+ pc.addEventListener('signalingstatechange', this.onSignalingChange);
6437
+ pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6438
+ return pc;
6439
+ };
6440
+ /**
6441
+ * Attempts to restart ICE on the `RTCPeerConnection`.
6442
+ * This method intentionally doesn't await the `restartIce()` method,
6443
+ * allowing it to run in the background and handle any errors that may occur.
6444
+ */
6445
+ this.tryRestartIce = () => {
6446
+ this.restartIce().catch((e) => {
6447
+ const reason = 'restartICE() failed, initiating reconnect';
6448
+ this.logger('error', reason, e);
6449
+ const strategy = e instanceof NegotiationError &&
6450
+ e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
6451
+ ? WebsocketReconnectStrategy.FAST
6452
+ : WebsocketReconnectStrategy.REJOIN;
6453
+ this.onReconnectionNeeded?.(strategy, reason);
6454
+ });
6455
+ };
6411
6456
  /**
6412
6457
  * Handles events synchronously.
6413
6458
  * Consecutive events are queued and executed one after the other.
@@ -6460,6 +6505,18 @@ class BasePeerConnection {
6460
6505
  this.getTrackType = (trackId) => {
6461
6506
  return this.trackIdToTrackType.get(trackId);
6462
6507
  };
6508
+ /**
6509
+ * Checks if the `RTCPeerConnection` is healthy.
6510
+ * It checks the ICE connection state and the peer connection state.
6511
+ * If either state is `failed`, `disconnected`, or `closed`,
6512
+ * it returns `false`, otherwise it returns `true`.
6513
+ */
6514
+ this.isHealthy = () => {
6515
+ const failedStates = new Set(['failed', 'closed']);
6516
+ const iceState = this.pc.iceConnectionState;
6517
+ const connectionState = this.pc.connectionState;
6518
+ return !failedStates.has(iceState) && !failedStates.has(connectionState);
6519
+ };
6463
6520
  /**
6464
6521
  * Handles the ICECandidate event and
6465
6522
  * Initiates an ICE Trickle process with the SFU.
@@ -6498,9 +6555,7 @@ class BasePeerConnection {
6498
6555
  this.onConnectionStateChange = async () => {
6499
6556
  const state = this.pc.connectionState;
6500
6557
  this.logger('debug', `Connection state changed`, state);
6501
- if (!this.tracer)
6502
- return;
6503
- if (state === 'connected' || state === 'failed') {
6558
+ if (this.tracer && (state === 'connected' || state === 'failed')) {
6504
6559
  try {
6505
6560
  const stats = await this.stats.get();
6506
6561
  this.tracer.trace('getstats', stats.delta);
@@ -6509,6 +6564,12 @@ class BasePeerConnection {
6509
6564
  this.tracer.trace('getstatsOnFailure', err.toString());
6510
6565
  }
6511
6566
  }
6567
+ // we can't recover from a failed connection state (contrary to ICE)
6568
+ if (state === 'failed') {
6569
+ this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, 'Connection failed');
6570
+ return;
6571
+ }
6572
+ this.handleConnectionStateUpdate(state);
6512
6573
  };
6513
6574
  /**
6514
6575
  * Handles the ICE connection state change event.
@@ -6516,34 +6577,53 @@ class BasePeerConnection {
6516
6577
  this.onIceConnectionStateChange = () => {
6517
6578
  const state = this.pc.iceConnectionState;
6518
6579
  this.logger('debug', `ICE connection state changed`, state);
6519
- if (this.state.callingState === exports.CallingState.OFFLINE)
6580
+ this.handleConnectionStateUpdate(state);
6581
+ };
6582
+ this.handleConnectionStateUpdate = (state) => {
6583
+ const { callingState } = this.state;
6584
+ if (callingState === exports.CallingState.OFFLINE)
6520
6585
  return;
6521
- if (this.state.callingState === exports.CallingState.RECONNECTING)
6586
+ if (callingState === exports.CallingState.RECONNECTING)
6522
6587
  return;
6523
6588
  // do nothing when ICE is restarting
6524
6589
  if (this.isIceRestarting)
6525
6590
  return;
6526
- if (state === 'failed') {
6527
- this.onUnrecoverableError?.('ICE connection failed');
6528
- }
6529
- else if (state === 'disconnected') {
6530
- this.logger('debug', `Attempting to restart ICE`);
6531
- this.restartIce().catch((e) => {
6532
- const reason = `ICE restart failed`;
6533
- this.logger('error', reason, e);
6534
- this.onUnrecoverableError?.(`${reason}: ${e}`);
6535
- });
6591
+ switch (state) {
6592
+ case 'failed':
6593
+ // in the `failed` state, we try to restart ICE immediately
6594
+ this.logger('info', 'restartICE due to failed connection');
6595
+ this.tryRestartIce();
6596
+ break;
6597
+ case 'disconnected':
6598
+ // in the `disconnected` state, we schedule a restartICE() after a delay
6599
+ // as the browser might recover the connection in the meantime
6600
+ this.logger('info', 'disconnected connection, scheduling restartICE');
6601
+ clearTimeout(this.iceRestartTimeout);
6602
+ this.iceRestartTimeout = setTimeout(() => {
6603
+ const currentState = this.pc.iceConnectionState;
6604
+ if (currentState === 'disconnected' || currentState === 'failed') {
6605
+ this.tryRestartIce();
6606
+ }
6607
+ }, this.iceRestartDelay);
6608
+ break;
6609
+ case 'connected':
6610
+ // in the `connected` state, we clear the ice restart timeout if it exists
6611
+ if (this.iceRestartTimeout) {
6612
+ this.logger('info', 'connected connection, canceling restartICE');
6613
+ clearTimeout(this.iceRestartTimeout);
6614
+ this.iceRestartTimeout = undefined;
6615
+ }
6616
+ break;
6536
6617
  }
6537
6618
  };
6538
6619
  /**
6539
6620
  * Handles the ICE candidate error event.
6540
6621
  */
6541
6622
  this.onIceCandidateError = (e) => {
6542
- const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
6543
- `${e.errorCode}: ${e.errorText}`;
6544
- const iceState = this.pc.iceConnectionState;
6545
- const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
6546
- this.logger(logLevel, `ICE Candidate error`, errorMessage);
6623
+ const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent
6624
+ ? `${e.errorCode}: ${e.errorText}`
6625
+ : e;
6626
+ this.logger('debug', 'ICE Candidate error', errorMessage);
6547
6627
  };
6548
6628
  /**
6549
6629
  * Handles the ICE gathering state change event.
@@ -6561,18 +6641,13 @@ class BasePeerConnection {
6561
6641
  this.sfuClient = sfuClient;
6562
6642
  this.state = state;
6563
6643
  this.dispatcher = dispatcher;
6564
- this.onUnrecoverableError = onUnrecoverableError;
6644
+ this.iceRestartDelay = iceRestartDelay;
6645
+ this.onReconnectionNeeded = onReconnectionNeeded;
6565
6646
  this.logger = getLogger([
6566
6647
  peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
6567
6648
  logTag,
6568
6649
  ]);
6569
- this.pc = new RTCPeerConnection(connectionConfig);
6570
- this.pc.addEventListener('icecandidate', this.onIceCandidate);
6571
- this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
6572
- this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6573
- this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
6574
- this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
6575
- this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6650
+ this.pc = this.createPeerConnection(connectionConfig);
6576
6651
  this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
6577
6652
  if (enableTracing) {
6578
6653
  const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
@@ -6588,7 +6663,9 @@ class BasePeerConnection {
6588
6663
  * Disposes the `RTCPeerConnection` instance.
6589
6664
  */
6590
6665
  dispose() {
6591
- this.onUnrecoverableError = undefined;
6666
+ clearTimeout(this.iceRestartTimeout);
6667
+ this.iceRestartTimeout = undefined;
6668
+ this.onReconnectionNeeded = undefined;
6592
6669
  this.isDisposed = true;
6593
6670
  this.detachEventHandlers();
6594
6671
  this.pc.close();
@@ -6598,11 +6675,12 @@ class BasePeerConnection {
6598
6675
  * Detaches the event handlers from the `RTCPeerConnection`.
6599
6676
  */
6600
6677
  detachEventHandlers() {
6601
- this.pc.removeEventListener('icecandidate', this.onIceCandidate);
6602
- this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
6603
- this.pc.removeEventListener('signalingstatechange', this.onSignalingChange);
6604
- this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6605
- this.pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
6678
+ const pc = this.pc;
6679
+ pc.removeEventListener('icecandidate', this.onIceCandidate);
6680
+ pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
6681
+ pc.removeEventListener('signalingstatechange', this.onSignalingChange);
6682
+ pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6683
+ pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
6606
6684
  this.unsubscribeIceTrickle?.();
6607
6685
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
6608
6686
  }
@@ -6912,6 +6990,45 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
6912
6990
  return '';
6913
6991
  return String(transceiverInitIndex);
6914
6992
  };
6993
+ /**
6994
+ * Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
6995
+ *
6996
+ * @param offerSdp the offer SDP containing the stereo configuration.
6997
+ * @param answerSdp the answer SDP to be modified.
6998
+ */
6999
+ const enableStereo = (offerSdp, answerSdp) => {
7000
+ const offeredStereoMids = new Set();
7001
+ const parsedOfferSdp = sdpTransform.parse(offerSdp);
7002
+ for (const media of parsedOfferSdp.media) {
7003
+ if (media.type !== 'audio')
7004
+ continue;
7005
+ const opus = media.rtp.find((r) => r.codec === 'opus');
7006
+ if (!opus)
7007
+ continue;
7008
+ for (const fmtp of media.fmtp) {
7009
+ if (fmtp.payload === opus.payload && fmtp.config.includes('stereo=1')) {
7010
+ offeredStereoMids.add(media.mid);
7011
+ }
7012
+ }
7013
+ }
7014
+ // No stereo offered, return the original answerSdp
7015
+ if (offeredStereoMids.size === 0)
7016
+ return answerSdp;
7017
+ const parsedAnswerSdp = sdpTransform.parse(answerSdp);
7018
+ for (const media of parsedAnswerSdp.media) {
7019
+ if (media.type !== 'audio' || !offeredStereoMids.has(media.mid))
7020
+ continue;
7021
+ const opus = media.rtp.find((r) => r.codec === 'opus');
7022
+ if (!opus)
7023
+ continue;
7024
+ for (const fmtp of media.fmtp) {
7025
+ if (fmtp.payload === opus.payload && !fmtp.config.includes('stereo=1')) {
7026
+ fmtp.config += ';stereo=1';
7027
+ }
7028
+ }
7029
+ }
7030
+ return sdpTransform.write(parsedAnswerSdp);
7031
+ };
6915
7032
 
6916
7033
  /**
6917
7034
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -7025,11 +7142,11 @@ class Publisher extends BasePeerConnection {
7025
7142
  /**
7026
7143
  * Returns true if the given track type is currently being published to the SFU.
7027
7144
  *
7028
- * @param trackType the track type to check.
7145
+ * @param trackType the track type to check. If omitted, checks if any track is being published.
7029
7146
  */
7030
7147
  this.isPublishing = (trackType) => {
7031
7148
  for (const item of this.transceiverCache.items()) {
7032
- if (item.publishOption.trackType !== trackType)
7149
+ if (trackType && item.publishOption.trackType !== trackType)
7033
7150
  continue;
7034
7151
  const track = item.transceiver.sender.track;
7035
7152
  if (!track)
@@ -7152,10 +7269,17 @@ class Publisher extends BasePeerConnection {
7152
7269
  const { sdp = '' } = offer;
7153
7270
  const { response } = await this.sfuClient.setPublisher({ sdp, tracks });
7154
7271
  if (response.error)
7155
- throw new Error(response.error.message);
7272
+ throw new NegotiationError(response.error);
7156
7273
  const { sdp: answerSdp } = response;
7157
7274
  await this.pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
7158
7275
  }
7276
+ catch (err) {
7277
+ // negotiation failed, rollback to the previous state
7278
+ if (this.pc.signalingState === 'have-local-offer') {
7279
+ await this.pc.setLocalDescription({ type: 'rollback' });
7280
+ }
7281
+ throw err;
7282
+ }
7159
7283
  finally {
7160
7284
  this.isIceRestarting = false;
7161
7285
  }
@@ -7247,11 +7371,7 @@ class Publisher extends BasePeerConnection {
7247
7371
  this.on('iceRestart', (iceRestart) => {
7248
7372
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
7249
7373
  return;
7250
- this.restartIce().catch((err) => {
7251
- const reason = `ICE restart failed`;
7252
- this.logger('warn', reason, err);
7253
- this.onUnrecoverableError?.(`${reason}: ${err}`);
7254
- });
7374
+ this.tryRestartIce();
7255
7375
  });
7256
7376
  this.on('changePublishQuality', async (event) => {
7257
7377
  for (const videoSender of event.videoSenders) {
@@ -7299,11 +7419,13 @@ class Subscriber extends BasePeerConnection {
7299
7419
  return;
7300
7420
  }
7301
7421
  const previousIsIceRestarting = this.isIceRestarting;
7422
+ this.isIceRestarting = true;
7302
7423
  try {
7303
- this.isIceRestarting = true;
7304
- await this.sfuClient.iceRestart({
7424
+ const { response } = await this.sfuClient.iceRestart({
7305
7425
  peerType: PeerType.SUBSCRIBER,
7306
7426
  });
7427
+ if (response.error)
7428
+ throw new NegotiationError(response.error);
7307
7429
  }
7308
7430
  catch (e) {
7309
7431
  // restore the previous state, as our intent for restarting ICE failed
@@ -7372,6 +7494,9 @@ class Subscriber extends BasePeerConnection {
7372
7494
  });
7373
7495
  this.addTrickledIceCandidates();
7374
7496
  const answer = await this.pc.createAnswer();
7497
+ if (answer.sdp) {
7498
+ answer.sdp = enableStereo(subscriberOffer.sdp, answer.sdp);
7499
+ }
7375
7500
  await this.pc.setLocalDescription(answer);
7376
7501
  await this.sfuClient.sendAnswer({
7377
7502
  peerType: PeerType.SUBSCRIBER,
@@ -7661,11 +7786,15 @@ class StreamSfuClient {
7661
7786
  */
7662
7787
  this.isLeaving = false;
7663
7788
  /**
7664
- * Flag to indicate if the client is in the process of closing the connection.
7789
+ * Flag to indicate if the client is in the process of clean closing the connection.
7790
+ * When set to `true`, the client will not attempt to reconnect
7791
+ * and will close the WebSocket connection gracefully.
7792
+ * Otherwise, it will close the connection with an error code and
7793
+ * trigger a reconnection attempt.
7665
7794
  */
7666
- this.isClosing = false;
7667
- this.pingIntervalInMs = 10 * 1000;
7668
- this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
7795
+ this.isClosingClean = false;
7796
+ this.pingIntervalInMs = 5 * 1000;
7797
+ this.unhealthyTimeoutInMs = 15 * 1000;
7669
7798
  /**
7670
7799
  * Promise that resolves when the JoinResponse is received.
7671
7800
  * Rejects after a certain threshold if the response is not received.
@@ -7709,7 +7838,7 @@ class StreamSfuClient {
7709
7838
  // Normally, this shouldn't have any effect, because WS should never emit 'close'
7710
7839
  // before emitting 'open'. However, strager things have happened, and we don't
7711
7840
  // want to leave signalReady in pending state.
7712
- reject(new Error('SFU WS closed unexpectedly'));
7841
+ reject(new Error(`SFU WS closed or connection can't be established`));
7713
7842
  });
7714
7843
  }),
7715
7844
  new Promise((resolve, reject) => {
@@ -7727,7 +7856,7 @@ class StreamSfuClient {
7727
7856
  this.onSignalClose?.(`${e.code} ${e.reason}`);
7728
7857
  };
7729
7858
  this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
7730
- this.isClosing = true;
7859
+ this.isClosingClean = code !== StreamSfuClient.ERROR_CONNECTION_UNHEALTHY;
7731
7860
  if (this.signalWs.readyState === WebSocket.OPEN) {
7732
7861
  this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
7733
7862
  this.signalWs.close(code, `js-client: ${reason}`);
@@ -7967,7 +8096,11 @@ StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
7967
8096
  * Here, we don't use 1000 (normal closure) because we don't want the
7968
8097
  * SFU to clean up the resources associated with the current participant.
7969
8098
  */
7970
- StreamSfuClient.DISPOSE_OLD_SOCKET = 4002;
8099
+ StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
8100
+ /**
8101
+ * The close code used when the client fails to join the call (on the SFU).
8102
+ */
8103
+ StreamSfuClient.JOIN_FAILED = 4101;
7971
8104
 
7972
8105
  /**
7973
8106
  * Event handler that watched the delivery of `call.accepted`.
@@ -9481,21 +9614,30 @@ let getDisplayMediaExecId = 0;
9481
9614
  const getScreenShareStream = async (options, tracer) => {
9482
9615
  const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
9483
9616
  try {
9484
- tracer?.trace(tag, options);
9485
- const stream = await navigator.mediaDevices.getDisplayMedia({
9486
- video: true,
9487
- audio: {
9488
- channelCount: {
9489
- ideal: 2,
9490
- },
9491
- echoCancellation: false,
9492
- autoGainControl: false,
9493
- noiseSuppression: false,
9494
- },
9617
+ const constraints = {
9495
9618
  // @ts-expect-error - not present in types yet
9496
9619
  systemAudio: 'include',
9497
9620
  ...options,
9498
- });
9621
+ video: typeof options?.video === 'boolean'
9622
+ ? options.video // must be 'true'
9623
+ : {
9624
+ width: { max: 2560 },
9625
+ height: { max: 1440 },
9626
+ frameRate: { ideal: 30 },
9627
+ ...options?.video,
9628
+ },
9629
+ audio: typeof options?.audio === 'boolean'
9630
+ ? options.audio
9631
+ : {
9632
+ channelCount: { ideal: 2 },
9633
+ echoCancellation: false,
9634
+ autoGainControl: false,
9635
+ noiseSuppression: false,
9636
+ ...options?.audio,
9637
+ },
9638
+ };
9639
+ tracer?.trace(tag, constraints);
9640
+ const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
9499
9641
  tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
9500
9642
  return stream;
9501
9643
  }
@@ -10843,11 +10985,18 @@ class ScreenShareManager extends InputMediaDeviceManager {
10843
10985
  getDevices() {
10844
10986
  return rxjs.of([]); // there are no devices to be listed for Screen Share
10845
10987
  }
10846
- getStream(constraints) {
10988
+ async getStream(constraints) {
10847
10989
  if (!this.state.audioEnabled) {
10848
10990
  constraints.audio = false;
10849
10991
  }
10850
- return getScreenShareStream(constraints, this.call.tracer);
10992
+ const stream = await getScreenShareStream(constraints, this.call.tracer);
10993
+ const [track] = stream.getVideoTracks();
10994
+ const { contentHint } = this.state.settings || {};
10995
+ if (typeof contentHint !== 'undefined' && track && 'contentHint' in track) {
10996
+ this.call.tracer.trace('navigator.mediaDevices.getDisplayMedia.contentHint', contentHint);
10997
+ track.contentHint = contentHint;
10998
+ }
10999
+ return stream;
10851
11000
  }
10852
11001
  async stopPublishStream() {
10853
11002
  return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
@@ -11242,10 +11391,13 @@ class Call {
11242
11391
  * Leave the call and stop the media streams that were published by the call.
11243
11392
  */
11244
11393
  this.leave = async ({ reject, reason, message } = {}) => {
11394
+ if (this.state.callingState === exports.CallingState.LEFT) {
11395
+ throw new Error('Cannot leave call that has already been left.');
11396
+ }
11245
11397
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
11246
11398
  const callingState = this.state.callingState;
11247
11399
  if (callingState === exports.CallingState.LEFT) {
11248
- throw new Error('Cannot leave call that has already been left.');
11400
+ return;
11249
11401
  }
11250
11402
  if (callingState === exports.CallingState.JOINING) {
11251
11403
  const waitUntilCallJoined = () => {
@@ -11352,7 +11504,6 @@ class Call {
11352
11504
  this.state.setMembers(response.members);
11353
11505
  this.state.setOwnCapabilities(response.own_capabilities);
11354
11506
  if (params?.ring) {
11355
- // the call response can indicate where the call is still ringing or not
11356
11507
  this.ringingSubject.next(true);
11357
11508
  }
11358
11509
  if (this.streamClient._hasConnectionID()) {
@@ -11374,7 +11525,6 @@ class Call {
11374
11525
  this.state.setMembers(response.members);
11375
11526
  this.state.setOwnCapabilities(response.own_capabilities);
11376
11527
  if (data?.ring) {
11377
- // the call response can indicate where the call is still ringing or not
11378
11528
  this.ringingSubject.next(true);
11379
11529
  }
11380
11530
  if (this.streamClient._hasConnectionID()) {
@@ -11554,7 +11704,7 @@ class Call {
11554
11704
  }
11555
11705
  catch (error) {
11556
11706
  this.logger('warn', 'Join SFU request failed', error);
11557
- sfuClient.close(StreamSfuClient.ERROR_CONNECTION_UNHEALTHY, 'Join request failed, connection considered unhealthy');
11707
+ sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
11558
11708
  // restore the previous call state if the join-flow fails
11559
11709
  this.state.setCallingState(callingState);
11560
11710
  throw error;
@@ -11697,7 +11847,7 @@ class Call {
11697
11847
  }
11698
11848
  if (this.publisher) {
11699
11849
  this.publisher.setSfuClient(nextSfuClient);
11700
- if (includePublisher) {
11850
+ if (includePublisher && this.publisher.isPublishing()) {
11701
11851
  await this.publisher.restartIce();
11702
11852
  }
11703
11853
  }
@@ -11719,9 +11869,10 @@ class Call {
11719
11869
  connectionConfig,
11720
11870
  logTag: String(this.sfuClientTag),
11721
11871
  enableTracing,
11722
- onUnrecoverableError: (reason) => {
11723
- this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
11724
- this.logger('warn', `[Reconnect] Error reconnecting after a subscriber error: ${reason}`, err);
11872
+ onReconnectionNeeded: (kind, reason) => {
11873
+ this.reconnect(kind, reason).catch((err) => {
11874
+ const message = `[Reconnect] Error reconnecting after a subscriber error: ${reason}`;
11875
+ this.logger('warn', message, err);
11725
11876
  });
11726
11877
  },
11727
11878
  });
@@ -11740,9 +11891,10 @@ class Call {
11740
11891
  publishOptions,
11741
11892
  logTag: String(this.sfuClientTag),
11742
11893
  enableTracing,
11743
- onUnrecoverableError: (reason) => {
11744
- this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
11745
- this.logger('warn', `[Reconnect] Error reconnecting after a publisher error: ${reason}`, err);
11894
+ onReconnectionNeeded: (kind, reason) => {
11895
+ this.reconnect(kind, reason).catch((err) => {
11896
+ const message = `[Reconnect] Error reconnecting after a publisher error: ${reason}`;
11897
+ this.logger('warn', message, err);
11746
11898
  });
11747
11899
  },
11748
11900
  });
@@ -11826,9 +11978,12 @@ class Call {
11826
11978
  callingState === exports.CallingState.LEFT)
11827
11979
  return;
11828
11980
  // normal close, no need to reconnect
11829
- if (sfuClient.isLeaving || sfuClient.isClosing)
11981
+ if (sfuClient.isLeaving || sfuClient.isClosingClean)
11830
11982
  return;
11831
- this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
11983
+ const strategy = this.publisher?.isHealthy() && this.subscriber?.isHealthy()
11984
+ ? WebsocketReconnectStrategy.FAST
11985
+ : WebsocketReconnectStrategy.REJOIN;
11986
+ this.reconnect(strategy, reason).catch((err) => {
11832
11987
  this.logger('warn', '[Reconnect] Error reconnecting', err);
11833
11988
  });
11834
11989
  };
@@ -11849,10 +12004,12 @@ class Call {
11849
12004
  const reconnectStartTime = Date.now();
11850
12005
  this.reconnectStrategy = strategy;
11851
12006
  this.reconnectReason = reason;
12007
+ let attempt = 0;
11852
12008
  do {
11853
- if (this.disconnectionTimeoutSeconds > 0 &&
11854
- (Date.now() - reconnectStartTime) / 1000 >
11855
- this.disconnectionTimeoutSeconds) {
12009
+ const reconnectingTime = Date.now() - reconnectStartTime;
12010
+ const shouldGiveUpReconnecting = this.disconnectionTimeoutSeconds > 0 &&
12011
+ reconnectingTime / 1000 > this.disconnectionTimeoutSeconds;
12012
+ if (shouldGiveUpReconnecting) {
11856
12013
  this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
11857
12014
  this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
11858
12015
  return;
@@ -11861,7 +12018,7 @@ class Call {
11861
12018
  if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
11862
12019
  this.reconnectAttempts++;
11863
12020
  }
11864
- const current = WebsocketReconnectStrategy[this.reconnectStrategy];
12021
+ const currentStrategy = WebsocketReconnectStrategy[this.reconnectStrategy];
11865
12022
  try {
11866
12023
  // wait until the network is available
11867
12024
  await this.networkAvailableTask?.promise;
@@ -11869,7 +12026,7 @@ class Call {
11869
12026
  switch (this.reconnectStrategy) {
11870
12027
  case WebsocketReconnectStrategy.UNSPECIFIED:
11871
12028
  case WebsocketReconnectStrategy.DISCONNECT:
11872
- this.logger('debug', `[Reconnect] No-op strategy ${current}`);
12029
+ this.logger('debug', `[Reconnect] No-op strategy ${currentStrategy}`);
11873
12030
  break;
11874
12031
  case WebsocketReconnectStrategy.FAST:
11875
12032
  await this.reconnectFast();
@@ -11888,7 +12045,7 @@ class Call {
11888
12045
  }
11889
12046
  catch (error) {
11890
12047
  if (this.state.callingState === exports.CallingState.OFFLINE) {
11891
- this.logger('trace', `[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
12048
+ this.logger('debug', `[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
11892
12049
  break;
11893
12050
  // we don't need to handle the error if the call is offline
11894
12051
  // network change event will trigger the reconnection
@@ -11898,9 +12055,24 @@ class Call {
11898
12055
  this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
11899
12056
  return;
11900
12057
  }
11901
- this.logger('warn', `[Reconnect] ${current} (${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
11902
12058
  await sleep(500);
11903
- this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
12059
+ const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
12060
+ const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
12061
+ this.fastReconnectDeadlineSeconds;
12062
+ // don't immediately switch to the REJOIN strategy, but instead attempt
12063
+ // to reconnect with the FAST strategy for a few times before switching.
12064
+ // in some cases, we immediately switch to the REJOIN strategy.
12065
+ const shouldRejoin = mustPerformRejoin || // if we are past the fast reconnect deadline
12066
+ wasMigrating || // if we were migrating, but the migration failed
12067
+ attempt >= 3 || // after 3 failed attempts
12068
+ !(this.publisher?.isHealthy() ?? true) || // if the publisher is not healthy
12069
+ !(this.subscriber?.isHealthy() ?? true); // if the subscriber is not healthy
12070
+ attempt++;
12071
+ const nextStrategy = shouldRejoin
12072
+ ? WebsocketReconnectStrategy.REJOIN
12073
+ : WebsocketReconnectStrategy.FAST;
12074
+ this.reconnectStrategy = nextStrategy;
12075
+ this.logger('info', `[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
11904
12076
  }
11905
12077
  } while (this.state.callingState !== exports.CallingState.JOINED &&
11906
12078
  this.state.callingState !== exports.CallingState.RECONNECTING_FAILED &&
@@ -12913,6 +13085,38 @@ class Call {
12913
13085
  }
12914
13086
  }
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.