@stream-io/video-client 1.5.0 → 1.6.0-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 (74) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/dist/index.browser.es.js +1986 -1482
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1983 -1478
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1986 -1482
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +93 -9
  9. package/dist/src/StreamSfuClient.d.ts +73 -56
  10. package/dist/src/StreamVideoClient.d.ts +2 -2
  11. package/dist/src/coordinator/connection/client.d.ts +3 -4
  12. package/dist/src/coordinator/connection/types.d.ts +5 -1
  13. package/dist/src/devices/InputMediaDeviceManager.d.ts +4 -0
  14. package/dist/src/devices/MicrophoneManager.d.ts +1 -1
  15. package/dist/src/events/callEventHandlers.d.ts +1 -3
  16. package/dist/src/events/internal.d.ts +4 -0
  17. package/dist/src/gen/video/sfu/event/events.d.ts +106 -4
  18. package/dist/src/gen/video/sfu/models/models.d.ts +64 -65
  19. package/dist/src/helpers/ensureExhausted.d.ts +1 -0
  20. package/dist/src/helpers/withResolvers.d.ts +14 -0
  21. package/dist/src/logger.d.ts +1 -0
  22. package/dist/src/rpc/createClient.d.ts +2 -0
  23. package/dist/src/rpc/index.d.ts +1 -0
  24. package/dist/src/rpc/retryable.d.ts +23 -0
  25. package/dist/src/rtc/Dispatcher.d.ts +1 -1
  26. package/dist/src/rtc/IceTrickleBuffer.d.ts +0 -1
  27. package/dist/src/rtc/Publisher.d.ts +24 -25
  28. package/dist/src/rtc/Subscriber.d.ts +12 -11
  29. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -0
  30. package/dist/src/rtc/helpers/tracks.d.ts +3 -3
  31. package/dist/src/rtc/signal.d.ts +1 -1
  32. package/dist/src/store/CallState.d.ts +46 -2
  33. package/package.json +3 -3
  34. package/src/Call.ts +628 -566
  35. package/src/StreamSfuClient.ts +276 -246
  36. package/src/StreamVideoClient.ts +15 -16
  37. package/src/coordinator/connection/client.ts +25 -8
  38. package/src/coordinator/connection/connection.ts +1 -0
  39. package/src/coordinator/connection/types.ts +6 -0
  40. package/src/devices/CameraManager.ts +1 -1
  41. package/src/devices/InputMediaDeviceManager.ts +12 -3
  42. package/src/devices/MicrophoneManager.ts +3 -3
  43. package/src/devices/devices.ts +1 -1
  44. package/src/events/__tests__/mutes.test.ts +10 -13
  45. package/src/events/__tests__/participant.test.ts +75 -0
  46. package/src/events/callEventHandlers.ts +4 -7
  47. package/src/events/internal.ts +20 -3
  48. package/src/events/mutes.ts +5 -3
  49. package/src/events/participant.ts +48 -15
  50. package/src/gen/video/sfu/event/events.ts +451 -8
  51. package/src/gen/video/sfu/models/models.ts +211 -204
  52. package/src/helpers/ensureExhausted.ts +5 -0
  53. package/src/helpers/withResolvers.ts +43 -0
  54. package/src/logger.ts +3 -1
  55. package/src/rpc/__tests__/retryable.test.ts +72 -0
  56. package/src/rpc/createClient.ts +21 -0
  57. package/src/rpc/index.ts +1 -0
  58. package/src/rpc/retryable.ts +57 -0
  59. package/src/rtc/Dispatcher.ts +6 -2
  60. package/src/rtc/IceTrickleBuffer.ts +2 -2
  61. package/src/rtc/Publisher.ts +127 -163
  62. package/src/rtc/Subscriber.ts +92 -155
  63. package/src/rtc/__tests__/Publisher.test.ts +18 -95
  64. package/src/rtc/__tests__/Subscriber.test.ts +63 -99
  65. package/src/rtc/__tests__/videoLayers.test.ts +2 -2
  66. package/src/rtc/helpers/rtcConfiguration.ts +11 -0
  67. package/src/rtc/helpers/tracks.ts +27 -7
  68. package/src/rtc/signal.ts +3 -3
  69. package/src/rtc/videoLayers.ts +1 -10
  70. package/src/stats/SfuStatsReporter.ts +1 -0
  71. package/src/store/CallState.ts +109 -2
  72. package/src/store/__tests__/CallState.test.ts +48 -37
  73. package/dist/src/rtc/flows/join.d.ts +0 -20
  74. package/src/rtc/flows/join.ts +0 -65
package/dist/index.cjs.js CHANGED
@@ -8,8 +8,8 @@ var twirpTransport = require('@protobuf-ts/twirp-transport');
8
8
  var rxjs = require('rxjs');
9
9
  var SDP = require('sdp-transform');
10
10
  var uaParserJs = require('ua-parser-js');
11
- var WebSocket = require('isomorphic-ws');
12
11
  var https = require('https');
12
+ var WebSocket$1 = require('isomorphic-ws');
13
13
  var base64Js = require('base64-js');
14
14
 
15
15
  function _interopNamespaceDefault(e) {
@@ -1035,6 +1035,10 @@ var CallEndedReason;
1035
1035
  * @generated from protobuf enum value: CALL_ENDED_REASON_KICKED = 3;
1036
1036
  */
1037
1037
  CallEndedReason[CallEndedReason["KICKED"] = 3] = "KICKED";
1038
+ /**
1039
+ * @generated from protobuf enum value: CALL_ENDED_REASON_SESSION_ENDED = 4;
1040
+ */
1041
+ CallEndedReason[CallEndedReason["SESSION_ENDED"] = 4] = "SESSION_ENDED";
1038
1042
  })(CallEndedReason || (CallEndedReason = {}));
1039
1043
  /**
1040
1044
  * WebsocketReconnectStrategy defines the ws strategies available for handling reconnections.
@@ -1048,7 +1052,7 @@ var WebsocketReconnectStrategy;
1048
1052
  */
1049
1053
  WebsocketReconnectStrategy[WebsocketReconnectStrategy["UNSPECIFIED"] = 0] = "UNSPECIFIED";
1050
1054
  /**
1051
- * Sent after reaching the maximum reconnection attempts, leading to permanent disconnect.
1055
+ * Sent after reaching the maximum reconnection attempts, or any other unrecoverable error leading to permanent disconnect.
1052
1056
  *
1053
1057
  * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_DISCONNECT = 1;
1054
1058
  */
@@ -1061,25 +1065,18 @@ var WebsocketReconnectStrategy;
1061
1065
  */
1062
1066
  WebsocketReconnectStrategy[WebsocketReconnectStrategy["FAST"] = 2] = "FAST";
1063
1067
  /**
1064
- * SDK should drop existing pc instances and creates a fresh WebSocket connection,
1065
- * ensuring a clean state for the reconnection.
1066
- *
1067
- * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_CLEAN = 3;
1068
- */
1069
- WebsocketReconnectStrategy[WebsocketReconnectStrategy["CLEAN"] = 3] = "CLEAN";
1070
- /**
1071
- * SDK should obtain new credentials from the coordinator, drops existing pc instances, and initializes
1068
+ * SDK should obtain new credentials from the coordinator, drops existing pc instances, set a new session_id and initializes
1072
1069
  * a completely new WebSocket connection, ensuring a comprehensive reset.
1073
1070
  *
1074
- * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_FULL = 4;
1071
+ * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_REJOIN = 3;
1075
1072
  */
1076
- WebsocketReconnectStrategy[WebsocketReconnectStrategy["FULL"] = 4] = "FULL";
1073
+ WebsocketReconnectStrategy[WebsocketReconnectStrategy["REJOIN"] = 3] = "REJOIN";
1077
1074
  /**
1078
1075
  * SDK should migrate to a new SFU instance
1079
1076
  *
1080
- * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE = 5;
1077
+ * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE = 4;
1081
1078
  */
1082
- WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] = 5] = "MIGRATE";
1079
+ WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] = 4] = "MIGRATE";
1083
1080
  })(WebsocketReconnectStrategy || (WebsocketReconnectStrategy = {}));
1084
1081
  // @generated message type with reflection information, may provide speed optimized methods
1085
1082
  class CallState$Type extends runtime.MessageType {
@@ -1881,6 +1878,7 @@ class TrackInfo$Type extends runtime.MessageType {
1881
1878
  { no: 7, name: 'dtx', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1882
1879
  { no: 8, name: 'stereo', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1883
1880
  { no: 9, name: 'red', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1881
+ { no: 10, name: 'muted', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1884
1882
  ]);
1885
1883
  }
1886
1884
  create(value) {
@@ -1892,6 +1890,7 @@ class TrackInfo$Type extends runtime.MessageType {
1892
1890
  message.dtx = false;
1893
1891
  message.stereo = false;
1894
1892
  message.red = false;
1893
+ message.muted = false;
1895
1894
  if (value !== undefined)
1896
1895
  runtime.reflectionMergePartial(this, message, value);
1897
1896
  return message;
@@ -1922,6 +1921,9 @@ class TrackInfo$Type extends runtime.MessageType {
1922
1921
  case /* bool red */ 9:
1923
1922
  message.red = reader.bool();
1924
1923
  break;
1924
+ case /* bool muted */ 10:
1925
+ message.muted = reader.bool();
1926
+ break;
1925
1927
  default:
1926
1928
  let u = options.readUnknownField;
1927
1929
  if (u === 'throw')
@@ -1955,6 +1957,9 @@ class TrackInfo$Type extends runtime.MessageType {
1955
1957
  /* bool red = 9; */
1956
1958
  if (message.red !== false)
1957
1959
  writer.tag(9, runtime.WireType.Varint).bool(message.red);
1960
+ /* bool muted = 10; */
1961
+ if (message.muted !== false)
1962
+ writer.tag(10, runtime.WireType.Varint).bool(message.muted);
1958
1963
  let u = options.writeUnknownFields;
1959
1964
  if (u !== false)
1960
1965
  (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1966,108 +1971,6 @@ class TrackInfo$Type extends runtime.MessageType {
1966
1971
  */
1967
1972
  const TrackInfo = new TrackInfo$Type();
1968
1973
  // @generated message type with reflection information, may provide speed optimized methods
1969
- class Call$Type extends runtime.MessageType {
1970
- constructor() {
1971
- super('stream.video.sfu.models.Call', [
1972
- { no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1973
- { no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1974
- {
1975
- no: 3,
1976
- name: 'created_by_user_id',
1977
- kind: 'scalar',
1978
- T: 9 /*ScalarType.STRING*/,
1979
- },
1980
- {
1981
- no: 4,
1982
- name: 'host_user_id',
1983
- kind: 'scalar',
1984
- T: 9 /*ScalarType.STRING*/,
1985
- },
1986
- { no: 5, name: 'custom', kind: 'message', T: () => Struct },
1987
- { no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
1988
- { no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
1989
- ]);
1990
- }
1991
- create(value) {
1992
- const message = globalThis.Object.create(this.messagePrototype);
1993
- message.type = '';
1994
- message.id = '';
1995
- message.createdByUserId = '';
1996
- message.hostUserId = '';
1997
- if (value !== undefined)
1998
- runtime.reflectionMergePartial(this, message, value);
1999
- return message;
2000
- }
2001
- internalBinaryRead(reader, length, options, target) {
2002
- let message = target ?? this.create(), end = reader.pos + length;
2003
- while (reader.pos < end) {
2004
- let [fieldNo, wireType] = reader.tag();
2005
- switch (fieldNo) {
2006
- case /* string type */ 1:
2007
- message.type = reader.string();
2008
- break;
2009
- case /* string id */ 2:
2010
- message.id = reader.string();
2011
- break;
2012
- case /* string created_by_user_id */ 3:
2013
- message.createdByUserId = reader.string();
2014
- break;
2015
- case /* string host_user_id */ 4:
2016
- message.hostUserId = reader.string();
2017
- break;
2018
- case /* google.protobuf.Struct custom */ 5:
2019
- message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
2020
- break;
2021
- case /* google.protobuf.Timestamp created_at */ 6:
2022
- message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
2023
- break;
2024
- case /* google.protobuf.Timestamp updated_at */ 7:
2025
- message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
2026
- break;
2027
- default:
2028
- let u = options.readUnknownField;
2029
- if (u === 'throw')
2030
- throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
2031
- let d = reader.skip(wireType);
2032
- if (u !== false)
2033
- (u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
2034
- }
2035
- }
2036
- return message;
2037
- }
2038
- internalBinaryWrite(message, writer, options) {
2039
- /* string type = 1; */
2040
- if (message.type !== '')
2041
- writer.tag(1, runtime.WireType.LengthDelimited).string(message.type);
2042
- /* string id = 2; */
2043
- if (message.id !== '')
2044
- writer.tag(2, runtime.WireType.LengthDelimited).string(message.id);
2045
- /* string created_by_user_id = 3; */
2046
- if (message.createdByUserId !== '')
2047
- writer.tag(3, runtime.WireType.LengthDelimited).string(message.createdByUserId);
2048
- /* string host_user_id = 4; */
2049
- if (message.hostUserId !== '')
2050
- writer.tag(4, runtime.WireType.LengthDelimited).string(message.hostUserId);
2051
- /* google.protobuf.Struct custom = 5; */
2052
- if (message.custom)
2053
- Struct.internalBinaryWrite(message.custom, writer.tag(5, runtime.WireType.LengthDelimited).fork(), options).join();
2054
- /* google.protobuf.Timestamp created_at = 6; */
2055
- if (message.createdAt)
2056
- Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, runtime.WireType.LengthDelimited).fork(), options).join();
2057
- /* google.protobuf.Timestamp updated_at = 7; */
2058
- if (message.updatedAt)
2059
- Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, runtime.WireType.LengthDelimited).fork(), options).join();
2060
- let u = options.writeUnknownFields;
2061
- if (u !== false)
2062
- (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
2063
- return writer;
2064
- }
2065
- }
2066
- /**
2067
- * @generated MessageType for protobuf message stream.video.sfu.models.Call
2068
- */
2069
- const Call$1 = new Call$Type();
2070
- // @generated message type with reflection information, may provide speed optimized methods
2071
1974
  let Error$Type$1 = class Error$Type extends runtime.MessageType {
2072
1975
  constructor() {
2073
1976
  super('stream.video.sfu.models.Error', [
@@ -2461,6 +2364,108 @@ class Device$Type extends runtime.MessageType {
2461
2364
  */
2462
2365
  const Device = new Device$Type();
2463
2366
  // @generated message type with reflection information, may provide speed optimized methods
2367
+ class Call$Type extends runtime.MessageType {
2368
+ constructor() {
2369
+ super('stream.video.sfu.models.Call', [
2370
+ { no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2371
+ { no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2372
+ {
2373
+ no: 3,
2374
+ name: 'created_by_user_id',
2375
+ kind: 'scalar',
2376
+ T: 9 /*ScalarType.STRING*/,
2377
+ },
2378
+ {
2379
+ no: 4,
2380
+ name: 'host_user_id',
2381
+ kind: 'scalar',
2382
+ T: 9 /*ScalarType.STRING*/,
2383
+ },
2384
+ { no: 5, name: 'custom', kind: 'message', T: () => Struct },
2385
+ { no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
2386
+ { no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
2387
+ ]);
2388
+ }
2389
+ create(value) {
2390
+ const message = globalThis.Object.create(this.messagePrototype);
2391
+ message.type = '';
2392
+ message.id = '';
2393
+ message.createdByUserId = '';
2394
+ message.hostUserId = '';
2395
+ if (value !== undefined)
2396
+ runtime.reflectionMergePartial(this, message, value);
2397
+ return message;
2398
+ }
2399
+ internalBinaryRead(reader, length, options, target) {
2400
+ let message = target ?? this.create(), end = reader.pos + length;
2401
+ while (reader.pos < end) {
2402
+ let [fieldNo, wireType] = reader.tag();
2403
+ switch (fieldNo) {
2404
+ case /* string type */ 1:
2405
+ message.type = reader.string();
2406
+ break;
2407
+ case /* string id */ 2:
2408
+ message.id = reader.string();
2409
+ break;
2410
+ case /* string created_by_user_id */ 3:
2411
+ message.createdByUserId = reader.string();
2412
+ break;
2413
+ case /* string host_user_id */ 4:
2414
+ message.hostUserId = reader.string();
2415
+ break;
2416
+ case /* google.protobuf.Struct custom */ 5:
2417
+ message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
2418
+ break;
2419
+ case /* google.protobuf.Timestamp created_at */ 6:
2420
+ message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
2421
+ break;
2422
+ case /* google.protobuf.Timestamp updated_at */ 7:
2423
+ message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
2424
+ break;
2425
+ default:
2426
+ let u = options.readUnknownField;
2427
+ if (u === 'throw')
2428
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
2429
+ let d = reader.skip(wireType);
2430
+ if (u !== false)
2431
+ (u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
2432
+ }
2433
+ }
2434
+ return message;
2435
+ }
2436
+ internalBinaryWrite(message, writer, options) {
2437
+ /* string type = 1; */
2438
+ if (message.type !== '')
2439
+ writer.tag(1, runtime.WireType.LengthDelimited).string(message.type);
2440
+ /* string id = 2; */
2441
+ if (message.id !== '')
2442
+ writer.tag(2, runtime.WireType.LengthDelimited).string(message.id);
2443
+ /* string created_by_user_id = 3; */
2444
+ if (message.createdByUserId !== '')
2445
+ writer.tag(3, runtime.WireType.LengthDelimited).string(message.createdByUserId);
2446
+ /* string host_user_id = 4; */
2447
+ if (message.hostUserId !== '')
2448
+ writer.tag(4, runtime.WireType.LengthDelimited).string(message.hostUserId);
2449
+ /* google.protobuf.Struct custom = 5; */
2450
+ if (message.custom)
2451
+ Struct.internalBinaryWrite(message.custom, writer.tag(5, runtime.WireType.LengthDelimited).fork(), options).join();
2452
+ /* google.protobuf.Timestamp created_at = 6; */
2453
+ if (message.createdAt)
2454
+ Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, runtime.WireType.LengthDelimited).fork(), options).join();
2455
+ /* google.protobuf.Timestamp updated_at = 7; */
2456
+ if (message.updatedAt)
2457
+ Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, runtime.WireType.LengthDelimited).fork(), options).join();
2458
+ let u = options.writeUnknownFields;
2459
+ if (u !== false)
2460
+ (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
2461
+ return writer;
2462
+ }
2463
+ }
2464
+ /**
2465
+ * @generated MessageType for protobuf message stream.video.sfu.models.Call
2466
+ */
2467
+ const Call$1 = new Call$Type();
2468
+ // @generated message type with reflection information, may provide speed optimized methods
2464
2469
  class CallGrants$Type extends runtime.MessageType {
2465
2470
  constructor() {
2466
2471
  super('stream.video.sfu.models.CallGrants', [
@@ -3991,6 +3996,13 @@ class SfuEvent$Type extends runtime.MessageType {
3991
3996
  oneof: 'eventPayload',
3992
3997
  T: () => ParticipantUpdated,
3993
3998
  },
3999
+ {
4000
+ no: 25,
4001
+ name: 'participant_migration_complete',
4002
+ kind: 'message',
4003
+ oneof: 'eventPayload',
4004
+ T: () => ParticipantMigrationComplete,
4005
+ },
3994
4006
  ]);
3995
4007
  }
3996
4008
  create(value) {
@@ -4125,6 +4137,12 @@ class SfuEvent$Type extends runtime.MessageType {
4125
4137
  participantUpdated: ParticipantUpdated.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantUpdated),
4126
4138
  };
4127
4139
  break;
4140
+ case /* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete */ 25:
4141
+ message.eventPayload = {
4142
+ oneofKind: 'participantMigrationComplete',
4143
+ participantMigrationComplete: ParticipantMigrationComplete.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantMigrationComplete),
4144
+ };
4145
+ break;
4128
4146
  default:
4129
4147
  let u = options.readUnknownField;
4130
4148
  if (u === 'throw')
@@ -4197,6 +4215,9 @@ class SfuEvent$Type extends runtime.MessageType {
4197
4215
  /* stream.video.sfu.event.ParticipantUpdated participant_updated = 24; */
4198
4216
  if (message.eventPayload.oneofKind === 'participantUpdated')
4199
4217
  ParticipantUpdated.internalBinaryWrite(message.eventPayload.participantUpdated, writer.tag(24, runtime.WireType.LengthDelimited).fork(), options).join();
4218
+ /* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete = 25; */
4219
+ if (message.eventPayload.oneofKind === 'participantMigrationComplete')
4220
+ ParticipantMigrationComplete.internalBinaryWrite(message.eventPayload.participantMigrationComplete, writer.tag(25, runtime.WireType.LengthDelimited).fork(), options).join();
4200
4221
  let u = options.writeUnknownFields;
4201
4222
  if (u !== false)
4202
4223
  (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -4208,6 +4229,31 @@ class SfuEvent$Type extends runtime.MessageType {
4208
4229
  */
4209
4230
  const SfuEvent = new SfuEvent$Type();
4210
4231
  // @generated message type with reflection information, may provide speed optimized methods
4232
+ class ParticipantMigrationComplete$Type extends runtime.MessageType {
4233
+ constructor() {
4234
+ super('stream.video.sfu.event.ParticipantMigrationComplete', []);
4235
+ }
4236
+ create(value) {
4237
+ const message = globalThis.Object.create(this.messagePrototype);
4238
+ if (value !== undefined)
4239
+ runtime.reflectionMergePartial(this, message, value);
4240
+ return message;
4241
+ }
4242
+ internalBinaryRead(reader, length, options, target) {
4243
+ return target ?? this.create();
4244
+ }
4245
+ internalBinaryWrite(message, writer, options) {
4246
+ let u = options.writeUnknownFields;
4247
+ if (u !== false)
4248
+ (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
4249
+ return writer;
4250
+ }
4251
+ }
4252
+ /**
4253
+ * @generated MessageType for protobuf message stream.video.sfu.event.ParticipantMigrationComplete
4254
+ */
4255
+ const ParticipantMigrationComplete = new ParticipantMigrationComplete$Type();
4256
+ // @generated message type with reflection information, may provide speed optimized methods
4211
4257
  class PinsChanged$Type extends runtime.MessageType {
4212
4258
  constructor() {
4213
4259
  super('stream.video.sfu.event.PinsChanged', [
@@ -4458,6 +4504,13 @@ class SfuRequest$Type extends runtime.MessageType {
4458
4504
  oneof: 'requestPayload',
4459
4505
  T: () => HealthCheckRequest,
4460
4506
  },
4507
+ {
4508
+ no: 3,
4509
+ name: 'leave_call_request',
4510
+ kind: 'message',
4511
+ oneof: 'requestPayload',
4512
+ T: () => LeaveCallRequest,
4513
+ },
4461
4514
  ]);
4462
4515
  }
4463
4516
  create(value) {
@@ -4484,6 +4537,12 @@ class SfuRequest$Type extends runtime.MessageType {
4484
4537
  healthCheckRequest: HealthCheckRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.healthCheckRequest),
4485
4538
  };
4486
4539
  break;
4540
+ case /* stream.video.sfu.event.LeaveCallRequest leave_call_request */ 3:
4541
+ message.requestPayload = {
4542
+ oneofKind: 'leaveCallRequest',
4543
+ leaveCallRequest: LeaveCallRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.leaveCallRequest),
4544
+ };
4545
+ break;
4487
4546
  default:
4488
4547
  let u = options.readUnknownField;
4489
4548
  if (u === 'throw')
@@ -4502,6 +4561,9 @@ class SfuRequest$Type extends runtime.MessageType {
4502
4561
  /* stream.video.sfu.event.HealthCheckRequest health_check_request = 2; */
4503
4562
  if (message.requestPayload.oneofKind === 'healthCheckRequest')
4504
4563
  HealthCheckRequest.internalBinaryWrite(message.requestPayload.healthCheckRequest, writer.tag(2, runtime.WireType.LengthDelimited).fork(), options).join();
4564
+ /* stream.video.sfu.event.LeaveCallRequest leave_call_request = 3; */
4565
+ if (message.requestPayload.oneofKind === 'leaveCallRequest')
4566
+ LeaveCallRequest.internalBinaryWrite(message.requestPayload.leaveCallRequest, writer.tag(3, runtime.WireType.LengthDelimited).fork(), options).join();
4505
4567
  let u = options.writeUnknownFields;
4506
4568
  if (u !== false)
4507
4569
  (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -4513,19 +4575,74 @@ class SfuRequest$Type extends runtime.MessageType {
4513
4575
  */
4514
4576
  const SfuRequest = new SfuRequest$Type();
4515
4577
  // @generated message type with reflection information, may provide speed optimized methods
4516
- class HealthCheckRequest$Type extends runtime.MessageType {
4578
+ class LeaveCallRequest$Type extends runtime.MessageType {
4517
4579
  constructor() {
4518
- super('stream.video.sfu.event.HealthCheckRequest', []);
4580
+ super('stream.video.sfu.event.LeaveCallRequest', [
4581
+ { no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
4582
+ { no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
4583
+ ]);
4519
4584
  }
4520
4585
  create(value) {
4521
4586
  const message = globalThis.Object.create(this.messagePrototype);
4587
+ message.sessionId = '';
4588
+ message.reason = '';
4522
4589
  if (value !== undefined)
4523
4590
  runtime.reflectionMergePartial(this, message, value);
4524
4591
  return message;
4525
4592
  }
4526
4593
  internalBinaryRead(reader, length, options, target) {
4527
- return target ?? this.create();
4528
- }
4594
+ let message = target ?? this.create(), end = reader.pos + length;
4595
+ while (reader.pos < end) {
4596
+ let [fieldNo, wireType] = reader.tag();
4597
+ switch (fieldNo) {
4598
+ case /* string session_id */ 1:
4599
+ message.sessionId = reader.string();
4600
+ break;
4601
+ case /* string reason */ 2:
4602
+ message.reason = reader.string();
4603
+ break;
4604
+ default:
4605
+ let u = options.readUnknownField;
4606
+ if (u === 'throw')
4607
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
4608
+ let d = reader.skip(wireType);
4609
+ if (u !== false)
4610
+ (u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
4611
+ }
4612
+ }
4613
+ return message;
4614
+ }
4615
+ internalBinaryWrite(message, writer, options) {
4616
+ /* string session_id = 1; */
4617
+ if (message.sessionId !== '')
4618
+ writer.tag(1, runtime.WireType.LengthDelimited).string(message.sessionId);
4619
+ /* string reason = 2; */
4620
+ if (message.reason !== '')
4621
+ writer.tag(2, runtime.WireType.LengthDelimited).string(message.reason);
4622
+ let u = options.writeUnknownFields;
4623
+ if (u !== false)
4624
+ (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
4625
+ return writer;
4626
+ }
4627
+ }
4628
+ /**
4629
+ * @generated MessageType for protobuf message stream.video.sfu.event.LeaveCallRequest
4630
+ */
4631
+ const LeaveCallRequest = new LeaveCallRequest$Type();
4632
+ // @generated message type with reflection information, may provide speed optimized methods
4633
+ class HealthCheckRequest$Type extends runtime.MessageType {
4634
+ constructor() {
4635
+ super('stream.video.sfu.event.HealthCheckRequest', []);
4636
+ }
4637
+ create(value) {
4638
+ const message = globalThis.Object.create(this.messagePrototype);
4639
+ if (value !== undefined)
4640
+ runtime.reflectionMergePartial(this, message, value);
4641
+ return message;
4642
+ }
4643
+ internalBinaryRead(reader, length, options, target) {
4644
+ return target ?? this.create();
4645
+ }
4529
4646
  internalBinaryWrite(message, writer, options) {
4530
4647
  let u = options.writeUnknownFields;
4531
4648
  if (u !== false)
@@ -4788,6 +4905,12 @@ class JoinRequest$Type extends runtime.MessageType {
4788
4905
  kind: 'scalar',
4789
4906
  T: 8 /*ScalarType.BOOL*/,
4790
4907
  },
4908
+ {
4909
+ no: 7,
4910
+ name: 'reconnect_details',
4911
+ kind: 'message',
4912
+ T: () => ReconnectDetails,
4913
+ },
4791
4914
  ]);
4792
4915
  }
4793
4916
  create(value) {
@@ -4817,12 +4940,15 @@ class JoinRequest$Type extends runtime.MessageType {
4817
4940
  case /* stream.video.sfu.models.ClientDetails client_details */ 4:
4818
4941
  message.clientDetails = ClientDetails.internalBinaryRead(reader, reader.uint32(), options, message.clientDetails);
4819
4942
  break;
4820
- case /* stream.video.sfu.event.Migration migration */ 5:
4943
+ case /* stream.video.sfu.event.Migration migration = 5 [deprecated = true];*/ 5:
4821
4944
  message.migration = Migration.internalBinaryRead(reader, reader.uint32(), options, message.migration);
4822
4945
  break;
4823
- case /* bool fast_reconnect */ 6:
4946
+ case /* bool fast_reconnect = 6 [deprecated = true];*/ 6:
4824
4947
  message.fastReconnect = reader.bool();
4825
4948
  break;
4949
+ case /* stream.video.sfu.event.ReconnectDetails reconnect_details */ 7:
4950
+ message.reconnectDetails = ReconnectDetails.internalBinaryRead(reader, reader.uint32(), options, message.reconnectDetails);
4951
+ break;
4826
4952
  default:
4827
4953
  let u = options.readUnknownField;
4828
4954
  if (u === 'throw')
@@ -4847,12 +4973,15 @@ class JoinRequest$Type extends runtime.MessageType {
4847
4973
  /* stream.video.sfu.models.ClientDetails client_details = 4; */
4848
4974
  if (message.clientDetails)
4849
4975
  ClientDetails.internalBinaryWrite(message.clientDetails, writer.tag(4, runtime.WireType.LengthDelimited).fork(), options).join();
4850
- /* stream.video.sfu.event.Migration migration = 5; */
4976
+ /* stream.video.sfu.event.Migration migration = 5 [deprecated = true]; */
4851
4977
  if (message.migration)
4852
4978
  Migration.internalBinaryWrite(message.migration, writer.tag(5, runtime.WireType.LengthDelimited).fork(), options).join();
4853
- /* bool fast_reconnect = 6; */
4979
+ /* bool fast_reconnect = 6 [deprecated = true]; */
4854
4980
  if (message.fastReconnect !== false)
4855
4981
  writer.tag(6, runtime.WireType.Varint).bool(message.fastReconnect);
4982
+ /* stream.video.sfu.event.ReconnectDetails reconnect_details = 7; */
4983
+ if (message.reconnectDetails)
4984
+ ReconnectDetails.internalBinaryWrite(message.reconnectDetails, writer.tag(7, runtime.WireType.LengthDelimited).fork(), options).join();
4856
4985
  let u = options.writeUnknownFields;
4857
4986
  if (u !== false)
4858
4987
  (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -4864,6 +4993,129 @@ class JoinRequest$Type extends runtime.MessageType {
4864
4993
  */
4865
4994
  const JoinRequest = new JoinRequest$Type();
4866
4995
  // @generated message type with reflection information, may provide speed optimized methods
4996
+ class ReconnectDetails$Type extends runtime.MessageType {
4997
+ constructor() {
4998
+ super('stream.video.sfu.event.ReconnectDetails', [
4999
+ {
5000
+ no: 1,
5001
+ name: 'strategy',
5002
+ kind: 'enum',
5003
+ T: () => [
5004
+ 'stream.video.sfu.models.WebsocketReconnectStrategy',
5005
+ WebsocketReconnectStrategy,
5006
+ 'WEBSOCKET_RECONNECT_STRATEGY_',
5007
+ ],
5008
+ },
5009
+ {
5010
+ no: 3,
5011
+ name: 'announced_tracks',
5012
+ kind: 'message',
5013
+ repeat: 1 /*RepeatType.PACKED*/,
5014
+ T: () => TrackInfo,
5015
+ },
5016
+ {
5017
+ no: 4,
5018
+ name: 'subscriptions',
5019
+ kind: 'message',
5020
+ repeat: 1 /*RepeatType.PACKED*/,
5021
+ T: () => TrackSubscriptionDetails,
5022
+ },
5023
+ {
5024
+ no: 5,
5025
+ name: 'reconnect_attempt',
5026
+ kind: 'scalar',
5027
+ T: 13 /*ScalarType.UINT32*/,
5028
+ },
5029
+ {
5030
+ no: 6,
5031
+ name: 'from_sfu_id',
5032
+ kind: 'scalar',
5033
+ T: 9 /*ScalarType.STRING*/,
5034
+ },
5035
+ {
5036
+ no: 7,
5037
+ name: 'previous_session_id',
5038
+ kind: 'scalar',
5039
+ T: 9 /*ScalarType.STRING*/,
5040
+ },
5041
+ ]);
5042
+ }
5043
+ create(value) {
5044
+ const message = globalThis.Object.create(this.messagePrototype);
5045
+ message.strategy = 0;
5046
+ message.announcedTracks = [];
5047
+ message.subscriptions = [];
5048
+ message.reconnectAttempt = 0;
5049
+ message.fromSfuId = '';
5050
+ message.previousSessionId = '';
5051
+ if (value !== undefined)
5052
+ runtime.reflectionMergePartial(this, message, value);
5053
+ return message;
5054
+ }
5055
+ internalBinaryRead(reader, length, options, target) {
5056
+ let message = target ?? this.create(), end = reader.pos + length;
5057
+ while (reader.pos < end) {
5058
+ let [fieldNo, wireType] = reader.tag();
5059
+ switch (fieldNo) {
5060
+ case /* stream.video.sfu.models.WebsocketReconnectStrategy strategy */ 1:
5061
+ message.strategy = reader.int32();
5062
+ break;
5063
+ case /* repeated stream.video.sfu.models.TrackInfo announced_tracks */ 3:
5064
+ message.announcedTracks.push(TrackInfo.internalBinaryRead(reader, reader.uint32(), options));
5065
+ break;
5066
+ case /* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions */ 4:
5067
+ message.subscriptions.push(TrackSubscriptionDetails.internalBinaryRead(reader, reader.uint32(), options));
5068
+ break;
5069
+ case /* uint32 reconnect_attempt */ 5:
5070
+ message.reconnectAttempt = reader.uint32();
5071
+ break;
5072
+ case /* string from_sfu_id */ 6:
5073
+ message.fromSfuId = reader.string();
5074
+ break;
5075
+ case /* string previous_session_id */ 7:
5076
+ message.previousSessionId = reader.string();
5077
+ break;
5078
+ default:
5079
+ let u = options.readUnknownField;
5080
+ if (u === 'throw')
5081
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
5082
+ let d = reader.skip(wireType);
5083
+ if (u !== false)
5084
+ (u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
5085
+ }
5086
+ }
5087
+ return message;
5088
+ }
5089
+ internalBinaryWrite(message, writer, options) {
5090
+ /* stream.video.sfu.models.WebsocketReconnectStrategy strategy = 1; */
5091
+ if (message.strategy !== 0)
5092
+ writer.tag(1, runtime.WireType.Varint).int32(message.strategy);
5093
+ /* repeated stream.video.sfu.models.TrackInfo announced_tracks = 3; */
5094
+ for (let i = 0; i < message.announcedTracks.length; i++)
5095
+ TrackInfo.internalBinaryWrite(message.announcedTracks[i], writer.tag(3, runtime.WireType.LengthDelimited).fork(), options).join();
5096
+ /* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions = 4; */
5097
+ for (let i = 0; i < message.subscriptions.length; i++)
5098
+ TrackSubscriptionDetails.internalBinaryWrite(message.subscriptions[i], writer.tag(4, runtime.WireType.LengthDelimited).fork(), options).join();
5099
+ /* uint32 reconnect_attempt = 5; */
5100
+ if (message.reconnectAttempt !== 0)
5101
+ writer.tag(5, runtime.WireType.Varint).uint32(message.reconnectAttempt);
5102
+ /* string from_sfu_id = 6; */
5103
+ if (message.fromSfuId !== '')
5104
+ writer.tag(6, runtime.WireType.LengthDelimited).string(message.fromSfuId);
5105
+ /* string previous_session_id = 7; */
5106
+ if (message.previousSessionId !== '')
5107
+ writer.tag(7, runtime.WireType.LengthDelimited).string(message.previousSessionId);
5108
+ let u = options.writeUnknownFields;
5109
+ if (u !== false)
5110
+ (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
5111
+ return writer;
5112
+ }
5113
+ }
5114
+ /**
5115
+ * @generated MessageType for protobuf message stream.video.sfu.event.ReconnectDetails
5116
+ */
5117
+ const ReconnectDetails = new ReconnectDetails$Type();
5118
+ // @generated message type with reflection information, may provide speed optimized methods
4867
5119
  class Migration$Type extends runtime.MessageType {
4868
5120
  constructor() {
4869
5121
  super('stream.video.sfu.event.Migration', [
@@ -4949,11 +5201,18 @@ class JoinResponse$Type extends runtime.MessageType {
4949
5201
  super('stream.video.sfu.event.JoinResponse', [
4950
5202
  { no: 1, name: 'call_state', kind: 'message', T: () => CallState$1 },
4951
5203
  { no: 2, name: 'reconnected', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
5204
+ {
5205
+ no: 3,
5206
+ name: 'fast_reconnect_deadline_seconds',
5207
+ kind: 'scalar',
5208
+ T: 5 /*ScalarType.INT32*/,
5209
+ },
4952
5210
  ]);
4953
5211
  }
4954
5212
  create(value) {
4955
5213
  const message = globalThis.Object.create(this.messagePrototype);
4956
5214
  message.reconnected = false;
5215
+ message.fastReconnectDeadlineSeconds = 0;
4957
5216
  if (value !== undefined)
4958
5217
  runtime.reflectionMergePartial(this, message, value);
4959
5218
  return message;
@@ -4969,6 +5228,9 @@ class JoinResponse$Type extends runtime.MessageType {
4969
5228
  case /* bool reconnected */ 2:
4970
5229
  message.reconnected = reader.bool();
4971
5230
  break;
5231
+ case /* int32 fast_reconnect_deadline_seconds */ 3:
5232
+ message.fastReconnectDeadlineSeconds = reader.int32();
5233
+ break;
4972
5234
  default:
4973
5235
  let u = options.readUnknownField;
4974
5236
  if (u === 'throw')
@@ -4987,6 +5249,11 @@ class JoinResponse$Type extends runtime.MessageType {
4987
5249
  /* bool reconnected = 2; */
4988
5250
  if (message.reconnected !== false)
4989
5251
  writer.tag(2, runtime.WireType.Varint).bool(message.reconnected);
5252
+ /* int32 fast_reconnect_deadline_seconds = 3; */
5253
+ if (message.fastReconnectDeadlineSeconds !== 0)
5254
+ writer
5255
+ .tag(3, runtime.WireType.Varint)
5256
+ .int32(message.fastReconnectDeadlineSeconds);
4990
5257
  let u = options.writeUnknownFields;
4991
5258
  if (u !== false)
4992
5259
  (u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -6187,12 +6454,15 @@ var events = /*#__PURE__*/Object.freeze({
6187
6454
  ICETrickle: ICETrickle,
6188
6455
  JoinRequest: JoinRequest,
6189
6456
  JoinResponse: JoinResponse,
6457
+ LeaveCallRequest: LeaveCallRequest,
6190
6458
  Migration: Migration,
6191
6459
  ParticipantJoined: ParticipantJoined,
6192
6460
  ParticipantLeft: ParticipantLeft,
6461
+ ParticipantMigrationComplete: ParticipantMigrationComplete,
6193
6462
  ParticipantUpdated: ParticipantUpdated,
6194
6463
  PinsChanged: PinsChanged,
6195
6464
  PublisherAnswer: PublisherAnswer,
6465
+ ReconnectDetails: ReconnectDetails,
6196
6466
  SfuEvent: SfuEvent,
6197
6467
  SfuRequest: SfuRequest,
6198
6468
  SubscriberOffer: SubscriberOffer,
@@ -6318,6 +6588,17 @@ const withHeaders = (headers) => {
6318
6588
  },
6319
6589
  };
6320
6590
  };
6591
+ const withRequestLogger = (logger, level) => {
6592
+ return {
6593
+ interceptUnary: (next, method, input, options) => {
6594
+ logger(level, `Calling SFU RPC method ${method.name}`, {
6595
+ input,
6596
+ options,
6597
+ });
6598
+ return next(method, input, options);
6599
+ },
6600
+ };
6601
+ };
6321
6602
  /**
6322
6603
  * Creates new SignalServerClient instance.
6323
6604
  *
@@ -6331,68 +6612,196 @@ const createSignalClient = (options) => {
6331
6612
  return new SignalServerClient(transport);
6332
6613
  };
6333
6614
 
6615
+ const sleep = (m) => new Promise((r) => setTimeout(r, m));
6616
+ function isFunction(value) {
6617
+ return (value &&
6618
+ (Object.prototype.toString.call(value) === '[object Function]' ||
6619
+ 'function' === typeof value ||
6620
+ value instanceof Function));
6621
+ }
6334
6622
  /**
6335
- * Checks whether we are using React Native
6623
+ * A map of known error codes.
6336
6624
  */
6337
- const isReactNative = () => {
6338
- if (typeof navigator === 'undefined')
6339
- return false;
6340
- return navigator.product?.toLowerCase() === 'reactnative';
6625
+ const KnownCodes = {
6626
+ TOKEN_EXPIRED: 40,
6627
+ WS_CLOSED_SUCCESS: 1000,
6628
+ WS_CLOSED_ABRUPTLY: 1006,
6629
+ WS_POLICY_VIOLATION: 1008,
6341
6630
  };
6342
-
6343
- // log levels, sorted by verbosity
6344
- const logLevels = Object.freeze({
6345
- trace: 0,
6346
- debug: 1,
6347
- info: 2,
6348
- warn: 3,
6349
- error: 4,
6350
- });
6351
- let logger$4;
6352
- let level = 'info';
6353
- const logToConsole = (logLevel, message, ...args) => {
6354
- let logMethod;
6355
- switch (logLevel) {
6356
- case 'error':
6357
- if (isReactNative()) {
6358
- message = `ERROR: ${message}`;
6359
- logMethod = console.info;
6360
- break;
6361
- }
6362
- logMethod = console.error;
6363
- break;
6364
- case 'warn':
6365
- if (isReactNative()) {
6366
- message = `WARN: ${message}`;
6367
- logMethod = console.info;
6368
- break;
6369
- }
6370
- logMethod = console.warn;
6371
- break;
6372
- case 'info':
6373
- logMethod = console.info;
6374
- break;
6375
- case 'trace':
6376
- logMethod = console.trace;
6377
- break;
6378
- default:
6379
- logMethod = console.log;
6380
- break;
6631
+ /**
6632
+ * retryInterval - A retry interval which increases acc to number of failures
6633
+ *
6634
+ * @return {number} Duration to wait in milliseconds
6635
+ */
6636
+ function retryInterval(numberOfFailures) {
6637
+ // try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
6638
+ const max = Math.min(500 + numberOfFailures * 2000, 5000);
6639
+ const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
6640
+ return Math.floor(Math.random() * (max - min) + min);
6641
+ }
6642
+ function randomId() {
6643
+ return generateUUIDv4();
6644
+ }
6645
+ function hex(bytes) {
6646
+ let s = '';
6647
+ for (let i = 0; i < bytes.length; i++) {
6648
+ s += bytes[i].toString(16).padStart(2, '0');
6381
6649
  }
6382
- logMethod(message, ...args);
6383
- };
6384
- const setLogger = (l, lvl) => {
6385
- logger$4 = l;
6386
- if (lvl) {
6387
- setLogLevel(lvl);
6650
+ return s;
6651
+ }
6652
+ // https://tools.ietf.org/html/rfc4122
6653
+ function generateUUIDv4() {
6654
+ const bytes = getRandomBytes(16);
6655
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
6656
+ bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
6657
+ return (hex(bytes.subarray(0, 4)) +
6658
+ '-' +
6659
+ hex(bytes.subarray(4, 6)) +
6660
+ '-' +
6661
+ hex(bytes.subarray(6, 8)) +
6662
+ '-' +
6663
+ hex(bytes.subarray(8, 10)) +
6664
+ '-' +
6665
+ hex(bytes.subarray(10, 16)));
6666
+ }
6667
+ function getRandomValuesWithMathRandom(bytes) {
6668
+ const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
6669
+ for (let i = 0; i < bytes.length; i++) {
6670
+ bytes[i] = Math.random() * max;
6388
6671
  }
6389
- };
6390
- const setLogLevel = (l) => {
6391
- level = l;
6392
- };
6672
+ }
6673
+ const getRandomValues = (() => {
6674
+ if (typeof crypto !== 'undefined' &&
6675
+ typeof crypto?.getRandomValues !== 'undefined') {
6676
+ return crypto.getRandomValues.bind(crypto);
6677
+ }
6678
+ else if (typeof msCrypto !== 'undefined') {
6679
+ return msCrypto.getRandomValues.bind(msCrypto);
6680
+ }
6681
+ else {
6682
+ return getRandomValuesWithMathRandom;
6683
+ }
6684
+ })();
6685
+ function getRandomBytes(length) {
6686
+ const bytes = new Uint8Array(length);
6687
+ getRandomValues(bytes);
6688
+ return bytes;
6689
+ }
6690
+ function convertErrorToJson(err) {
6691
+ const jsonObj = {};
6692
+ if (!err)
6693
+ return jsonObj;
6694
+ try {
6695
+ Object.getOwnPropertyNames(err).forEach((key) => {
6696
+ jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
6697
+ });
6698
+ }
6699
+ catch (_) {
6700
+ return {
6701
+ error: 'failed to serialize the error',
6702
+ };
6703
+ }
6704
+ return jsonObj;
6705
+ }
6706
+ /**
6707
+ * isOnline safely return the navigator.online value for browser env
6708
+ * if navigator is not in global object, it always return true
6709
+ */
6710
+ function isOnline(logger) {
6711
+ const nav = typeof navigator !== 'undefined'
6712
+ ? navigator
6713
+ : typeof window !== 'undefined' && window.navigator
6714
+ ? window.navigator
6715
+ : undefined;
6716
+ if (!nav) {
6717
+ logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
6718
+ return true;
6719
+ }
6720
+ // RN navigator has undefined for onLine
6721
+ if (typeof nav.onLine !== 'boolean') {
6722
+ return true;
6723
+ }
6724
+ return nav.onLine;
6725
+ }
6726
+ /**
6727
+ * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
6728
+ */
6729
+ function addConnectionEventListeners(cb) {
6730
+ if (typeof window !== 'undefined' && window.addEventListener) {
6731
+ window.addEventListener('offline', cb);
6732
+ window.addEventListener('online', cb);
6733
+ }
6734
+ }
6735
+ function removeConnectionEventListeners(cb) {
6736
+ if (typeof window !== 'undefined' && window.removeEventListener) {
6737
+ window.removeEventListener('offline', cb);
6738
+ window.removeEventListener('online', cb);
6739
+ }
6740
+ }
6741
+
6742
+ /**
6743
+ * Checks whether we are using React Native
6744
+ */
6745
+ const isReactNative = () => {
6746
+ if (typeof navigator === 'undefined')
6747
+ return false;
6748
+ return navigator.product?.toLowerCase() === 'reactnative';
6749
+ };
6750
+
6751
+ // log levels, sorted by verbosity
6752
+ const logLevels = Object.freeze({
6753
+ trace: 0,
6754
+ debug: 1,
6755
+ info: 2,
6756
+ warn: 3,
6757
+ error: 4,
6758
+ });
6759
+ let logger$2;
6760
+ let level = 'info';
6761
+ const logToConsole = (logLevel, message, ...args) => {
6762
+ let logMethod;
6763
+ switch (logLevel) {
6764
+ case 'error':
6765
+ if (isReactNative()) {
6766
+ message = `ERROR: ${message}`;
6767
+ logMethod = console.info;
6768
+ break;
6769
+ }
6770
+ logMethod = console.error;
6771
+ break;
6772
+ case 'warn':
6773
+ if (isReactNative()) {
6774
+ message = `WARN: ${message}`;
6775
+ logMethod = console.info;
6776
+ break;
6777
+ }
6778
+ logMethod = console.warn;
6779
+ break;
6780
+ case 'info':
6781
+ logMethod = console.info;
6782
+ break;
6783
+ case 'trace':
6784
+ logMethod = console.trace;
6785
+ break;
6786
+ default:
6787
+ logMethod = console.log;
6788
+ break;
6789
+ }
6790
+ logMethod(message, ...args);
6791
+ };
6792
+ const setLogger = (l, lvl) => {
6793
+ logger$2 = l;
6794
+ if (lvl) {
6795
+ setLogLevel(lvl);
6796
+ }
6797
+ };
6798
+ const setLogLevel = (l) => {
6799
+ level = l;
6800
+ };
6801
+ const getLogLevel = () => level;
6393
6802
  const getLogger = (withTags) => {
6394
- const loggerMethod = logger$4 || logToConsole;
6395
- const tags = (withTags || []).join(':');
6803
+ const loggerMethod = logger$2 || logToConsole;
6804
+ const tags = (withTags || []).filter(Boolean).join(':');
6396
6805
  const result = (logLevel, message, ...args) => {
6397
6806
  if (logLevels[logLevel] >= logLevels[level]) {
6398
6807
  loggerMethod(logLevel, `[${tags}]: ${message}`, ...args);
@@ -6401,6 +6810,37 @@ const getLogger = (withTags) => {
6401
6810
  return result;
6402
6811
  };
6403
6812
 
6813
+ /**
6814
+ * Creates a closure which wraps the given RPC call and retries invoking
6815
+ * the RPC until it succeeds or the maximum number of retries is reached.
6816
+ *
6817
+ * For each retry, there would be a delay to avoid request bursts toward the SFU.
6818
+ *
6819
+ * @param rpc the closure around the RPC call to execute.
6820
+ * @param signal the signal to abort the RPC call and retries loop.
6821
+ */
6822
+ const retryable = async (rpc, signal) => {
6823
+ let attempt = 0;
6824
+ let result = undefined;
6825
+ do {
6826
+ if (attempt > 0)
6827
+ await sleep(retryInterval(attempt));
6828
+ try {
6829
+ result = await rpc();
6830
+ }
6831
+ catch (err) {
6832
+ const isRequestCancelled = err instanceof runtimeRpc.RpcError &&
6833
+ err.code === twirpTransport.TwirpErrorCode[twirpTransport.TwirpErrorCode.cancelled];
6834
+ const isAborted = signal?.aborted ?? false;
6835
+ if (isRequestCancelled || isAborted)
6836
+ throw err;
6837
+ getLogger(['sfu-client', 'rpc'])('debug', `rpc failed (${attempt})`, err);
6838
+ attempt++;
6839
+ }
6840
+ } while (!result || result.response.error?.shouldRetry);
6841
+ return result;
6842
+ };
6843
+
6404
6844
  const getPreferredCodecs = (kind, preferredCodec, codecToRemove) => {
6405
6845
  const logger = getLogger(['codecs']);
6406
6846
  if (!('getCapabilities' in RTCRtpReceiver)) {
@@ -6473,6 +6913,7 @@ const sfuEventKinds = {
6473
6913
  pinsUpdated: undefined,
6474
6914
  callEnded: undefined,
6475
6915
  participantUpdated: undefined,
6916
+ participantMigrationComplete: undefined,
6476
6917
  };
6477
6918
  const isSfuEvent = (eventName) => {
6478
6919
  return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
@@ -6481,12 +6922,12 @@ class Dispatcher {
6481
6922
  constructor() {
6482
6923
  this.logger = getLogger(['Dispatcher']);
6483
6924
  this.subscribers = {};
6484
- this.dispatch = (message) => {
6925
+ this.dispatch = (message, logTag) => {
6485
6926
  const eventKind = message.eventPayload.oneofKind;
6486
6927
  if (!eventKind)
6487
6928
  return;
6488
6929
  const payload = message.eventPayload[eventKind];
6489
- this.logger('debug', `Dispatching ${eventKind}`, payload);
6930
+ this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
6490
6931
  const listeners = this.subscribers[eventKind];
6491
6932
  if (!listeners)
6492
6933
  return;
@@ -6528,7 +6969,6 @@ class IceTrickleBuffer {
6528
6969
  constructor() {
6529
6970
  this.subscriberCandidates = new rxjs.ReplaySubject();
6530
6971
  this.publisherCandidates = new rxjs.ReplaySubject();
6531
- this.logger = getLogger(['sfu-client']);
6532
6972
  this.push = (iceTrickle) => {
6533
6973
  if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
6534
6974
  this.subscriberCandidates.next(iceTrickle);
@@ -6537,7 +6977,8 @@ class IceTrickleBuffer {
6537
6977
  this.publisherCandidates.next(iceTrickle);
6538
6978
  }
6539
6979
  else {
6540
- this.logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
6980
+ const logger = getLogger(['sfu-client']);
6981
+ logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
6541
6982
  }
6542
6983
  };
6543
6984
  }
@@ -6556,72 +6997,6 @@ function getIceCandidate(candidate) {
6556
6997
  }
6557
6998
  }
6558
6999
 
6559
- const version = "1.5.0" ;
6560
- const [major, minor, patch] = version.split('.');
6561
- let sdkInfo = {
6562
- type: SdkType.PLAIN_JAVASCRIPT,
6563
- major,
6564
- minor,
6565
- patch,
6566
- };
6567
- let osInfo;
6568
- let deviceInfo;
6569
- let webRtcInfo;
6570
- const setSdkInfo = (info) => {
6571
- sdkInfo = info;
6572
- };
6573
- const getSdkInfo = () => {
6574
- return sdkInfo;
6575
- };
6576
- const setOSInfo = (info) => {
6577
- osInfo = info;
6578
- };
6579
- const getOSInfo = () => {
6580
- return osInfo;
6581
- };
6582
- const setDeviceInfo = (info) => {
6583
- deviceInfo = info;
6584
- };
6585
- const getDeviceInfo = () => {
6586
- return deviceInfo;
6587
- };
6588
- const getWebRTCInfo = () => {
6589
- return webRtcInfo;
6590
- };
6591
- const setWebRTCInfo = (info) => {
6592
- webRtcInfo = info;
6593
- };
6594
- const getClientDetails = () => {
6595
- if (isReactNative()) {
6596
- // Since RN doesn't support web, sharing browser info is not required
6597
- return {
6598
- sdk: getSdkInfo(),
6599
- os: getOSInfo(),
6600
- device: getDeviceInfo(),
6601
- };
6602
- }
6603
- const userAgent = new uaParserJs.UAParser(navigator.userAgent);
6604
- const { browser, os, device, cpu } = userAgent.getResult();
6605
- return {
6606
- sdk: getSdkInfo(),
6607
- browser: {
6608
- name: browser.name || navigator.userAgent,
6609
- version: browser.version || '',
6610
- },
6611
- os: {
6612
- name: os.name || '',
6613
- version: os.version || '',
6614
- architecture: cpu.architecture || '',
6615
- },
6616
- device: {
6617
- name: [device.vendor, device.model, device.type]
6618
- .filter(Boolean)
6619
- .join(' '),
6620
- version: '',
6621
- },
6622
- };
6623
- };
6624
-
6625
7000
  const DEFAULT_BITRATE = 1250000;
6626
7001
  const defaultTargetResolution = {
6627
7002
  bitrate: DEFAULT_BITRATE,
@@ -6644,7 +7019,6 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
6644
7019
  const optimalVideoLayers = [];
6645
7020
  const settings = videoTrack.getSettings();
6646
7021
  const { width: w = 0, height: h = 0 } = settings;
6647
- const isRNIos = isReactNative() && getOSInfo()?.name.toLowerCase() === 'ios';
6648
7022
  const maxBitrate = getComputedMaxBitrate(targetResolution, w, h);
6649
7023
  let downscaleFactor = 1;
6650
7024
  ['f', 'h', 'q'].forEach((rid) => {
@@ -6658,12 +7032,7 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
6658
7032
  height: Math.round(h / downscaleFactor),
6659
7033
  maxBitrate: Math.round(maxBitrate / downscaleFactor) || defaultBitratePerRid[rid],
6660
7034
  scaleResolutionDownBy: downscaleFactor,
6661
- // Simulcast on iOS React-Native requires all encodings to share the same framerate
6662
- maxFramerate: {
6663
- f: 30,
6664
- h: isRNIos ? 30 : 25,
6665
- q: isRNIos ? 30 : 20,
6666
- }[rid],
7035
+ maxFramerate: 30,
6667
7036
  });
6668
7037
  downscaleFactor *= 2;
6669
7038
  });
@@ -6738,6 +7107,10 @@ const findOptimalScreenSharingLayers = (videoTrack, preferences, defaultMaxBitra
6738
7107
  ];
6739
7108
  };
6740
7109
 
7110
+ const ensureExhausted = (x, message) => {
7111
+ getLogger(['helpers'])('warn', message, x);
7112
+ };
7113
+
6741
7114
  const trackTypeToParticipantStreamKey = (trackType) => {
6742
7115
  switch (trackType) {
6743
7116
  case TrackType.SCREEN_SHARE:
@@ -6751,8 +7124,7 @@ const trackTypeToParticipantStreamKey = (trackType) => {
6751
7124
  case TrackType.UNSPECIFIED:
6752
7125
  throw new Error('Track type is unspecified');
6753
7126
  default:
6754
- const exhaustiveTrackTypeCheck = trackType;
6755
- throw new Error(`Unknown track type: ${exhaustiveTrackTypeCheck}`);
7127
+ ensureExhausted(trackType, 'Unknown track type');
6756
7128
  }
6757
7129
  };
6758
7130
  const muteTypeToTrackType = (muteType) => {
@@ -6766,8 +7138,21 @@ const muteTypeToTrackType = (muteType) => {
6766
7138
  case 'screenshare_audio':
6767
7139
  return TrackType.SCREEN_SHARE_AUDIO;
6768
7140
  default:
6769
- const exhaustiveMuteTypeCheck = muteType;
6770
- throw new Error(`Unknown mute type: ${exhaustiveMuteTypeCheck}`);
7141
+ ensureExhausted(muteType, 'Unknown mute type');
7142
+ }
7143
+ };
7144
+ const toTrackType = (trackType) => {
7145
+ switch (trackType) {
7146
+ case 'TRACK_TYPE_AUDIO':
7147
+ return TrackType.AUDIO;
7148
+ case 'TRACK_TYPE_VIDEO':
7149
+ return TrackType.VIDEO;
7150
+ case 'TRACK_TYPE_SCREEN_SHARE':
7151
+ return TrackType.SCREEN_SHARE;
7152
+ case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
7153
+ return TrackType.SCREEN_SHARE_AUDIO;
7154
+ default:
7155
+ return undefined;
6771
7156
  }
6772
7157
  };
6773
7158
 
@@ -7287,6 +7672,11 @@ class CallState {
7287
7672
  this.anonymousParticipantCountSubject = new rxjs.BehaviorSubject(0);
7288
7673
  this.participantsSubject = new rxjs.BehaviorSubject([]);
7289
7674
  this.callStatsReportSubject = new rxjs.BehaviorSubject(undefined);
7675
+ // These are tracks that were delivered to the Subscriber's onTrack event
7676
+ // that we couldn't associate with a participant yet.
7677
+ // This happens when the participantJoined event hasn't been received yet.
7678
+ // We keep these tracks around until we can associate them with a participant.
7679
+ this.orphanedTracks = [];
7290
7680
  this.logger = getLogger(['CallState']);
7291
7681
  /**
7292
7682
  * A list of comparators that are used to sort the participants.
@@ -7476,7 +7866,7 @@ class CallState {
7476
7866
  */
7477
7867
  this.updateParticipants = (patch) => {
7478
7868
  if (Object.keys(patch).length === 0)
7479
- return;
7869
+ return this.participants;
7480
7870
  return this.setParticipants((participants) => participants.map((p) => {
7481
7871
  const thePatch = patch[p.sessionId];
7482
7872
  if (thePatch) {
@@ -7535,6 +7925,41 @@ class CallState {
7535
7925
  return participant;
7536
7926
  }));
7537
7927
  };
7928
+ /**
7929
+ * Adds an orphaned track to the call state.
7930
+ *
7931
+ * @internal
7932
+ *
7933
+ * @param orphanedTrack the orphaned track to add.
7934
+ */
7935
+ this.registerOrphanedTrack = (orphanedTrack) => {
7936
+ this.orphanedTracks.push(orphanedTrack);
7937
+ };
7938
+ /**
7939
+ * Removes an orphaned track from the call state.
7940
+ *
7941
+ * @internal
7942
+ *
7943
+ * @param id the ID of the orphaned track to remove.
7944
+ */
7945
+ this.removeOrphanedTrack = (id) => {
7946
+ this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
7947
+ };
7948
+ /**
7949
+ * Takes all orphaned tracks with the given track lookup prefix.
7950
+ * All orphaned tracks with the given track lookup prefix are removed from the call state.
7951
+ *
7952
+ * @internal
7953
+ *
7954
+ * @param trackLookupPrefix the track lookup prefix to match the orphaned tracks by.
7955
+ */
7956
+ this.takeOrphanedTracks = (trackLookupPrefix) => {
7957
+ const orphans = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix === trackLookupPrefix);
7958
+ if (orphans.length > 0) {
7959
+ this.orphanedTracks = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix !== trackLookupPrefix);
7960
+ }
7961
+ return orphans;
7962
+ };
7538
7963
  /**
7539
7964
  * Updates the call state with the data received from the server.
7540
7965
  *
@@ -7559,6 +7984,43 @@ class CallState {
7559
7984
  this.setCurrentValue(this.transcribingSubject, call.transcribing);
7560
7985
  this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
7561
7986
  };
7987
+ /**
7988
+ * Updates the call state with the data received from the SFU server.
7989
+ *
7990
+ * @internal
7991
+ *
7992
+ * @param callState the call state from the SFU server.
7993
+ * @param currentSessionId the session ID of the current user.
7994
+ * @param reconnectDetails optional reconnect details.
7995
+ */
7996
+ this.updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
7997
+ const { participants, participantCount, startedAt, pins } = callState;
7998
+ const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
7999
+ this.setParticipants(() => {
8000
+ const participantLookup = this.getParticipantLookupBySessionId();
8001
+ return participants.map((p) => {
8002
+ // We need to preserve the local state of the participant
8003
+ // (e.g. videoDimension, visibilityState, pinnedAt, etc.)
8004
+ // as it doesn't exist on the server.
8005
+ const existingParticipant = participantLookup[p.sessionId];
8006
+ const isLocalParticipant = p.sessionId === currentSessionId;
8007
+ return Object.assign({}, existingParticipant, p, {
8008
+ isLocalParticipant,
8009
+ publishedTracks: isLocalParticipant
8010
+ ? localPublishedTracks
8011
+ : p.publishedTracks,
8012
+ viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
8013
+ videoTrack: exports.VisibilityState.UNKNOWN,
8014
+ screenShareTrack: exports.VisibilityState.UNKNOWN,
8015
+ },
8016
+ });
8017
+ });
8018
+ });
8019
+ this.setParticipantCount(participantCount?.total || 0);
8020
+ this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
8021
+ this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
8022
+ this.setServerSidePins(pins);
8023
+ };
7562
8024
  this.updateFromMemberRemoved = (event) => {
7563
8025
  this.updateFromCallResponse(event.call);
7564
8026
  this.setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
@@ -8270,13 +8732,79 @@ const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
8270
8732
  return SDP__namespace.write(parsedSdp);
8271
8733
  };
8272
8734
 
8273
- const logger$3 = getLogger(['Publisher']);
8274
- /**
8275
- * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
8276
- * @internal
8277
- */
8278
- class Publisher {
8279
- /**
8735
+ const version = "1.6.0-0" ;
8736
+ const [major, minor, patch] = version.split('.');
8737
+ let sdkInfo = {
8738
+ type: SdkType.PLAIN_JAVASCRIPT,
8739
+ major,
8740
+ minor,
8741
+ patch,
8742
+ };
8743
+ let osInfo;
8744
+ let deviceInfo;
8745
+ let webRtcInfo;
8746
+ const setSdkInfo = (info) => {
8747
+ sdkInfo = info;
8748
+ };
8749
+ const getSdkInfo = () => {
8750
+ return sdkInfo;
8751
+ };
8752
+ const setOSInfo = (info) => {
8753
+ osInfo = info;
8754
+ };
8755
+ const getOSInfo = () => {
8756
+ return osInfo;
8757
+ };
8758
+ const setDeviceInfo = (info) => {
8759
+ deviceInfo = info;
8760
+ };
8761
+ const getDeviceInfo = () => {
8762
+ return deviceInfo;
8763
+ };
8764
+ const getWebRTCInfo = () => {
8765
+ return webRtcInfo;
8766
+ };
8767
+ const setWebRTCInfo = (info) => {
8768
+ webRtcInfo = info;
8769
+ };
8770
+ const getClientDetails = () => {
8771
+ if (isReactNative()) {
8772
+ // Since RN doesn't support web, sharing browser info is not required
8773
+ return {
8774
+ sdk: getSdkInfo(),
8775
+ os: getOSInfo(),
8776
+ device: getDeviceInfo(),
8777
+ };
8778
+ }
8779
+ const userAgent = new uaParserJs.UAParser(navigator.userAgent);
8780
+ const { browser, os, device, cpu } = userAgent.getResult();
8781
+ return {
8782
+ sdk: getSdkInfo(),
8783
+ browser: {
8784
+ name: browser.name || navigator.userAgent,
8785
+ version: browser.version || '',
8786
+ },
8787
+ os: {
8788
+ name: os.name || '',
8789
+ version: os.version || '',
8790
+ architecture: cpu.architecture || '',
8791
+ },
8792
+ device: {
8793
+ name: [device.vendor, device.model, device.type]
8794
+ .filter(Boolean)
8795
+ .join(' '),
8796
+ version: '',
8797
+ },
8798
+ };
8799
+ };
8800
+
8801
+ /**
8802
+ * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
8803
+ *
8804
+ * @internal
8805
+ */
8806
+ class Publisher {
8807
+ /**
8280
8808
  * Returns the current connection configuration.
8281
8809
  *
8282
8810
  * @internal
@@ -8297,8 +8825,9 @@ class Publisher {
8297
8825
  * @param isRedEnabled whether RED is enabled.
8298
8826
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
8299
8827
  * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
8828
+ * @param logTag the log tag to use.
8300
8829
  */
8301
- constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, iceRestartDelay = 2500, onUnrecoverableError, }) {
8830
+ constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, onUnrecoverableError, logTag, }) {
8302
8831
  this.transceiverRegistry = {
8303
8832
  [TrackType.AUDIO]: undefined,
8304
8833
  [TrackType.VIDEO]: undefined,
@@ -8312,7 +8841,7 @@ class Publisher {
8312
8841
  * This is needed because some browsers (Firefox) don't reliably report
8313
8842
  * trackId and `mid` parameters.
8314
8843
  *
8315
- * @private
8844
+ * @internal
8316
8845
  */
8317
8846
  this.transceiverInitOrder = [];
8318
8847
  this.trackKindMapping = {
@@ -8344,7 +8873,7 @@ class Publisher {
8344
8873
  /**
8345
8874
  * Closes the publisher PeerConnection and cleans up the resources.
8346
8875
  */
8347
- this.close = ({ stopTracks = true } = {}) => {
8876
+ this.close = ({ stopTracks }) => {
8348
8877
  if (stopTracks) {
8349
8878
  this.stopPublishing();
8350
8879
  Object.keys(this.transceiverRegistry).forEach((trackType) => {
@@ -8356,10 +8885,22 @@ class Publisher {
8356
8885
  this.trackLayersCache[trackType] = undefined;
8357
8886
  });
8358
8887
  }
8359
- clearTimeout(this.iceRestartTimeout);
8888
+ this.detachEventHandlers();
8889
+ this.pc.close();
8890
+ };
8891
+ /**
8892
+ * Detaches the event handlers from the `RTCPeerConnection`.
8893
+ * This is useful when we want to replace the `RTCPeerConnection`
8894
+ * instance with a new one (in case of migration).
8895
+ */
8896
+ this.detachEventHandlers = () => {
8360
8897
  this.unsubscribeOnIceRestart();
8898
+ this.pc.removeEventListener('icecandidate', this.onIceCandidate);
8361
8899
  this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
8362
- this.pc.close();
8900
+ this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
8901
+ this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
8902
+ this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
8903
+ this.pc.removeEventListener('signalingstatechange', this.onSignalingStateChange);
8363
8904
  };
8364
8905
  /**
8365
8906
  * Starts publishing the given track of the given media stream.
@@ -8386,7 +8927,7 @@ class Publisher {
8386
8927
  * Once the track has ended, it will notify the SFU and update the state.
8387
8928
  */
8388
8929
  const handleTrackEnded = async () => {
8389
- logger$3('info', `Track ${TrackType[trackType]} has ended, notifying the SFU`);
8930
+ this.logger('info', `Track ${TrackType[trackType]} has ended, notifying the SFU`);
8390
8931
  await this.notifyTrackMuteStateChanged(mediaStream, trackType, true);
8391
8932
  // clean-up, this event listener needs to run only once.
8392
8933
  track.removeEventListener('ended', handleTrackEnded);
@@ -8402,21 +8943,18 @@ class Publisher {
8402
8943
  ? findOptimalScreenSharingLayers(track, opts.screenShareSettings, screenShareBitrate)
8403
8944
  : undefined;
8404
8945
  let preferredCodec = opts.preferredCodec;
8405
- if (!preferredCodec && trackType === TrackType.VIDEO) {
8406
- if (isReactNative()) {
8407
- const osName = getOSInfo()?.name.toLowerCase();
8408
- if (osName === 'ipados') {
8409
- // in ipads it was noticed that if vp8 codec is used
8410
- // then the bytes sent is 0 in the outbound-rtp
8411
- // so we are forcing h264 codec for ipads
8412
- preferredCodec = 'H264';
8413
- }
8414
- else if (osName === 'android') {
8415
- preferredCodec = 'VP8';
8416
- }
8946
+ if (!preferredCodec && trackType === TrackType.VIDEO && isReactNative()) {
8947
+ const osName = getOSInfo()?.name.toLowerCase();
8948
+ if (osName === 'ipados') {
8949
+ // in ipads it was noticed that if vp8 codec is used
8950
+ // then the bytes sent is 0 in the outbound-rtp
8951
+ // so we are forcing h264 codec for ipads
8952
+ preferredCodec = 'H264';
8953
+ }
8954
+ else if (osName === 'android') {
8955
+ preferredCodec = 'VP8';
8417
8956
  }
8418
8957
  }
8419
- const codecPreferences = this.getCodecPreferences(trackType, preferredCodec);
8420
8958
  // listen for 'ended' event on the track as it might be ended abruptly
8421
8959
  // by an external factor as permission revokes, device disconnected, etc.
8422
8960
  // keep in mind that `track.stop()` doesn't trigger this event.
@@ -8431,17 +8969,20 @@ class Publisher {
8431
8969
  : undefined,
8432
8970
  sendEncodings: videoEncodings,
8433
8971
  });
8434
- logger$3('debug', `Added ${TrackType[trackType]} transceiver`);
8972
+ this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
8435
8973
  this.transceiverInitOrder.push(trackType);
8436
8974
  this.transceiverRegistry[trackType] = transceiver;
8437
8975
  this.publishOptionsPerTrackType.set(trackType, opts);
8438
- if ('setCodecPreferences' in transceiver && codecPreferences) {
8439
- logger$3('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
8976
+ const codecPreferences = 'setCodecPreferences' in transceiver
8977
+ ? this.getCodecPreferences(trackType, preferredCodec)
8978
+ : undefined;
8979
+ if (codecPreferences) {
8980
+ this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
8440
8981
  try {
8441
8982
  transceiver.setCodecPreferences(codecPreferences);
8442
8983
  }
8443
8984
  catch (err) {
8444
- logger$3('warn', `Couldn't set codec preferences`, err);
8985
+ this.logger('warn', `Couldn't set codec preferences`, err);
8445
8986
  }
8446
8987
  }
8447
8988
  }
@@ -8490,31 +9031,17 @@ class Publisher {
8490
9031
  * @param trackType the track type to check.
8491
9032
  */
8492
9033
  this.isPublishing = (trackType) => {
8493
- const transceiverForTrackType = this.transceiverRegistry[trackType];
8494
- if (transceiverForTrackType && transceiverForTrackType.sender) {
8495
- const sender = transceiverForTrackType.sender;
8496
- return (!!sender.track &&
8497
- sender.track.readyState === 'live' &&
8498
- sender.track.enabled);
8499
- }
8500
- return false;
8501
- };
8502
- /**
8503
- * Returns true if the given track type is currently live
8504
- *
8505
- * @param trackType the track type to check.
8506
- */
8507
- this.isLive = (trackType) => {
8508
- const transceiverForTrackType = this.transceiverRegistry[trackType];
8509
- if (transceiverForTrackType && transceiverForTrackType.sender) {
8510
- const sender = transceiverForTrackType.sender;
8511
- return !!sender.track && sender.track.readyState === 'live';
8512
- }
8513
- return false;
9034
+ const transceiver = this.transceiverRegistry[trackType];
9035
+ if (!transceiver || !transceiver.sender)
9036
+ return false;
9037
+ const track = transceiver.sender.track;
9038
+ return !!track && track.readyState === 'live' && track.enabled;
8514
9039
  };
8515
9040
  this.notifyTrackMuteStateChanged = async (mediaStream, trackType, isMuted) => {
8516
9041
  await this.sfuClient.updateMuteState(trackType, isMuted);
8517
9042
  const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
9043
+ if (!audioOrVideoOrScreenShareStream)
9044
+ return;
8518
9045
  if (isMuted) {
8519
9046
  this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
8520
9047
  publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
@@ -8536,7 +9063,7 @@ class Publisher {
8536
9063
  * Stops publishing all tracks and stop all tracks.
8537
9064
  */
8538
9065
  this.stopPublishing = () => {
8539
- logger$3('debug', 'Stopping publishing all tracks');
9066
+ this.logger('debug', 'Stopping publishing all tracks');
8540
9067
  this.pc.getSenders().forEach((s) => {
8541
9068
  s.track?.stop();
8542
9069
  if (this.pc.signalingState !== 'closed') {
@@ -8545,15 +9072,15 @@ class Publisher {
8545
9072
  });
8546
9073
  };
8547
9074
  this.updateVideoPublishQuality = async (enabledLayers) => {
8548
- logger$3('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
9075
+ this.logger('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
8549
9076
  const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
8550
9077
  if (!videoSender) {
8551
- logger$3('warn', 'Update publish quality, no video sender found.');
9078
+ this.logger('warn', 'Update publish quality, no video sender found.');
8552
9079
  return;
8553
9080
  }
8554
9081
  const params = videoSender.getParameters();
8555
9082
  if (params.encodings.length === 0) {
8556
- logger$3('warn', 'Update publish quality, No suitable video encoding quality found');
9083
+ this.logger('warn', 'Update publish quality, No suitable video encoding quality found');
8557
9084
  return;
8558
9085
  }
8559
9086
  let changed = false;
@@ -8572,18 +9099,18 @@ class Publisher {
8572
9099
  if (layer !== undefined) {
8573
9100
  if (layer.scaleResolutionDownBy >= 1 &&
8574
9101
  layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy) {
8575
- logger$3('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
9102
+ this.logger('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
8576
9103
  enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
8577
9104
  changed = true;
8578
9105
  }
8579
9106
  if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
8580
- logger$3('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
9107
+ this.logger('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
8581
9108
  enc.maxBitrate = layer.maxBitrate;
8582
9109
  changed = true;
8583
9110
  }
8584
9111
  if (layer.maxFramerate > 0 &&
8585
9112
  layer.maxFramerate !== enc.maxFramerate) {
8586
- logger$3('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
9113
+ this.logger('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
8587
9114
  enc.maxFramerate = layer.maxFramerate;
8588
9115
  changed = true;
8589
9116
  }
@@ -8593,10 +9120,10 @@ class Publisher {
8593
9120
  const activeLayers = params.encodings.filter((e) => e.active);
8594
9121
  if (changed) {
8595
9122
  await videoSender.setParameters(params);
8596
- logger$3('info', `Update publish quality, enabled rids: `, activeLayers);
9123
+ this.logger('info', `Update publish quality, enabled rids: `, activeLayers);
8597
9124
  }
8598
9125
  else {
8599
- logger$3('info', `Update publish quality, no change: `, activeLayers);
9126
+ this.logger('info', `Update publish quality, no change: `, activeLayers);
8600
9127
  }
8601
9128
  };
8602
9129
  /**
@@ -8620,7 +9147,7 @@ class Publisher {
8620
9147
  this.onIceCandidate = (e) => {
8621
9148
  const { candidate } = e;
8622
9149
  if (!candidate) {
8623
- logger$3('debug', 'null ice candidate');
9150
+ this.logger('debug', 'null ice candidate');
8624
9151
  return;
8625
9152
  }
8626
9153
  this.sfuClient
@@ -8629,7 +9156,7 @@ class Publisher {
8629
9156
  peerType: PeerType.PUBLISHER_UNSPECIFIED,
8630
9157
  })
8631
9158
  .catch((err) => {
8632
- logger$3('warn', `ICETrickle failed`, err);
9159
+ this.logger('warn', `ICETrickle failed`, err);
8633
9160
  });
8634
9161
  };
8635
9162
  /**
@@ -8640,38 +9167,23 @@ class Publisher {
8640
9167
  this.setSfuClient = (sfuClient) => {
8641
9168
  this.sfuClient = sfuClient;
8642
9169
  };
8643
- /**
8644
- * Performs a migration of this publisher instance to a new SFU.
8645
- *
8646
- * Initiates a new `iceRestart` offer/answer exchange with the new SFU.
8647
- *
8648
- * @param sfuClient the new SFU client to migrate to.
8649
- * @param connectionConfig the new connection configuration to use.
8650
- */
8651
- this.migrateTo = async (sfuClient, connectionConfig) => {
8652
- this.sfuClient = sfuClient;
8653
- this.pc.setConfiguration(connectionConfig);
8654
- this._connectionConfiguration = connectionConfig;
8655
- const shouldRestartIce = this.pc.iceConnectionState === 'connected';
8656
- if (shouldRestartIce) {
8657
- // negotiate only if there are tracks to publish
8658
- await this.negotiate({ iceRestart: true });
8659
- }
8660
- };
8661
9170
  /**
8662
9171
  * Restarts the ICE connection and renegotiates with the SFU.
8663
9172
  */
8664
9173
  this.restartIce = async () => {
8665
- logger$3('debug', 'Restarting ICE connection');
9174
+ this.logger('debug', 'Restarting ICE connection');
8666
9175
  const signalingState = this.pc.signalingState;
8667
9176
  if (this.isIceRestarting || signalingState === 'have-local-offer') {
8668
- logger$3('debug', 'ICE restart is already in progress');
9177
+ this.logger('debug', 'ICE restart is already in progress');
8669
9178
  return;
8670
9179
  }
8671
9180
  await this.negotiate({ iceRestart: true });
8672
9181
  };
8673
9182
  this.onNegotiationNeeded = () => {
8674
- this.negotiate().catch((err) => logger$3('warn', `Negotiation failed.`, err));
9183
+ this.negotiate().catch((err) => {
9184
+ this.logger('warn', `Negotiation failed.`, err);
9185
+ this.onUnrecoverableError?.();
9186
+ });
8675
9187
  };
8676
9188
  /**
8677
9189
  * Initiates a new offer/answer exchange with the currently connected SFU.
@@ -8683,59 +9195,62 @@ class Publisher {
8683
9195
  const offer = await this.pc.createOffer(options);
8684
9196
  let sdp = this.mungeCodecs(offer.sdp);
8685
9197
  if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
8686
- const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
8687
- if (transceiver && transceiver.sender.track) {
8688
- const mid = transceiver.mid ??
8689
- this.extractMid(sdp, transceiver.sender.track, TrackType.SCREEN_SHARE_AUDIO);
8690
- sdp = enableHighQualityAudio(sdp, mid);
8691
- }
9198
+ sdp = this.enableHighQualityAudio(sdp);
8692
9199
  }
8693
9200
  // set the munged SDP back to the offer
8694
9201
  offer.sdp = sdp;
8695
- const trackInfos = this.getCurrentTrackInfos(offer.sdp);
9202
+ const trackInfos = this.getAnnouncedTracks(offer.sdp);
8696
9203
  if (trackInfos.length === 0) {
8697
- throw new Error(`Can't initiate negotiation without announcing any tracks`);
9204
+ throw new Error(`Can't negotiate without announcing any tracks`);
8698
9205
  }
8699
9206
  await this.pc.setLocalDescription(offer);
8700
9207
  const { response } = await this.sfuClient.setPublisher({
8701
9208
  sdp: offer.sdp || '',
8702
9209
  tracks: trackInfos,
8703
9210
  });
9211
+ const { sdp: remoteSdp, error } = response;
8704
9212
  try {
8705
- await this.pc.setRemoteDescription({
8706
- type: 'answer',
8707
- sdp: response.sdp,
8708
- });
9213
+ await this.pc.setRemoteDescription({ type: 'answer', sdp: remoteSdp });
8709
9214
  }
8710
9215
  catch (e) {
8711
- logger$3('error', `setRemoteDescription error`, {
8712
- sdp: response.sdp,
8713
- error: e,
8714
- });
9216
+ this.logger('error', `setRemoteDescription error`, remoteSdp, error, e);
9217
+ throw e;
9218
+ }
9219
+ finally {
9220
+ this.isIceRestarting = false;
8715
9221
  }
8716
- this.isIceRestarting = false;
8717
9222
  this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(async (candidate) => {
8718
9223
  try {
8719
9224
  const iceCandidate = JSON.parse(candidate.iceCandidate);
8720
9225
  await this.pc.addIceCandidate(iceCandidate);
8721
9226
  }
8722
9227
  catch (e) {
8723
- logger$3('warn', `ICE candidate error`, [e, candidate]);
9228
+ this.logger('warn', `ICE candidate error`, e, candidate);
8724
9229
  }
8725
9230
  });
8726
9231
  };
9232
+ this.enableHighQualityAudio = (sdp) => {
9233
+ const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
9234
+ if (!transceiver)
9235
+ return sdp;
9236
+ const mid = this.extractMid(transceiver, sdp, TrackType.SCREEN_SHARE_AUDIO);
9237
+ return enableHighQualityAudio(sdp, mid);
9238
+ };
8727
9239
  this.mungeCodecs = (sdp) => {
8728
9240
  if (sdp) {
8729
9241
  sdp = toggleDtx(sdp, this.isDtxEnabled);
8730
9242
  }
8731
9243
  return sdp;
8732
9244
  };
8733
- this.extractMid = (sdp, track, trackType) => {
9245
+ this.extractMid = (transceiver, sdp, trackType) => {
9246
+ if (transceiver.mid)
9247
+ return transceiver.mid;
8734
9248
  if (!sdp) {
8735
- logger$3('warn', 'No SDP found. Returning empty mid');
9249
+ this.logger('warn', 'No SDP found. Returning empty mid');
8736
9250
  return '';
8737
9251
  }
8738
- logger$3('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
9252
+ this.logger('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
9253
+ const track = transceiver.sender.track;
8739
9254
  const parsedSdp = SDP__namespace.parse(sdp);
8740
9255
  const media = parsedSdp.media.find((m) => {
8741
9256
  return (m.type === track.kind &&
@@ -8743,17 +9258,23 @@ class Publisher {
8743
9258
  (m.msid?.includes(track.id) ?? true));
8744
9259
  });
8745
9260
  if (typeof media?.mid === 'undefined') {
8746
- logger$3('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find a heuristic mid`);
9261
+ this.logger('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find it heuristically`);
8747
9262
  const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
8748
9263
  if (heuristicMid !== -1) {
8749
9264
  return String(heuristicMid);
8750
9265
  }
8751
- logger$3('debug', 'No heuristic mid found. Returning empty mid');
9266
+ this.logger('debug', 'No heuristic mid found. Returning empty mid');
8752
9267
  return '';
8753
9268
  }
8754
9269
  return String(media.mid);
8755
9270
  };
8756
- this.getCurrentTrackInfos = (sdp) => {
9271
+ /**
9272
+ * Returns a list of tracks that are currently being published.
9273
+ *
9274
+ * @internal
9275
+ * @param sdp an optional SDP to extract the `mid` from.
9276
+ */
9277
+ this.getAnnouncedTracks = (sdp) => {
8757
9278
  sdp = sdp || this.pc.localDescription?.sdp;
8758
9279
  const { settings } = this.state;
8759
9280
  const targetResolution = settings?.video
@@ -8765,7 +9286,8 @@ class Publisher {
8765
9286
  const trackType = Number(Object.keys(this.transceiverRegistry).find((key) => this.transceiverRegistry[key] === transceiver));
8766
9287
  const track = transceiver.sender.track;
8767
9288
  let optimalLayers;
8768
- if (track.readyState === 'live') {
9289
+ const isTrackLive = track.readyState === 'live';
9290
+ if (isTrackLive) {
8769
9291
  const publishOpts = this.publishOptionsPerTrackType.get(trackType);
8770
9292
  optimalLayers =
8771
9293
  trackType === TrackType.VIDEO
@@ -8778,7 +9300,7 @@ class Publisher {
8778
9300
  else {
8779
9301
  // we report the last known optimal layers for ended tracks
8780
9302
  optimalLayers = this.trackLayersCache[trackType] || [];
8781
- logger$3('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
9303
+ this.logger('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
8782
9304
  }
8783
9305
  const layers = optimalLayers.map((optimalLayer) => ({
8784
9306
  rid: optimalLayer.rid || '',
@@ -8800,10 +9322,11 @@ class Publisher {
8800
9322
  trackId: track.id,
8801
9323
  layers: layers,
8802
9324
  trackType,
8803
- mid: transceiver.mid ?? this.extractMid(sdp, track, trackType),
9325
+ mid: this.extractMid(transceiver, sdp, trackType),
8804
9326
  stereo: isStereo,
8805
9327
  dtx: isAudioTrack && this.isDtxEnabled,
8806
9328
  red: isAudioTrack && this.isRedEnabled,
9329
+ muted: !isTrackLive,
8807
9330
  };
8808
9331
  });
8809
9332
  };
@@ -8812,44 +9335,26 @@ class Publisher {
8812
9335
  `${e.errorCode}: ${e.errorText}`;
8813
9336
  const iceState = this.pc.iceConnectionState;
8814
9337
  const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
8815
- logger$3(logLevel, `ICE Candidate error`, errorMessage);
9338
+ this.logger(logLevel, `ICE Candidate error`, errorMessage);
8816
9339
  };
8817
9340
  this.onIceConnectionStateChange = () => {
8818
9341
  const state = this.pc.iceConnectionState;
8819
- logger$3('debug', `ICE Connection state changed to`, state);
8820
- const hasNetworkConnection = this.state.callingState !== exports.CallingState.OFFLINE;
8821
- if (state === 'failed') {
8822
- logger$3('debug', `Attempting to restart ICE`);
9342
+ this.logger('debug', `ICE Connection state changed to`, state);
9343
+ if (this.state.callingState === exports.CallingState.RECONNECTING)
9344
+ return;
9345
+ if (state === 'failed' || state === 'disconnected') {
9346
+ this.logger('debug', `Attempting to restart ICE`);
8823
9347
  this.restartIce().catch((e) => {
8824
- logger$3('error', `ICE restart error`, e);
9348
+ this.logger('error', `ICE restart error`, e);
8825
9349
  this.onUnrecoverableError?.();
8826
9350
  });
8827
9351
  }
8828
- else if (state === 'disconnected' && hasNetworkConnection) {
8829
- // when in `disconnected` state, the browser may recover automatically,
8830
- // hence, we delay the ICE restart
8831
- logger$3('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
8832
- this.iceRestartTimeout = setTimeout(() => {
8833
- // check if the state is still `disconnected` or `failed`
8834
- // as the connection may have recovered (or failed) in the meantime
8835
- if (this.pc.iceConnectionState === 'disconnected' ||
8836
- this.pc.iceConnectionState === 'failed') {
8837
- this.restartIce().catch((e) => {
8838
- logger$3('error', `ICE restart error`, e);
8839
- this.onUnrecoverableError?.();
8840
- });
8841
- }
8842
- else {
8843
- logger$3('debug', `Scheduled ICE restart: connection recovered, canceled.`);
8844
- }
8845
- }, this.iceRestartDelay);
8846
- }
8847
9352
  };
8848
9353
  this.onIceGatheringStateChange = () => {
8849
- logger$3('debug', `ICE Gathering State`, this.pc.iceGatheringState);
9354
+ this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
8850
9355
  };
8851
9356
  this.onSignalingStateChange = () => {
8852
- logger$3('debug', `Signaling state changed`, this.pc.signalingState);
9357
+ this.logger('debug', `Signaling state changed`, this.pc.signalingState);
8853
9358
  };
8854
9359
  this.ridToVideoQuality = (rid) => {
8855
9360
  return rid === 'q'
@@ -8858,28 +9363,29 @@ class Publisher {
8858
9363
  ? VideoQuality.MID
8859
9364
  : VideoQuality.HIGH; // default to HIGH
8860
9365
  };
9366
+ this.logger = getLogger(['Publisher', logTag]);
8861
9367
  this.pc = this.createPeerConnection(connectionConfig);
8862
9368
  this.sfuClient = sfuClient;
8863
9369
  this.state = state;
8864
9370
  this.isDtxEnabled = isDtxEnabled;
8865
9371
  this.isRedEnabled = isRedEnabled;
8866
- this.iceRestartDelay = iceRestartDelay;
8867
9372
  this.onUnrecoverableError = onUnrecoverableError;
8868
9373
  this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
8869
9374
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
8870
9375
  return;
8871
9376
  this.restartIce().catch((err) => {
8872
- logger$3('warn', `ICERestart failed`, err);
9377
+ this.logger('warn', `ICERestart failed`, err);
8873
9378
  this.onUnrecoverableError?.();
8874
9379
  });
8875
9380
  });
8876
9381
  }
8877
9382
  }
8878
9383
 
8879
- const logger$2 = getLogger(['Subscriber']);
8880
9384
  /**
8881
9385
  * A wrapper around the `RTCPeerConnection` that handles the incoming
8882
9386
  * media streams from the SFU.
9387
+ *
9388
+ * @internal
8883
9389
  */
8884
9390
  class Subscriber {
8885
9391
  /**
@@ -8901,8 +9407,9 @@ class Subscriber {
8901
9407
  * @param connectionConfig the connection configuration to use.
8902
9408
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
8903
9409
  * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
9410
+ * @param logTag a tag to use for logging.
8904
9411
  */
8905
- constructor({ sfuClient, dispatcher, state, connectionConfig, iceRestartDelay = 2500, onUnrecoverableError, }) {
9412
+ constructor({ sfuClient, dispatcher, state, connectionConfig, onUnrecoverableError, logTag, }) {
8906
9413
  this.isIceRestarting = false;
8907
9414
  /**
8908
9415
  * Creates a new `RTCPeerConnection` instance with the given configuration.
@@ -8923,10 +9430,22 @@ class Subscriber {
8923
9430
  * Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
8924
9431
  */
8925
9432
  this.close = () => {
8926
- clearTimeout(this.iceRestartTimeout);
9433
+ this.detachEventHandlers();
9434
+ this.pc.close();
9435
+ };
9436
+ /**
9437
+ * Detaches the event handlers from the `RTCPeerConnection`.
9438
+ * This is useful when we want to replace the `RTCPeerConnection`
9439
+ * instance with a new one (in case of migration).
9440
+ */
9441
+ this.detachEventHandlers = () => {
8927
9442
  this.unregisterOnSubscriberOffer();
8928
9443
  this.unregisterOnIceRestart();
8929
- this.pc.close();
9444
+ this.pc.removeEventListener('icecandidate', this.onIceCandidate);
9445
+ this.pc.removeEventListener('track', this.handleOnTrack);
9446
+ this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
9447
+ this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
9448
+ this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
8930
9449
  };
8931
9450
  /**
8932
9451
  * Returns the result of the `RTCPeerConnection.getStats()` method
@@ -8944,76 +9463,17 @@ class Subscriber {
8944
9463
  this.setSfuClient = (sfuClient) => {
8945
9464
  this.sfuClient = sfuClient;
8946
9465
  };
8947
- /**
8948
- * Migrates the subscriber to a new SFU client.
8949
- *
8950
- * @param sfuClient the new SFU client to migrate to.
8951
- * @param connectionConfig the new connection configuration to use.
8952
- */
8953
- this.migrateTo = (sfuClient, connectionConfig) => {
8954
- this.setSfuClient(sfuClient);
8955
- // when migrating, we want to keep the previous subscriber open
8956
- // until the new one is connected
8957
- const previousPC = this.pc;
8958
- // we keep a record of previously available video tracks
8959
- // so that we can monitor when they become available on the new
8960
- // subscriber and close the previous one.
8961
- const trackIdsToMigrate = new Set();
8962
- previousPC.getReceivers().forEach((r) => {
8963
- if (r.track.kind === 'video') {
8964
- trackIdsToMigrate.add(r.track.id);
8965
- }
8966
- });
8967
- // set up a new subscriber peer connection, configured to connect
8968
- // to the new SFU node
8969
- const pc = this.createPeerConnection(connectionConfig);
8970
- let migrationTimeoutId;
8971
- const cleanupMigration = () => {
8972
- previousPC.close();
8973
- clearTimeout(migrationTimeoutId);
8974
- };
8975
- // When migrating, we want to keep track of the video tracks
8976
- // that are migrating to the new subscriber.
8977
- // Once all of them are available, we can close the previous subscriber.
8978
- const handleTrackMigration = (e) => {
8979
- logger$2('debug', `[Migration]: Migrated track: ${e.track.id}, ${e.track.kind}`);
8980
- trackIdsToMigrate.delete(e.track.id);
8981
- if (trackIdsToMigrate.size === 0) {
8982
- logger$2('debug', `[Migration]: Migration complete`);
8983
- pc.removeEventListener('track', handleTrackMigration);
8984
- cleanupMigration();
8985
- }
8986
- };
8987
- // When migrating, we want to keep track of the connection state
8988
- // of the new subscriber.
8989
- // Once it is connected, we give it a 2-second grace period to receive
8990
- // all the video tracks that are migrating from the previous subscriber.
8991
- // After this threshold, we abruptly close the previous subscriber.
8992
- const handleConnectionStateChange = () => {
8993
- if (pc.connectionState === 'connected') {
8994
- migrationTimeoutId = setTimeout(() => {
8995
- pc.removeEventListener('track', handleTrackMigration);
8996
- cleanupMigration();
8997
- }, 2000);
8998
- pc.removeEventListener('connectionstatechange', handleConnectionStateChange);
8999
- }
9000
- };
9001
- pc.addEventListener('track', handleTrackMigration);
9002
- pc.addEventListener('connectionstatechange', handleConnectionStateChange);
9003
- // replace the PeerConnection instance
9004
- this.pc = pc;
9005
- };
9006
9466
  /**
9007
9467
  * Restarts the ICE connection and renegotiates with the SFU.
9008
9468
  */
9009
9469
  this.restartIce = async () => {
9010
- logger$2('debug', 'Restarting ICE connection');
9470
+ this.logger('debug', 'Restarting ICE connection');
9011
9471
  if (this.pc.signalingState === 'have-remote-offer') {
9012
- logger$2('debug', 'ICE restart is already in progress');
9472
+ this.logger('debug', 'ICE restart is already in progress');
9013
9473
  return;
9014
9474
  }
9015
9475
  if (this.pc.connectionState === 'new') {
9016
- logger$2('debug', `ICE connection is not yet established, skipping restart.`);
9476
+ this.logger('debug', `ICE connection is not yet established, skipping restart.`);
9017
9477
  return;
9018
9478
  }
9019
9479
  const previousIsIceRestarting = this.isIceRestarting;
@@ -9032,36 +9492,42 @@ class Subscriber {
9032
9492
  this.handleOnTrack = (e) => {
9033
9493
  const [primaryStream] = e.streams;
9034
9494
  // example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
9035
- const [trackId, trackType] = primaryStream.id.split(':');
9495
+ const [trackId, rawTrackType] = primaryStream.id.split(':');
9036
9496
  const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
9037
- logger$2('debug', `[onTrack]: Got remote ${trackType} track for userId: ${participantToUpdate?.userId}`, e.track.id, e.track);
9038
- if (!participantToUpdate) {
9039
- logger$2('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
9040
- return;
9041
- }
9042
- const trackDebugInfo = `${participantToUpdate.userId} ${trackType}:${trackId}`;
9497
+ this.logger('debug', `[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, e.track.id, e.track);
9498
+ const trackDebugInfo = `${participantToUpdate?.userId} ${rawTrackType}:${trackId}`;
9043
9499
  e.track.addEventListener('mute', () => {
9044
- logger$2('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
9500
+ this.logger('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
9045
9501
  });
9046
9502
  e.track.addEventListener('unmute', () => {
9047
- logger$2('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
9503
+ this.logger('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
9048
9504
  });
9049
9505
  e.track.addEventListener('ended', () => {
9050
- logger$2('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
9506
+ this.logger('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
9507
+ this.state.removeOrphanedTrack(primaryStream.id);
9051
9508
  });
9052
- const streamKindProp = {
9053
- TRACK_TYPE_AUDIO: 'audioStream',
9054
- TRACK_TYPE_VIDEO: 'videoStream',
9055
- TRACK_TYPE_SCREEN_SHARE: 'screenShareStream',
9056
- TRACK_TYPE_SCREEN_SHARE_AUDIO: 'screenShareAudioStream',
9057
- }[trackType];
9509
+ const trackType = toTrackType(rawTrackType);
9510
+ if (!trackType) {
9511
+ return this.logger('error', `Unknown track type: ${rawTrackType}`);
9512
+ }
9513
+ if (!participantToUpdate) {
9514
+ this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
9515
+ this.state.registerOrphanedTrack({
9516
+ id: primaryStream.id,
9517
+ trackLookupPrefix: trackId,
9518
+ track: primaryStream,
9519
+ trackType,
9520
+ });
9521
+ return;
9522
+ }
9523
+ const streamKindProp = trackTypeToParticipantStreamKey(trackType);
9058
9524
  if (!streamKindProp) {
9059
- logger$2('error', `Unknown track type: ${trackType}`);
9525
+ this.logger('error', `Unknown track type: ${rawTrackType}`);
9060
9526
  return;
9061
9527
  }
9062
9528
  const previousStream = participantToUpdate[streamKindProp];
9063
9529
  if (previousStream) {
9064
- logger$2('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
9530
+ this.logger('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
9065
9531
  previousStream.getTracks().forEach((t) => {
9066
9532
  t.stop();
9067
9533
  previousStream.removeTrack(t);
@@ -9074,7 +9540,7 @@ class Subscriber {
9074
9540
  this.onIceCandidate = (e) => {
9075
9541
  const { candidate } = e;
9076
9542
  if (!candidate) {
9077
- logger$2('debug', 'null ice candidate');
9543
+ this.logger('debug', 'null ice candidate');
9078
9544
  return;
9079
9545
  }
9080
9546
  this.sfuClient
@@ -9083,11 +9549,11 @@ class Subscriber {
9083
9549
  peerType: PeerType.SUBSCRIBER,
9084
9550
  })
9085
9551
  .catch((err) => {
9086
- logger$2('warn', `ICETrickle failed`, err);
9552
+ this.logger('warn', `ICETrickle failed`, err);
9087
9553
  });
9088
9554
  };
9089
9555
  this.negotiate = async (subscriberOffer) => {
9090
- logger$2('info', `Received subscriberOffer`, subscriberOffer);
9556
+ this.logger('info', `Received subscriberOffer`, subscriberOffer);
9091
9557
  await this.pc.setRemoteDescription({
9092
9558
  type: 'offer',
9093
9559
  sdp: subscriberOffer.sdp,
@@ -9098,7 +9564,7 @@ class Subscriber {
9098
9564
  await this.pc.addIceCandidate(iceCandidate);
9099
9565
  }
9100
9566
  catch (e) {
9101
- logger$2('warn', `ICE candidate error`, [e, candidate]);
9567
+ this.logger('warn', `ICE candidate error`, [e, candidate]);
9102
9568
  }
9103
9569
  });
9104
9570
  const answer = await this.pc.createAnswer();
@@ -9111,63 +9577,51 @@ class Subscriber {
9111
9577
  };
9112
9578
  this.onIceConnectionStateChange = () => {
9113
9579
  const state = this.pc.iceConnectionState;
9114
- logger$2('debug', `ICE connection state changed`, state);
9580
+ this.logger('debug', `ICE connection state changed`, state);
9581
+ if (this.state.callingState === exports.CallingState.RECONNECTING)
9582
+ return;
9115
9583
  // do nothing when ICE is restarting
9116
9584
  if (this.isIceRestarting)
9117
9585
  return;
9118
- const hasNetworkConnection = this.state.callingState !== exports.CallingState.OFFLINE;
9119
- if (state === 'failed') {
9120
- logger$2('debug', `Attempting to restart ICE`);
9586
+ if (state === 'failed' || state === 'disconnected') {
9587
+ this.logger('debug', `Attempting to restart ICE`);
9121
9588
  this.restartIce().catch((e) => {
9122
- logger$2('error', `ICE restart failed`, e);
9589
+ this.logger('error', `ICE restart failed`, e);
9123
9590
  this.onUnrecoverableError?.();
9124
9591
  });
9125
9592
  }
9126
- else if (state === 'disconnected' && hasNetworkConnection) {
9127
- // when in `disconnected` state, the browser may recover automatically,
9128
- // hence, we delay the ICE restart
9129
- logger$2('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
9130
- this.iceRestartTimeout = setTimeout(() => {
9131
- // check if the state is still `disconnected` or `failed`
9132
- // as the connection may have recovered (or failed) in the meantime
9133
- if (this.pc.iceConnectionState === 'disconnected' ||
9134
- this.pc.iceConnectionState === 'failed') {
9135
- this.restartIce().catch((e) => {
9136
- logger$2('error', `ICE restart failed`, e);
9137
- this.onUnrecoverableError?.();
9138
- });
9139
- }
9140
- else {
9141
- logger$2('debug', `Scheduled ICE restart: connection recovered, canceled.`);
9142
- }
9143
- }, this.iceRestartDelay);
9144
- }
9145
9593
  };
9146
9594
  this.onIceGatheringStateChange = () => {
9147
- logger$2('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
9595
+ this.logger('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
9148
9596
  };
9149
9597
  this.onIceCandidateError = (e) => {
9150
9598
  const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
9151
9599
  `${e.errorCode}: ${e.errorText}`;
9152
9600
  const iceState = this.pc.iceConnectionState;
9153
9601
  const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
9154
- logger$2(logLevel, `ICE Candidate error`, errorMessage);
9602
+ this.logger(logLevel, `ICE Candidate error`, errorMessage);
9155
9603
  };
9604
+ this.logger = getLogger(['Subscriber', logTag]);
9156
9605
  this.sfuClient = sfuClient;
9157
9606
  this.state = state;
9158
- this.iceRestartDelay = iceRestartDelay;
9159
9607
  this.onUnrecoverableError = onUnrecoverableError;
9160
9608
  this.pc = this.createPeerConnection(connectionConfig);
9609
+ const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
9161
9610
  this.unregisterOnSubscriberOffer = dispatcher.on('subscriberOffer', (subscriberOffer) => {
9162
- this.negotiate(subscriberOffer).catch((err) => {
9163
- logger$2('warn', `Negotiation failed.`, err);
9611
+ withoutConcurrency(subscriberOfferConcurrencyTag, () => {
9612
+ return this.negotiate(subscriberOffer);
9613
+ }).catch((err) => {
9614
+ this.logger('warn', `Negotiation failed.`, err);
9164
9615
  });
9165
9616
  });
9617
+ const iceRestartConcurrencyTag = Symbol('iceRestart');
9166
9618
  this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
9167
- if (iceRestart.peerType !== PeerType.SUBSCRIBER)
9168
- return;
9169
- this.restartIce().catch((err) => {
9170
- logger$2('warn', `ICERestart failed`, err);
9619
+ withoutConcurrency(iceRestartConcurrencyTag, async () => {
9620
+ if (iceRestart.peerType !== PeerType.SUBSCRIBER)
9621
+ return;
9622
+ await this.restartIce();
9623
+ }).catch((err) => {
9624
+ this.logger('warn', `ICERestart failed`, err);
9171
9625
  this.onUnrecoverableError?.();
9172
9626
  });
9173
9627
  });
@@ -9175,8 +9629,8 @@ class Subscriber {
9175
9629
  }
9176
9630
 
9177
9631
  const createWebSocketSignalChannel = (opts) => {
9178
- const logger = getLogger(['sfu-client']);
9179
- const { endpoint, onMessage } = opts;
9632
+ const { endpoint, onMessage, logTag } = opts;
9633
+ const logger = getLogger(['sfu-client-ws', logTag]);
9180
9634
  const ws = new WebSocket(endpoint);
9181
9635
  ws.binaryType = 'arraybuffer'; // do we need this?
9182
9636
  ws.addEventListener('error', (e) => {
@@ -9194,140 +9648,45 @@ const createWebSocketSignalChannel = (opts) => {
9194
9648
  ? SfuEvent.fromBinary(new Uint8Array(e.data))
9195
9649
  : SfuEvent.fromJsonString(e.data.toString());
9196
9650
  onMessage(message);
9197
- }
9198
- catch (err) {
9199
- logger('error', 'Failed to decode a message. Check whether the Proto models match.', { event: e, error: err });
9200
- }
9201
- });
9202
- return ws;
9203
- };
9204
-
9205
- const sleep = (m) => new Promise((r) => setTimeout(r, m));
9206
- function isFunction(value) {
9207
- return (value &&
9208
- (Object.prototype.toString.call(value) === '[object Function]' ||
9209
- 'function' === typeof value ||
9210
- value instanceof Function));
9211
- }
9212
- /**
9213
- * A map of known error codes.
9214
- */
9215
- const KnownCodes = {
9216
- TOKEN_EXPIRED: 40,
9217
- WS_CLOSED_SUCCESS: 1000,
9218
- WS_CLOSED_ABRUPTLY: 1006,
9219
- WS_POLICY_VIOLATION: 1008,
9220
- };
9221
- /**
9222
- * retryInterval - A retry interval which increases acc to number of failures
9223
- *
9224
- * @return {number} Duration to wait in milliseconds
9225
- */
9226
- function retryInterval(numberOfFailures) {
9227
- // try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
9228
- const max = Math.min(500 + numberOfFailures * 2000, 5000);
9229
- const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
9230
- return Math.floor(Math.random() * (max - min) + min);
9231
- }
9232
- function randomId() {
9233
- return generateUUIDv4();
9234
- }
9235
- function hex(bytes) {
9236
- let s = '';
9237
- for (let i = 0; i < bytes.length; i++) {
9238
- s += bytes[i].toString(16).padStart(2, '0');
9239
- }
9240
- return s;
9241
- }
9242
- // https://tools.ietf.org/html/rfc4122
9243
- function generateUUIDv4() {
9244
- const bytes = getRandomBytes(16);
9245
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
9246
- bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
9247
- return (hex(bytes.subarray(0, 4)) +
9248
- '-' +
9249
- hex(bytes.subarray(4, 6)) +
9250
- '-' +
9251
- hex(bytes.subarray(6, 8)) +
9252
- '-' +
9253
- hex(bytes.subarray(8, 10)) +
9254
- '-' +
9255
- hex(bytes.subarray(10, 16)));
9256
- }
9257
- function getRandomValuesWithMathRandom(bytes) {
9258
- const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
9259
- for (let i = 0; i < bytes.length; i++) {
9260
- bytes[i] = Math.random() * max;
9261
- }
9262
- }
9263
- const getRandomValues = (() => {
9264
- if (typeof crypto !== 'undefined' &&
9265
- typeof crypto?.getRandomValues !== 'undefined') {
9266
- return crypto.getRandomValues.bind(crypto);
9267
- }
9268
- else if (typeof msCrypto !== 'undefined') {
9269
- return msCrypto.getRandomValues.bind(msCrypto);
9270
- }
9271
- else {
9272
- return getRandomValuesWithMathRandom;
9273
- }
9274
- })();
9275
- function getRandomBytes(length) {
9276
- const bytes = new Uint8Array(length);
9277
- getRandomValues(bytes);
9278
- return bytes;
9279
- }
9280
- function convertErrorToJson(err) {
9281
- const jsonObj = {};
9282
- if (!err)
9283
- return jsonObj;
9284
- try {
9285
- Object.getOwnPropertyNames(err).forEach((key) => {
9286
- jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
9287
- });
9288
- }
9289
- catch (_) {
9290
- return {
9291
- error: 'failed to serialize the error',
9292
- };
9293
- }
9294
- return jsonObj;
9295
- }
9296
- /**
9297
- * isOnline safely return the navigator.online value for browser env
9298
- * if navigator is not in global object, it always return true
9299
- */
9300
- function isOnline(logger) {
9301
- const nav = typeof navigator !== 'undefined'
9302
- ? navigator
9303
- : typeof window !== 'undefined' && window.navigator
9304
- ? window.navigator
9305
- : undefined;
9306
- if (!nav) {
9307
- logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
9308
- return true;
9309
- }
9310
- // RN navigator has undefined for onLine
9311
- if (typeof nav.onLine !== 'boolean') {
9312
- return true;
9313
- }
9314
- return nav.onLine;
9315
- }
9316
- /**
9317
- * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
9318
- */
9319
- function addConnectionEventListeners(cb) {
9320
- if (typeof window !== 'undefined' && window.addEventListener) {
9321
- window.addEventListener('offline', cb);
9322
- window.addEventListener('online', cb);
9323
- }
9324
- }
9325
- function removeConnectionEventListeners(cb) {
9326
- if (typeof window !== 'undefined' && window.removeEventListener) {
9327
- window.removeEventListener('offline', cb);
9328
- window.removeEventListener('online', cb);
9329
- }
9330
- }
9651
+ }
9652
+ catch (err) {
9653
+ logger('error', 'Failed to decode a message. Check whether the Proto models match.', { event: e, error: err });
9654
+ }
9655
+ });
9656
+ return ws;
9657
+ };
9658
+
9659
+ /**
9660
+ * Creates a new promise with resolvers.
9661
+ *
9662
+ * Based on:
9663
+ * - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
9664
+ */
9665
+ const promiseWithResolvers = () => {
9666
+ let resolve;
9667
+ let reject;
9668
+ const promise = new Promise((_resolve, _reject) => {
9669
+ resolve = _resolve;
9670
+ reject = _reject;
9671
+ });
9672
+ let isResolved = false;
9673
+ let isRejected = false;
9674
+ const resolver = (value) => {
9675
+ isResolved = true;
9676
+ resolve(value);
9677
+ };
9678
+ const rejecter = (reason) => {
9679
+ isRejected = true;
9680
+ reject(reason);
9681
+ };
9682
+ return {
9683
+ promise,
9684
+ resolve: resolver,
9685
+ reject: rejecter,
9686
+ isResolved,
9687
+ isRejected,
9688
+ };
9689
+ };
9331
9690
 
9332
9691
  /**
9333
9692
  * The client used for exchanging information with the SFU.
@@ -9335,133 +9694,224 @@ function removeConnectionEventListeners(cb) {
9335
9694
  class StreamSfuClient {
9336
9695
  /**
9337
9696
  * Constructs a new SFU client.
9338
- *
9339
- * @param dispatcher the event dispatcher to use.
9340
- * @param sfuServer the SFU server to connect to.
9341
- * @param token the JWT token to use for authentication.
9342
- * @param sessionId the `sessionId` of the currently connected participant.
9343
9697
  */
9344
- constructor({ dispatcher, sfuServer, token, sessionId, }) {
9698
+ constructor({ dispatcher, credentials, sessionId, logTag, joinResponseTimeout = 5000, onSignalClose, }) {
9345
9699
  /**
9346
9700
  * A buffer for ICE Candidates that are received before
9347
- * the PeerConnections are ready to handle them.
9701
+ * the Publisher and Subscriber Peer Connections are ready to handle them.
9348
9702
  */
9349
9703
  this.iceTrickleBuffer = new IceTrickleBuffer();
9350
9704
  /**
9351
- * A flag indicating whether the client is currently migrating away
9352
- * from this SFU.
9353
- */
9354
- this.isMigratingAway = false;
9355
- /**
9356
- * A flag indicating that the client connection is broken for the current
9357
- * client and that a fast-reconnect with a new client should be attempted.
9705
+ * Flag to indicate if the client is in the process of leaving the call.
9706
+ * This is set to `true` when the user initiates the leave process.
9358
9707
  */
9359
- this.isFastReconnecting = false;
9708
+ this.isLeaving = false;
9360
9709
  this.pingIntervalInMs = 10 * 1000;
9361
9710
  this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
9362
- this.close = (code, reason) => {
9363
- this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
9364
- if (this.signalWs.readyState !== this.signalWs.CLOSED) {
9711
+ this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
9712
+ /**
9713
+ * Promise that resolves when the JoinResponse is received.
9714
+ * Rejects after a certain threshold if the response is not received.
9715
+ */
9716
+ this.joinResponseTask = promiseWithResolvers();
9717
+ /**
9718
+ * A controller to abort the current requests.
9719
+ */
9720
+ this.abortController = new AbortController();
9721
+ this.createWebSocket = () => {
9722
+ this.signalWs = createWebSocketSignalChannel({
9723
+ logTag: this.logTag,
9724
+ endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
9725
+ onMessage: (message) => {
9726
+ this.lastMessageTimestamp = new Date();
9727
+ this.scheduleConnectionCheck();
9728
+ this.dispatcher.dispatch(message, this.logTag);
9729
+ },
9730
+ });
9731
+ this.signalWs.addEventListener('close', this.handleWebSocketClose);
9732
+ this.signalWs.addEventListener('error', this.restoreWebSocket);
9733
+ this.signalReady = new Promise((resolve) => {
9734
+ const onOpen = () => {
9735
+ this.signalWs.removeEventListener('open', onOpen);
9736
+ resolve(this.signalWs);
9737
+ };
9738
+ this.signalWs.addEventListener('open', onOpen);
9739
+ });
9740
+ };
9741
+ this.cleanUpWebSocket = () => {
9742
+ this.signalWs.removeEventListener('error', this.restoreWebSocket);
9743
+ this.signalWs.removeEventListener('close', this.handleWebSocketClose);
9744
+ };
9745
+ this.restoreWebSocket = () => {
9746
+ withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
9747
+ this.logger('debug', 'Restoring SFU WS connection');
9748
+ this.cleanUpWebSocket();
9749
+ await sleep(500);
9750
+ this.createWebSocket();
9751
+ }).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
9752
+ };
9753
+ this.handleWebSocketClose = (e) => {
9754
+ this.signalWs.removeEventListener('close', this.handleWebSocketClose);
9755
+ clearInterval(this.keepAliveInterval);
9756
+ clearTimeout(this.connectionCheckTimeout);
9757
+ if (this.onSignalClose) {
9758
+ this.onSignalClose(e);
9759
+ }
9760
+ };
9761
+ this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
9762
+ if (this.signalWs.readyState === WebSocket.OPEN) {
9763
+ this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
9365
9764
  this.signalWs.close(code, `js-client: ${reason}`);
9765
+ this.cleanUpWebSocket();
9366
9766
  }
9767
+ this.dispose();
9768
+ };
9769
+ this.dispose = () => {
9770
+ this.logger('debug', 'Disposing SFU client');
9367
9771
  this.unsubscribeIceTrickle();
9368
9772
  clearInterval(this.keepAliveInterval);
9369
9773
  clearTimeout(this.connectionCheckTimeout);
9774
+ clearTimeout(this.migrateAwayTimeout);
9775
+ this.abortController.abort();
9776
+ this.migrationTask?.resolve();
9777
+ };
9778
+ this.leaveAndClose = async (reason) => {
9779
+ await this.joinResponseTask.promise;
9780
+ try {
9781
+ this.isLeaving = true;
9782
+ await this.notifyLeave(reason);
9783
+ }
9784
+ catch (err) {
9785
+ this.logger('debug', 'Error notifying SFU about leaving call', err);
9786
+ }
9787
+ this.close(StreamSfuClient.NORMAL_CLOSURE, reason.substring(0, 115));
9370
9788
  };
9371
- this.updateSubscriptions = async (subscriptions) => {
9372
- return retryable(() => this.rpc.updateSubscriptions({
9373
- sessionId: this.sessionId,
9374
- tracks: subscriptions,
9375
- }), this.logger, 'debug');
9789
+ this.updateSubscriptions = async (tracks) => {
9790
+ await this.joinResponseTask.promise;
9791
+ return retryable(() => this.rpc.updateSubscriptions({ sessionId: this.sessionId, tracks }), this.abortController.signal);
9376
9792
  };
9377
9793
  this.setPublisher = async (data) => {
9378
- return retryable(() => this.rpc.setPublisher({
9379
- ...data,
9380
- sessionId: this.sessionId,
9381
- }), this.logger);
9794
+ await this.joinResponseTask.promise;
9795
+ return retryable(() => this.rpc.setPublisher({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9382
9796
  };
9383
9797
  this.sendAnswer = async (data) => {
9384
- return retryable(() => this.rpc.sendAnswer({
9385
- ...data,
9386
- sessionId: this.sessionId,
9387
- }), this.logger);
9798
+ await this.joinResponseTask.promise;
9799
+ return retryable(() => this.rpc.sendAnswer({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9388
9800
  };
9389
9801
  this.iceTrickle = async (data) => {
9390
- return retryable(() => this.rpc.iceTrickle({
9391
- ...data,
9392
- sessionId: this.sessionId,
9393
- }), this.logger);
9802
+ await this.joinResponseTask.promise;
9803
+ return retryable(() => this.rpc.iceTrickle({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9394
9804
  };
9395
9805
  this.iceRestart = async (data) => {
9396
- return retryable(() => this.rpc.iceRestart({
9397
- ...data,
9398
- sessionId: this.sessionId,
9399
- }), this.logger);
9806
+ await this.joinResponseTask.promise;
9807
+ return retryable(() => this.rpc.iceRestart({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9400
9808
  };
9401
9809
  this.updateMuteState = async (trackType, muted) => {
9402
- return this.updateMuteStates({
9403
- muteStates: [
9404
- {
9405
- trackType,
9406
- muted,
9407
- },
9408
- ],
9409
- });
9810
+ await this.joinResponseTask.promise;
9811
+ return this.updateMuteStates({ muteStates: [{ trackType, muted }] });
9410
9812
  };
9411
9813
  this.updateMuteStates = async (data) => {
9412
- return retryable(() => this.rpc.updateMuteStates({
9413
- ...data,
9414
- sessionId: this.sessionId,
9415
- }), this.logger);
9814
+ await this.joinResponseTask.promise;
9815
+ return retryable(() => this.rpc.updateMuteStates({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9416
9816
  };
9417
9817
  this.sendStats = async (stats) => {
9418
- return retryable(() => this.rpc.sendStats({
9419
- ...stats,
9420
- sessionId: this.sessionId,
9421
- }), this.logger, 'debug');
9818
+ await this.joinResponseTask.promise;
9819
+ return retryable(() => this.rpc.sendStats({ ...stats, sessionId: this.sessionId }), this.abortController.signal);
9422
9820
  };
9423
9821
  this.startNoiseCancellation = async () => {
9424
- return retryable(() => this.rpc.startNoiseCancellation({
9425
- sessionId: this.sessionId,
9426
- }), this.logger);
9822
+ await this.joinResponseTask.promise;
9823
+ return retryable(() => this.rpc.startNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
9427
9824
  };
9428
9825
  this.stopNoiseCancellation = async () => {
9429
- return retryable(() => this.rpc.stopNoiseCancellation({
9430
- sessionId: this.sessionId,
9431
- }), this.logger);
9826
+ await this.joinResponseTask.promise;
9827
+ return retryable(() => this.rpc.stopNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
9828
+ };
9829
+ this.enterMigration = async (opts = {}) => {
9830
+ this.isLeaving = true;
9831
+ const { timeout = 7 * 1000 } = opts;
9832
+ this.migrationTask?.reject(new Error('Cancelled previous migration'));
9833
+ const task = (this.migrationTask = promiseWithResolvers());
9834
+ const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
9835
+ unsubscribe();
9836
+ clearTimeout(this.migrateAwayTimeout);
9837
+ task.resolve();
9838
+ });
9839
+ this.migrateAwayTimeout = setTimeout(() => {
9840
+ unsubscribe();
9841
+ task.reject(new Error(`Migration (${this.logTag}) failed to complete in ${timeout}ms`));
9842
+ }, timeout);
9843
+ return task.promise;
9432
9844
  };
9433
9845
  this.join = async (data) => {
9434
- const joinRequest = JoinRequest.create({
9435
- ...data,
9436
- sessionId: this.sessionId,
9437
- token: this.token,
9846
+ // wait for the signal web socket to be ready before sending "joinRequest"
9847
+ await this.signalReady;
9848
+ if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
9849
+ // we need to lock the RPC requests until we receive a JoinResponse.
9850
+ // that's why we have this primitive lock mechanism.
9851
+ // the client starts with already initialized joinResponseTask,
9852
+ // and this code creates a new one for the next join request.
9853
+ this.joinResponseTask = promiseWithResolvers();
9854
+ }
9855
+ // capture a reference to the current joinResponseTask as it might
9856
+ // be replaced with a new one in case a second join request is made
9857
+ const current = this.joinResponseTask;
9858
+ let timeoutId;
9859
+ const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
9860
+ this.logger('debug', 'Received joinResponse', joinResponse);
9861
+ clearTimeout(timeoutId);
9862
+ unsubscribe();
9863
+ this.keepAlive();
9864
+ current.resolve(joinResponse);
9438
9865
  });
9439
- return this.send(SfuRequest.create({
9866
+ timeoutId = setTimeout(() => {
9867
+ unsubscribe();
9868
+ current.reject(new Error('Waiting for "joinResponse" has timed out'));
9869
+ }, this.joinResponseTimeout);
9870
+ await this.send(SfuRequest.create({
9440
9871
  requestPayload: {
9441
9872
  oneofKind: 'joinRequest',
9442
- joinRequest,
9873
+ joinRequest: JoinRequest.create({
9874
+ ...data,
9875
+ sessionId: this.sessionId,
9876
+ token: this.credentials.token,
9877
+ }),
9878
+ },
9879
+ }));
9880
+ return current.promise;
9881
+ };
9882
+ this.ping = async () => {
9883
+ return this.send(SfuRequest.create({
9884
+ requestPayload: {
9885
+ oneofKind: 'healthCheckRequest',
9886
+ healthCheckRequest: {},
9887
+ },
9888
+ }));
9889
+ };
9890
+ this.notifyLeave = async (reason) => {
9891
+ return this.send(SfuRequest.create({
9892
+ requestPayload: {
9893
+ oneofKind: 'leaveCallRequest',
9894
+ leaveCallRequest: {
9895
+ sessionId: this.sessionId,
9896
+ reason,
9897
+ },
9443
9898
  },
9444
9899
  }));
9445
9900
  };
9446
9901
  this.send = async (message) => {
9447
- return this.signalReady.then((signal) => {
9448
- if (signal.readyState !== signal.OPEN)
9449
- return;
9450
- this.logger('debug', `Sending message to: ${this.edgeName}`, SfuRequest.toJson(message));
9451
- signal.send(SfuRequest.toBinary(message));
9452
- });
9902
+ await this.signalReady; // wait for the signal ws to be open
9903
+ const msgJson = SfuRequest.toJson(message);
9904
+ if (this.signalWs.readyState !== WebSocket.OPEN) {
9905
+ this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
9906
+ return;
9907
+ }
9908
+ this.logger('debug', `Sending message to: ${this.edgeName}`, msgJson);
9909
+ this.signalWs.send(SfuRequest.toBinary(message));
9453
9910
  };
9454
9911
  this.keepAlive = () => {
9455
9912
  clearInterval(this.keepAliveInterval);
9456
9913
  this.keepAliveInterval = setInterval(() => {
9457
- this.logger('trace', 'Sending healthCheckRequest to SFU');
9458
- const message = SfuRequest.create({
9459
- requestPayload: {
9460
- oneofKind: 'healthCheckRequest',
9461
- healthCheckRequest: {},
9462
- },
9463
- });
9464
- this.send(message).catch((e) => {
9914
+ this.ping().catch((e) => {
9465
9915
  this.logger('error', 'Error sending healthCheckRequest to SFU', e);
9466
9916
  });
9467
9917
  }, this.pingIntervalInMs);
@@ -9477,53 +9927,37 @@ class StreamSfuClient {
9477
9927
  }
9478
9928
  }, this.unhealthyTimeoutInMs);
9479
9929
  };
9930
+ this.dispatcher = dispatcher;
9480
9931
  this.sessionId = sessionId || generateUUIDv4();
9481
- this.sfuServer = sfuServer;
9482
- this.edgeName = sfuServer.edge_name;
9483
- this.token = token;
9484
- this.logger = getLogger(['sfu-client']);
9485
- const logInterceptor = {
9486
- interceptUnary: (next, method, input, options) => {
9487
- this.logger('trace', `Calling SFU RPC method ${method.name}`, {
9488
- input,
9489
- options,
9490
- });
9491
- return next(method, input, options);
9492
- },
9493
- };
9932
+ this.onSignalClose = onSignalClose;
9933
+ this.credentials = credentials;
9934
+ const { server, token } = credentials;
9935
+ this.edgeName = server.edge_name;
9936
+ this.joinResponseTimeout = joinResponseTimeout;
9937
+ this.logTag = logTag;
9938
+ this.logger = getLogger(['sfu-client', logTag]);
9494
9939
  this.rpc = createSignalClient({
9495
- baseUrl: sfuServer.url,
9940
+ baseUrl: server.url,
9496
9941
  interceptors: [
9497
9942
  withHeaders({
9498
9943
  Authorization: `Bearer ${token}`,
9499
9944
  }),
9500
- logInterceptor,
9501
- ],
9945
+ getLogLevel() === 'trace' && withRequestLogger(this.logger, 'trace'),
9946
+ ].filter((v) => !!v),
9502
9947
  });
9503
9948
  // Special handling for the ICETrickle kind of events.
9504
- // These events might be triggered by the SFU before the initial RTC
9505
- // connection is established. In that case, those events (ICE candidates)
9506
- // need to be buffered and later added to the appropriate PeerConnection
9949
+ // The SFU might trigger these events before the initial RTC
9950
+ // connection is established or "JoinResponse" received.
9951
+ // In that case, those events (ICE candidates) need to be buffered
9952
+ // and later added to the appropriate PeerConnection
9507
9953
  // once the remoteDescription is known and set.
9508
9954
  this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (iceTrickle) => {
9509
9955
  this.iceTrickleBuffer.push(iceTrickle);
9510
9956
  });
9511
- this.signalWs = createWebSocketSignalChannel({
9512
- endpoint: sfuServer.ws_endpoint,
9513
- onMessage: (message) => {
9514
- this.lastMessageTimestamp = new Date();
9515
- this.scheduleConnectionCheck();
9516
- dispatcher.dispatch(message);
9517
- },
9518
- });
9519
- this.signalReady = new Promise((resolve) => {
9520
- const onOpen = () => {
9521
- this.signalWs.removeEventListener('open', onOpen);
9522
- this.keepAlive();
9523
- resolve(this.signalWs);
9524
- };
9525
- this.signalWs.addEventListener('open', onOpen);
9526
- });
9957
+ this.createWebSocket();
9958
+ }
9959
+ get isHealthy() {
9960
+ return this.signalWs.readyState === WebSocket.OPEN;
9527
9961
  }
9528
9962
  }
9529
9963
  /**
@@ -9536,45 +9970,15 @@ StreamSfuClient.NORMAL_CLOSURE = 1000;
9536
9970
  * a certain amount of time (`connectionCheckTimeout`).
9537
9971
  */
9538
9972
  StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
9539
- /**
9540
- * The error code used when the SFU connection is broken.
9541
- * Usually, this means that the WS connection has been closed unexpectedly.
9542
- * This error code is used to announce a fast-reconnect.
9543
- */
9544
- StreamSfuClient.ERROR_CONNECTION_BROKEN = 4002; // used in fast-reconnects
9545
- const MAX_RETRIES = 5;
9546
- /**
9547
- * Creates a closure which wraps the given RPC call and retries invoking
9548
- * the RPC until it succeeds or the maximum number of retries is reached.
9549
- *
9550
- * Between each retry, there would be a random delay in order to avoid
9551
- * request bursts towards the SFU.
9552
- *
9553
- * @param rpc the closure around the RPC call to execute.
9554
- * @param logger a logger instance to use.
9555
- * @param <I> the type of the request object.
9556
- * @param <O> the type of the response object.
9557
- */
9558
- const retryable = async (rpc, logger, level = 'error') => {
9559
- let retryAttempt = 0;
9560
- let rpcCallResult;
9561
- do {
9562
- // don't delay the first invocation
9563
- if (retryAttempt > 0) {
9564
- await sleep(retryInterval(retryAttempt));
9565
- }
9566
- rpcCallResult = await rpc();
9567
- // if the RPC call failed, log the error and retry
9568
- if (rpcCallResult.response.error) {
9569
- logger(level, `SFU RPC Error (${rpcCallResult.method.name}):`, rpcCallResult.response.error);
9570
- }
9571
- retryAttempt++;
9572
- } while (rpcCallResult.response.error?.shouldRetry &&
9573
- retryAttempt < MAX_RETRIES);
9574
- if (rpcCallResult.response.error) {
9575
- throw rpcCallResult.response.error;
9576
- }
9577
- return rpcCallResult;
9973
+
9974
+ const toRtcConfiguration = (config) => {
9975
+ return {
9976
+ iceServers: config.map((ice) => ({
9977
+ urls: ice.urls,
9978
+ username: ice.username,
9979
+ credential: ice.password,
9980
+ })),
9981
+ };
9578
9982
  };
9579
9983
 
9580
9984
  /**
@@ -9729,9 +10133,10 @@ const watchSfuErrorReports = (dispatcher) => {
9729
10133
  return dispatcher.on('error', (e) => {
9730
10134
  if (!e.error)
9731
10135
  return;
9732
- const { error } = e;
10136
+ const { error, reconnectStrategy } = e;
9733
10137
  logger$1('error', 'SFU reported error', {
9734
10138
  code: ErrorCode[error.code],
10139
+ reconnectStrategy: WebsocketReconnectStrategy[reconnectStrategy],
9735
10140
  message: error.message,
9736
10141
  shouldRetry: error.shouldRetry,
9737
10142
  });
@@ -9747,6 +10152,17 @@ const watchPinsUpdated = (state) => {
9747
10152
  state.setServerSidePins(pins);
9748
10153
  };
9749
10154
  };
10155
+ /**
10156
+ * Watches for `callEnded` events.
10157
+ */
10158
+ const watchSfuCallEnded = (call) => {
10159
+ return call.on('callEnded', (e) => {
10160
+ const reason = CallEndedReason[e.reason];
10161
+ call.leave({ reason }).catch((err) => {
10162
+ logger$1('error', 'Failed to leave call after call ended by the SFU', err);
10163
+ });
10164
+ });
10165
+ };
9750
10166
 
9751
10167
  /**
9752
10168
  * An event handler that handles soft mutes.
@@ -9768,12 +10184,13 @@ const handleRemoteSoftMute = (call) => {
9768
10184
  else if (type === TrackType.AUDIO) {
9769
10185
  await call.microphone.disable();
9770
10186
  }
10187
+ else if (type === TrackType.SCREEN_SHARE ||
10188
+ type === TrackType.SCREEN_SHARE_AUDIO) {
10189
+ await call.screenShare.disable();
10190
+ }
9771
10191
  else {
9772
10192
  logger('warn', 'Unsupported track type to soft mute', TrackType[type]);
9773
10193
  }
9774
- if (call.publisher?.isPublishing(type)) {
9775
- await call.stopPublish(type);
9776
- }
9777
10194
  }
9778
10195
  catch (error) {
9779
10196
  logger('error', 'Failed to stop publishing', error);
@@ -9794,11 +10211,12 @@ const watchParticipantJoined = (state) => {
9794
10211
  // potential duplicate events from the SFU.
9795
10212
  //
9796
10213
  // Although the SFU should not send duplicate events, we have seen
9797
- // some race conditions in the past during the `join-flow` where
9798
- // the SFU would send participant info as part of the `join`
10214
+ // some race conditions in the past during the `join-flow`.
10215
+ // The SFU would send participant info as part of the `join`
9799
10216
  // response and then follow up with a `participantJoined` event for
9800
10217
  // already announced participants.
9801
- state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, {
10218
+ const orphanedTracks = reconcileOrphanedTracks(state, participant);
10219
+ state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
9802
10220
  viewportVisibilityState: {
9803
10221
  videoTrack: exports.VisibilityState.UNKNOWN,
9804
10222
  screenShareTrack: exports.VisibilityState.UNKNOWN,
@@ -9834,12 +10252,14 @@ const watchParticipantUpdated = (state) => {
9834
10252
  */
9835
10253
  const watchTrackPublished = (state) => {
9836
10254
  return function onTrackPublished(e) {
9837
- const { type, sessionId, participant } = e;
10255
+ const { type, sessionId } = e;
9838
10256
  // An optimization for large calls.
9839
10257
  // After a certain threshold, the SFU would stop emitting `participantJoined`
9840
10258
  // events, and instead, it would only provide the participant's information
9841
10259
  // once they start publishing a track.
9842
- if (participant) {
10260
+ if (e.participant) {
10261
+ const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
10262
+ const participant = Object.assign(e.participant, orphanedTracks);
9843
10263
  state.updateOrAddParticipant(sessionId, participant);
9844
10264
  }
9845
10265
  else {
@@ -9855,9 +10275,11 @@ const watchTrackPublished = (state) => {
9855
10275
  */
9856
10276
  const watchTrackUnpublished = (state) => {
9857
10277
  return function onTrackUnpublished(e) {
9858
- const { type, sessionId, participant } = e;
10278
+ const { type, sessionId } = e;
9859
10279
  // An optimization for large calls. See `watchTrackPublished`.
9860
- if (participant) {
10280
+ if (e.participant) {
10281
+ const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
10282
+ const participant = Object.assign(e.participant, orphanedTracks);
9861
10283
  state.updateOrAddParticipant(sessionId, participant);
9862
10284
  }
9863
10285
  else {
@@ -9868,6 +10290,25 @@ const watchTrackUnpublished = (state) => {
9868
10290
  };
9869
10291
  };
9870
10292
  const unique = (v, i, arr) => arr.indexOf(v) === i;
10293
+ /**
10294
+ * Reconciles orphaned tracks (if any) for the given participant.
10295
+ *
10296
+ * @param state the call state.
10297
+ * @param participant the participant.
10298
+ */
10299
+ const reconcileOrphanedTracks = (state, participant) => {
10300
+ const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
10301
+ if (!orphanTracks.length)
10302
+ return;
10303
+ const reconciledTracks = {};
10304
+ for (const orphan of orphanTracks) {
10305
+ const key = trackTypeToParticipantStreamKey(orphan.trackType);
10306
+ if (!key)
10307
+ continue;
10308
+ reconciledTracks[key] = orphan.track;
10309
+ }
10310
+ return reconciledTracks;
10311
+ };
9871
10312
 
9872
10313
  /**
9873
10314
  * Watches for `dominantSpeakerChanged` events.
@@ -9916,12 +10357,13 @@ const watchAudioLevelChanged = (dispatcher, state) => {
9916
10357
  * Registers the default event handlers for a call during its lifecycle.
9917
10358
  *
9918
10359
  * @param call the call to register event handlers for.
9919
- * @param state the call state.
9920
10360
  * @param dispatcher the dispatcher.
9921
10361
  */
9922
- const registerEventHandlers = (call, state, dispatcher) => {
10362
+ const registerEventHandlers = (call, dispatcher) => {
10363
+ const state = call.state;
9923
10364
  const eventHandlers = [
9924
10365
  call.on('call.ended', watchCallEnded(call)),
10366
+ watchSfuCallEnded(call),
9925
10367
  watchLiveEnded(dispatcher, call),
9926
10368
  watchSfuErrorReports(dispatcher),
9927
10369
  watchChangePublishQuality(dispatcher, call),
@@ -9965,48 +10407,6 @@ const registerRingingCallEventHandlers = (call) => {
9965
10407
  };
9966
10408
  };
9967
10409
 
9968
- /**
9969
- * Collects all necessary information to join a call, talks to the coordinator
9970
- * and returns the necessary information to join the call.
9971
- *
9972
- * @param httpClient the http client to use.
9973
- * @param type the type of the call.
9974
- * @param id the id of the call.
9975
- * @param data the data for the call.
9976
- */
9977
- const join = async (httpClient, type, id, data) => {
9978
- const { call, credentials, members, own_capabilities, stats_options } = await doJoin(httpClient, type, id, data);
9979
- return {
9980
- connectionConfig: toRtcConfiguration(credentials.ice_servers),
9981
- sfuServer: credentials.server,
9982
- token: credentials.token,
9983
- metadata: call,
9984
- members,
9985
- ownCapabilities: own_capabilities,
9986
- statsOptions: stats_options,
9987
- };
9988
- };
9989
- const doJoin = async (httpClient, type, id, data) => {
9990
- const location = await httpClient.getLocationHint();
9991
- const request = {
9992
- ...data,
9993
- location,
9994
- };
9995
- return httpClient.post(`/call/${type}/${id}/join`, request);
9996
- };
9997
- const toRtcConfiguration = (config) => {
9998
- if (!config || config.length === 0)
9999
- return undefined;
10000
- const rtcConfig = {
10001
- iceServers: config.map((ice) => ({
10002
- urls: ice.urls,
10003
- username: ice.username,
10004
- credential: ice.password,
10005
- })),
10006
- };
10007
- return rtcConfig;
10008
- };
10009
-
10010
10410
  /**
10011
10411
  * Flatten the stats report into an array of stats objects.
10012
10412
  *
@@ -10286,6 +10686,7 @@ class SfuStatsReporter {
10286
10686
  this.start = () => {
10287
10687
  if (this.options.reporting_interval_ms <= 0)
10288
10688
  return;
10689
+ clearInterval(this.intervalId);
10289
10690
  this.intervalId = setInterval(() => {
10290
10691
  this.run().catch((err) => {
10291
10692
  this.logger('warn', 'Failed to report stats', err);
@@ -10969,7 +11370,7 @@ function lazy(factory) {
10969
11370
  * Returns an Observable that emits the list of available devices
10970
11371
  * that meet the given constraints.
10971
11372
  *
10972
- * @param constraints the constraints to use when requesting the devices.
11373
+ * @param permission a BrowserPermission instance.
10973
11374
  * @param kind the kind of devices to enumerate.
10974
11375
  */
10975
11376
  const getDevices = (permission, kind) => {
@@ -11222,6 +11623,12 @@ class InputMediaDeviceManager {
11222
11623
  listDevices() {
11223
11624
  return this.getDevices();
11224
11625
  }
11626
+ /**
11627
+ * Returns `true` when this device is in enabled state.
11628
+ */
11629
+ get enabled() {
11630
+ return this.state.status === 'enabled';
11631
+ }
11225
11632
  /**
11226
11633
  * Starts stream.
11227
11634
  */
@@ -11344,7 +11751,7 @@ class InputMediaDeviceManager {
11344
11751
  await this.applySettingsToStream();
11345
11752
  }
11346
11753
  async applySettingsToStream() {
11347
- if (this.state.status === 'enabled') {
11754
+ if (this.enabled) {
11348
11755
  await this.muteStream();
11349
11756
  await this.unmuteStream();
11350
11757
  }
@@ -11481,19 +11888,22 @@ class InputMediaDeviceManager {
11481
11888
  return output;
11482
11889
  })
11483
11890
  .then(chainWith(parent), (error) => {
11484
- this.logger('warn', 'Fitler failed to start and will be ignored', error);
11891
+ this.logger('warn', 'Filter failed to start and will be ignored', error);
11485
11892
  return parent;
11486
11893
  }), rootStream);
11487
11894
  }
11488
11895
  if (this.call.state.callingState === exports.CallingState.JOINED) {
11489
11896
  await this.publishStream(stream);
11490
11897
  }
11898
+ else {
11899
+ this.logger('debug', 'Stream is not published as the call is not joined');
11900
+ }
11491
11901
  if (this.state.mediaStream !== stream) {
11492
11902
  this.state.setMediaStream(stream, await rootStream);
11493
11903
  this.getTracks().forEach((track) => {
11494
11904
  track.addEventListener('ended', async () => {
11495
11905
  await this.statusChangeSettled();
11496
- if (this.state.status === 'enabled') {
11906
+ if (this.enabled) {
11497
11907
  this.isTrackStoppedDueToTrackEnd = true;
11498
11908
  setTimeout(() => {
11499
11909
  this.isTrackStoppedDueToTrackEnd = false;
@@ -11786,7 +12196,7 @@ class CameraManager extends InputMediaDeviceManager {
11786
12196
  this.logger('warn', 'could not apply target resolution', error);
11787
12197
  }
11788
12198
  }
11789
- if (this.state.status === 'enabled') {
12199
+ if (this.enabled) {
11790
12200
  const { width, height } = this.state
11791
12201
  .mediaStream.getVideoTracks()[0]
11792
12202
  ?.getSettings();
@@ -12090,7 +12500,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
12090
12500
  });
12091
12501
  const registrationResult = this.registerFilter(noiseCancellation.toFilter());
12092
12502
  this.noiseCancellationRegistration = registrationResult.registered;
12093
- this.uregisterNoiseCancellation = registrationResult.unregister;
12503
+ this.unregisterNoiseCancellation = registrationResult.unregister;
12094
12504
  await this.noiseCancellationRegistration;
12095
12505
  // handles an edge case where a noise cancellation is enabled after
12096
12506
  // the participant as joined the call -> we immediately enable NC
@@ -12116,7 +12526,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
12116
12526
  if (isReactNative()) {
12117
12527
  throw new Error('Noise cancellation is not supported in React Native');
12118
12528
  }
12119
- await (this.uregisterNoiseCancellation?.() ?? Promise.resolve())
12529
+ await (this.unregisterNoiseCancellation?.() ?? Promise.resolve())
12120
12530
  .then(() => this.noiseCancellation?.disable())
12121
12531
  .then(() => this.noiseCancellationChangeUnsubscribe?.())
12122
12532
  .catch((err) => {
@@ -12498,9 +12908,17 @@ class Call {
12498
12908
  */
12499
12909
  this.dispatcher = new Dispatcher();
12500
12910
  this.trackSubscriptionsSubject = new rxjs.BehaviorSubject({ type: exports.DebounceType.MEDIUM, data: [] });
12911
+ this.sfuClientTag = 0;
12912
+ this.reconnectConcurrencyTag = Symbol('reconnectConcurrencyTag');
12501
12913
  this.reconnectAttempts = 0;
12502
- this.maxReconnectAttempts = 10;
12503
- this.isLeaving = false;
12914
+ this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
12915
+ this.fastReconnectDeadlineSeconds = 0;
12916
+ this.lastOfflineTimestamp = 0;
12917
+ // maintain the order of publishing tracks to restore them after a reconnection
12918
+ // it shouldn't contain duplicates
12919
+ this.trackPublishOrder = [];
12920
+ this.hasJoinedOnce = false;
12921
+ this.deviceSettingsAppliedOnce = false;
12504
12922
  this.initialized = false;
12505
12923
  this.joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
12506
12924
  /**
@@ -12510,6 +12928,42 @@ class Call {
12510
12928
  */
12511
12929
  this.leaveCallHooks = new Set();
12512
12930
  this.streamClientEventHandlers = new Map();
12931
+ this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
12932
+ // update the permission context.
12933
+ this.permissionsContext.setPermissions(ownCapabilities);
12934
+ if (!this.publisher)
12935
+ return;
12936
+ // check if the user still has publishing permissions and stop publishing if not.
12937
+ const permissionToTrackType = {
12938
+ [OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
12939
+ [OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
12940
+ [OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
12941
+ };
12942
+ for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
12943
+ const hasPermission = this.permissionsContext.hasPermission(permission);
12944
+ if (hasPermission)
12945
+ continue;
12946
+ try {
12947
+ switch (trackType) {
12948
+ case TrackType.AUDIO:
12949
+ if (this.microphone.enabled)
12950
+ await this.microphone.disable();
12951
+ break;
12952
+ case TrackType.VIDEO:
12953
+ if (this.camera.enabled)
12954
+ await this.camera.disable();
12955
+ break;
12956
+ case TrackType.SCREEN_SHARE:
12957
+ if (this.screenShare.enabled)
12958
+ await this.screenShare.disable();
12959
+ break;
12960
+ }
12961
+ }
12962
+ catch (err) {
12963
+ this.logger('error', `Can't disable mic/camera/screenshare after revoked permissions`, err);
12964
+ }
12965
+ }
12966
+ };
12513
12967
  /**
12514
12968
  * You can subscribe to WebSocket events provided by the API. To remove a subscription, call the `off` method.
12515
12969
  * Please note that subscribing to WebSocket events is an advanced use-case.
@@ -12560,9 +13014,8 @@ class Call {
12560
13014
  throw new Error('Cannot leave call that has already been left.');
12561
13015
  }
12562
13016
  if (callingState === exports.CallingState.JOINING) {
12563
- await this.assertCallJoined();
13017
+ await this.waitUntilCallJoined();
12564
13018
  }
12565
- this.isLeaving = true;
12566
13019
  if (this.ringing) {
12567
13020
  // I'm the one who started the call, so I should cancel it.
12568
13021
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
@@ -12584,14 +13037,15 @@ class Call {
12584
13037
  this.sfuStatsReporter = undefined;
12585
13038
  this.subscriber?.close();
12586
13039
  this.subscriber = undefined;
12587
- this.publisher?.close();
13040
+ this.publisher?.close({ stopTracks: true });
12588
13041
  this.publisher = undefined;
12589
- this.sfuClient?.close(StreamSfuClient.NORMAL_CLOSURE, reason);
13042
+ await this.sfuClient?.leaveAndClose(reason);
12590
13043
  this.sfuClient = undefined;
12591
13044
  this.state.setCallingState(exports.CallingState.LEFT);
12592
13045
  // Call all leave call hooks, e.g. to clean up global event handlers
12593
13046
  this.leaveCallHooks.forEach((hook) => hook());
12594
13047
  this.initialized = false;
13048
+ this.hasJoinedOnce = false;
12595
13049
  this.clientStore.unregisterCall(this);
12596
13050
  this.camera.dispose();
12597
13051
  this.microphone.dispose();
@@ -12631,7 +13085,7 @@ class Call {
12631
13085
  this.watching = true;
12632
13086
  this.clientStore.registerCall(this);
12633
13087
  }
12634
- await this.applyDeviceConfig();
13088
+ await this.applyDeviceConfig(false);
12635
13089
  return response;
12636
13090
  };
12637
13091
  /**
@@ -12653,7 +13107,7 @@ class Call {
12653
13107
  this.watching = true;
12654
13108
  this.clientStore.registerCall(this);
12655
13109
  }
12656
- await this.applyDeviceConfig();
13110
+ await this.applyDeviceConfig(false);
12657
13111
  return response;
12658
13112
  };
12659
13113
  /**
@@ -12709,253 +13163,174 @@ class Call {
12709
13163
  await this.setup();
12710
13164
  const callingState = this.state.callingState;
12711
13165
  if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
12712
- this.logger('warn', 'Join method called twice, you should only call this once');
12713
- throw new Error(`Illegal State: Already joined.`);
13166
+ throw new Error(`Illegal State: call.join() shall be called only once`);
12714
13167
  }
12715
- const isMigrating = callingState === exports.CallingState.MIGRATING;
12716
- const isReconnecting = callingState === exports.CallingState.RECONNECTING;
12717
- this.state.setCallingState(exports.CallingState.JOINING);
13168
+ this.joinCallData = data;
12718
13169
  this.logger('debug', 'Starting join flow');
12719
- if (data?.ring && !this.ringing) {
12720
- this.ringingSubject.next(true);
12721
- }
12722
- if (this.ringing && !this.isCreatedByMe) {
12723
- // signals other users that I have accepted the incoming call.
12724
- await this.accept();
12725
- }
12726
- let sfuServer;
12727
- let sfuToken;
12728
- let connectionConfig;
12729
- let statsOptions;
12730
- try {
12731
- if (this.sfuClient?.isFastReconnecting) {
12732
- // use previous SFU configuration and values
12733
- connectionConfig = this.publisher?.connectionConfiguration;
12734
- sfuServer = this.sfuClient.sfuServer;
12735
- sfuToken = this.sfuClient.token;
12736
- statsOptions = this.sfuStatsReporter?.options;
12737
- }
12738
- else {
12739
- // full join flow - let the Coordinator pick a new SFU for us
12740
- const call = await join(this.streamClient, this.type, this.id, data);
12741
- this.state.updateFromCallResponse(call.metadata);
12742
- this.state.setMembers(call.members);
12743
- this.state.setOwnCapabilities(call.ownCapabilities);
12744
- connectionConfig = call.connectionConfig;
12745
- sfuServer = call.sfuServer;
12746
- sfuToken = call.token;
12747
- statsOptions = call.statsOptions;
13170
+ this.state.setCallingState(exports.CallingState.JOINING);
13171
+ const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
13172
+ const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
13173
+ const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
13174
+ let statsOptions = this.sfuStatsReporter?.options;
13175
+ if (!this.credentials ||
13176
+ !statsOptions ||
13177
+ performingRejoin ||
13178
+ performingMigration) {
13179
+ try {
13180
+ const joinResponse = await this.doJoinRequest(data);
13181
+ this.credentials = joinResponse.credentials;
13182
+ statsOptions = joinResponse.stats_options;
12748
13183
  }
12749
- if (this.streamClient._hasConnectionID()) {
12750
- this.watching = true;
12751
- this.clientStore.registerCall(this);
13184
+ catch (error) {
13185
+ // restore the previous call state if the join-flow fails
13186
+ this.state.setCallingState(callingState);
13187
+ throw error;
12752
13188
  }
12753
13189
  }
12754
- catch (error) {
12755
- // restore the previous call state if the join-flow fails
12756
- this.state.setCallingState(callingState);
12757
- throw error;
12758
- }
12759
13190
  const previousSfuClient = this.sfuClient;
12760
- const sfuClient = (this.sfuClient = new StreamSfuClient({
12761
- dispatcher: this.dispatcher,
12762
- sfuServer,
12763
- token: sfuToken,
12764
- sessionId: previousSfuClient?.sessionId,
12765
- }));
12766
- /**
12767
- * A closure which hides away the re-connection logic.
12768
- */
12769
- const reconnect = async (strategy, reason) => {
12770
- const currentState = this.state.callingState;
12771
- if (currentState === exports.CallingState.MIGRATING ||
12772
- currentState === exports.CallingState.RECONNECTING) {
12773
- // prevent parallel reconnection attempts
12774
- return;
12775
- }
12776
- this.reconnectAttempts++;
12777
- this.state.setCallingState(strategy === 'migrate'
12778
- ? exports.CallingState.MIGRATING
12779
- : exports.CallingState.RECONNECTING);
12780
- if (strategy === 'migrate') {
12781
- this.logger('debug', `[Migration]: migrating call ${this.cid} away from ${sfuServer.edge_name}`);
12782
- sfuClient.isMigratingAway = true;
12783
- }
12784
- else {
12785
- this.logger('debug', `[Rejoin]: ${strategy} rejoin call ${this.cid} (${this.reconnectAttempts})...`);
12786
- }
12787
- // take a snapshot of the current "local participant" state
12788
- // we'll need it for restoring the previous publishing state later
12789
- const localParticipant = this.state.localParticipant;
12790
- if (strategy === 'fast') {
12791
- sfuClient.close(StreamSfuClient.ERROR_CONNECTION_BROKEN, `attempting fast reconnect: ${reason}`);
12792
- }
12793
- else if (strategy === 'full') {
12794
- // in migration or recovery scenarios, we don't want to
12795
- // wait before attempting to reconnect to an SFU server
12796
- await sleep(retryInterval(this.reconnectAttempts));
12797
- // in full-reconnect, we need to dispose all Peer Connections
12798
- this.subscriber?.close();
12799
- this.subscriber = undefined;
12800
- this.publisher?.close({ stopTracks: false });
12801
- this.publisher = undefined;
12802
- this.statsReporter?.stop();
12803
- this.statsReporter = undefined;
12804
- this.sfuStatsReporter?.stop();
12805
- this.sfuStatsReporter = undefined;
12806
- // clean up current connection
12807
- sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, `attempting full reconnect: ${reason}`);
12808
- }
12809
- await this.join({
12810
- ...data,
12811
- ...(strategy === 'migrate' && { migrating_from: sfuServer.edge_name }),
13191
+ const previousSessionId = previousSfuClient?.sessionId;
13192
+ const isWsHealthy = !!previousSfuClient?.isHealthy;
13193
+ const sfuClient = performingRejoin || performingMigration || !isWsHealthy
13194
+ ? new StreamSfuClient({
13195
+ logTag: String(this.sfuClientTag++),
13196
+ dispatcher: this.dispatcher,
13197
+ credentials: this.credentials,
13198
+ // a new session_id is necessary for the REJOIN strategy.
13199
+ // we use the previous session_id if available
13200
+ sessionId: performingRejoin ? undefined : previousSessionId,
13201
+ onSignalClose: () => this.handleSfuSignalClose(sfuClient),
13202
+ })
13203
+ : previousSfuClient;
13204
+ this.sfuClient = sfuClient;
13205
+ const clientDetails = getClientDetails();
13206
+ // we don't need to send JoinRequest if we are re-using an existing healthy SFU client
13207
+ if (previousSfuClient !== sfuClient) {
13208
+ // prepare a generic SDP and send it to the SFU.
13209
+ // this is a throw-away SDP that the SFU will use to determine
13210
+ // the capabilities of the client (codec support, etc.)
13211
+ const receivingCapabilitiesSdp = await getGenericSdp('recvonly');
13212
+ const reconnectDetails = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED
13213
+ ? this.getReconnectDetails(data?.migrating_from, previousSessionId)
13214
+ : undefined;
13215
+ const { callState, fastReconnectDeadlineSeconds } = await sfuClient.join({
13216
+ subscriberSdp: receivingCapabilitiesSdp,
13217
+ clientDetails,
13218
+ fastReconnect: performingFastReconnect,
13219
+ reconnectDetails,
12812
13220
  });
12813
- // clean up previous connection
12814
- if (strategy === 'migrate') {
12815
- sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'attempting migration');
12816
- }
12817
- this.logger('info', `[Rejoin]: Attempt ${this.reconnectAttempts} successful!`);
12818
- // we shouldn't be republishing the streams if we're migrating
12819
- // as the underlying peer connection will take care of it as part
12820
- // of the ice-restart process
12821
- if (localParticipant && strategy === 'full') {
12822
- const { audioStream, videoStream, screenShareStream, screenShareAudioStream, } = localParticipant;
12823
- let screenShare;
12824
- if (screenShareStream || screenShareAudioStream) {
12825
- screenShare = new MediaStream();
12826
- screenShareStream?.getVideoTracks().forEach((track) => {
12827
- screenShare?.addTrack(track);
12828
- });
12829
- screenShareAudioStream?.getAudioTracks().forEach((track) => {
12830
- screenShare?.addTrack(track);
12831
- });
12832
- }
12833
- // restore previous publishing state
12834
- if (audioStream)
12835
- await this.publishAudioStream(audioStream);
12836
- if (videoStream) {
12837
- await this.publishVideoStream(videoStream, {
12838
- preferredCodec: this.camera.preferredCodec,
12839
- });
12840
- }
12841
- if (screenShare)
12842
- await this.publishScreenShareStream(screenShare);
12843
- this.logger('info', `[Rejoin]: State restored. Attempt: ${this.reconnectAttempts}`);
13221
+ this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
13222
+ if (callState) {
13223
+ this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
12844
13224
  }
12845
- };
12846
- // reconnect if the connection was closed unexpectedly. example:
12847
- // - SFU crash or restart
12848
- // - network change
12849
- sfuClient.signalReady.then(() => {
12850
- // register a handler for the "goAway" event
12851
- const unregisterGoAway = this.dispatcher.on('goAway', (event) => {
12852
- const { reason } = event;
12853
- this.logger('info', `[Migration]: Going away from SFU... Reason: ${GoAwayReason[reason]}`);
12854
- reconnect('migrate', GoAwayReason[reason]).catch((err) => {
12855
- this.logger('warn', `[Migration]: Failed to migrate to another SFU.`, err);
12856
- });
12857
- });
12858
- sfuClient.signalWs.addEventListener('close', (e) => {
12859
- // unregister the "goAway" handler, as we won't need it anymore for this connection.
12860
- // the upcoming re-join will register a new handler anyway
12861
- unregisterGoAway();
12862
- // when the user has initiated "call.leave()" operation, we shouldn't
12863
- // care for the WS close code and we shouldn't ever attempt to reconnect
12864
- if (this.isLeaving)
12865
- return;
12866
- // do nothing if the connection was closed on purpose
12867
- if (e.code === StreamSfuClient.NORMAL_CLOSURE)
12868
- return;
12869
- // do nothing if the connection was closed because of a policy violation
12870
- // e.g., the user has been blocked by an admin or moderator
12871
- if (e.code === KnownCodes.WS_POLICY_VIOLATION)
12872
- return;
12873
- // When the SFU is being shut down, it sends a goAway message.
12874
- // While we migrate to another SFU, we might have the WS connection
12875
- // to the old SFU closed abruptly. In this case, we don't want
12876
- // to reconnect to the old SFU, but rather to the new one.
12877
- const isMigratingAway = e.code === KnownCodes.WS_CLOSED_ABRUPTLY && sfuClient.isMigratingAway;
12878
- const isFastReconnecting = e.code === KnownCodes.WS_CLOSED_ABRUPTLY &&
12879
- sfuClient.isFastReconnecting;
12880
- if (isMigratingAway || isFastReconnecting)
12881
- return;
12882
- // do nothing if the connection was closed because of a fast reconnect
12883
- if (e.code === StreamSfuClient.ERROR_CONNECTION_BROKEN)
12884
- return;
12885
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
12886
- sfuClient.isFastReconnecting = this.reconnectAttempts === 0;
12887
- const strategy = sfuClient.isFastReconnecting ? 'fast' : 'full';
12888
- reconnect(strategy, `SFU closed the WS with code: ${e.code}`).catch((err) => {
12889
- this.logger('error', `[Rejoin]: ${strategy} rejoin failed for ${this.reconnectAttempts} times. Giving up.`, err);
12890
- this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
12891
- });
12892
- }
12893
- else {
12894
- this.logger('error', '[Rejoin]: Reconnect attempts exceeded. Giving up...');
12895
- this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
12896
- }
12897
- });
12898
- });
12899
- // handlers for connection online/offline events
12900
- const unsubscribeOnlineEvent = this.streamClient.on('connection.changed', async (e) => {
12901
- if (e.type !== 'connection.changed')
12902
- return;
12903
- if (!e.online)
12904
- return;
12905
- unsubscribeOnlineEvent();
12906
- const currentCallingState = this.state.callingState;
12907
- const shouldReconnect = currentCallingState === exports.CallingState.OFFLINE ||
12908
- currentCallingState === exports.CallingState.RECONNECTING_FAILED;
12909
- if (!shouldReconnect)
12910
- return;
12911
- this.logger('info', '[Rejoin]: Going online...');
12912
- let isFirstReconnectAttempt = true;
12913
- do {
12914
- try {
12915
- sfuClient.isFastReconnecting = isFirstReconnectAttempt;
12916
- await reconnect(isFirstReconnectAttempt ? 'fast' : 'full', 'Network: online');
12917
- return; // break the loop if rejoin is successful
12918
- }
12919
- catch (err) {
12920
- this.logger('error', `[Rejoin][Network]: Rejoin failed for attempt ${this.reconnectAttempts}`, err);
12921
- }
12922
- // wait for a bit before trying to reconnect again
12923
- await sleep(retryInterval(this.reconnectAttempts));
12924
- isFirstReconnectAttempt = false;
12925
- } while (this.reconnectAttempts < this.maxReconnectAttempts);
12926
- // if we're here, it means that we've exhausted all the reconnect attempts
12927
- this.logger('error', `[Rejoin][Network]: Rejoin failed. Giving up.`);
12928
- this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
12929
- });
12930
- const unsubscribeOfflineEvent = this.streamClient.on('connection.changed', (e) => {
12931
- if (e.type !== 'connection.changed')
12932
- return;
12933
- if (e.online)
12934
- return;
12935
- unsubscribeOfflineEvent();
12936
- this.state.setCallingState(exports.CallingState.OFFLINE);
12937
- });
12938
- this.leaveCallHooks.add(() => {
12939
- unsubscribeOnlineEvent();
12940
- unsubscribeOfflineEvent();
12941
- });
12942
- if (!this.subscriber) {
12943
- this.subscriber = new Subscriber({
13225
+ }
13226
+ if (!performingMigration) {
13227
+ // in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
13228
+ this.state.setCallingState(exports.CallingState.JOINED);
13229
+ }
13230
+ this.hasJoinedOnce = true;
13231
+ // when performing fast reconnect, or when we reuse the same SFU client,
13232
+ // (ws remained healthy), we just need to restore the ICE connection
13233
+ if (performingFastReconnect) {
13234
+ // the SFU automatically issues an ICE restart on the subscriber
13235
+ // we don't have to do it ourselves
13236
+ await this.restoreICE(sfuClient, { includeSubscriber: false });
13237
+ }
13238
+ else {
13239
+ const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
13240
+ this.initPublisherAndSubscriber({
12944
13241
  sfuClient,
12945
- dispatcher: this.dispatcher,
12946
- state: this.state,
12947
13242
  connectionConfig,
12948
- onUnrecoverableError: () => {
12949
- reconnect('full', 'unrecoverable subscriber error').catch((err) => {
12950
- this.logger('debug', '[Rejoin]: Rejoin failed', err);
12951
- });
12952
- },
13243
+ clientDetails,
13244
+ statsOptions,
13245
+ closePreviousInstances: !performingMigration,
12953
13246
  });
12954
13247
  }
13248
+ if (performingRejoin) {
13249
+ const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
13250
+ await previousSfuClient?.leaveAndClose(`Closing previous WS after reconnect with strategy: ${strategy}`);
13251
+ }
13252
+ else if (!isWsHealthy) {
13253
+ previousSfuClient?.close(4002, 'Closing unhealthy WS after reconnect');
13254
+ }
13255
+ // device settings should be applied only once, we don't have to
13256
+ // re-apply them on later reconnections or server-side data fetches
13257
+ if (!this.deviceSettingsAppliedOnce) {
13258
+ await this.applyDeviceConfig(true);
13259
+ this.deviceSettingsAppliedOnce = true;
13260
+ }
13261
+ this.logger('info', `Joined call ${this.cid}`);
13262
+ };
13263
+ /**
13264
+ * Prepares Reconnect Details object.
13265
+ * @internal
13266
+ */
13267
+ this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
13268
+ const strategy = this.reconnectStrategy;
13269
+ const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
13270
+ const announcedTracks = this.publisher?.getAnnouncedTracks() || [];
13271
+ const subscribedTracks = getCurrentValue(this.trackSubscriptionsSubject);
13272
+ return {
13273
+ strategy,
13274
+ announcedTracks,
13275
+ subscriptions: subscribedTracks.data || [],
13276
+ reconnectAttempt: this.reconnectAttempts,
13277
+ fromSfuId: migratingFromSfuId || '',
13278
+ previousSessionId: performingRejoin ? previousSessionId || '' : '',
13279
+ };
13280
+ };
13281
+ /**
13282
+ * Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
13283
+ * Uses the provided SFU client to restore the ICE connection.
13284
+ *
13285
+ * This method can throw an error if the ICE restart fails.
13286
+ * This error should be handled by the reconnect loop,
13287
+ * and a new reconnection shall be attempted.
13288
+ *
13289
+ * @internal
13290
+ */
13291
+ this.restoreICE = async (nextSfuClient, opts = {}) => {
13292
+ const { includeSubscriber = true, includePublisher = true } = opts;
13293
+ if (this.subscriber) {
13294
+ this.subscriber.setSfuClient(nextSfuClient);
13295
+ if (includeSubscriber) {
13296
+ await this.subscriber.restartIce();
13297
+ }
13298
+ }
13299
+ if (this.publisher) {
13300
+ this.publisher.setSfuClient(nextSfuClient);
13301
+ if (includePublisher) {
13302
+ await this.publisher.restartIce();
13303
+ }
13304
+ }
13305
+ };
13306
+ /**
13307
+ * Initializes the Publisher and Subscriber Peer Connections.
13308
+ * @internal
13309
+ */
13310
+ this.initPublisherAndSubscriber = (opts) => {
13311
+ const { sfuClient, connectionConfig, clientDetails, statsOptions, closePreviousInstances, } = opts;
13312
+ if (closePreviousInstances && this.subscriber) {
13313
+ this.subscriber.close();
13314
+ }
13315
+ this.subscriber = new Subscriber({
13316
+ sfuClient,
13317
+ dispatcher: this.dispatcher,
13318
+ state: this.state,
13319
+ connectionConfig,
13320
+ logTag: String(this.reconnectAttempts),
13321
+ onUnrecoverableError: () => {
13322
+ this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
13323
+ this.logger('warn', '[Reconnect] Error reconnecting after a subscriber error', err);
13324
+ });
13325
+ },
13326
+ });
12955
13327
  // anonymous users can't publish anything hence, there is no need
12956
13328
  // to create Publisher Peer Connection for them
12957
13329
  const isAnonymous = this.streamClient.user?.type === 'anonymous';
12958
- if (!this.publisher && !isAnonymous) {
13330
+ if (!isAnonymous) {
13331
+ if (closePreviousInstances && this.publisher) {
13332
+ this.publisher.close({ stopTracks: false });
13333
+ }
12959
13334
  const audioSettings = this.state.settings?.audio;
12960
13335
  const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
12961
13336
  const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
@@ -12966,23 +13341,23 @@ class Call {
12966
13341
  connectionConfig,
12967
13342
  isDtxEnabled,
12968
13343
  isRedEnabled,
13344
+ logTag: String(this.reconnectAttempts),
12969
13345
  onUnrecoverableError: () => {
12970
- reconnect('full', 'unrecoverable publisher error').catch((err) => {
12971
- this.logger('debug', '[Rejoin]: Rejoin failed', err);
13346
+ this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
13347
+ this.logger('warn', '[Reconnect] Error reconnecting after a publisher error', err);
12972
13348
  });
12973
13349
  },
12974
13350
  });
12975
13351
  }
12976
- if (!this.statsReporter) {
12977
- this.statsReporter = createStatsReporter({
12978
- subscriber: this.subscriber,
12979
- publisher: this.publisher,
12980
- state: this.state,
12981
- datacenter: this.sfuClient.edgeName,
12982
- });
12983
- }
12984
- const clientDetails = getClientDetails();
12985
- if (!this.sfuStatsReporter && statsOptions) {
13352
+ this.statsReporter?.stop();
13353
+ this.statsReporter = createStatsReporter({
13354
+ subscriber: this.subscriber,
13355
+ publisher: this.publisher,
13356
+ state: this.state,
13357
+ datacenter: sfuClient.edgeName,
13358
+ });
13359
+ this.sfuStatsReporter?.stop();
13360
+ if (statsOptions?.reporting_interval_ms > 0) {
12986
13361
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
12987
13362
  clientDetails,
12988
13363
  options: statsOptions,
@@ -12991,129 +13366,262 @@ class Call {
12991
13366
  });
12992
13367
  this.sfuStatsReporter.start();
12993
13368
  }
12994
- try {
12995
- // 1. wait for the signal server to be ready before sending "joinRequest"
12996
- sfuClient.signalReady
12997
- .catch((err) => this.logger('error', 'Signal ready failed', err))
12998
- // prepare a generic SDP and send it to the SFU.
12999
- // this is a throw-away SDP that the SFU will use to determine
13000
- // the capabilities of the client (codec support, etc.)
13001
- .then(() => getGenericSdp('recvonly'))
13002
- .then((sdp) => {
13003
- const subscriptions = getCurrentValue(this.trackSubscriptionsSubject);
13004
- const migration = isMigrating
13005
- ? {
13006
- fromSfuId: data?.migrating_from || '',
13007
- subscriptions: subscriptions.data || [],
13008
- announcedTracks: this.publisher?.getCurrentTrackInfos() || [],
13369
+ };
13370
+ /**
13371
+ * Retrieves credentials for joining the call.
13372
+ *
13373
+ * @internal
13374
+ *
13375
+ * @param data the join call data.
13376
+ */
13377
+ this.doJoinRequest = async (data) => {
13378
+ const location = await this.streamClient.getLocationHint();
13379
+ const request = { ...data, location };
13380
+ const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
13381
+ this.state.updateFromCallResponse(joinResponse.call);
13382
+ this.state.setMembers(joinResponse.members);
13383
+ this.state.setOwnCapabilities(joinResponse.own_capabilities);
13384
+ if (data?.ring && !this.ringing) {
13385
+ this.ringingSubject.next(true);
13386
+ }
13387
+ if (this.ringing && !this.isCreatedByMe) {
13388
+ // signals other users that I have accepted the incoming call.
13389
+ await this.accept();
13390
+ }
13391
+ if (this.streamClient._hasConnectionID()) {
13392
+ this.watching = true;
13393
+ this.clientStore.registerCall(this);
13394
+ }
13395
+ return joinResponse;
13396
+ };
13397
+ /**
13398
+ * Handles the reconnection flow.
13399
+ *
13400
+ * @internal
13401
+ *
13402
+ * @param strategy the reconnection strategy to use.
13403
+ */
13404
+ this.reconnect = async (strategy) => {
13405
+ return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
13406
+ this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
13407
+ this.reconnectStrategy = strategy;
13408
+ do {
13409
+ this.reconnectAttempts++;
13410
+ const current = WebsocketReconnectStrategy[this.reconnectStrategy];
13411
+ try {
13412
+ // wait until the network is available
13413
+ await this.networkAvailableTask?.promise;
13414
+ switch (this.reconnectStrategy) {
13415
+ case WebsocketReconnectStrategy.UNSPECIFIED:
13416
+ case WebsocketReconnectStrategy.DISCONNECT:
13417
+ this.logger('debug', `[Reconnect] No-op strategy ${current}`);
13418
+ break;
13419
+ case WebsocketReconnectStrategy.FAST:
13420
+ await this.reconnectFast();
13421
+ break;
13422
+ case WebsocketReconnectStrategy.REJOIN:
13423
+ await this.reconnectRejoin();
13424
+ break;
13425
+ case WebsocketReconnectStrategy.MIGRATE:
13426
+ await this.reconnectMigrate();
13427
+ break;
13428
+ default:
13429
+ ensureExhausted(this.reconnectStrategy, 'Unknown reconnection strategy');
13430
+ break;
13009
13431
  }
13010
- : undefined;
13011
- return sfuClient.join({
13012
- subscriberSdp: sdp || '',
13013
- clientDetails,
13014
- migration,
13015
- fastReconnect: previousSfuClient?.isFastReconnecting ?? false,
13016
- });
13432
+ break; // do-while loop, reconnection worked, exit the loop
13433
+ }
13434
+ catch (error) {
13435
+ this.logger('warn', `[Reconnect] ${current}(${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
13436
+ await sleep(500);
13437
+ this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
13438
+ }
13439
+ } while (this.state.callingState !== exports.CallingState.JOINED &&
13440
+ this.state.callingState !== exports.CallingState.LEFT);
13441
+ });
13442
+ };
13443
+ /**
13444
+ * Initiates the reconnection flow with the "fast" strategy.
13445
+ * @internal
13446
+ */
13447
+ this.reconnectFast = async () => {
13448
+ this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
13449
+ this.state.setCallingState(exports.CallingState.RECONNECTING);
13450
+ return this.join(this.joinCallData);
13451
+ };
13452
+ /**
13453
+ * Initiates the reconnection flow with the "rejoin" strategy.
13454
+ * @internal
13455
+ */
13456
+ this.reconnectRejoin = async () => {
13457
+ this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
13458
+ this.state.setCallingState(exports.CallingState.RECONNECTING);
13459
+ await this.join(this.joinCallData);
13460
+ await this.restorePublishedTracks();
13461
+ this.restoreSubscribedTracks();
13462
+ };
13463
+ /**
13464
+ * Initiates the reconnection flow with the "migrate" strategy.
13465
+ * @internal
13466
+ */
13467
+ this.reconnectMigrate = async () => {
13468
+ const currentSfuClient = this.sfuClient;
13469
+ if (!currentSfuClient) {
13470
+ throw new Error('Cannot migrate without an active SFU client');
13471
+ }
13472
+ this.reconnectStrategy = WebsocketReconnectStrategy.MIGRATE;
13473
+ this.state.setCallingState(exports.CallingState.MIGRATING);
13474
+ const currentSubscriber = this.subscriber;
13475
+ const currentPublisher = this.publisher;
13476
+ currentSubscriber?.detachEventHandlers();
13477
+ currentPublisher?.detachEventHandlers();
13478
+ const migrationTask = currentSfuClient.enterMigration();
13479
+ try {
13480
+ const currentSfu = currentSfuClient.edgeName;
13481
+ await this.join({ ...this.joinCallData, migrating_from: currentSfu });
13482
+ }
13483
+ finally {
13484
+ // cleanup the migration_from field after the migration is complete or failed
13485
+ // as we don't want to keep dirty data in the join call data
13486
+ delete this.joinCallData?.migrating_from;
13487
+ }
13488
+ await this.restorePublishedTracks();
13489
+ this.restoreSubscribedTracks();
13490
+ try {
13491
+ // Wait for the migration to complete, then close the previous SFU client
13492
+ // and the peer connection instances. In case of failure, the migration
13493
+ // task would throw an error and REJOIN would be attempted.
13494
+ await migrationTask;
13495
+ // in MIGRATE, we can consider the call as joined only after
13496
+ // `participantMigrationComplete` event is received, signaled by
13497
+ // the `migrationTask`
13498
+ this.state.setCallingState(exports.CallingState.JOINED);
13499
+ }
13500
+ finally {
13501
+ currentSubscriber?.close();
13502
+ currentPublisher?.close({ stopTracks: false });
13503
+ // and close the previous SFU client, without specifying close code
13504
+ currentSfuClient.close();
13505
+ }
13506
+ };
13507
+ /**
13508
+ * Registers the various event handlers for reconnection.
13509
+ *
13510
+ * @internal
13511
+ */
13512
+ this.registerReconnectHandlers = () => {
13513
+ // handles the legacy "goAway" event
13514
+ const unregisterGoAway = this.on('goAway', () => {
13515
+ this.reconnect(WebsocketReconnectStrategy.MIGRATE).catch((err) => {
13516
+ this.logger('warn', '[Reconnect] Error reconnecting', err);
13017
13517
  });
13018
- // 2. in parallel, wait for the SFU to send us the "joinResponse"
13019
- // this will throw an error if the SFU rejects the join request or
13020
- // fails to respond in time
13021
- const { callState, reconnected } = await this.waitForJoinResponse();
13022
- if (isReconnecting) {
13023
- this.logger('debug', '[Rejoin] fast reconnected:', reconnected);
13518
+ });
13519
+ // handles the "error" event, through which the SFU can request a reconnect
13520
+ const unregisterOnError = this.on('error', (e) => {
13521
+ const { reconnectStrategy: strategy } = e;
13522
+ if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
13523
+ return;
13524
+ if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
13525
+ this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
13526
+ this.logger('warn', `Can't leave call after disconnect request`, err);
13527
+ });
13024
13528
  }
13025
- if (isMigrating) {
13026
- await this.subscriber.migrateTo(sfuClient, connectionConfig);
13027
- await this.publisher?.migrateTo(sfuClient, connectionConfig);
13529
+ else {
13530
+ this.reconnect(strategy).catch((err) => {
13531
+ this.logger('warn', '[Reconnect] Error reconnecting', err);
13532
+ });
13028
13533
  }
13029
- else if (isReconnecting) {
13030
- if (reconnected) {
13031
- // update the SFU client instance on the subscriber and publisher
13032
- this.subscriber.setSfuClient(sfuClient);
13033
- // publisher might not be there (anonymous users)
13034
- if (this.publisher) {
13035
- this.publisher.setSfuClient(sfuClient);
13036
- // and perform a full ICE restart on the publisher
13037
- await this.publisher.restartIce();
13534
+ });
13535
+ const unregisterNetworkChanged = this.streamClient.on('network.changed', (e) => {
13536
+ if (!e.online) {
13537
+ this.logger('debug', '[Reconnect] Going offline');
13538
+ if (!this.hasJoinedOnce)
13539
+ return;
13540
+ this.lastOfflineTimestamp = Date.now();
13541
+ // create a new task that would resolve when the network is available
13542
+ const networkAvailableTask = promiseWithResolvers();
13543
+ networkAvailableTask.promise.then(() => {
13544
+ let strategy = WebsocketReconnectStrategy.FAST;
13545
+ if (this.lastOfflineTimestamp) {
13546
+ const offline = (Date.now() - this.lastOfflineTimestamp) / 1000;
13547
+ if (offline > this.fastReconnectDeadlineSeconds) {
13548
+ // We shouldn't attempt FAST if we have exceeded the deadline.
13549
+ // The SFU would have already wiped out the session.
13550
+ strategy = WebsocketReconnectStrategy.REJOIN;
13551
+ }
13038
13552
  }
13039
- }
13040
- else if (previousSfuClient?.isFastReconnecting) {
13041
- // reconnection wasn't possible, so we need to do a full rejoin
13042
- return await reconnect('full', 're-attempting').catch((err) => {
13043
- this.logger('error', `[Rejoin]: Rejoin failed forced full rejoin.`, err);
13044
- });
13045
- }
13046
- }
13047
- const currentParticipants = callState?.participants || [];
13048
- const participantCount = callState?.participantCount;
13049
- const startedAt = callState?.startedAt
13050
- ? Timestamp.toDate(callState.startedAt)
13051
- : new Date();
13052
- const pins = callState?.pins ?? [];
13053
- this.state.setParticipants(() => {
13054
- const participantLookup = this.state.getParticipantLookupBySessionId();
13055
- return currentParticipants.map((p) => {
13056
- // We need to preserve the local state of the participant
13057
- // (e.g. videoDimension, visibilityState, pinnedAt, etc.)
13058
- // as it doesn't exist on the server.
13059
- const existingParticipant = participantLookup[p.sessionId];
13060
- return Object.assign(p, existingParticipant, {
13061
- isLocalParticipant: p.sessionId === sfuClient.sessionId,
13062
- viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
13063
- videoTrack: exports.VisibilityState.UNKNOWN,
13064
- screenShareTrack: exports.VisibilityState.UNKNOWN,
13065
- },
13553
+ this.reconnect(strategy).catch((err) => {
13554
+ this.logger('warn', '[Reconnect] Error restoring connection after going online', err);
13066
13555
  });
13067
13556
  });
13068
- });
13069
- this.state.setParticipantCount(participantCount?.total || 0);
13070
- this.state.setAnonymousParticipantCount(participantCount?.anonymous || 0);
13071
- this.state.setStartedAt(startedAt);
13072
- this.state.setServerSidePins(pins);
13073
- this.reconnectAttempts = 0; // reset the reconnect attempts counter
13074
- this.state.setCallingState(exports.CallingState.JOINED);
13075
- try {
13076
- await this.initCamera({ setStatus: true });
13077
- await this.initMic({ setStatus: true });
13078
- }
13079
- catch (error) {
13080
- this.logger('warn', 'Camera and/or mic init failed during join call', error);
13081
- }
13082
- // 3. once we have the "joinResponse", and possibly reconciled the local state
13083
- // we schedule a fast subscription update for all remote participants
13084
- // that were visible before we reconnected or migrated to a new SFU.
13085
- const { remoteParticipants } = this.state;
13086
- if (remoteParticipants.length > 0) {
13087
- this.updateSubscriptions(remoteParticipants, exports.DebounceType.FAST);
13088
- }
13089
- this.logger('info', `Joined call ${this.cid}`);
13090
- }
13091
- catch (err) {
13092
- // join failed, try to rejoin
13093
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
13094
- this.logger('error', `[Rejoin]: Rejoin ${this.reconnectAttempts} failed.`, err);
13095
- await reconnect('full', 'previous attempt failed');
13096
- this.logger('info', `[Rejoin]: Rejoin ${this.reconnectAttempts} successful!`);
13557
+ this.networkAvailableTask = networkAvailableTask;
13558
+ this.sfuStatsReporter?.stop();
13559
+ this.state.setCallingState(exports.CallingState.OFFLINE);
13097
13560
  }
13098
13561
  else {
13099
- this.logger('error', `[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`);
13100
- this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
13101
- throw new Error('Join failed');
13562
+ this.logger('debug', '[Reconnect] Going online');
13563
+ // TODO try to remove this .close call
13564
+ this.sfuClient?.close(4002, 'Closing WS to reconnect after going online');
13565
+ // we went online, release the previous waiters and reset the state
13566
+ this.networkAvailableTask?.resolve();
13567
+ this.networkAvailableTask = undefined;
13568
+ this.sfuStatsReporter?.start();
13569
+ }
13570
+ });
13571
+ this.leaveCallHooks.add(unregisterGoAway);
13572
+ this.leaveCallHooks.add(unregisterOnError);
13573
+ this.leaveCallHooks.add(unregisterNetworkChanged);
13574
+ };
13575
+ /**
13576
+ * Restores the published tracks after a reconnection.
13577
+ * @internal
13578
+ */
13579
+ this.restorePublishedTracks = async () => {
13580
+ // the tracks need to be restored in their original order of publishing
13581
+ // otherwise, we might get `m-lines order mismatch` errors
13582
+ for (const trackType of this.trackPublishOrder) {
13583
+ switch (trackType) {
13584
+ case TrackType.AUDIO:
13585
+ const audioStream = this.microphone.state.mediaStream;
13586
+ if (audioStream) {
13587
+ await this.publishAudioStream(audioStream);
13588
+ }
13589
+ break;
13590
+ case TrackType.VIDEO:
13591
+ const videoStream = this.camera.state.mediaStream;
13592
+ if (videoStream) {
13593
+ await this.publishVideoStream(videoStream, {
13594
+ preferredCodec: this.camera.preferredCodec,
13595
+ });
13596
+ }
13597
+ break;
13598
+ case TrackType.SCREEN_SHARE:
13599
+ const screenShareStream = this.screenShare.state.mediaStream;
13600
+ if (screenShareStream) {
13601
+ await this.publishScreenShareStream(screenShareStream, {
13602
+ screenShareSettings: this.screenShare.getSettings(),
13603
+ });
13604
+ }
13605
+ break;
13606
+ // screen share audio can't exist without a screen share, so we handle it there
13607
+ case TrackType.SCREEN_SHARE_AUDIO:
13608
+ case TrackType.UNSPECIFIED:
13609
+ break;
13610
+ default:
13611
+ ensureExhausted(trackType, 'Unknown track type');
13612
+ break;
13102
13613
  }
13103
13614
  }
13104
13615
  };
13105
- this.waitForJoinResponse = (timeout = 5000) => {
13106
- return new Promise((resolve, reject) => {
13107
- const unsubscribe = this.on('joinResponse', (event) => {
13108
- clearTimeout(timeoutId);
13109
- unsubscribe();
13110
- resolve(event);
13111
- });
13112
- const timeoutId = setTimeout(() => {
13113
- unsubscribe();
13114
- reject(new Error('Waiting for "joinResponse" has timed out'));
13115
- }, timeout);
13116
- });
13616
+ /**
13617
+ * Restores the subscribed tracks after a reconnection.
13618
+ * @internal
13619
+ */
13620
+ this.restoreSubscribedTracks = () => {
13621
+ const { remoteParticipants } = this.state;
13622
+ if (remoteParticipants.length <= 0)
13623
+ return;
13624
+ this.updateSubscriptions(remoteParticipants, exports.DebounceType.FAST);
13117
13625
  };
13118
13626
  /**
13119
13627
  * Starts publishing the given video stream to the call.
@@ -13128,7 +13636,7 @@ class Call {
13128
13636
  this.publishVideoStream = async (videoStream, opts = {}) => {
13129
13637
  // we should wait until we get a JoinResponse from the SFU,
13130
13638
  // otherwise we risk breaking the ICETrickle flow.
13131
- await this.assertCallJoined();
13639
+ await this.waitUntilCallJoined();
13132
13640
  if (!this.publisher) {
13133
13641
  this.logger('error', 'Trying to publish video before join is completed');
13134
13642
  throw new Error(`Call not joined yet.`);
@@ -13138,6 +13646,9 @@ class Call {
13138
13646
  this.logger('error', `There is no video track to publish in the stream.`);
13139
13647
  return;
13140
13648
  }
13649
+ if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
13650
+ this.trackPublishOrder.push(TrackType.VIDEO);
13651
+ }
13141
13652
  await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, opts);
13142
13653
  };
13143
13654
  /**
@@ -13152,7 +13663,7 @@ class Call {
13152
13663
  this.publishAudioStream = async (audioStream) => {
13153
13664
  // we should wait until we get a JoinResponse from the SFU,
13154
13665
  // otherwise we risk breaking the ICETrickle flow.
13155
- await this.assertCallJoined();
13666
+ await this.waitUntilCallJoined();
13156
13667
  if (!this.publisher) {
13157
13668
  this.logger('error', 'Trying to publish audio before join is completed');
13158
13669
  throw new Error(`Call not joined yet.`);
@@ -13162,6 +13673,9 @@ class Call {
13162
13673
  this.logger('error', `There is no audio track in the stream to publish`);
13163
13674
  return;
13164
13675
  }
13676
+ if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
13677
+ this.trackPublishOrder.push(TrackType.AUDIO);
13678
+ }
13165
13679
  await this.publisher.publishStream(audioStream, audioTrack, TrackType.AUDIO);
13166
13680
  };
13167
13681
  /**
@@ -13176,7 +13690,7 @@ class Call {
13176
13690
  this.publishScreenShareStream = async (screenShareStream, opts = {}) => {
13177
13691
  // we should wait until we get a JoinResponse from the SFU,
13178
13692
  // otherwise we risk breaking the ICETrickle flow.
13179
- await this.assertCallJoined();
13693
+ await this.waitUntilCallJoined();
13180
13694
  if (!this.publisher) {
13181
13695
  this.logger('error', 'Trying to publish screen share before join is completed');
13182
13696
  throw new Error(`Call not joined yet.`);
@@ -13186,9 +13700,15 @@ class Call {
13186
13700
  this.logger('error', `There is no video track in the screen share stream to publish`);
13187
13701
  return;
13188
13702
  }
13703
+ if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
13704
+ this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
13705
+ }
13189
13706
  await this.publisher.publishStream(screenShareStream, screenShareTrack, TrackType.SCREEN_SHARE, opts);
13190
13707
  const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
13191
13708
  if (screenShareAudioTrack) {
13709
+ if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE_AUDIO)) {
13710
+ this.trackPublishOrder.push(TrackType.SCREEN_SHARE_AUDIO);
13711
+ }
13192
13712
  await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
13193
13713
  }
13194
13714
  };
@@ -13233,19 +13753,9 @@ class Call {
13233
13753
  * @param type the debounce type to use for the update.
13234
13754
  */
13235
13755
  this.updateSubscriptionsPartial = (trackType, changes, type = exports.DebounceType.SLOW) => {
13236
- if (trackType === 'video') {
13237
- this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'videoTrack'`);
13238
- trackType = 'videoTrack';
13239
- }
13240
- else if (trackType === 'screen') {
13241
- this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'screenShareTrack'`);
13242
- trackType = 'screenShareTrack';
13243
- }
13244
13756
  const participants = this.state.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
13245
- if (change.dimension?.height) {
13757
+ if (change.dimension) {
13246
13758
  change.dimension.height = Math.ceil(change.dimension.height);
13247
- }
13248
- if (change.dimension?.width) {
13249
13759
  change.dimension.width = Math.ceil(change.dimension.width);
13250
13760
  }
13251
13761
  const prop = trackType === 'videoTrack'
@@ -13260,9 +13770,7 @@ class Call {
13260
13770
  }
13261
13771
  return acc;
13262
13772
  }, {}));
13263
- if (participants) {
13264
- this.updateSubscriptions(participants, type);
13265
- }
13773
+ this.updateSubscriptions(participants, type);
13266
13774
  };
13267
13775
  this.updateSubscriptions = (participants, type = exports.DebounceType.SLOW) => {
13268
13776
  const subscriptions = [];
@@ -13345,10 +13853,15 @@ class Call {
13345
13853
  this.updatePublishQuality = async (enabledLayers) => {
13346
13854
  return this.publisher?.updateVideoPublishQuality(enabledLayers);
13347
13855
  };
13348
- this.assertCallJoined = () => {
13856
+ this.waitUntilCallJoined = () => {
13857
+ if (this.sfuClient) {
13858
+ // if we have an SFU client, we can wait for the join response
13859
+ return this.sfuClient.joinResponseTask.promise;
13860
+ }
13861
+ // otherwise, fall back to the calling state
13349
13862
  return new Promise((resolve) => {
13350
13863
  this.state.callingState$
13351
- .pipe(rxjs.takeWhile((state) => state !== exports.CallingState.JOINED, true), rxjs.filter((s) => s === exports.CallingState.JOINED))
13864
+ .pipe(rxjs.takeWhile((state) => state !== exports.CallingState.JOINED, true))
13352
13865
  .subscribe(() => resolve());
13353
13866
  });
13354
13867
  };
@@ -13732,14 +14245,72 @@ class Call {
13732
14245
  *
13733
14246
  * @internal
13734
14247
  */
13735
- this.applyDeviceConfig = async () => {
13736
- await this.initCamera({ setStatus: false }).catch((err) => {
14248
+ this.applyDeviceConfig = async (status) => {
14249
+ await this.initCamera({ setStatus: status }).catch((err) => {
13737
14250
  this.logger('warn', 'Camera init failed', err);
13738
14251
  });
13739
- await this.initMic({ setStatus: false }).catch((err) => {
14252
+ await this.initMic({ setStatus: status }).catch((err) => {
13740
14253
  this.logger('warn', 'Mic init failed', err);
13741
14254
  });
13742
14255
  };
14256
+ this.initCamera = async (options) => {
14257
+ // Wait for any in progress camera operation
14258
+ await this.camera.statusChangeSettled();
14259
+ if (this.state.localParticipant?.videoStream ||
14260
+ !this.permissionsContext.hasPermission('send-video')) {
14261
+ return;
14262
+ }
14263
+ // Set camera direction if it's not yet set
14264
+ if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
14265
+ let defaultDirection = 'front';
14266
+ const backendSetting = this.state.settings?.video.camera_facing;
14267
+ if (backendSetting) {
14268
+ defaultDirection = backendSetting === 'front' ? 'front' : 'back';
14269
+ }
14270
+ this.camera.state.setDirection(defaultDirection);
14271
+ }
14272
+ // Set target resolution
14273
+ const targetResolution = this.state.settings?.video.target_resolution;
14274
+ if (targetResolution) {
14275
+ await this.camera.selectTargetResolution(targetResolution);
14276
+ }
14277
+ if (options.setStatus) {
14278
+ // Publish already that was set before we joined
14279
+ if (this.camera.enabled &&
14280
+ this.camera.state.mediaStream &&
14281
+ !this.publisher?.isPublishing(TrackType.VIDEO)) {
14282
+ await this.publishVideoStream(this.camera.state.mediaStream, {
14283
+ preferredCodec: this.camera.preferredCodec,
14284
+ });
14285
+ }
14286
+ // Start camera if backend config specifies, and there is no local setting
14287
+ if (this.camera.state.status === undefined &&
14288
+ this.state.settings?.video.camera_default_on) {
14289
+ await this.camera.enable();
14290
+ }
14291
+ }
14292
+ };
14293
+ this.initMic = async (options) => {
14294
+ // Wait for any in progress mic operation
14295
+ await this.microphone.statusChangeSettled();
14296
+ if (this.state.localParticipant?.audioStream ||
14297
+ !this.permissionsContext.hasPermission('send-audio')) {
14298
+ return;
14299
+ }
14300
+ if (options.setStatus) {
14301
+ // Publish media stream that was set before we joined
14302
+ if (this.microphone.enabled &&
14303
+ this.microphone.state.mediaStream &&
14304
+ !this.publisher?.isPublishing(TrackType.AUDIO)) {
14305
+ await this.publishAudioStream(this.microphone.state.mediaStream);
14306
+ }
14307
+ // Start mic if backend config specifies, and there is no local setting
14308
+ if (this.microphone.state.status === undefined &&
14309
+ this.state.settings?.audio.mic_default_on) {
14310
+ await this.microphone.enable();
14311
+ }
14312
+ }
14313
+ };
13743
14314
  /**
13744
14315
  * Will begin tracking the given element for visibility changes within the
13745
14316
  * configured viewport element (`call.setViewport`).
@@ -13854,15 +14425,15 @@ class Call {
13854
14425
  }
13855
14426
  async setup() {
13856
14427
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
13857
- if (this.initialized) {
14428
+ if (this.initialized)
13858
14429
  return;
13859
- }
13860
14430
  this.leaveCallHooks.add(this.on('all', (event) => {
13861
14431
  // update state with the latest event data
13862
14432
  this.state.updateFromEvent(event);
13863
14433
  }));
13864
- this.leaveCallHooks.add(registerEventHandlers(this, this.state, this.dispatcher));
14434
+ this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
13865
14435
  this.registerEffects();
14436
+ this.registerReconnectHandlers();
13866
14437
  this.leaveCallHooks.add(createSubscription(this.trackSubscriptionsSubject.pipe(rxjs.debounce((v) => rxjs.timer(v.type)), rxjs.map((v) => v.data)), (subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions).catch((err) => {
13867
14438
  this.logger('debug', `Failed to update track subscriptions`, err);
13868
14439
  })));
@@ -13882,44 +14453,7 @@ class Call {
13882
14453
  }));
13883
14454
  this.leaveCallHooks.add(
13884
14455
  // handle the case when the user permissions are modified.
13885
- createSubscription(this.state.ownCapabilities$, (ownCapabilities) => {
13886
- // update the permission context.
13887
- this.permissionsContext.setPermissions(ownCapabilities);
13888
- if (!this.publisher)
13889
- return;
13890
- // check if the user still has publishing permissions and stop publishing if not.
13891
- const permissionToTrackType = {
13892
- [OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
13893
- [OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
13894
- [OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
13895
- };
13896
- for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
13897
- const hasPermission = this.permissionsContext.hasPermission(permission);
13898
- if (!hasPermission &&
13899
- (this.publisher.isPublishing(trackType) ||
13900
- this.publisher.isLive(trackType))) {
13901
- // Stop tracks, then notify device manager
13902
- this.stopPublish(trackType)
13903
- .catch((err) => {
13904
- this.logger('error', `Error stopping publish ${trackType}`, err);
13905
- })
13906
- .then(() => {
13907
- if (trackType === TrackType.VIDEO &&
13908
- this.camera.state.status === 'enabled') {
13909
- this.camera
13910
- .disable()
13911
- .catch((err) => this.logger('error', `Error disabling camera after permission revoked`, err));
13912
- }
13913
- if (trackType === TrackType.AUDIO &&
13914
- this.microphone.state.status === 'enabled') {
13915
- this.microphone
13916
- .disable()
13917
- .catch((err) => this.logger('error', `Error disabling microphone after permission revoked`, err));
13918
- }
13919
- });
13920
- }
13921
- }
13922
- }));
14456
+ createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
13923
14457
  this.leaveCallHooks.add(
13924
14458
  // handles the case when the user is blocked by the call owner.
13925
14459
  createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
@@ -14011,63 +14545,20 @@ class Call {
14011
14545
  get isCreatedByMe() {
14012
14546
  return this.state.createdBy?.id === this.currentUserId;
14013
14547
  }
14014
- async initCamera(options) {
14015
- // Wait for any in progress camera operation
14016
- await this.camera.statusChangeSettled();
14017
- if (this.state.localParticipant?.videoStream ||
14018
- !this.permissionsContext.hasPermission('send-video')) {
14019
- return;
14020
- }
14021
- // Set camera direction if it's not yet set
14022
- if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
14023
- let defaultDirection = 'front';
14024
- const backendSetting = this.state.settings?.video.camera_facing;
14025
- if (backendSetting) {
14026
- defaultDirection = backendSetting === 'front' ? 'front' : 'back';
14027
- }
14028
- this.camera.state.setDirection(defaultDirection);
14029
- }
14030
- // Set target resolution
14031
- const targetResolution = this.state.settings?.video.target_resolution;
14032
- if (targetResolution) {
14033
- await this.camera.selectTargetResolution(targetResolution);
14034
- }
14035
- if (options.setStatus) {
14036
- // Publish already that was set before we joined
14037
- if (this.camera.state.status === 'enabled' &&
14038
- this.camera.state.mediaStream &&
14039
- !this.publisher?.isPublishing(TrackType.VIDEO)) {
14040
- await this.publishVideoStream(this.camera.state.mediaStream, {
14041
- preferredCodec: this.camera.preferredCodec,
14042
- });
14043
- }
14044
- // Start camera if backend config specifies, and there is no local setting
14045
- if (this.camera.state.status === undefined &&
14046
- this.state.settings?.video.camera_default_on) {
14047
- await this.camera.enable();
14048
- }
14049
- }
14050
- }
14051
- async initMic(options) {
14052
- // Wait for any in progress mic operation
14053
- await this.microphone.statusChangeSettled();
14054
- if (this.state.localParticipant?.audioStream ||
14055
- !this.permissionsContext.hasPermission('send-audio')) {
14548
+ /**
14549
+ * Handles the closing of the SFU signal connection.
14550
+ *
14551
+ * @internal
14552
+ * @param sfuClient the SFU client instance that was closed.
14553
+ */
14554
+ handleSfuSignalClose(sfuClient) {
14555
+ this.logger('debug', '[Reconnect] SFU signal connection closed');
14556
+ // normal close, no need to reconnect
14557
+ if (sfuClient.isLeaving)
14056
14558
  return;
14057
- }
14058
- if (options.setStatus) {
14059
- // Publish media stream that was set before we joined
14060
- if (this.microphone.state.status === 'enabled' &&
14061
- this.microphone.state.mediaStream &&
14062
- !this.publisher?.isPublishing(TrackType.AUDIO)) {
14063
- await this.publishAudioStream(this.microphone.state.mediaStream);
14064
- }
14065
- // Start mic if backend config specifies, and there is no local setting
14066
- if (this.microphone.state.status === undefined &&
14067
- this.state.settings?.audio.mic_default_on) {
14068
- await this.microphone.enable();
14069
- }
14070
- }
14559
+ this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
14560
+ this.logger('warn', '[Reconnect] Error reconnecting', err);
14561
+ });
14071
14562
  }
14072
14563
  }
14073
14564
 
@@ -14270,6 +14761,7 @@ class StableWSConnection {
14270
14761
  }
14271
14762
  }
14272
14763
  if (data) {
14764
+ data.received_at = new Date();
14273
14765
  this.client.dispatchEvent(data);
14274
14766
  }
14275
14767
  this.scheduleConnectionCheck();
@@ -14610,7 +15102,7 @@ class StableWSConnection {
14610
15102
  wsURL,
14611
15103
  requestID: this.requestID,
14612
15104
  });
14613
- this.ws = new WebSocket(wsURL);
15105
+ this.ws = new WebSocket$1(wsURL);
14614
15106
  this.ws.onopen = this.onopen.bind(this, this.wsID);
14615
15107
  this.ws.onclose = this.onclose.bind(this, this.wsID);
14616
15108
  this.ws.onerror = this.onerror.bind(this, this.wsID);
@@ -15226,6 +15718,7 @@ class StreamClient {
15226
15718
  const wsPromise = this.openConnection();
15227
15719
  this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then((result) => result[1]);
15228
15720
  try {
15721
+ addConnectionEventListeners(this.updateNetworkConnectionStatus);
15229
15722
  return await this.setUserPromise;
15230
15723
  }
15231
15724
  catch (err) {
@@ -15321,6 +15814,7 @@ class StreamClient {
15321
15814
  delete this.userID;
15322
15815
  this.anonymous = false;
15323
15816
  await this.closeConnection(timeout);
15817
+ removeConnectionEventListeners(this.updateNetworkConnectionStatus);
15324
15818
  this.tokenManager.reset();
15325
15819
  this.connectionIdPromise = undefined;
15326
15820
  this.rejectConnectionId = undefined;
@@ -15340,6 +15834,7 @@ class StreamClient {
15340
15834
  * connectAnonymousUser - Set an anonymous user and open a WebSocket connection
15341
15835
  */
15342
15836
  this.connectAnonymousUser = async (user, tokenOrProvider) => {
15837
+ addConnectionEventListeners(this.updateNetworkConnectionStatus);
15343
15838
  this.connectionIdPromise = new Promise((resolve, reject) => {
15344
15839
  this.resolveConnectionId = resolve;
15345
15840
  this.rejectConnectionId = reject;
@@ -15499,8 +15994,6 @@ class StreamClient {
15499
15994
  return data;
15500
15995
  };
15501
15996
  this.dispatchEvent = (event) => {
15502
- if (!event.received_at)
15503
- event.received_at = new Date();
15504
15997
  this.logger('debug', `Dispatching event: ${event.type}`, event);
15505
15998
  if (!this.listeners)
15506
15999
  return;
@@ -15591,7 +16084,7 @@ class StreamClient {
15591
16084
  });
15592
16085
  };
15593
16086
  this.getUserAgent = () => {
15594
- const version = "1.5.0" ;
16087
+ const version = "1.6.0-0" ;
15595
16088
  return (this.userAgent ||
15596
16089
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
15597
16090
  };
@@ -15657,11 +16150,15 @@ class StreamClient {
15657
16150
  client_request_id,
15658
16151
  });
15659
16152
  };
15660
- /**
15661
- * creates an abort controller that will be used by the next HTTP Request.
15662
- */
15663
- this.createAbortControllerForNextRequest = () => {
15664
- return (this.nextRequestAbortController = new AbortController());
16153
+ this.updateNetworkConnectionStatus = (event) => {
16154
+ if (event.type === 'offline') {
16155
+ this.logger('debug', 'device went offline');
16156
+ this.dispatchEvent({ type: 'network.changed', online: false });
16157
+ }
16158
+ else if (event.type === 'online') {
16159
+ this.logger('debug', 'device went online');
16160
+ this.dispatchEvent({ type: 'network.changed', online: true });
16161
+ }
15665
16162
  };
15666
16163
  // set the key
15667
16164
  this.key = key;
@@ -15691,10 +16188,14 @@ class StreamClient {
15691
16188
  });
15692
16189
  }
15693
16190
  this.setBaseURL(this.options.baseURL || 'https://video.stream-io-api.com/video');
15694
- if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_RUN) {
16191
+ if (typeof process !== 'undefined' &&
16192
+ 'env' in process &&
16193
+ process.env.STREAM_LOCAL_TEST_RUN) {
15695
16194
  this.setBaseURL('http://localhost:3030/video');
15696
16195
  }
15697
- if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_HOST) {
16196
+ if (typeof process !== 'undefined' &&
16197
+ 'env' in process &&
16198
+ process.env.STREAM_LOCAL_TEST_HOST) {
15698
16199
  this.setBaseURL(`http://${process.env.STREAM_LOCAL_TEST_HOST}/video`);
15699
16200
  }
15700
16201
  this.axiosInstance = axios.create({
@@ -15729,6 +16230,99 @@ class StreamVideoClient {
15729
16230
  constructor(apiKeyOrArgs, opts) {
15730
16231
  this.logLevel = 'warn';
15731
16232
  this.eventHandlersToUnregister = [];
16233
+ /**
16234
+ * Connects the given user to the client.
16235
+ * Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
16236
+ * If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
16237
+ *
16238
+ * @param user the user to connect.
16239
+ * @param token a token or a function that returns a token.
16240
+ */
16241
+ this.connectUser = async (user, token) => {
16242
+ if (user.type === 'anonymous') {
16243
+ user.id = '!anon';
16244
+ return this.connectAnonymousUser(user, token);
16245
+ }
16246
+ let connectUser = () => {
16247
+ return this.streamClient.connectUser(user, token);
16248
+ };
16249
+ if (user.type === 'guest') {
16250
+ connectUser = async () => {
16251
+ return this.streamClient.connectGuestUser(user);
16252
+ };
16253
+ }
16254
+ this.connectionPromise = this.disconnectionPromise
16255
+ ? this.disconnectionPromise.then(() => connectUser())
16256
+ : connectUser();
16257
+ this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
16258
+ const connectUserResponse = await this.connectionPromise;
16259
+ // connectUserResponse will be void if connectUser called twice for the same user
16260
+ if (connectUserResponse?.me) {
16261
+ this.writeableStateStore.setConnectedUser(connectUserResponse.me);
16262
+ }
16263
+ this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
16264
+ if (event.online) {
16265
+ const callsToReWatch = this.writeableStateStore.calls
16266
+ .filter((call) => call.watching)
16267
+ .map((call) => call.cid);
16268
+ this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
16269
+ if (callsToReWatch.length > 0) {
16270
+ this.queryCalls({
16271
+ watch: true,
16272
+ filter_conditions: {
16273
+ cid: { $in: callsToReWatch },
16274
+ },
16275
+ sort: [{ field: 'cid', direction: 1 }],
16276
+ }).catch((err) => {
16277
+ this.logger('error', 'Failed to re-watch calls', err);
16278
+ });
16279
+ }
16280
+ }
16281
+ }));
16282
+ this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
16283
+ const { call, members } = event;
16284
+ if (user.id === call.created_by.id) {
16285
+ this.logger('warn', 'Received `call.created` sent by the current user');
16286
+ return;
16287
+ }
16288
+ this.logger('info', `New call created and registered: ${call.cid}`);
16289
+ const newCall = new Call({
16290
+ streamClient: this.streamClient,
16291
+ type: call.type,
16292
+ id: call.id,
16293
+ members,
16294
+ clientStore: this.writeableStateStore,
16295
+ });
16296
+ newCall.state.updateFromCallResponse(call);
16297
+ this.writeableStateStore.registerCall(newCall);
16298
+ }));
16299
+ this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
16300
+ const { call, members } = event;
16301
+ if (user.id === call.created_by.id) {
16302
+ this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
16303
+ return;
16304
+ }
16305
+ // The call might already be tracked by the client,
16306
+ // if `call.created` was received before `call.ring`.
16307
+ // In that case, we cleanup the already tracked call.
16308
+ const prevCall = this.writeableStateStore.findCall(call.type, call.id);
16309
+ await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
16310
+ // we create a new call
16311
+ const theCall = new Call({
16312
+ streamClient: this.streamClient,
16313
+ type: call.type,
16314
+ id: call.id,
16315
+ members,
16316
+ clientStore: this.writeableStateStore,
16317
+ ringing: true,
16318
+ });
16319
+ theCall.state.updateFromCallResponse(call);
16320
+ // we fetch the latest metadata for the call from the server
16321
+ await theCall.get();
16322
+ this.writeableStateStore.registerCall(theCall);
16323
+ }));
16324
+ return connectUserResponse;
16325
+ };
15732
16326
  /**
15733
16327
  * Disconnects the currently connected user from the client.
15734
16328
  *
@@ -15817,7 +16411,7 @@ class StreamVideoClient {
15817
16411
  clientStore: this.writeableStateStore,
15818
16412
  });
15819
16413
  call.state.updateFromCallResponse(c.call);
15820
- await call.applyDeviceConfig();
16414
+ await call.applyDeviceConfig(false);
15821
16415
  if (data.watch) {
15822
16416
  this.writeableStateStore.registerCall(call);
15823
16417
  }
@@ -15861,6 +16455,17 @@ class StreamVideoClient {
15861
16455
  ...(push_provider_name != null ? { push_provider_name } : {}),
15862
16456
  });
15863
16457
  };
16458
+ /**
16459
+ * addDevice - Adds a push device for a user.
16460
+ *
16461
+ * @param {string} id the device id
16462
+ * @param {string} push_provider the push provider name (eg. apn, firebase)
16463
+ * @param {string} push_provider_name user provided push provider name
16464
+ * @param {string} [userID] the user id (defaults to current user)
16465
+ */
16466
+ this.addVoipDevice = async (id, push_provider, push_provider_name, userID) => {
16467
+ return await this.addDevice(id, push_provider, push_provider_name, userID, true);
16468
+ };
15864
16469
  /**
15865
16470
  * getDevices - Returns the devices associated with a current user
15866
16471
  * @param {string} [userID] User ID. Only works on serverside
@@ -15888,7 +16493,7 @@ class StreamVideoClient {
15888
16493
  this.onRingingCall = async (call_cid) => {
15889
16494
  // if we find the call and is already ringing, we don't need to create a new call
15890
16495
  // as client would have received the call.ring state because the app had WS alive when receiving push notifications
15891
- let call = this.readOnlyStateStore.calls.find((c) => c.cid === call_cid && c.ringing);
16496
+ let call = this.state.calls.find((c) => c.cid === call_cid && c.ringing);
15892
16497
  if (!call) {
15893
16498
  // if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
15894
16499
  const [callType, callId] = call_cid.split(':');
@@ -15929,12 +16534,13 @@ class StreamVideoClient {
15929
16534
  }
15930
16535
  setLogger(logger, logLevel);
15931
16536
  this.logger = getLogger(['client']);
16537
+ const coordinatorLogger = getLogger(['coordinator']);
15932
16538
  if (typeof apiKeyOrArgs === 'string') {
15933
16539
  this.streamClient = new StreamClient(apiKeyOrArgs, {
15934
16540
  persistUserOnConnectionFailure: true,
15935
16541
  ...opts,
15936
16542
  logLevel,
15937
- logger: this.logger,
16543
+ logger: coordinatorLogger,
15938
16544
  });
15939
16545
  }
15940
16546
  else {
@@ -15942,12 +16548,14 @@ class StreamVideoClient {
15942
16548
  persistUserOnConnectionFailure: true,
15943
16549
  ...apiKeyOrArgs.options,
15944
16550
  logLevel,
15945
- logger: this.logger,
16551
+ logger: coordinatorLogger,
15946
16552
  });
15947
16553
  const sdkInfo = getSdkInfo();
15948
16554
  if (sdkInfo) {
15949
- this.streamClient.setUserAgent(this.streamClient.getUserAgent() +
15950
- `-video-${SdkType[sdkInfo.type].toLowerCase()}-sdk-${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`);
16555
+ const sdkName = SdkType[sdkInfo.type].toLowerCase();
16556
+ const sdkVersion = `${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`;
16557
+ const userAgent = this.streamClient.getUserAgent();
16558
+ this.streamClient.setUserAgent(`${userAgent}-video-${sdkName}-sdk-${sdkVersion}`);
15951
16559
  }
15952
16560
  }
15953
16561
  this.writeableStateStore = new StreamVideoWriteableStateStore();
@@ -16000,110 +16608,6 @@ class StreamVideoClient {
16000
16608
  get state() {
16001
16609
  return this.readOnlyStateStore;
16002
16610
  }
16003
- /**
16004
- * Connects the given user to the client.
16005
- * Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
16006
- * If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
16007
- *
16008
- * @param user the user to connect.
16009
- * @param token a token or a function that returns a token.
16010
- */
16011
- async connectUser(user, token) {
16012
- if (user.type === 'anonymous') {
16013
- user.id = '!anon';
16014
- return this.connectAnonymousUser(user, token);
16015
- }
16016
- let connectUser = () => {
16017
- return this.streamClient.connectUser(user, token);
16018
- };
16019
- if (user.type === 'guest') {
16020
- connectUser = async () => {
16021
- return this.streamClient.connectGuestUser(user);
16022
- };
16023
- }
16024
- this.connectionPromise = this.disconnectionPromise
16025
- ? this.disconnectionPromise.then(() => connectUser())
16026
- : connectUser();
16027
- this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
16028
- const connectUserResponse = await this.connectionPromise;
16029
- // connectUserResponse will be void if connectUser called twice for the same user
16030
- if (connectUserResponse?.me) {
16031
- this.writeableStateStore.setConnectedUser(connectUserResponse.me);
16032
- }
16033
- this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
16034
- if (event.online) {
16035
- const callsToReWatch = this.writeableStateStore.calls
16036
- .filter((call) => call.watching)
16037
- .map((call) => call.cid);
16038
- this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
16039
- if (callsToReWatch.length > 0) {
16040
- this.queryCalls({
16041
- watch: true,
16042
- filter_conditions: {
16043
- cid: { $in: callsToReWatch },
16044
- },
16045
- sort: [{ field: 'cid', direction: 1 }],
16046
- }).catch((err) => {
16047
- this.logger('error', 'Failed to re-watch calls', err);
16048
- });
16049
- }
16050
- }
16051
- }));
16052
- this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
16053
- const { call, members } = event;
16054
- if (user.id === call.created_by.id) {
16055
- this.logger('warn', 'Received `call.created` sent by the current user');
16056
- return;
16057
- }
16058
- this.logger('info', `New call created and registered: ${call.cid}`);
16059
- const newCall = new Call({
16060
- streamClient: this.streamClient,
16061
- type: call.type,
16062
- id: call.id,
16063
- members,
16064
- clientStore: this.writeableStateStore,
16065
- });
16066
- newCall.state.updateFromCallResponse(call);
16067
- this.writeableStateStore.registerCall(newCall);
16068
- }));
16069
- this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
16070
- const { call, members } = event;
16071
- if (user.id === call.created_by.id) {
16072
- this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
16073
- return;
16074
- }
16075
- // The call might already be tracked by the client,
16076
- // if `call.created` was received before `call.ring`.
16077
- // In that case, we cleanup the already tracked call.
16078
- const prevCall = this.writeableStateStore.findCall(call.type, call.id);
16079
- await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
16080
- // we create a new call
16081
- const theCall = new Call({
16082
- streamClient: this.streamClient,
16083
- type: call.type,
16084
- id: call.id,
16085
- members,
16086
- clientStore: this.writeableStateStore,
16087
- ringing: true,
16088
- });
16089
- theCall.state.updateFromCallResponse(call);
16090
- // we fetch the latest metadata for the call from the server
16091
- await theCall.get();
16092
- this.writeableStateStore.registerCall(theCall);
16093
- }));
16094
- return connectUserResponse;
16095
- }
16096
- /**
16097
- * addDevice - Adds a push device for a user.
16098
- *
16099
- * @param {string} id the device id
16100
- * @param {string} push_provider the push provider name (eg. apn, firebase)
16101
- * @param {string} push_provider_name user provided push provider name
16102
- * @param {string} [userID] the user id (defaults to current user)
16103
- */
16104
- async addVoipDevice(id, push_provider, push_provider_name, userID) {
16105
- return await this.addDevice(id, push_provider, push_provider_name, userID, true);
16106
- }
16107
16611
  }
16108
16612
  StreamVideoClient._instanceMap = new Map();
16109
16613
 
@@ -16166,6 +16670,7 @@ exports.getAudioOutputDevices = getAudioOutputDevices;
16166
16670
  exports.getAudioStream = getAudioStream;
16167
16671
  exports.getClientDetails = getClientDetails;
16168
16672
  exports.getDeviceInfo = getDeviceInfo;
16673
+ exports.getLogLevel = getLogLevel;
16169
16674
  exports.getLogger = getLogger;
16170
16675
  exports.getOSInfo = getOSInfo;
16171
16676
  exports.getScreenShareStream = getScreenShareStream;