@stream-io/video-client 1.5.0-0 → 1.5.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 (76) hide show
  1. package/CHANGELOG.md +6 -230
  2. package/dist/index.browser.es.js +1498 -1963
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1495 -1961
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1498 -1963
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +9 -93
  9. package/dist/src/StreamSfuClient.d.ts +56 -72
  10. package/dist/src/StreamVideoClient.d.ts +10 -2
  11. package/dist/src/coordinator/connection/client.d.ts +4 -3
  12. package/dist/src/coordinator/connection/types.d.ts +1 -5
  13. package/dist/src/devices/InputMediaDeviceManager.d.ts +0 -4
  14. package/dist/src/devices/MicrophoneManager.d.ts +1 -1
  15. package/dist/src/events/callEventHandlers.d.ts +3 -1
  16. package/dist/src/events/internal.d.ts +0 -4
  17. package/dist/src/gen/video/sfu/event/events.d.ts +4 -106
  18. package/dist/src/gen/video/sfu/models/models.d.ts +65 -64
  19. package/dist/src/logger.d.ts +0 -1
  20. package/dist/src/rpc/createClient.d.ts +0 -2
  21. package/dist/src/rpc/index.d.ts +0 -1
  22. package/dist/src/rtc/Dispatcher.d.ts +1 -1
  23. package/dist/src/rtc/IceTrickleBuffer.d.ts +1 -0
  24. package/dist/src/rtc/Publisher.d.ts +25 -24
  25. package/dist/src/rtc/Subscriber.d.ts +11 -12
  26. package/dist/src/rtc/flows/join.d.ts +20 -0
  27. package/dist/src/rtc/helpers/tracks.d.ts +3 -3
  28. package/dist/src/rtc/signal.d.ts +1 -1
  29. package/dist/src/store/CallState.d.ts +2 -46
  30. package/package.json +3 -3
  31. package/src/Call.ts +562 -615
  32. package/src/StreamSfuClient.ts +246 -277
  33. package/src/StreamVideoClient.ts +65 -15
  34. package/src/coordinator/connection/client.ts +8 -25
  35. package/src/coordinator/connection/connection.ts +0 -1
  36. package/src/coordinator/connection/token_manager.ts +1 -1
  37. package/src/coordinator/connection/types.ts +0 -6
  38. package/src/devices/BrowserPermission.ts +1 -5
  39. package/src/devices/CameraManager.ts +1 -1
  40. package/src/devices/InputMediaDeviceManager.ts +3 -12
  41. package/src/devices/MicrophoneManager.ts +3 -3
  42. package/src/devices/devices.ts +1 -1
  43. package/src/events/__tests__/mutes.test.ts +13 -10
  44. package/src/events/__tests__/participant.test.ts +0 -75
  45. package/src/events/callEventHandlers.ts +7 -4
  46. package/src/events/internal.ts +3 -20
  47. package/src/events/mutes.ts +3 -5
  48. package/src/events/participant.ts +15 -48
  49. package/src/gen/video/sfu/event/events.ts +8 -451
  50. package/src/gen/video/sfu/models/models.ts +204 -211
  51. package/src/logger.ts +1 -3
  52. package/src/rpc/createClient.ts +0 -21
  53. package/src/rpc/index.ts +0 -1
  54. package/src/rtc/Dispatcher.ts +2 -6
  55. package/src/rtc/IceTrickleBuffer.ts +2 -2
  56. package/src/rtc/Publisher.ts +163 -127
  57. package/src/rtc/Subscriber.ts +155 -94
  58. package/src/rtc/__tests__/Publisher.test.ts +95 -18
  59. package/src/rtc/__tests__/Subscriber.test.ts +99 -63
  60. package/src/rtc/__tests__/videoLayers.test.ts +2 -2
  61. package/src/rtc/flows/join.ts +65 -0
  62. package/src/rtc/helpers/tracks.ts +7 -27
  63. package/src/rtc/signal.ts +3 -3
  64. package/src/rtc/videoLayers.ts +10 -1
  65. package/src/stats/SfuStatsReporter.ts +0 -1
  66. package/src/store/CallState.ts +2 -109
  67. package/src/store/__tests__/CallState.test.ts +37 -48
  68. package/dist/src/helpers/ensureExhausted.d.ts +0 -1
  69. package/dist/src/helpers/withResolvers.d.ts +0 -14
  70. package/dist/src/rpc/retryable.d.ts +0 -23
  71. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +0 -2
  72. package/src/helpers/ensureExhausted.ts +0 -5
  73. package/src/helpers/withResolvers.ts +0 -43
  74. package/src/rpc/__tests__/retryable.test.ts +0 -72
  75. package/src/rpc/retryable.ts +0 -57
  76. package/src/rtc/helpers/rtcConfiguration.ts +0 -11
@@ -1,13 +1,13 @@
1
1
  import 'webrtc-adapter';
2
2
  import { MessageType, isJsonObject, typeofJsonValue, reflectionMergePartial, UnknownFieldHandler, WireType, PbLong } from '@protobuf-ts/runtime';
3
- import { ServiceType, stackIntercept, RpcError } from '@protobuf-ts/runtime-rpc';
3
+ import { ServiceType, stackIntercept } from '@protobuf-ts/runtime-rpc';
4
4
  import axios, { AxiosHeaders } from 'axios';
5
5
  export { AxiosError } from 'axios';
6
- import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transport';
6
+ import { TwirpFetchTransport } from '@protobuf-ts/twirp-transport';
7
7
  import { ReplaySubject, combineLatest, BehaviorSubject, map as map$1, shareReplay, distinctUntilChanged, takeWhile, distinctUntilKeyChanged, fromEventPattern, startWith, concatMap, merge, from, fromEvent, debounceTime, pairwise, of, filter, debounce, timer } from 'rxjs';
8
8
  import * as SDP from 'sdp-transform';
9
9
  import { UAParser } from 'ua-parser-js';
10
- import WebSocket$1 from 'isomorphic-ws';
10
+ import WebSocket from 'isomorphic-ws';
11
11
  import { fromByteArray } from 'base64-js';
12
12
 
13
13
  /**
@@ -1014,10 +1014,6 @@ var CallEndedReason;
1014
1014
  * @generated from protobuf enum value: CALL_ENDED_REASON_KICKED = 3;
1015
1015
  */
1016
1016
  CallEndedReason[CallEndedReason["KICKED"] = 3] = "KICKED";
1017
- /**
1018
- * @generated from protobuf enum value: CALL_ENDED_REASON_SESSION_ENDED = 4;
1019
- */
1020
- CallEndedReason[CallEndedReason["SESSION_ENDED"] = 4] = "SESSION_ENDED";
1021
1017
  })(CallEndedReason || (CallEndedReason = {}));
1022
1018
  /**
1023
1019
  * WebsocketReconnectStrategy defines the ws strategies available for handling reconnections.
@@ -1031,7 +1027,7 @@ var WebsocketReconnectStrategy;
1031
1027
  */
1032
1028
  WebsocketReconnectStrategy[WebsocketReconnectStrategy["UNSPECIFIED"] = 0] = "UNSPECIFIED";
1033
1029
  /**
1034
- * Sent after reaching the maximum reconnection attempts, or any other unrecoverable error leading to permanent disconnect.
1030
+ * Sent after reaching the maximum reconnection attempts, leading to permanent disconnect.
1035
1031
  *
1036
1032
  * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_DISCONNECT = 1;
1037
1033
  */
@@ -1044,18 +1040,25 @@ var WebsocketReconnectStrategy;
1044
1040
  */
1045
1041
  WebsocketReconnectStrategy[WebsocketReconnectStrategy["FAST"] = 2] = "FAST";
1046
1042
  /**
1047
- * SDK should obtain new credentials from the coordinator, drops existing pc instances, set a new session_id and initializes
1043
+ * SDK should drop existing pc instances and creates a fresh WebSocket connection,
1044
+ * ensuring a clean state for the reconnection.
1045
+ *
1046
+ * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_CLEAN = 3;
1047
+ */
1048
+ WebsocketReconnectStrategy[WebsocketReconnectStrategy["CLEAN"] = 3] = "CLEAN";
1049
+ /**
1050
+ * SDK should obtain new credentials from the coordinator, drops existing pc instances, and initializes
1048
1051
  * a completely new WebSocket connection, ensuring a comprehensive reset.
1049
1052
  *
1050
- * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_REJOIN = 3;
1053
+ * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_FULL = 4;
1051
1054
  */
1052
- WebsocketReconnectStrategy[WebsocketReconnectStrategy["REJOIN"] = 3] = "REJOIN";
1055
+ WebsocketReconnectStrategy[WebsocketReconnectStrategy["FULL"] = 4] = "FULL";
1053
1056
  /**
1054
1057
  * SDK should migrate to a new SFU instance
1055
1058
  *
1056
- * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE = 4;
1059
+ * @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE = 5;
1057
1060
  */
1058
- WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] = 4] = "MIGRATE";
1061
+ WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] = 5] = "MIGRATE";
1059
1062
  })(WebsocketReconnectStrategy || (WebsocketReconnectStrategy = {}));
1060
1063
  // @generated message type with reflection information, may provide speed optimized methods
1061
1064
  class CallState$Type extends MessageType {
@@ -1857,7 +1860,6 @@ class TrackInfo$Type extends MessageType {
1857
1860
  { no: 7, name: 'dtx', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1858
1861
  { no: 8, name: 'stereo', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1859
1862
  { no: 9, name: 'red', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1860
- { no: 10, name: 'muted', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1861
1863
  ]);
1862
1864
  }
1863
1865
  create(value) {
@@ -1869,7 +1871,6 @@ class TrackInfo$Type extends MessageType {
1869
1871
  message.dtx = false;
1870
1872
  message.stereo = false;
1871
1873
  message.red = false;
1872
- message.muted = false;
1873
1874
  if (value !== undefined)
1874
1875
  reflectionMergePartial(this, message, value);
1875
1876
  return message;
@@ -1900,9 +1901,6 @@ class TrackInfo$Type extends MessageType {
1900
1901
  case /* bool red */ 9:
1901
1902
  message.red = reader.bool();
1902
1903
  break;
1903
- case /* bool muted */ 10:
1904
- message.muted = reader.bool();
1905
- break;
1906
1904
  default:
1907
1905
  let u = options.readUnknownField;
1908
1906
  if (u === 'throw')
@@ -1936,9 +1934,6 @@ class TrackInfo$Type extends MessageType {
1936
1934
  /* bool red = 9; */
1937
1935
  if (message.red !== false)
1938
1936
  writer.tag(9, WireType.Varint).bool(message.red);
1939
- /* bool muted = 10; */
1940
- if (message.muted !== false)
1941
- writer.tag(10, WireType.Varint).bool(message.muted);
1942
1937
  let u = options.writeUnknownFields;
1943
1938
  if (u !== false)
1944
1939
  (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1950,6 +1945,108 @@ class TrackInfo$Type extends MessageType {
1950
1945
  */
1951
1946
  const TrackInfo = new TrackInfo$Type();
1952
1947
  // @generated message type with reflection information, may provide speed optimized methods
1948
+ class Call$Type extends MessageType {
1949
+ constructor() {
1950
+ super('stream.video.sfu.models.Call', [
1951
+ { no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1952
+ { no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1953
+ {
1954
+ no: 3,
1955
+ name: 'created_by_user_id',
1956
+ kind: 'scalar',
1957
+ T: 9 /*ScalarType.STRING*/,
1958
+ },
1959
+ {
1960
+ no: 4,
1961
+ name: 'host_user_id',
1962
+ kind: 'scalar',
1963
+ T: 9 /*ScalarType.STRING*/,
1964
+ },
1965
+ { no: 5, name: 'custom', kind: 'message', T: () => Struct },
1966
+ { no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
1967
+ { no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
1968
+ ]);
1969
+ }
1970
+ create(value) {
1971
+ const message = globalThis.Object.create(this.messagePrototype);
1972
+ message.type = '';
1973
+ message.id = '';
1974
+ message.createdByUserId = '';
1975
+ message.hostUserId = '';
1976
+ if (value !== undefined)
1977
+ reflectionMergePartial(this, message, value);
1978
+ return message;
1979
+ }
1980
+ internalBinaryRead(reader, length, options, target) {
1981
+ let message = target ?? this.create(), end = reader.pos + length;
1982
+ while (reader.pos < end) {
1983
+ let [fieldNo, wireType] = reader.tag();
1984
+ switch (fieldNo) {
1985
+ case /* string type */ 1:
1986
+ message.type = reader.string();
1987
+ break;
1988
+ case /* string id */ 2:
1989
+ message.id = reader.string();
1990
+ break;
1991
+ case /* string created_by_user_id */ 3:
1992
+ message.createdByUserId = reader.string();
1993
+ break;
1994
+ case /* string host_user_id */ 4:
1995
+ message.hostUserId = reader.string();
1996
+ break;
1997
+ case /* google.protobuf.Struct custom */ 5:
1998
+ message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
1999
+ break;
2000
+ case /* google.protobuf.Timestamp created_at */ 6:
2001
+ message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
2002
+ break;
2003
+ case /* google.protobuf.Timestamp updated_at */ 7:
2004
+ message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
2005
+ break;
2006
+ default:
2007
+ let u = options.readUnknownField;
2008
+ if (u === 'throw')
2009
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
2010
+ let d = reader.skip(wireType);
2011
+ if (u !== false)
2012
+ (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
2013
+ }
2014
+ }
2015
+ return message;
2016
+ }
2017
+ internalBinaryWrite(message, writer, options) {
2018
+ /* string type = 1; */
2019
+ if (message.type !== '')
2020
+ writer.tag(1, WireType.LengthDelimited).string(message.type);
2021
+ /* string id = 2; */
2022
+ if (message.id !== '')
2023
+ writer.tag(2, WireType.LengthDelimited).string(message.id);
2024
+ /* string created_by_user_id = 3; */
2025
+ if (message.createdByUserId !== '')
2026
+ writer.tag(3, WireType.LengthDelimited).string(message.createdByUserId);
2027
+ /* string host_user_id = 4; */
2028
+ if (message.hostUserId !== '')
2029
+ writer.tag(4, WireType.LengthDelimited).string(message.hostUserId);
2030
+ /* google.protobuf.Struct custom = 5; */
2031
+ if (message.custom)
2032
+ Struct.internalBinaryWrite(message.custom, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
2033
+ /* google.protobuf.Timestamp created_at = 6; */
2034
+ if (message.createdAt)
2035
+ Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
2036
+ /* google.protobuf.Timestamp updated_at = 7; */
2037
+ if (message.updatedAt)
2038
+ Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
2039
+ let u = options.writeUnknownFields;
2040
+ if (u !== false)
2041
+ (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
2042
+ return writer;
2043
+ }
2044
+ }
2045
+ /**
2046
+ * @generated MessageType for protobuf message stream.video.sfu.models.Call
2047
+ */
2048
+ const Call$1 = new Call$Type();
2049
+ // @generated message type with reflection information, may provide speed optimized methods
1953
2050
  let Error$Type$1 = class Error$Type extends MessageType {
1954
2051
  constructor() {
1955
2052
  super('stream.video.sfu.models.Error', [
@@ -2343,108 +2440,6 @@ class Device$Type extends MessageType {
2343
2440
  */
2344
2441
  const Device = new Device$Type();
2345
2442
  // @generated message type with reflection information, may provide speed optimized methods
2346
- class Call$Type extends MessageType {
2347
- constructor() {
2348
- super('stream.video.sfu.models.Call', [
2349
- { no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2350
- { no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2351
- {
2352
- no: 3,
2353
- name: 'created_by_user_id',
2354
- kind: 'scalar',
2355
- T: 9 /*ScalarType.STRING*/,
2356
- },
2357
- {
2358
- no: 4,
2359
- name: 'host_user_id',
2360
- kind: 'scalar',
2361
- T: 9 /*ScalarType.STRING*/,
2362
- },
2363
- { no: 5, name: 'custom', kind: 'message', T: () => Struct },
2364
- { no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
2365
- { no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
2366
- ]);
2367
- }
2368
- create(value) {
2369
- const message = globalThis.Object.create(this.messagePrototype);
2370
- message.type = '';
2371
- message.id = '';
2372
- message.createdByUserId = '';
2373
- message.hostUserId = '';
2374
- if (value !== undefined)
2375
- reflectionMergePartial(this, message, value);
2376
- return message;
2377
- }
2378
- internalBinaryRead(reader, length, options, target) {
2379
- let message = target ?? this.create(), end = reader.pos + length;
2380
- while (reader.pos < end) {
2381
- let [fieldNo, wireType] = reader.tag();
2382
- switch (fieldNo) {
2383
- case /* string type */ 1:
2384
- message.type = reader.string();
2385
- break;
2386
- case /* string id */ 2:
2387
- message.id = reader.string();
2388
- break;
2389
- case /* string created_by_user_id */ 3:
2390
- message.createdByUserId = reader.string();
2391
- break;
2392
- case /* string host_user_id */ 4:
2393
- message.hostUserId = reader.string();
2394
- break;
2395
- case /* google.protobuf.Struct custom */ 5:
2396
- message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
2397
- break;
2398
- case /* google.protobuf.Timestamp created_at */ 6:
2399
- message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
2400
- break;
2401
- case /* google.protobuf.Timestamp updated_at */ 7:
2402
- message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
2403
- break;
2404
- default:
2405
- let u = options.readUnknownField;
2406
- if (u === 'throw')
2407
- throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
2408
- let d = reader.skip(wireType);
2409
- if (u !== false)
2410
- (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
2411
- }
2412
- }
2413
- return message;
2414
- }
2415
- internalBinaryWrite(message, writer, options) {
2416
- /* string type = 1; */
2417
- if (message.type !== '')
2418
- writer.tag(1, WireType.LengthDelimited).string(message.type);
2419
- /* string id = 2; */
2420
- if (message.id !== '')
2421
- writer.tag(2, WireType.LengthDelimited).string(message.id);
2422
- /* string created_by_user_id = 3; */
2423
- if (message.createdByUserId !== '')
2424
- writer.tag(3, WireType.LengthDelimited).string(message.createdByUserId);
2425
- /* string host_user_id = 4; */
2426
- if (message.hostUserId !== '')
2427
- writer.tag(4, WireType.LengthDelimited).string(message.hostUserId);
2428
- /* google.protobuf.Struct custom = 5; */
2429
- if (message.custom)
2430
- Struct.internalBinaryWrite(message.custom, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
2431
- /* google.protobuf.Timestamp created_at = 6; */
2432
- if (message.createdAt)
2433
- Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
2434
- /* google.protobuf.Timestamp updated_at = 7; */
2435
- if (message.updatedAt)
2436
- Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
2437
- let u = options.writeUnknownFields;
2438
- if (u !== false)
2439
- (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
2440
- return writer;
2441
- }
2442
- }
2443
- /**
2444
- * @generated MessageType for protobuf message stream.video.sfu.models.Call
2445
- */
2446
- const Call$1 = new Call$Type();
2447
- // @generated message type with reflection information, may provide speed optimized methods
2448
2443
  class CallGrants$Type extends MessageType {
2449
2444
  constructor() {
2450
2445
  super('stream.video.sfu.models.CallGrants', [
@@ -3975,13 +3970,6 @@ class SfuEvent$Type extends MessageType {
3975
3970
  oneof: 'eventPayload',
3976
3971
  T: () => ParticipantUpdated,
3977
3972
  },
3978
- {
3979
- no: 25,
3980
- name: 'participant_migration_complete',
3981
- kind: 'message',
3982
- oneof: 'eventPayload',
3983
- T: () => ParticipantMigrationComplete,
3984
- },
3985
3973
  ]);
3986
3974
  }
3987
3975
  create(value) {
@@ -4116,12 +4104,6 @@ class SfuEvent$Type extends MessageType {
4116
4104
  participantUpdated: ParticipantUpdated.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantUpdated),
4117
4105
  };
4118
4106
  break;
4119
- case /* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete */ 25:
4120
- message.eventPayload = {
4121
- oneofKind: 'participantMigrationComplete',
4122
- participantMigrationComplete: ParticipantMigrationComplete.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantMigrationComplete),
4123
- };
4124
- break;
4125
4107
  default:
4126
4108
  let u = options.readUnknownField;
4127
4109
  if (u === 'throw')
@@ -4194,9 +4176,6 @@ class SfuEvent$Type extends MessageType {
4194
4176
  /* stream.video.sfu.event.ParticipantUpdated participant_updated = 24; */
4195
4177
  if (message.eventPayload.oneofKind === 'participantUpdated')
4196
4178
  ParticipantUpdated.internalBinaryWrite(message.eventPayload.participantUpdated, writer.tag(24, WireType.LengthDelimited).fork(), options).join();
4197
- /* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete = 25; */
4198
- if (message.eventPayload.oneofKind === 'participantMigrationComplete')
4199
- ParticipantMigrationComplete.internalBinaryWrite(message.eventPayload.participantMigrationComplete, writer.tag(25, WireType.LengthDelimited).fork(), options).join();
4200
4179
  let u = options.writeUnknownFields;
4201
4180
  if (u !== false)
4202
4181
  (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -4208,31 +4187,6 @@ class SfuEvent$Type extends MessageType {
4208
4187
  */
4209
4188
  const SfuEvent = new SfuEvent$Type();
4210
4189
  // @generated message type with reflection information, may provide speed optimized methods
4211
- class ParticipantMigrationComplete$Type extends MessageType {
4212
- constructor() {
4213
- super('stream.video.sfu.event.ParticipantMigrationComplete', []);
4214
- }
4215
- create(value) {
4216
- const message = globalThis.Object.create(this.messagePrototype);
4217
- if (value !== undefined)
4218
- reflectionMergePartial(this, message, value);
4219
- return message;
4220
- }
4221
- internalBinaryRead(reader, length, options, target) {
4222
- return target ?? this.create();
4223
- }
4224
- internalBinaryWrite(message, writer, options) {
4225
- let u = options.writeUnknownFields;
4226
- if (u !== false)
4227
- (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
4228
- return writer;
4229
- }
4230
- }
4231
- /**
4232
- * @generated MessageType for protobuf message stream.video.sfu.event.ParticipantMigrationComplete
4233
- */
4234
- const ParticipantMigrationComplete = new ParticipantMigrationComplete$Type();
4235
- // @generated message type with reflection information, may provide speed optimized methods
4236
4190
  class PinsChanged$Type extends MessageType {
4237
4191
  constructor() {
4238
4192
  super('stream.video.sfu.event.PinsChanged', [
@@ -4483,13 +4437,6 @@ class SfuRequest$Type extends MessageType {
4483
4437
  oneof: 'requestPayload',
4484
4438
  T: () => HealthCheckRequest,
4485
4439
  },
4486
- {
4487
- no: 3,
4488
- name: 'leave_call_request',
4489
- kind: 'message',
4490
- oneof: 'requestPayload',
4491
- T: () => LeaveCallRequest,
4492
- },
4493
4440
  ]);
4494
4441
  }
4495
4442
  create(value) {
@@ -4516,12 +4463,6 @@ class SfuRequest$Type extends MessageType {
4516
4463
  healthCheckRequest: HealthCheckRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.healthCheckRequest),
4517
4464
  };
4518
4465
  break;
4519
- case /* stream.video.sfu.event.LeaveCallRequest leave_call_request */ 3:
4520
- message.requestPayload = {
4521
- oneofKind: 'leaveCallRequest',
4522
- leaveCallRequest: LeaveCallRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.leaveCallRequest),
4523
- };
4524
- break;
4525
4466
  default:
4526
4467
  let u = options.readUnknownField;
4527
4468
  if (u === 'throw')
@@ -4540,9 +4481,6 @@ class SfuRequest$Type extends MessageType {
4540
4481
  /* stream.video.sfu.event.HealthCheckRequest health_check_request = 2; */
4541
4482
  if (message.requestPayload.oneofKind === 'healthCheckRequest')
4542
4483
  HealthCheckRequest.internalBinaryWrite(message.requestPayload.healthCheckRequest, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
4543
- /* stream.video.sfu.event.LeaveCallRequest leave_call_request = 3; */
4544
- if (message.requestPayload.oneofKind === 'leaveCallRequest')
4545
- LeaveCallRequest.internalBinaryWrite(message.requestPayload.leaveCallRequest, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
4546
4484
  let u = options.writeUnknownFields;
4547
4485
  if (u !== false)
4548
4486
  (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -4554,64 +4492,9 @@ class SfuRequest$Type extends MessageType {
4554
4492
  */
4555
4493
  const SfuRequest = new SfuRequest$Type();
4556
4494
  // @generated message type with reflection information, may provide speed optimized methods
4557
- class LeaveCallRequest$Type extends MessageType {
4495
+ class HealthCheckRequest$Type extends MessageType {
4558
4496
  constructor() {
4559
- super('stream.video.sfu.event.LeaveCallRequest', [
4560
- { no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
4561
- { no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
4562
- ]);
4563
- }
4564
- create(value) {
4565
- const message = globalThis.Object.create(this.messagePrototype);
4566
- message.sessionId = '';
4567
- message.reason = '';
4568
- if (value !== undefined)
4569
- reflectionMergePartial(this, message, value);
4570
- return message;
4571
- }
4572
- internalBinaryRead(reader, length, options, target) {
4573
- let message = target ?? this.create(), end = reader.pos + length;
4574
- while (reader.pos < end) {
4575
- let [fieldNo, wireType] = reader.tag();
4576
- switch (fieldNo) {
4577
- case /* string session_id */ 1:
4578
- message.sessionId = reader.string();
4579
- break;
4580
- case /* string reason */ 2:
4581
- message.reason = reader.string();
4582
- break;
4583
- default:
4584
- let u = options.readUnknownField;
4585
- if (u === 'throw')
4586
- throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
4587
- let d = reader.skip(wireType);
4588
- if (u !== false)
4589
- (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
4590
- }
4591
- }
4592
- return message;
4593
- }
4594
- internalBinaryWrite(message, writer, options) {
4595
- /* string session_id = 1; */
4596
- if (message.sessionId !== '')
4597
- writer.tag(1, WireType.LengthDelimited).string(message.sessionId);
4598
- /* string reason = 2; */
4599
- if (message.reason !== '')
4600
- writer.tag(2, WireType.LengthDelimited).string(message.reason);
4601
- let u = options.writeUnknownFields;
4602
- if (u !== false)
4603
- (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
4604
- return writer;
4605
- }
4606
- }
4607
- /**
4608
- * @generated MessageType for protobuf message stream.video.sfu.event.LeaveCallRequest
4609
- */
4610
- const LeaveCallRequest = new LeaveCallRequest$Type();
4611
- // @generated message type with reflection information, may provide speed optimized methods
4612
- class HealthCheckRequest$Type extends MessageType {
4613
- constructor() {
4614
- super('stream.video.sfu.event.HealthCheckRequest', []);
4497
+ super('stream.video.sfu.event.HealthCheckRequest', []);
4615
4498
  }
4616
4499
  create(value) {
4617
4500
  const message = globalThis.Object.create(this.messagePrototype);
@@ -4884,12 +4767,6 @@ class JoinRequest$Type extends MessageType {
4884
4767
  kind: 'scalar',
4885
4768
  T: 8 /*ScalarType.BOOL*/,
4886
4769
  },
4887
- {
4888
- no: 7,
4889
- name: 'reconnect_details',
4890
- kind: 'message',
4891
- T: () => ReconnectDetails,
4892
- },
4893
4770
  ]);
4894
4771
  }
4895
4772
  create(value) {
@@ -4919,15 +4796,12 @@ class JoinRequest$Type extends MessageType {
4919
4796
  case /* stream.video.sfu.models.ClientDetails client_details */ 4:
4920
4797
  message.clientDetails = ClientDetails.internalBinaryRead(reader, reader.uint32(), options, message.clientDetails);
4921
4798
  break;
4922
- case /* stream.video.sfu.event.Migration migration = 5 [deprecated = true];*/ 5:
4799
+ case /* stream.video.sfu.event.Migration migration */ 5:
4923
4800
  message.migration = Migration.internalBinaryRead(reader, reader.uint32(), options, message.migration);
4924
4801
  break;
4925
- case /* bool fast_reconnect = 6 [deprecated = true];*/ 6:
4802
+ case /* bool fast_reconnect */ 6:
4926
4803
  message.fastReconnect = reader.bool();
4927
4804
  break;
4928
- case /* stream.video.sfu.event.ReconnectDetails reconnect_details */ 7:
4929
- message.reconnectDetails = ReconnectDetails.internalBinaryRead(reader, reader.uint32(), options, message.reconnectDetails);
4930
- break;
4931
4805
  default:
4932
4806
  let u = options.readUnknownField;
4933
4807
  if (u === 'throw')
@@ -4952,15 +4826,12 @@ class JoinRequest$Type extends MessageType {
4952
4826
  /* stream.video.sfu.models.ClientDetails client_details = 4; */
4953
4827
  if (message.clientDetails)
4954
4828
  ClientDetails.internalBinaryWrite(message.clientDetails, writer.tag(4, WireType.LengthDelimited).fork(), options).join();
4955
- /* stream.video.sfu.event.Migration migration = 5 [deprecated = true]; */
4829
+ /* stream.video.sfu.event.Migration migration = 5; */
4956
4830
  if (message.migration)
4957
4831
  Migration.internalBinaryWrite(message.migration, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
4958
- /* bool fast_reconnect = 6 [deprecated = true]; */
4832
+ /* bool fast_reconnect = 6; */
4959
4833
  if (message.fastReconnect !== false)
4960
4834
  writer.tag(6, WireType.Varint).bool(message.fastReconnect);
4961
- /* stream.video.sfu.event.ReconnectDetails reconnect_details = 7; */
4962
- if (message.reconnectDetails)
4963
- ReconnectDetails.internalBinaryWrite(message.reconnectDetails, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
4964
4835
  let u = options.writeUnknownFields;
4965
4836
  if (u !== false)
4966
4837
  (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -4972,129 +4843,6 @@ class JoinRequest$Type extends MessageType {
4972
4843
  */
4973
4844
  const JoinRequest = new JoinRequest$Type();
4974
4845
  // @generated message type with reflection information, may provide speed optimized methods
4975
- class ReconnectDetails$Type extends MessageType {
4976
- constructor() {
4977
- super('stream.video.sfu.event.ReconnectDetails', [
4978
- {
4979
- no: 1,
4980
- name: 'strategy',
4981
- kind: 'enum',
4982
- T: () => [
4983
- 'stream.video.sfu.models.WebsocketReconnectStrategy',
4984
- WebsocketReconnectStrategy,
4985
- 'WEBSOCKET_RECONNECT_STRATEGY_',
4986
- ],
4987
- },
4988
- {
4989
- no: 3,
4990
- name: 'announced_tracks',
4991
- kind: 'message',
4992
- repeat: 1 /*RepeatType.PACKED*/,
4993
- T: () => TrackInfo,
4994
- },
4995
- {
4996
- no: 4,
4997
- name: 'subscriptions',
4998
- kind: 'message',
4999
- repeat: 1 /*RepeatType.PACKED*/,
5000
- T: () => TrackSubscriptionDetails,
5001
- },
5002
- {
5003
- no: 5,
5004
- name: 'reconnect_attempt',
5005
- kind: 'scalar',
5006
- T: 13 /*ScalarType.UINT32*/,
5007
- },
5008
- {
5009
- no: 6,
5010
- name: 'from_sfu_id',
5011
- kind: 'scalar',
5012
- T: 9 /*ScalarType.STRING*/,
5013
- },
5014
- {
5015
- no: 7,
5016
- name: 'previous_session_id',
5017
- kind: 'scalar',
5018
- T: 9 /*ScalarType.STRING*/,
5019
- },
5020
- ]);
5021
- }
5022
- create(value) {
5023
- const message = globalThis.Object.create(this.messagePrototype);
5024
- message.strategy = 0;
5025
- message.announcedTracks = [];
5026
- message.subscriptions = [];
5027
- message.reconnectAttempt = 0;
5028
- message.fromSfuId = '';
5029
- message.previousSessionId = '';
5030
- if (value !== undefined)
5031
- reflectionMergePartial(this, message, value);
5032
- return message;
5033
- }
5034
- internalBinaryRead(reader, length, options, target) {
5035
- let message = target ?? this.create(), end = reader.pos + length;
5036
- while (reader.pos < end) {
5037
- let [fieldNo, wireType] = reader.tag();
5038
- switch (fieldNo) {
5039
- case /* stream.video.sfu.models.WebsocketReconnectStrategy strategy */ 1:
5040
- message.strategy = reader.int32();
5041
- break;
5042
- case /* repeated stream.video.sfu.models.TrackInfo announced_tracks */ 3:
5043
- message.announcedTracks.push(TrackInfo.internalBinaryRead(reader, reader.uint32(), options));
5044
- break;
5045
- case /* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions */ 4:
5046
- message.subscriptions.push(TrackSubscriptionDetails.internalBinaryRead(reader, reader.uint32(), options));
5047
- break;
5048
- case /* uint32 reconnect_attempt */ 5:
5049
- message.reconnectAttempt = reader.uint32();
5050
- break;
5051
- case /* string from_sfu_id */ 6:
5052
- message.fromSfuId = reader.string();
5053
- break;
5054
- case /* string previous_session_id */ 7:
5055
- message.previousSessionId = reader.string();
5056
- break;
5057
- default:
5058
- let u = options.readUnknownField;
5059
- if (u === 'throw')
5060
- throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
5061
- let d = reader.skip(wireType);
5062
- if (u !== false)
5063
- (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
5064
- }
5065
- }
5066
- return message;
5067
- }
5068
- internalBinaryWrite(message, writer, options) {
5069
- /* stream.video.sfu.models.WebsocketReconnectStrategy strategy = 1; */
5070
- if (message.strategy !== 0)
5071
- writer.tag(1, WireType.Varint).int32(message.strategy);
5072
- /* repeated stream.video.sfu.models.TrackInfo announced_tracks = 3; */
5073
- for (let i = 0; i < message.announcedTracks.length; i++)
5074
- TrackInfo.internalBinaryWrite(message.announcedTracks[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join();
5075
- /* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions = 4; */
5076
- for (let i = 0; i < message.subscriptions.length; i++)
5077
- TrackSubscriptionDetails.internalBinaryWrite(message.subscriptions[i], writer.tag(4, WireType.LengthDelimited).fork(), options).join();
5078
- /* uint32 reconnect_attempt = 5; */
5079
- if (message.reconnectAttempt !== 0)
5080
- writer.tag(5, WireType.Varint).uint32(message.reconnectAttempt);
5081
- /* string from_sfu_id = 6; */
5082
- if (message.fromSfuId !== '')
5083
- writer.tag(6, WireType.LengthDelimited).string(message.fromSfuId);
5084
- /* string previous_session_id = 7; */
5085
- if (message.previousSessionId !== '')
5086
- writer.tag(7, WireType.LengthDelimited).string(message.previousSessionId);
5087
- let u = options.writeUnknownFields;
5088
- if (u !== false)
5089
- (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
5090
- return writer;
5091
- }
5092
- }
5093
- /**
5094
- * @generated MessageType for protobuf message stream.video.sfu.event.ReconnectDetails
5095
- */
5096
- const ReconnectDetails = new ReconnectDetails$Type();
5097
- // @generated message type with reflection information, may provide speed optimized methods
5098
4846
  class Migration$Type extends MessageType {
5099
4847
  constructor() {
5100
4848
  super('stream.video.sfu.event.Migration', [
@@ -5180,18 +4928,11 @@ class JoinResponse$Type extends MessageType {
5180
4928
  super('stream.video.sfu.event.JoinResponse', [
5181
4929
  { no: 1, name: 'call_state', kind: 'message', T: () => CallState$1 },
5182
4930
  { no: 2, name: 'reconnected', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
5183
- {
5184
- no: 3,
5185
- name: 'fast_reconnect_deadline_seconds',
5186
- kind: 'scalar',
5187
- T: 5 /*ScalarType.INT32*/,
5188
- },
5189
4931
  ]);
5190
4932
  }
5191
4933
  create(value) {
5192
4934
  const message = globalThis.Object.create(this.messagePrototype);
5193
4935
  message.reconnected = false;
5194
- message.fastReconnectDeadlineSeconds = 0;
5195
4936
  if (value !== undefined)
5196
4937
  reflectionMergePartial(this, message, value);
5197
4938
  return message;
@@ -5207,9 +4948,6 @@ class JoinResponse$Type extends MessageType {
5207
4948
  case /* bool reconnected */ 2:
5208
4949
  message.reconnected = reader.bool();
5209
4950
  break;
5210
- case /* int32 fast_reconnect_deadline_seconds */ 3:
5211
- message.fastReconnectDeadlineSeconds = reader.int32();
5212
- break;
5213
4951
  default:
5214
4952
  let u = options.readUnknownField;
5215
4953
  if (u === 'throw')
@@ -5228,11 +4966,6 @@ class JoinResponse$Type extends MessageType {
5228
4966
  /* bool reconnected = 2; */
5229
4967
  if (message.reconnected !== false)
5230
4968
  writer.tag(2, WireType.Varint).bool(message.reconnected);
5231
- /* int32 fast_reconnect_deadline_seconds = 3; */
5232
- if (message.fastReconnectDeadlineSeconds !== 0)
5233
- writer
5234
- .tag(3, WireType.Varint)
5235
- .int32(message.fastReconnectDeadlineSeconds);
5236
4969
  let u = options.writeUnknownFields;
5237
4970
  if (u !== false)
5238
4971
  (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -6433,15 +6166,12 @@ var events = /*#__PURE__*/Object.freeze({
6433
6166
  ICETrickle: ICETrickle,
6434
6167
  JoinRequest: JoinRequest,
6435
6168
  JoinResponse: JoinResponse,
6436
- LeaveCallRequest: LeaveCallRequest,
6437
6169
  Migration: Migration,
6438
6170
  ParticipantJoined: ParticipantJoined,
6439
6171
  ParticipantLeft: ParticipantLeft,
6440
- ParticipantMigrationComplete: ParticipantMigrationComplete,
6441
6172
  ParticipantUpdated: ParticipantUpdated,
6442
6173
  PinsChanged: PinsChanged,
6443
6174
  PublisherAnswer: PublisherAnswer,
6444
- ReconnectDetails: ReconnectDetails,
6445
6175
  SfuEvent: SfuEvent,
6446
6176
  SfuRequest: SfuRequest,
6447
6177
  SubscriberOffer: SubscriberOffer,
@@ -6567,17 +6297,6 @@ const withHeaders = (headers) => {
6567
6297
  },
6568
6298
  };
6569
6299
  };
6570
- const withRequestLogger = (logger, level) => {
6571
- return {
6572
- interceptUnary: (next, method, input, options) => {
6573
- logger(level, `Calling SFU RPC method ${method.name}`, {
6574
- input,
6575
- options,
6576
- });
6577
- return next(method, input, options);
6578
- },
6579
- };
6580
- };
6581
6300
  /**
6582
6301
  * Creates new SignalServerClient instance.
6583
6302
  *
@@ -6591,196 +6310,68 @@ const createSignalClient = (options) => {
6591
6310
  return new SignalServerClient(transport);
6592
6311
  };
6593
6312
 
6594
- const sleep = (m) => new Promise((r) => setTimeout(r, m));
6595
- function isFunction(value) {
6596
- return (value &&
6597
- (Object.prototype.toString.call(value) === '[object Function]' ||
6598
- 'function' === typeof value ||
6599
- value instanceof Function));
6600
- }
6601
6313
  /**
6602
- * A map of known error codes.
6314
+ * Checks whether we are using React Native
6603
6315
  */
6604
- const KnownCodes = {
6605
- TOKEN_EXPIRED: 40,
6606
- WS_CLOSED_SUCCESS: 1000,
6607
- WS_CLOSED_ABRUPTLY: 1006,
6608
- WS_POLICY_VIOLATION: 1008,
6316
+ const isReactNative = () => {
6317
+ if (typeof navigator === 'undefined')
6318
+ return false;
6319
+ return navigator.product?.toLowerCase() === 'reactnative';
6609
6320
  };
6610
- /**
6611
- * retryInterval - A retry interval which increases acc to number of failures
6612
- *
6613
- * @return {number} Duration to wait in milliseconds
6614
- */
6615
- function retryInterval(numberOfFailures) {
6616
- // try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
6617
- const max = Math.min(500 + numberOfFailures * 2000, 5000);
6618
- const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
6619
- return Math.floor(Math.random() * (max - min) + min);
6620
- }
6621
- function randomId() {
6622
- return generateUUIDv4();
6623
- }
6624
- function hex(bytes) {
6625
- let s = '';
6626
- for (let i = 0; i < bytes.length; i++) {
6627
- s += bytes[i].toString(16).padStart(2, '0');
6628
- }
6629
- return s;
6630
- }
6631
- // https://tools.ietf.org/html/rfc4122
6632
- function generateUUIDv4() {
6633
- const bytes = getRandomBytes(16);
6634
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
6635
- bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
6636
- return (hex(bytes.subarray(0, 4)) +
6637
- '-' +
6638
- hex(bytes.subarray(4, 6)) +
6639
- '-' +
6640
- hex(bytes.subarray(6, 8)) +
6641
- '-' +
6642
- hex(bytes.subarray(8, 10)) +
6643
- '-' +
6644
- hex(bytes.subarray(10, 16)));
6645
- }
6646
- function getRandomValuesWithMathRandom(bytes) {
6647
- const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
6648
- for (let i = 0; i < bytes.length; i++) {
6649
- bytes[i] = Math.random() * max;
6321
+
6322
+ // log levels, sorted by verbosity
6323
+ const logLevels = Object.freeze({
6324
+ trace: 0,
6325
+ debug: 1,
6326
+ info: 2,
6327
+ warn: 3,
6328
+ error: 4,
6329
+ });
6330
+ let logger$4;
6331
+ let level = 'info';
6332
+ const logToConsole = (logLevel, message, ...args) => {
6333
+ let logMethod;
6334
+ switch (logLevel) {
6335
+ case 'error':
6336
+ if (isReactNative()) {
6337
+ message = `ERROR: ${message}`;
6338
+ logMethod = console.info;
6339
+ break;
6340
+ }
6341
+ logMethod = console.error;
6342
+ break;
6343
+ case 'warn':
6344
+ if (isReactNative()) {
6345
+ message = `WARN: ${message}`;
6346
+ logMethod = console.info;
6347
+ break;
6348
+ }
6349
+ logMethod = console.warn;
6350
+ break;
6351
+ case 'info':
6352
+ logMethod = console.info;
6353
+ break;
6354
+ case 'trace':
6355
+ logMethod = console.trace;
6356
+ break;
6357
+ default:
6358
+ logMethod = console.log;
6359
+ break;
6650
6360
  }
6651
- }
6652
- const getRandomValues = (() => {
6653
- if (typeof crypto !== 'undefined' &&
6654
- typeof crypto?.getRandomValues !== 'undefined') {
6655
- return crypto.getRandomValues.bind(crypto);
6656
- }
6657
- else if (typeof msCrypto !== 'undefined') {
6658
- return msCrypto.getRandomValues.bind(msCrypto);
6659
- }
6660
- else {
6661
- return getRandomValuesWithMathRandom;
6662
- }
6663
- })();
6664
- function getRandomBytes(length) {
6665
- const bytes = new Uint8Array(length);
6666
- getRandomValues(bytes);
6667
- return bytes;
6668
- }
6669
- function convertErrorToJson(err) {
6670
- const jsonObj = {};
6671
- if (!err)
6672
- return jsonObj;
6673
- try {
6674
- Object.getOwnPropertyNames(err).forEach((key) => {
6675
- jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
6676
- });
6677
- }
6678
- catch (_) {
6679
- return {
6680
- error: 'failed to serialize the error',
6681
- };
6682
- }
6683
- return jsonObj;
6684
- }
6685
- /**
6686
- * isOnline safely return the navigator.online value for browser env
6687
- * if navigator is not in global object, it always return true
6688
- */
6689
- function isOnline(logger) {
6690
- const nav = typeof navigator !== 'undefined'
6691
- ? navigator
6692
- : typeof window !== 'undefined' && window.navigator
6693
- ? window.navigator
6694
- : undefined;
6695
- if (!nav) {
6696
- logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
6697
- return true;
6698
- }
6699
- // RN navigator has undefined for onLine
6700
- if (typeof nav.onLine !== 'boolean') {
6701
- return true;
6702
- }
6703
- return nav.onLine;
6704
- }
6705
- /**
6706
- * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
6707
- */
6708
- function addConnectionEventListeners(cb) {
6709
- if (typeof window !== 'undefined' && window.addEventListener) {
6710
- window.addEventListener('offline', cb);
6711
- window.addEventListener('online', cb);
6712
- }
6713
- }
6714
- function removeConnectionEventListeners(cb) {
6715
- if (typeof window !== 'undefined' && window.removeEventListener) {
6716
- window.removeEventListener('offline', cb);
6717
- window.removeEventListener('online', cb);
6718
- }
6719
- }
6720
-
6721
- /**
6722
- * Checks whether we are using React Native
6723
- */
6724
- const isReactNative = () => {
6725
- if (typeof navigator === 'undefined')
6726
- return false;
6727
- return navigator.product?.toLowerCase() === 'reactnative';
6728
- };
6729
-
6730
- // log levels, sorted by verbosity
6731
- const logLevels = Object.freeze({
6732
- trace: 0,
6733
- debug: 1,
6734
- info: 2,
6735
- warn: 3,
6736
- error: 4,
6737
- });
6738
- let logger$2;
6739
- let level = 'info';
6740
- const logToConsole = (logLevel, message, ...args) => {
6741
- let logMethod;
6742
- switch (logLevel) {
6743
- case 'error':
6744
- if (isReactNative()) {
6745
- message = `ERROR: ${message}`;
6746
- logMethod = console.info;
6747
- break;
6748
- }
6749
- logMethod = console.error;
6750
- break;
6751
- case 'warn':
6752
- if (isReactNative()) {
6753
- message = `WARN: ${message}`;
6754
- logMethod = console.info;
6755
- break;
6756
- }
6757
- logMethod = console.warn;
6758
- break;
6759
- case 'info':
6760
- logMethod = console.info;
6761
- break;
6762
- case 'trace':
6763
- logMethod = console.trace;
6764
- break;
6765
- default:
6766
- logMethod = console.log;
6767
- break;
6768
- }
6769
- logMethod(message, ...args);
6770
- };
6771
- const setLogger = (l, lvl) => {
6772
- logger$2 = l;
6773
- if (lvl) {
6774
- setLogLevel(lvl);
6361
+ logMethod(message, ...args);
6362
+ };
6363
+ const setLogger = (l, lvl) => {
6364
+ logger$4 = l;
6365
+ if (lvl) {
6366
+ setLogLevel(lvl);
6775
6367
  }
6776
6368
  };
6777
6369
  const setLogLevel = (l) => {
6778
6370
  level = l;
6779
6371
  };
6780
- const getLogLevel = () => level;
6781
6372
  const getLogger = (withTags) => {
6782
- const loggerMethod = logger$2 || logToConsole;
6783
- const tags = (withTags || []).filter(Boolean).join(':');
6373
+ const loggerMethod = logger$4 || logToConsole;
6374
+ const tags = (withTags || []).join(':');
6784
6375
  const result = (logLevel, message, ...args) => {
6785
6376
  if (logLevels[logLevel] >= logLevels[level]) {
6786
6377
  loggerMethod(logLevel, `[${tags}]: ${message}`, ...args);
@@ -6789,37 +6380,6 @@ const getLogger = (withTags) => {
6789
6380
  return result;
6790
6381
  };
6791
6382
 
6792
- /**
6793
- * Creates a closure which wraps the given RPC call and retries invoking
6794
- * the RPC until it succeeds or the maximum number of retries is reached.
6795
- *
6796
- * For each retry, there would be a delay to avoid request bursts toward the SFU.
6797
- *
6798
- * @param rpc the closure around the RPC call to execute.
6799
- * @param signal the signal to abort the RPC call and retries loop.
6800
- */
6801
- const retryable = async (rpc, signal) => {
6802
- let attempt = 0;
6803
- let result = undefined;
6804
- do {
6805
- if (attempt > 0)
6806
- await sleep(retryInterval(attempt));
6807
- try {
6808
- result = await rpc();
6809
- }
6810
- catch (err) {
6811
- const isRequestCancelled = err instanceof RpcError &&
6812
- err.code === TwirpErrorCode[TwirpErrorCode.cancelled];
6813
- const isAborted = signal?.aborted ?? false;
6814
- if (isRequestCancelled || isAborted)
6815
- throw err;
6816
- getLogger(['sfu-client', 'rpc'])('debug', `rpc failed (${attempt})`, err);
6817
- attempt++;
6818
- }
6819
- } while (!result || result.response.error?.shouldRetry);
6820
- return result;
6821
- };
6822
-
6823
6383
  const getPreferredCodecs = (kind, preferredCodec, codecToRemove) => {
6824
6384
  const logger = getLogger(['codecs']);
6825
6385
  if (!('getCapabilities' in RTCRtpReceiver)) {
@@ -6892,7 +6452,6 @@ const sfuEventKinds = {
6892
6452
  pinsUpdated: undefined,
6893
6453
  callEnded: undefined,
6894
6454
  participantUpdated: undefined,
6895
- participantMigrationComplete: undefined,
6896
6455
  };
6897
6456
  const isSfuEvent = (eventName) => {
6898
6457
  return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
@@ -6901,12 +6460,12 @@ class Dispatcher {
6901
6460
  constructor() {
6902
6461
  this.logger = getLogger(['Dispatcher']);
6903
6462
  this.subscribers = {};
6904
- this.dispatch = (message, logTag) => {
6463
+ this.dispatch = (message) => {
6905
6464
  const eventKind = message.eventPayload.oneofKind;
6906
6465
  if (!eventKind)
6907
6466
  return;
6908
6467
  const payload = message.eventPayload[eventKind];
6909
- this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
6468
+ this.logger('debug', `Dispatching ${eventKind}`, payload);
6910
6469
  const listeners = this.subscribers[eventKind];
6911
6470
  if (!listeners)
6912
6471
  return;
@@ -6948,6 +6507,7 @@ class IceTrickleBuffer {
6948
6507
  constructor() {
6949
6508
  this.subscriberCandidates = new ReplaySubject();
6950
6509
  this.publisherCandidates = new ReplaySubject();
6510
+ this.logger = getLogger(['sfu-client']);
6951
6511
  this.push = (iceTrickle) => {
6952
6512
  if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
6953
6513
  this.subscriberCandidates.next(iceTrickle);
@@ -6956,8 +6516,7 @@ class IceTrickleBuffer {
6956
6516
  this.publisherCandidates.next(iceTrickle);
6957
6517
  }
6958
6518
  else {
6959
- const logger = getLogger(['sfu-client']);
6960
- logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
6519
+ this.logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
6961
6520
  }
6962
6521
  };
6963
6522
  }
@@ -6976,6 +6535,72 @@ function getIceCandidate(candidate) {
6976
6535
  }
6977
6536
  }
6978
6537
 
6538
+ const version = "1.5.0" ;
6539
+ const [major, minor, patch] = version.split('.');
6540
+ let sdkInfo = {
6541
+ type: SdkType.PLAIN_JAVASCRIPT,
6542
+ major,
6543
+ minor,
6544
+ patch,
6545
+ };
6546
+ let osInfo;
6547
+ let deviceInfo;
6548
+ let webRtcInfo;
6549
+ const setSdkInfo = (info) => {
6550
+ sdkInfo = info;
6551
+ };
6552
+ const getSdkInfo = () => {
6553
+ return sdkInfo;
6554
+ };
6555
+ const setOSInfo = (info) => {
6556
+ osInfo = info;
6557
+ };
6558
+ const getOSInfo = () => {
6559
+ return osInfo;
6560
+ };
6561
+ const setDeviceInfo = (info) => {
6562
+ deviceInfo = info;
6563
+ };
6564
+ const getDeviceInfo = () => {
6565
+ return deviceInfo;
6566
+ };
6567
+ const getWebRTCInfo = () => {
6568
+ return webRtcInfo;
6569
+ };
6570
+ const setWebRTCInfo = (info) => {
6571
+ webRtcInfo = info;
6572
+ };
6573
+ const getClientDetails = () => {
6574
+ if (isReactNative()) {
6575
+ // Since RN doesn't support web, sharing browser info is not required
6576
+ return {
6577
+ sdk: getSdkInfo(),
6578
+ os: getOSInfo(),
6579
+ device: getDeviceInfo(),
6580
+ };
6581
+ }
6582
+ const userAgent = new UAParser(navigator.userAgent);
6583
+ const { browser, os, device, cpu } = userAgent.getResult();
6584
+ return {
6585
+ sdk: getSdkInfo(),
6586
+ browser: {
6587
+ name: browser.name || navigator.userAgent,
6588
+ version: browser.version || '',
6589
+ },
6590
+ os: {
6591
+ name: os.name || '',
6592
+ version: os.version || '',
6593
+ architecture: cpu.architecture || '',
6594
+ },
6595
+ device: {
6596
+ name: [device.vendor, device.model, device.type]
6597
+ .filter(Boolean)
6598
+ .join(' '),
6599
+ version: '',
6600
+ },
6601
+ };
6602
+ };
6603
+
6979
6604
  const DEFAULT_BITRATE = 1250000;
6980
6605
  const defaultTargetResolution = {
6981
6606
  bitrate: DEFAULT_BITRATE,
@@ -6998,6 +6623,7 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
6998
6623
  const optimalVideoLayers = [];
6999
6624
  const settings = videoTrack.getSettings();
7000
6625
  const { width: w = 0, height: h = 0 } = settings;
6626
+ const isRNIos = isReactNative() && getOSInfo()?.name.toLowerCase() === 'ios';
7001
6627
  const maxBitrate = getComputedMaxBitrate(targetResolution, w, h);
7002
6628
  let downscaleFactor = 1;
7003
6629
  ['f', 'h', 'q'].forEach((rid) => {
@@ -7011,7 +6637,12 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
7011
6637
  height: Math.round(h / downscaleFactor),
7012
6638
  maxBitrate: Math.round(maxBitrate / downscaleFactor) || defaultBitratePerRid[rid],
7013
6639
  scaleResolutionDownBy: downscaleFactor,
7014
- maxFramerate: 30,
6640
+ // Simulcast on iOS React-Native requires all encodings to share the same framerate
6641
+ maxFramerate: {
6642
+ f: 30,
6643
+ h: isRNIos ? 30 : 25,
6644
+ q: isRNIos ? 30 : 20,
6645
+ }[rid],
7015
6646
  });
7016
6647
  downscaleFactor *= 2;
7017
6648
  });
@@ -7086,10 +6717,6 @@ const findOptimalScreenSharingLayers = (videoTrack, preferences, defaultMaxBitra
7086
6717
  ];
7087
6718
  };
7088
6719
 
7089
- const ensureExhausted = (x, message) => {
7090
- getLogger(['helpers'])('warn', message, x);
7091
- };
7092
-
7093
6720
  const trackTypeToParticipantStreamKey = (trackType) => {
7094
6721
  switch (trackType) {
7095
6722
  case TrackType.SCREEN_SHARE:
@@ -7103,7 +6730,8 @@ const trackTypeToParticipantStreamKey = (trackType) => {
7103
6730
  case TrackType.UNSPECIFIED:
7104
6731
  throw new Error('Track type is unspecified');
7105
6732
  default:
7106
- ensureExhausted(trackType, 'Unknown track type');
6733
+ const exhaustiveTrackTypeCheck = trackType;
6734
+ throw new Error(`Unknown track type: ${exhaustiveTrackTypeCheck}`);
7107
6735
  }
7108
6736
  };
7109
6737
  const muteTypeToTrackType = (muteType) => {
@@ -7117,21 +6745,8 @@ const muteTypeToTrackType = (muteType) => {
7117
6745
  case 'screenshare_audio':
7118
6746
  return TrackType.SCREEN_SHARE_AUDIO;
7119
6747
  default:
7120
- ensureExhausted(muteType, 'Unknown mute type');
7121
- }
7122
- };
7123
- const toTrackType = (trackType) => {
7124
- switch (trackType) {
7125
- case 'TRACK_TYPE_AUDIO':
7126
- return TrackType.AUDIO;
7127
- case 'TRACK_TYPE_VIDEO':
7128
- return TrackType.VIDEO;
7129
- case 'TRACK_TYPE_SCREEN_SHARE':
7130
- return TrackType.SCREEN_SHARE;
7131
- case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
7132
- return TrackType.SCREEN_SHARE_AUDIO;
7133
- default:
7134
- return undefined;
6748
+ const exhaustiveMuteTypeCheck = muteType;
6749
+ throw new Error(`Unknown mute type: ${exhaustiveMuteTypeCheck}`);
7135
6750
  }
7136
6751
  };
7137
6752
 
@@ -7651,11 +7266,6 @@ class CallState {
7651
7266
  this.anonymousParticipantCountSubject = new BehaviorSubject(0);
7652
7267
  this.participantsSubject = new BehaviorSubject([]);
7653
7268
  this.callStatsReportSubject = new BehaviorSubject(undefined);
7654
- // These are tracks that were delivered to the Subscriber's onTrack event
7655
- // that we couldn't associate with a participant yet.
7656
- // This happens when the participantJoined event hasn't been received yet.
7657
- // We keep these tracks around until we can associate them with a participant.
7658
- this.orphanedTracks = [];
7659
7269
  this.logger = getLogger(['CallState']);
7660
7270
  /**
7661
7271
  * A list of comparators that are used to sort the participants.
@@ -7845,7 +7455,7 @@ class CallState {
7845
7455
  */
7846
7456
  this.updateParticipants = (patch) => {
7847
7457
  if (Object.keys(patch).length === 0)
7848
- return this.participants;
7458
+ return;
7849
7459
  return this.setParticipants((participants) => participants.map((p) => {
7850
7460
  const thePatch = patch[p.sessionId];
7851
7461
  if (thePatch) {
@@ -7904,41 +7514,6 @@ class CallState {
7904
7514
  return participant;
7905
7515
  }));
7906
7516
  };
7907
- /**
7908
- * Adds an orphaned track to the call state.
7909
- *
7910
- * @internal
7911
- *
7912
- * @param orphanedTrack the orphaned track to add.
7913
- */
7914
- this.registerOrphanedTrack = (orphanedTrack) => {
7915
- this.orphanedTracks.push(orphanedTrack);
7916
- };
7917
- /**
7918
- * Removes an orphaned track from the call state.
7919
- *
7920
- * @internal
7921
- *
7922
- * @param id the ID of the orphaned track to remove.
7923
- */
7924
- this.removeOrphanedTrack = (id) => {
7925
- this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
7926
- };
7927
- /**
7928
- * Takes all orphaned tracks with the given track lookup prefix.
7929
- * All orphaned tracks with the given track lookup prefix are removed from the call state.
7930
- *
7931
- * @internal
7932
- *
7933
- * @param trackLookupPrefix the track lookup prefix to match the orphaned tracks by.
7934
- */
7935
- this.takeOrphanedTracks = (trackLookupPrefix) => {
7936
- const orphans = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix === trackLookupPrefix);
7937
- if (orphans.length > 0) {
7938
- this.orphanedTracks = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix !== trackLookupPrefix);
7939
- }
7940
- return orphans;
7941
- };
7942
7517
  /**
7943
7518
  * Updates the call state with the data received from the server.
7944
7519
  *
@@ -7963,43 +7538,6 @@ class CallState {
7963
7538
  this.setCurrentValue(this.transcribingSubject, call.transcribing);
7964
7539
  this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
7965
7540
  };
7966
- /**
7967
- * Updates the call state with the data received from the SFU server.
7968
- *
7969
- * @internal
7970
- *
7971
- * @param callState the call state from the SFU server.
7972
- * @param currentSessionId the session ID of the current user.
7973
- * @param reconnectDetails optional reconnect details.
7974
- */
7975
- this.updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
7976
- const { participants, participantCount, startedAt, pins } = callState;
7977
- const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
7978
- this.setParticipants(() => {
7979
- const participantLookup = this.getParticipantLookupBySessionId();
7980
- return participants.map((p) => {
7981
- // We need to preserve the local state of the participant
7982
- // (e.g. videoDimension, visibilityState, pinnedAt, etc.)
7983
- // as it doesn't exist on the server.
7984
- const existingParticipant = participantLookup[p.sessionId];
7985
- const isLocalParticipant = p.sessionId === currentSessionId;
7986
- return Object.assign({}, existingParticipant, p, {
7987
- isLocalParticipant,
7988
- publishedTracks: isLocalParticipant
7989
- ? localPublishedTracks
7990
- : p.publishedTracks,
7991
- viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
7992
- videoTrack: VisibilityState.UNKNOWN,
7993
- screenShareTrack: VisibilityState.UNKNOWN,
7994
- },
7995
- });
7996
- });
7997
- });
7998
- this.setParticipantCount(participantCount?.total || 0);
7999
- this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
8000
- this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
8001
- this.setServerSidePins(pins);
8002
- };
8003
7541
  this.updateFromMemberRemoved = (event) => {
8004
7542
  this.updateFromCallResponse(event.call);
8005
7543
  this.setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
@@ -8711,75 +8249,9 @@ const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
8711
8249
  return SDP.write(parsedSdp);
8712
8250
  };
8713
8251
 
8714
- const version = "1.5.0-0" ;
8715
- const [major, minor, patch] = version.split('.');
8716
- let sdkInfo = {
8717
- type: SdkType.PLAIN_JAVASCRIPT,
8718
- major,
8719
- minor,
8720
- patch,
8721
- };
8722
- let osInfo;
8723
- let deviceInfo;
8724
- let webRtcInfo;
8725
- const setSdkInfo = (info) => {
8726
- sdkInfo = info;
8727
- };
8728
- const getSdkInfo = () => {
8729
- return sdkInfo;
8730
- };
8731
- const setOSInfo = (info) => {
8732
- osInfo = info;
8733
- };
8734
- const getOSInfo = () => {
8735
- return osInfo;
8736
- };
8737
- const setDeviceInfo = (info) => {
8738
- deviceInfo = info;
8739
- };
8740
- const getDeviceInfo = () => {
8741
- return deviceInfo;
8742
- };
8743
- const getWebRTCInfo = () => {
8744
- return webRtcInfo;
8745
- };
8746
- const setWebRTCInfo = (info) => {
8747
- webRtcInfo = info;
8748
- };
8749
- const getClientDetails = () => {
8750
- if (isReactNative()) {
8751
- // Since RN doesn't support web, sharing browser info is not required
8752
- return {
8753
- sdk: getSdkInfo(),
8754
- os: getOSInfo(),
8755
- device: getDeviceInfo(),
8756
- };
8757
- }
8758
- const userAgent = new UAParser(navigator.userAgent);
8759
- const { browser, os, device, cpu } = userAgent.getResult();
8760
- return {
8761
- sdk: getSdkInfo(),
8762
- browser: {
8763
- name: browser.name || navigator.userAgent,
8764
- version: browser.version || '',
8765
- },
8766
- os: {
8767
- name: os.name || '',
8768
- version: os.version || '',
8769
- architecture: cpu.architecture || '',
8770
- },
8771
- device: {
8772
- name: [device.vendor, device.model, device.type]
8773
- .filter(Boolean)
8774
- .join(' '),
8775
- version: '',
8776
- },
8777
- };
8778
- };
8779
-
8252
+ const logger$3 = getLogger(['Publisher']);
8780
8253
  /**
8781
8254
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
8782
- *
8783
8255
  * @internal
8784
8256
  */
8785
8257
  class Publisher {
@@ -8804,9 +8276,8 @@ class Publisher {
8804
8276
  * @param isRedEnabled whether RED is enabled.
8805
8277
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
8806
8278
  * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
8807
- * @param logTag the log tag to use.
8808
8279
  */
8809
- constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, onUnrecoverableError, logTag, }) {
8280
+ constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, iceRestartDelay = 2500, onUnrecoverableError, }) {
8810
8281
  this.transceiverRegistry = {
8811
8282
  [TrackType.AUDIO]: undefined,
8812
8283
  [TrackType.VIDEO]: undefined,
@@ -8820,7 +8291,7 @@ class Publisher {
8820
8291
  * This is needed because some browsers (Firefox) don't reliably report
8821
8292
  * trackId and `mid` parameters.
8822
8293
  *
8823
- * @internal
8294
+ * @private
8824
8295
  */
8825
8296
  this.transceiverInitOrder = [];
8826
8297
  this.trackKindMapping = {
@@ -8852,7 +8323,7 @@ class Publisher {
8852
8323
  /**
8853
8324
  * Closes the publisher PeerConnection and cleans up the resources.
8854
8325
  */
8855
- this.close = ({ stopTracks }) => {
8326
+ this.close = ({ stopTracks = true } = {}) => {
8856
8327
  if (stopTracks) {
8857
8328
  this.stopPublishing();
8858
8329
  Object.keys(this.transceiverRegistry).forEach((trackType) => {
@@ -8864,22 +8335,10 @@ class Publisher {
8864
8335
  this.trackLayersCache[trackType] = undefined;
8865
8336
  });
8866
8337
  }
8867
- this.detachEventHandlers();
8868
- this.pc.close();
8869
- };
8870
- /**
8871
- * Detaches the event handlers from the `RTCPeerConnection`.
8872
- * This is useful when we want to replace the `RTCPeerConnection`
8873
- * instance with a new one (in case of migration).
8874
- */
8875
- this.detachEventHandlers = () => {
8338
+ clearTimeout(this.iceRestartTimeout);
8876
8339
  this.unsubscribeOnIceRestart();
8877
- this.pc.removeEventListener('icecandidate', this.onIceCandidate);
8878
8340
  this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
8879
- this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
8880
- this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
8881
- this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
8882
- this.pc.removeEventListener('signalingstatechange', this.onSignalingStateChange);
8341
+ this.pc.close();
8883
8342
  };
8884
8343
  /**
8885
8344
  * Starts publishing the given track of the given media stream.
@@ -8906,7 +8365,7 @@ class Publisher {
8906
8365
  * Once the track has ended, it will notify the SFU and update the state.
8907
8366
  */
8908
8367
  const handleTrackEnded = async () => {
8909
- this.logger('info', `Track ${TrackType[trackType]} has ended, notifying the SFU`);
8368
+ logger$3('info', `Track ${TrackType[trackType]} has ended, notifying the SFU`);
8910
8369
  await this.notifyTrackMuteStateChanged(mediaStream, trackType, true);
8911
8370
  // clean-up, this event listener needs to run only once.
8912
8371
  track.removeEventListener('ended', handleTrackEnded);
@@ -8922,18 +8381,21 @@ class Publisher {
8922
8381
  ? findOptimalScreenSharingLayers(track, opts.screenShareSettings, screenShareBitrate)
8923
8382
  : undefined;
8924
8383
  let preferredCodec = opts.preferredCodec;
8925
- if (!preferredCodec && trackType === TrackType.VIDEO && isReactNative()) {
8926
- const osName = getOSInfo()?.name.toLowerCase();
8927
- if (osName === 'ipados') {
8928
- // in ipads it was noticed that if vp8 codec is used
8929
- // then the bytes sent is 0 in the outbound-rtp
8930
- // so we are forcing h264 codec for ipads
8931
- preferredCodec = 'H264';
8932
- }
8933
- else if (osName === 'android') {
8934
- preferredCodec = 'VP8';
8384
+ if (!preferredCodec && trackType === TrackType.VIDEO) {
8385
+ if (isReactNative()) {
8386
+ const osName = getOSInfo()?.name.toLowerCase();
8387
+ if (osName === 'ipados') {
8388
+ // in ipads it was noticed that if vp8 codec is used
8389
+ // then the bytes sent is 0 in the outbound-rtp
8390
+ // so we are forcing h264 codec for ipads
8391
+ preferredCodec = 'H264';
8392
+ }
8393
+ else if (osName === 'android') {
8394
+ preferredCodec = 'VP8';
8395
+ }
8935
8396
  }
8936
8397
  }
8398
+ const codecPreferences = this.getCodecPreferences(trackType, preferredCodec);
8937
8399
  // listen for 'ended' event on the track as it might be ended abruptly
8938
8400
  // by an external factor as permission revokes, device disconnected, etc.
8939
8401
  // keep in mind that `track.stop()` doesn't trigger this event.
@@ -8948,20 +8410,17 @@ class Publisher {
8948
8410
  : undefined,
8949
8411
  sendEncodings: videoEncodings,
8950
8412
  });
8951
- this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
8413
+ logger$3('debug', `Added ${TrackType[trackType]} transceiver`);
8952
8414
  this.transceiverInitOrder.push(trackType);
8953
8415
  this.transceiverRegistry[trackType] = transceiver;
8954
8416
  this.publishOptionsPerTrackType.set(trackType, opts);
8955
- const codecPreferences = 'setCodecPreferences' in transceiver
8956
- ? this.getCodecPreferences(trackType, preferredCodec)
8957
- : undefined;
8958
- if (codecPreferences) {
8959
- this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
8417
+ if ('setCodecPreferences' in transceiver && codecPreferences) {
8418
+ logger$3('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
8960
8419
  try {
8961
8420
  transceiver.setCodecPreferences(codecPreferences);
8962
8421
  }
8963
8422
  catch (err) {
8964
- this.logger('warn', `Couldn't set codec preferences`, err);
8423
+ logger$3('warn', `Couldn't set codec preferences`, err);
8965
8424
  }
8966
8425
  }
8967
8426
  }
@@ -9010,17 +8469,31 @@ class Publisher {
9010
8469
  * @param trackType the track type to check.
9011
8470
  */
9012
8471
  this.isPublishing = (trackType) => {
9013
- const transceiver = this.transceiverRegistry[trackType];
9014
- if (!transceiver || !transceiver.sender)
9015
- return false;
9016
- const track = transceiver.sender.track;
9017
- return !!track && track.readyState === 'live' && track.enabled;
8472
+ const transceiverForTrackType = this.transceiverRegistry[trackType];
8473
+ if (transceiverForTrackType && transceiverForTrackType.sender) {
8474
+ const sender = transceiverForTrackType.sender;
8475
+ return (!!sender.track &&
8476
+ sender.track.readyState === 'live' &&
8477
+ sender.track.enabled);
8478
+ }
8479
+ return false;
8480
+ };
8481
+ /**
8482
+ * Returns true if the given track type is currently live
8483
+ *
8484
+ * @param trackType the track type to check.
8485
+ */
8486
+ this.isLive = (trackType) => {
8487
+ const transceiverForTrackType = this.transceiverRegistry[trackType];
8488
+ if (transceiverForTrackType && transceiverForTrackType.sender) {
8489
+ const sender = transceiverForTrackType.sender;
8490
+ return !!sender.track && sender.track.readyState === 'live';
8491
+ }
8492
+ return false;
9018
8493
  };
9019
8494
  this.notifyTrackMuteStateChanged = async (mediaStream, trackType, isMuted) => {
9020
8495
  await this.sfuClient.updateMuteState(trackType, isMuted);
9021
8496
  const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
9022
- if (!audioOrVideoOrScreenShareStream)
9023
- return;
9024
8497
  if (isMuted) {
9025
8498
  this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
9026
8499
  publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
@@ -9042,7 +8515,7 @@ class Publisher {
9042
8515
  * Stops publishing all tracks and stop all tracks.
9043
8516
  */
9044
8517
  this.stopPublishing = () => {
9045
- this.logger('debug', 'Stopping publishing all tracks');
8518
+ logger$3('debug', 'Stopping publishing all tracks');
9046
8519
  this.pc.getSenders().forEach((s) => {
9047
8520
  s.track?.stop();
9048
8521
  if (this.pc.signalingState !== 'closed') {
@@ -9051,15 +8524,15 @@ class Publisher {
9051
8524
  });
9052
8525
  };
9053
8526
  this.updateVideoPublishQuality = async (enabledLayers) => {
9054
- this.logger('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
8527
+ logger$3('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
9055
8528
  const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
9056
8529
  if (!videoSender) {
9057
- this.logger('warn', 'Update publish quality, no video sender found.');
8530
+ logger$3('warn', 'Update publish quality, no video sender found.');
9058
8531
  return;
9059
8532
  }
9060
8533
  const params = videoSender.getParameters();
9061
8534
  if (params.encodings.length === 0) {
9062
- this.logger('warn', 'Update publish quality, No suitable video encoding quality found');
8535
+ logger$3('warn', 'Update publish quality, No suitable video encoding quality found');
9063
8536
  return;
9064
8537
  }
9065
8538
  let changed = false;
@@ -9078,18 +8551,18 @@ class Publisher {
9078
8551
  if (layer !== undefined) {
9079
8552
  if (layer.scaleResolutionDownBy >= 1 &&
9080
8553
  layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy) {
9081
- this.logger('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
8554
+ logger$3('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
9082
8555
  enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
9083
8556
  changed = true;
9084
8557
  }
9085
8558
  if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
9086
- this.logger('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
8559
+ logger$3('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
9087
8560
  enc.maxBitrate = layer.maxBitrate;
9088
8561
  changed = true;
9089
8562
  }
9090
8563
  if (layer.maxFramerate > 0 &&
9091
8564
  layer.maxFramerate !== enc.maxFramerate) {
9092
- this.logger('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
8565
+ logger$3('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
9093
8566
  enc.maxFramerate = layer.maxFramerate;
9094
8567
  changed = true;
9095
8568
  }
@@ -9099,10 +8572,10 @@ class Publisher {
9099
8572
  const activeLayers = params.encodings.filter((e) => e.active);
9100
8573
  if (changed) {
9101
8574
  await videoSender.setParameters(params);
9102
- this.logger('info', `Update publish quality, enabled rids: `, activeLayers);
8575
+ logger$3('info', `Update publish quality, enabled rids: `, activeLayers);
9103
8576
  }
9104
8577
  else {
9105
- this.logger('info', `Update publish quality, no change: `, activeLayers);
8578
+ logger$3('info', `Update publish quality, no change: `, activeLayers);
9106
8579
  }
9107
8580
  };
9108
8581
  /**
@@ -9126,7 +8599,7 @@ class Publisher {
9126
8599
  this.onIceCandidate = (e) => {
9127
8600
  const { candidate } = e;
9128
8601
  if (!candidate) {
9129
- this.logger('debug', 'null ice candidate');
8602
+ logger$3('debug', 'null ice candidate');
9130
8603
  return;
9131
8604
  }
9132
8605
  this.sfuClient
@@ -9135,7 +8608,7 @@ class Publisher {
9135
8608
  peerType: PeerType.PUBLISHER_UNSPECIFIED,
9136
8609
  })
9137
8610
  .catch((err) => {
9138
- this.logger('warn', `ICETrickle failed`, err);
8611
+ logger$3('warn', `ICETrickle failed`, err);
9139
8612
  });
9140
8613
  };
9141
8614
  /**
@@ -9146,23 +8619,38 @@ class Publisher {
9146
8619
  this.setSfuClient = (sfuClient) => {
9147
8620
  this.sfuClient = sfuClient;
9148
8621
  };
8622
+ /**
8623
+ * Performs a migration of this publisher instance to a new SFU.
8624
+ *
8625
+ * Initiates a new `iceRestart` offer/answer exchange with the new SFU.
8626
+ *
8627
+ * @param sfuClient the new SFU client to migrate to.
8628
+ * @param connectionConfig the new connection configuration to use.
8629
+ */
8630
+ this.migrateTo = async (sfuClient, connectionConfig) => {
8631
+ this.sfuClient = sfuClient;
8632
+ this.pc.setConfiguration(connectionConfig);
8633
+ this._connectionConfiguration = connectionConfig;
8634
+ const shouldRestartIce = this.pc.iceConnectionState === 'connected';
8635
+ if (shouldRestartIce) {
8636
+ // negotiate only if there are tracks to publish
8637
+ await this.negotiate({ iceRestart: true });
8638
+ }
8639
+ };
9149
8640
  /**
9150
8641
  * Restarts the ICE connection and renegotiates with the SFU.
9151
8642
  */
9152
8643
  this.restartIce = async () => {
9153
- this.logger('debug', 'Restarting ICE connection');
8644
+ logger$3('debug', 'Restarting ICE connection');
9154
8645
  const signalingState = this.pc.signalingState;
9155
8646
  if (this.isIceRestarting || signalingState === 'have-local-offer') {
9156
- this.logger('debug', 'ICE restart is already in progress');
8647
+ logger$3('debug', 'ICE restart is already in progress');
9157
8648
  return;
9158
8649
  }
9159
8650
  await this.negotiate({ iceRestart: true });
9160
8651
  };
9161
8652
  this.onNegotiationNeeded = () => {
9162
- this.negotiate().catch((err) => {
9163
- this.logger('warn', `Negotiation failed.`, err);
9164
- this.onUnrecoverableError?.();
9165
- });
8653
+ this.negotiate().catch((err) => logger$3('warn', `Negotiation failed.`, err));
9166
8654
  };
9167
8655
  /**
9168
8656
  * Initiates a new offer/answer exchange with the currently connected SFU.
@@ -9174,62 +8662,59 @@ class Publisher {
9174
8662
  const offer = await this.pc.createOffer(options);
9175
8663
  let sdp = this.mungeCodecs(offer.sdp);
9176
8664
  if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
9177
- sdp = this.enableHighQualityAudio(sdp);
8665
+ const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
8666
+ if (transceiver && transceiver.sender.track) {
8667
+ const mid = transceiver.mid ??
8668
+ this.extractMid(sdp, transceiver.sender.track, TrackType.SCREEN_SHARE_AUDIO);
8669
+ sdp = enableHighQualityAudio(sdp, mid);
8670
+ }
9178
8671
  }
9179
8672
  // set the munged SDP back to the offer
9180
8673
  offer.sdp = sdp;
9181
- const trackInfos = this.getAnnouncedTracks(offer.sdp);
8674
+ const trackInfos = this.getCurrentTrackInfos(offer.sdp);
9182
8675
  if (trackInfos.length === 0) {
9183
- throw new Error(`Can't negotiate without announcing any tracks`);
8676
+ throw new Error(`Can't initiate negotiation without announcing any tracks`);
9184
8677
  }
9185
8678
  await this.pc.setLocalDescription(offer);
9186
8679
  const { response } = await this.sfuClient.setPublisher({
9187
8680
  sdp: offer.sdp || '',
9188
8681
  tracks: trackInfos,
9189
8682
  });
9190
- const { sdp: remoteSdp, error } = response;
9191
8683
  try {
9192
- await this.pc.setRemoteDescription({ type: 'answer', sdp: remoteSdp });
8684
+ await this.pc.setRemoteDescription({
8685
+ type: 'answer',
8686
+ sdp: response.sdp,
8687
+ });
9193
8688
  }
9194
8689
  catch (e) {
9195
- this.logger('error', `setRemoteDescription error`, remoteSdp, error, e);
9196
- throw e;
9197
- }
9198
- finally {
9199
- this.isIceRestarting = false;
8690
+ logger$3('error', `setRemoteDescription error`, {
8691
+ sdp: response.sdp,
8692
+ error: e,
8693
+ });
9200
8694
  }
8695
+ this.isIceRestarting = false;
9201
8696
  this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(async (candidate) => {
9202
8697
  try {
9203
8698
  const iceCandidate = JSON.parse(candidate.iceCandidate);
9204
8699
  await this.pc.addIceCandidate(iceCandidate);
9205
8700
  }
9206
8701
  catch (e) {
9207
- this.logger('warn', `ICE candidate error`, e, candidate);
8702
+ logger$3('warn', `ICE candidate error`, [e, candidate]);
9208
8703
  }
9209
8704
  });
9210
8705
  };
9211
- this.enableHighQualityAudio = (sdp) => {
9212
- const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
9213
- if (!transceiver)
9214
- return sdp;
9215
- const mid = this.extractMid(transceiver, sdp, TrackType.SCREEN_SHARE_AUDIO);
9216
- return enableHighQualityAudio(sdp, mid);
9217
- };
9218
8706
  this.mungeCodecs = (sdp) => {
9219
8707
  if (sdp) {
9220
8708
  sdp = toggleDtx(sdp, this.isDtxEnabled);
9221
8709
  }
9222
8710
  return sdp;
9223
8711
  };
9224
- this.extractMid = (transceiver, sdp, trackType) => {
9225
- if (transceiver.mid)
9226
- return transceiver.mid;
8712
+ this.extractMid = (sdp, track, trackType) => {
9227
8713
  if (!sdp) {
9228
- this.logger('warn', 'No SDP found. Returning empty mid');
8714
+ logger$3('warn', 'No SDP found. Returning empty mid');
9229
8715
  return '';
9230
8716
  }
9231
- this.logger('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
9232
- const track = transceiver.sender.track;
8717
+ logger$3('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
9233
8718
  const parsedSdp = SDP.parse(sdp);
9234
8719
  const media = parsedSdp.media.find((m) => {
9235
8720
  return (m.type === track.kind &&
@@ -9237,23 +8722,17 @@ class Publisher {
9237
8722
  (m.msid?.includes(track.id) ?? true));
9238
8723
  });
9239
8724
  if (typeof media?.mid === 'undefined') {
9240
- this.logger('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find it heuristically`);
8725
+ logger$3('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find a heuristic mid`);
9241
8726
  const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
9242
8727
  if (heuristicMid !== -1) {
9243
8728
  return String(heuristicMid);
9244
8729
  }
9245
- this.logger('debug', 'No heuristic mid found. Returning empty mid');
8730
+ logger$3('debug', 'No heuristic mid found. Returning empty mid');
9246
8731
  return '';
9247
8732
  }
9248
8733
  return String(media.mid);
9249
8734
  };
9250
- /**
9251
- * Returns a list of tracks that are currently being published.
9252
- *
9253
- * @internal
9254
- * @param sdp an optional SDP to extract the `mid` from.
9255
- */
9256
- this.getAnnouncedTracks = (sdp) => {
8735
+ this.getCurrentTrackInfos = (sdp) => {
9257
8736
  sdp = sdp || this.pc.localDescription?.sdp;
9258
8737
  const { settings } = this.state;
9259
8738
  const targetResolution = settings?.video
@@ -9265,8 +8744,7 @@ class Publisher {
9265
8744
  const trackType = Number(Object.keys(this.transceiverRegistry).find((key) => this.transceiverRegistry[key] === transceiver));
9266
8745
  const track = transceiver.sender.track;
9267
8746
  let optimalLayers;
9268
- const isTrackLive = track.readyState === 'live';
9269
- if (isTrackLive) {
8747
+ if (track.readyState === 'live') {
9270
8748
  const publishOpts = this.publishOptionsPerTrackType.get(trackType);
9271
8749
  optimalLayers =
9272
8750
  trackType === TrackType.VIDEO
@@ -9279,7 +8757,7 @@ class Publisher {
9279
8757
  else {
9280
8758
  // we report the last known optimal layers for ended tracks
9281
8759
  optimalLayers = this.trackLayersCache[trackType] || [];
9282
- this.logger('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
8760
+ logger$3('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
9283
8761
  }
9284
8762
  const layers = optimalLayers.map((optimalLayer) => ({
9285
8763
  rid: optimalLayer.rid || '',
@@ -9301,11 +8779,10 @@ class Publisher {
9301
8779
  trackId: track.id,
9302
8780
  layers: layers,
9303
8781
  trackType,
9304
- mid: this.extractMid(transceiver, sdp, trackType),
8782
+ mid: transceiver.mid ?? this.extractMid(sdp, track, trackType),
9305
8783
  stereo: isStereo,
9306
8784
  dtx: isAudioTrack && this.isDtxEnabled,
9307
8785
  red: isAudioTrack && this.isRedEnabled,
9308
- muted: !isTrackLive,
9309
8786
  };
9310
8787
  });
9311
8788
  };
@@ -9314,26 +8791,44 @@ class Publisher {
9314
8791
  `${e.errorCode}: ${e.errorText}`;
9315
8792
  const iceState = this.pc.iceConnectionState;
9316
8793
  const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
9317
- this.logger(logLevel, `ICE Candidate error`, errorMessage);
8794
+ logger$3(logLevel, `ICE Candidate error`, errorMessage);
9318
8795
  };
9319
8796
  this.onIceConnectionStateChange = () => {
9320
8797
  const state = this.pc.iceConnectionState;
9321
- this.logger('debug', `ICE Connection state changed to`, state);
9322
- if (this.state.callingState === CallingState.RECONNECTING)
9323
- return;
9324
- if (state === 'failed' || state === 'disconnected') {
9325
- this.logger('debug', `Attempting to restart ICE`);
8798
+ logger$3('debug', `ICE Connection state changed to`, state);
8799
+ const hasNetworkConnection = this.state.callingState !== CallingState.OFFLINE;
8800
+ if (state === 'failed') {
8801
+ logger$3('debug', `Attempting to restart ICE`);
9326
8802
  this.restartIce().catch((e) => {
9327
- this.logger('error', `ICE restart error`, e);
8803
+ logger$3('error', `ICE restart error`, e);
9328
8804
  this.onUnrecoverableError?.();
9329
8805
  });
9330
8806
  }
8807
+ else if (state === 'disconnected' && hasNetworkConnection) {
8808
+ // when in `disconnected` state, the browser may recover automatically,
8809
+ // hence, we delay the ICE restart
8810
+ logger$3('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
8811
+ this.iceRestartTimeout = setTimeout(() => {
8812
+ // check if the state is still `disconnected` or `failed`
8813
+ // as the connection may have recovered (or failed) in the meantime
8814
+ if (this.pc.iceConnectionState === 'disconnected' ||
8815
+ this.pc.iceConnectionState === 'failed') {
8816
+ this.restartIce().catch((e) => {
8817
+ logger$3('error', `ICE restart error`, e);
8818
+ this.onUnrecoverableError?.();
8819
+ });
8820
+ }
8821
+ else {
8822
+ logger$3('debug', `Scheduled ICE restart: connection recovered, canceled.`);
8823
+ }
8824
+ }, this.iceRestartDelay);
8825
+ }
9331
8826
  };
9332
8827
  this.onIceGatheringStateChange = () => {
9333
- this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
8828
+ logger$3('debug', `ICE Gathering State`, this.pc.iceGatheringState);
9334
8829
  };
9335
8830
  this.onSignalingStateChange = () => {
9336
- this.logger('debug', `Signaling state changed`, this.pc.signalingState);
8831
+ logger$3('debug', `Signaling state changed`, this.pc.signalingState);
9337
8832
  };
9338
8833
  this.ridToVideoQuality = (rid) => {
9339
8834
  return rid === 'q'
@@ -9342,29 +8837,28 @@ class Publisher {
9342
8837
  ? VideoQuality.MID
9343
8838
  : VideoQuality.HIGH; // default to HIGH
9344
8839
  };
9345
- this.logger = getLogger(['Publisher', logTag]);
9346
8840
  this.pc = this.createPeerConnection(connectionConfig);
9347
8841
  this.sfuClient = sfuClient;
9348
8842
  this.state = state;
9349
8843
  this.isDtxEnabled = isDtxEnabled;
9350
8844
  this.isRedEnabled = isRedEnabled;
8845
+ this.iceRestartDelay = iceRestartDelay;
9351
8846
  this.onUnrecoverableError = onUnrecoverableError;
9352
8847
  this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
9353
8848
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
9354
8849
  return;
9355
8850
  this.restartIce().catch((err) => {
9356
- this.logger('warn', `ICERestart failed`, err);
8851
+ logger$3('warn', `ICERestart failed`, err);
9357
8852
  this.onUnrecoverableError?.();
9358
8853
  });
9359
8854
  });
9360
8855
  }
9361
8856
  }
9362
8857
 
8858
+ const logger$2 = getLogger(['Subscriber']);
9363
8859
  /**
9364
8860
  * A wrapper around the `RTCPeerConnection` that handles the incoming
9365
8861
  * media streams from the SFU.
9366
- *
9367
- * @internal
9368
8862
  */
9369
8863
  class Subscriber {
9370
8864
  /**
@@ -9386,9 +8880,8 @@ class Subscriber {
9386
8880
  * @param connectionConfig the connection configuration to use.
9387
8881
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
9388
8882
  * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
9389
- * @param logTag a tag to use for logging.
9390
8883
  */
9391
- constructor({ sfuClient, dispatcher, state, connectionConfig, onUnrecoverableError, logTag, }) {
8884
+ constructor({ sfuClient, dispatcher, state, connectionConfig, iceRestartDelay = 2500, onUnrecoverableError, }) {
9392
8885
  this.isIceRestarting = false;
9393
8886
  /**
9394
8887
  * Creates a new `RTCPeerConnection` instance with the given configuration.
@@ -9409,22 +8902,10 @@ class Subscriber {
9409
8902
  * Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
9410
8903
  */
9411
8904
  this.close = () => {
9412
- this.detachEventHandlers();
9413
- this.pc.close();
9414
- };
9415
- /**
9416
- * Detaches the event handlers from the `RTCPeerConnection`.
9417
- * This is useful when we want to replace the `RTCPeerConnection`
9418
- * instance with a new one (in case of migration).
9419
- */
9420
- this.detachEventHandlers = () => {
8905
+ clearTimeout(this.iceRestartTimeout);
9421
8906
  this.unregisterOnSubscriberOffer();
9422
8907
  this.unregisterOnIceRestart();
9423
- this.pc.removeEventListener('icecandidate', this.onIceCandidate);
9424
- this.pc.removeEventListener('track', this.handleOnTrack);
9425
- this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
9426
- this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
9427
- this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
8908
+ this.pc.close();
9428
8909
  };
9429
8910
  /**
9430
8911
  * Returns the result of the `RTCPeerConnection.getStats()` method
@@ -9442,17 +8923,76 @@ class Subscriber {
9442
8923
  this.setSfuClient = (sfuClient) => {
9443
8924
  this.sfuClient = sfuClient;
9444
8925
  };
8926
+ /**
8927
+ * Migrates the subscriber to a new SFU client.
8928
+ *
8929
+ * @param sfuClient the new SFU client to migrate to.
8930
+ * @param connectionConfig the new connection configuration to use.
8931
+ */
8932
+ this.migrateTo = (sfuClient, connectionConfig) => {
8933
+ this.setSfuClient(sfuClient);
8934
+ // when migrating, we want to keep the previous subscriber open
8935
+ // until the new one is connected
8936
+ const previousPC = this.pc;
8937
+ // we keep a record of previously available video tracks
8938
+ // so that we can monitor when they become available on the new
8939
+ // subscriber and close the previous one.
8940
+ const trackIdsToMigrate = new Set();
8941
+ previousPC.getReceivers().forEach((r) => {
8942
+ if (r.track.kind === 'video') {
8943
+ trackIdsToMigrate.add(r.track.id);
8944
+ }
8945
+ });
8946
+ // set up a new subscriber peer connection, configured to connect
8947
+ // to the new SFU node
8948
+ const pc = this.createPeerConnection(connectionConfig);
8949
+ let migrationTimeoutId;
8950
+ const cleanupMigration = () => {
8951
+ previousPC.close();
8952
+ clearTimeout(migrationTimeoutId);
8953
+ };
8954
+ // When migrating, we want to keep track of the video tracks
8955
+ // that are migrating to the new subscriber.
8956
+ // Once all of them are available, we can close the previous subscriber.
8957
+ const handleTrackMigration = (e) => {
8958
+ logger$2('debug', `[Migration]: Migrated track: ${e.track.id}, ${e.track.kind}`);
8959
+ trackIdsToMigrate.delete(e.track.id);
8960
+ if (trackIdsToMigrate.size === 0) {
8961
+ logger$2('debug', `[Migration]: Migration complete`);
8962
+ pc.removeEventListener('track', handleTrackMigration);
8963
+ cleanupMigration();
8964
+ }
8965
+ };
8966
+ // When migrating, we want to keep track of the connection state
8967
+ // of the new subscriber.
8968
+ // Once it is connected, we give it a 2-second grace period to receive
8969
+ // all the video tracks that are migrating from the previous subscriber.
8970
+ // After this threshold, we abruptly close the previous subscriber.
8971
+ const handleConnectionStateChange = () => {
8972
+ if (pc.connectionState === 'connected') {
8973
+ migrationTimeoutId = setTimeout(() => {
8974
+ pc.removeEventListener('track', handleTrackMigration);
8975
+ cleanupMigration();
8976
+ }, 2000);
8977
+ pc.removeEventListener('connectionstatechange', handleConnectionStateChange);
8978
+ }
8979
+ };
8980
+ pc.addEventListener('track', handleTrackMigration);
8981
+ pc.addEventListener('connectionstatechange', handleConnectionStateChange);
8982
+ // replace the PeerConnection instance
8983
+ this.pc = pc;
8984
+ };
9445
8985
  /**
9446
8986
  * Restarts the ICE connection and renegotiates with the SFU.
9447
8987
  */
9448
8988
  this.restartIce = async () => {
9449
- this.logger('debug', 'Restarting ICE connection');
8989
+ logger$2('debug', 'Restarting ICE connection');
9450
8990
  if (this.pc.signalingState === 'have-remote-offer') {
9451
- this.logger('debug', 'ICE restart is already in progress');
8991
+ logger$2('debug', 'ICE restart is already in progress');
9452
8992
  return;
9453
8993
  }
9454
8994
  if (this.pc.connectionState === 'new') {
9455
- this.logger('debug', `ICE connection is not yet established, skipping restart.`);
8995
+ logger$2('debug', `ICE connection is not yet established, skipping restart.`);
9456
8996
  return;
9457
8997
  }
9458
8998
  const previousIsIceRestarting = this.isIceRestarting;
@@ -9471,42 +9011,36 @@ class Subscriber {
9471
9011
  this.handleOnTrack = (e) => {
9472
9012
  const [primaryStream] = e.streams;
9473
9013
  // example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
9474
- const [trackId, rawTrackType] = primaryStream.id.split(':');
9014
+ const [trackId, trackType] = primaryStream.id.split(':');
9475
9015
  const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
9476
- this.logger('debug', `[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, e.track.id, e.track);
9477
- const trackDebugInfo = `${participantToUpdate?.userId} ${rawTrackType}:${trackId}`;
9016
+ logger$2('debug', `[onTrack]: Got remote ${trackType} track for userId: ${participantToUpdate?.userId}`, e.track.id, e.track);
9017
+ if (!participantToUpdate) {
9018
+ logger$2('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
9019
+ return;
9020
+ }
9021
+ const trackDebugInfo = `${participantToUpdate.userId} ${trackType}:${trackId}`;
9478
9022
  e.track.addEventListener('mute', () => {
9479
- this.logger('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
9023
+ logger$2('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
9480
9024
  });
9481
9025
  e.track.addEventListener('unmute', () => {
9482
- this.logger('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
9026
+ logger$2('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
9483
9027
  });
9484
9028
  e.track.addEventListener('ended', () => {
9485
- this.logger('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
9486
- this.state.removeOrphanedTrack(primaryStream.id);
9029
+ logger$2('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
9487
9030
  });
9488
- const trackType = toTrackType(rawTrackType);
9489
- if (!trackType) {
9490
- return this.logger('error', `Unknown track type: ${rawTrackType}`);
9491
- }
9492
- if (!participantToUpdate) {
9493
- this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
9494
- this.state.registerOrphanedTrack({
9495
- id: primaryStream.id,
9496
- trackLookupPrefix: trackId,
9497
- track: primaryStream,
9498
- trackType,
9499
- });
9500
- return;
9501
- }
9502
- const streamKindProp = trackTypeToParticipantStreamKey(trackType);
9031
+ const streamKindProp = {
9032
+ TRACK_TYPE_AUDIO: 'audioStream',
9033
+ TRACK_TYPE_VIDEO: 'videoStream',
9034
+ TRACK_TYPE_SCREEN_SHARE: 'screenShareStream',
9035
+ TRACK_TYPE_SCREEN_SHARE_AUDIO: 'screenShareAudioStream',
9036
+ }[trackType];
9503
9037
  if (!streamKindProp) {
9504
- this.logger('error', `Unknown track type: ${rawTrackType}`);
9038
+ logger$2('error', `Unknown track type: ${trackType}`);
9505
9039
  return;
9506
9040
  }
9507
9041
  const previousStream = participantToUpdate[streamKindProp];
9508
9042
  if (previousStream) {
9509
- this.logger('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
9043
+ logger$2('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
9510
9044
  previousStream.getTracks().forEach((t) => {
9511
9045
  t.stop();
9512
9046
  previousStream.removeTrack(t);
@@ -9519,7 +9053,7 @@ class Subscriber {
9519
9053
  this.onIceCandidate = (e) => {
9520
9054
  const { candidate } = e;
9521
9055
  if (!candidate) {
9522
- this.logger('debug', 'null ice candidate');
9056
+ logger$2('debug', 'null ice candidate');
9523
9057
  return;
9524
9058
  }
9525
9059
  this.sfuClient
@@ -9528,11 +9062,11 @@ class Subscriber {
9528
9062
  peerType: PeerType.SUBSCRIBER,
9529
9063
  })
9530
9064
  .catch((err) => {
9531
- this.logger('warn', `ICETrickle failed`, err);
9065
+ logger$2('warn', `ICETrickle failed`, err);
9532
9066
  });
9533
9067
  };
9534
9068
  this.negotiate = async (subscriberOffer) => {
9535
- this.logger('info', `Received subscriberOffer`, subscriberOffer);
9069
+ logger$2('info', `Received subscriberOffer`, subscriberOffer);
9536
9070
  await this.pc.setRemoteDescription({
9537
9071
  type: 'offer',
9538
9072
  sdp: subscriberOffer.sdp,
@@ -9543,7 +9077,7 @@ class Subscriber {
9543
9077
  await this.pc.addIceCandidate(iceCandidate);
9544
9078
  }
9545
9079
  catch (e) {
9546
- this.logger('warn', `ICE candidate error`, [e, candidate]);
9080
+ logger$2('warn', `ICE candidate error`, [e, candidate]);
9547
9081
  }
9548
9082
  });
9549
9083
  const answer = await this.pc.createAnswer();
@@ -9556,53 +9090,63 @@ class Subscriber {
9556
9090
  };
9557
9091
  this.onIceConnectionStateChange = () => {
9558
9092
  const state = this.pc.iceConnectionState;
9559
- this.logger('debug', `ICE connection state changed`, state);
9560
- if (this.state.callingState === CallingState.RECONNECTING)
9561
- return;
9093
+ logger$2('debug', `ICE connection state changed`, state);
9562
9094
  // do nothing when ICE is restarting
9563
9095
  if (this.isIceRestarting)
9564
9096
  return;
9565
- if (state === 'failed' || state === 'disconnected') {
9566
- this.logger('debug', `Attempting to restart ICE`);
9097
+ const hasNetworkConnection = this.state.callingState !== CallingState.OFFLINE;
9098
+ if (state === 'failed') {
9099
+ logger$2('debug', `Attempting to restart ICE`);
9567
9100
  this.restartIce().catch((e) => {
9568
- this.logger('error', `ICE restart failed`, e);
9101
+ logger$2('error', `ICE restart failed`, e);
9569
9102
  this.onUnrecoverableError?.();
9570
9103
  });
9571
9104
  }
9105
+ else if (state === 'disconnected' && hasNetworkConnection) {
9106
+ // when in `disconnected` state, the browser may recover automatically,
9107
+ // hence, we delay the ICE restart
9108
+ logger$2('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
9109
+ this.iceRestartTimeout = setTimeout(() => {
9110
+ // check if the state is still `disconnected` or `failed`
9111
+ // as the connection may have recovered (or failed) in the meantime
9112
+ if (this.pc.iceConnectionState === 'disconnected' ||
9113
+ this.pc.iceConnectionState === 'failed') {
9114
+ this.restartIce().catch((e) => {
9115
+ logger$2('error', `ICE restart failed`, e);
9116
+ this.onUnrecoverableError?.();
9117
+ });
9118
+ }
9119
+ else {
9120
+ logger$2('debug', `Scheduled ICE restart: connection recovered, canceled.`);
9121
+ }
9122
+ }, this.iceRestartDelay);
9123
+ }
9572
9124
  };
9573
9125
  this.onIceGatheringStateChange = () => {
9574
- this.logger('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
9126
+ logger$2('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
9575
9127
  };
9576
9128
  this.onIceCandidateError = (e) => {
9577
9129
  const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
9578
9130
  `${e.errorCode}: ${e.errorText}`;
9579
9131
  const iceState = this.pc.iceConnectionState;
9580
9132
  const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
9581
- this.logger(logLevel, `ICE Candidate error`, errorMessage);
9133
+ logger$2(logLevel, `ICE Candidate error`, errorMessage);
9582
9134
  };
9583
- this.logger = getLogger(['Subscriber', logTag]);
9584
9135
  this.sfuClient = sfuClient;
9585
9136
  this.state = state;
9137
+ this.iceRestartDelay = iceRestartDelay;
9586
9138
  this.onUnrecoverableError = onUnrecoverableError;
9587
9139
  this.pc = this.createPeerConnection(connectionConfig);
9588
- const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
9589
9140
  this.unregisterOnSubscriberOffer = dispatcher.on('subscriberOffer', (subscriberOffer) => {
9590
- // TODO: use queue per peer connection, otherwise
9591
- // it could happen we consume an offer for a different peer connection
9592
- withoutConcurrency(subscriberOfferConcurrencyTag, () => {
9593
- return this.negotiate(subscriberOffer);
9594
- }).catch((err) => {
9595
- this.logger('warn', `Negotiation failed.`, err);
9141
+ this.negotiate(subscriberOffer).catch((err) => {
9142
+ logger$2('warn', `Negotiation failed.`, err);
9596
9143
  });
9597
9144
  });
9598
- const iceRestartConcurrencyTag = Symbol('iceRestart');
9599
9145
  this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
9600
- withoutConcurrency(iceRestartConcurrencyTag, async () => {
9601
- if (iceRestart.peerType !== PeerType.SUBSCRIBER)
9602
- return;
9603
- await this.restartIce();
9604
- }).catch((err) => {
9605
- this.logger('warn', `ICERestart failed`, err);
9146
+ if (iceRestart.peerType !== PeerType.SUBSCRIBER)
9147
+ return;
9148
+ this.restartIce().catch((err) => {
9149
+ logger$2('warn', `ICERestart failed`, err);
9606
9150
  this.onUnrecoverableError?.();
9607
9151
  });
9608
9152
  });
@@ -9610,8 +9154,8 @@ class Subscriber {
9610
9154
  }
9611
9155
 
9612
9156
  const createWebSocketSignalChannel = (opts) => {
9613
- const { endpoint, onMessage, logTag } = opts;
9614
- const logger = getLogger(['sfu-client-ws', logTag]);
9157
+ const logger = getLogger(['sfu-client']);
9158
+ const { endpoint, onMessage } = opts;
9615
9159
  const ws = new WebSocket(endpoint);
9616
9160
  ws.binaryType = 'arraybuffer'; // do we need this?
9617
9161
  ws.addEventListener('error', (e) => {
@@ -9637,37 +9181,132 @@ const createWebSocketSignalChannel = (opts) => {
9637
9181
  return ws;
9638
9182
  };
9639
9183
 
9184
+ const sleep = (m) => new Promise((r) => setTimeout(r, m));
9185
+ function isFunction(value) {
9186
+ return (value &&
9187
+ (Object.prototype.toString.call(value) === '[object Function]' ||
9188
+ 'function' === typeof value ||
9189
+ value instanceof Function));
9190
+ }
9640
9191
  /**
9641
- * Creates a new promise with resolvers.
9642
- *
9643
- * Based on:
9644
- * - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
9192
+ * A map of known error codes.
9645
9193
  */
9646
- const promiseWithResolvers = () => {
9647
- let resolve;
9648
- let reject;
9649
- const promise = new Promise((_resolve, _reject) => {
9650
- resolve = _resolve;
9651
- reject = _reject;
9652
- });
9653
- let isResolved = false;
9654
- let isRejected = false;
9655
- const resolver = (value) => {
9656
- isResolved = true;
9657
- resolve(value);
9658
- };
9659
- const rejecter = (reason) => {
9660
- isRejected = true;
9661
- reject(reason);
9662
- };
9663
- return {
9664
- promise,
9665
- resolve: resolver,
9666
- reject: rejecter,
9667
- isResolved,
9668
- isRejected,
9669
- };
9194
+ const KnownCodes = {
9195
+ TOKEN_EXPIRED: 40,
9196
+ WS_CLOSED_SUCCESS: 1000,
9197
+ WS_CLOSED_ABRUPTLY: 1006,
9198
+ WS_POLICY_VIOLATION: 1008,
9670
9199
  };
9200
+ /**
9201
+ * retryInterval - A retry interval which increases acc to number of failures
9202
+ *
9203
+ * @return {number} Duration to wait in milliseconds
9204
+ */
9205
+ function retryInterval(numberOfFailures) {
9206
+ // try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
9207
+ const max = Math.min(500 + numberOfFailures * 2000, 5000);
9208
+ const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
9209
+ return Math.floor(Math.random() * (max - min) + min);
9210
+ }
9211
+ function randomId() {
9212
+ return generateUUIDv4();
9213
+ }
9214
+ function hex(bytes) {
9215
+ let s = '';
9216
+ for (let i = 0; i < bytes.length; i++) {
9217
+ s += bytes[i].toString(16).padStart(2, '0');
9218
+ }
9219
+ return s;
9220
+ }
9221
+ // https://tools.ietf.org/html/rfc4122
9222
+ function generateUUIDv4() {
9223
+ const bytes = getRandomBytes(16);
9224
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
9225
+ bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
9226
+ return (hex(bytes.subarray(0, 4)) +
9227
+ '-' +
9228
+ hex(bytes.subarray(4, 6)) +
9229
+ '-' +
9230
+ hex(bytes.subarray(6, 8)) +
9231
+ '-' +
9232
+ hex(bytes.subarray(8, 10)) +
9233
+ '-' +
9234
+ hex(bytes.subarray(10, 16)));
9235
+ }
9236
+ function getRandomValuesWithMathRandom(bytes) {
9237
+ const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
9238
+ for (let i = 0; i < bytes.length; i++) {
9239
+ bytes[i] = Math.random() * max;
9240
+ }
9241
+ }
9242
+ const getRandomValues = (() => {
9243
+ if (typeof crypto !== 'undefined' &&
9244
+ typeof crypto?.getRandomValues !== 'undefined') {
9245
+ return crypto.getRandomValues.bind(crypto);
9246
+ }
9247
+ else if (typeof msCrypto !== 'undefined') {
9248
+ return msCrypto.getRandomValues.bind(msCrypto);
9249
+ }
9250
+ else {
9251
+ return getRandomValuesWithMathRandom;
9252
+ }
9253
+ })();
9254
+ function getRandomBytes(length) {
9255
+ const bytes = new Uint8Array(length);
9256
+ getRandomValues(bytes);
9257
+ return bytes;
9258
+ }
9259
+ function convertErrorToJson(err) {
9260
+ const jsonObj = {};
9261
+ if (!err)
9262
+ return jsonObj;
9263
+ try {
9264
+ Object.getOwnPropertyNames(err).forEach((key) => {
9265
+ jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
9266
+ });
9267
+ }
9268
+ catch (_) {
9269
+ return {
9270
+ error: 'failed to serialize the error',
9271
+ };
9272
+ }
9273
+ return jsonObj;
9274
+ }
9275
+ /**
9276
+ * isOnline safely return the navigator.online value for browser env
9277
+ * if navigator is not in global object, it always return true
9278
+ */
9279
+ function isOnline(logger) {
9280
+ const nav = typeof navigator !== 'undefined'
9281
+ ? navigator
9282
+ : typeof window !== 'undefined' && window.navigator
9283
+ ? window.navigator
9284
+ : undefined;
9285
+ if (!nav) {
9286
+ logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
9287
+ return true;
9288
+ }
9289
+ // RN navigator has undefined for onLine
9290
+ if (typeof nav.onLine !== 'boolean') {
9291
+ return true;
9292
+ }
9293
+ return nav.onLine;
9294
+ }
9295
+ /**
9296
+ * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
9297
+ */
9298
+ function addConnectionEventListeners(cb) {
9299
+ if (typeof window !== 'undefined' && window.addEventListener) {
9300
+ window.addEventListener('offline', cb);
9301
+ window.addEventListener('online', cb);
9302
+ }
9303
+ }
9304
+ function removeConnectionEventListeners(cb) {
9305
+ if (typeof window !== 'undefined' && window.removeEventListener) {
9306
+ window.removeEventListener('offline', cb);
9307
+ window.removeEventListener('online', cb);
9308
+ }
9309
+ }
9671
9310
 
9672
9311
  /**
9673
9312
  * The client used for exchanging information with the SFU.
@@ -9675,228 +9314,133 @@ const promiseWithResolvers = () => {
9675
9314
  class StreamSfuClient {
9676
9315
  /**
9677
9316
  * Constructs a new SFU client.
9317
+ *
9318
+ * @param dispatcher the event dispatcher to use.
9319
+ * @param sfuServer the SFU server to connect to.
9320
+ * @param token the JWT token to use for authentication.
9321
+ * @param sessionId the `sessionId` of the currently connected participant.
9678
9322
  */
9679
- constructor({ dispatcher, credentials, sessionId, logTag, joinResponseTimeout = 5000, onSignalClose, }) {
9323
+ constructor({ dispatcher, sfuServer, token, sessionId, }) {
9680
9324
  /**
9681
9325
  * A buffer for ICE Candidates that are received before
9682
- * the Publisher and Subscriber Peer Connections are ready to handle them.
9326
+ * the PeerConnections are ready to handle them.
9683
9327
  */
9684
9328
  this.iceTrickleBuffer = new IceTrickleBuffer();
9685
9329
  /**
9686
- * Flag to indicate if the client is in the process of leaving the call.
9687
- * This is set to `true` when the user initiates the leave process.
9688
- */
9689
- this.isLeaving = false;
9690
- this.pingIntervalInMs = 10 * 1000;
9691
- this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
9692
- this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
9693
- /**
9694
- * Promise that resolves when the JoinResponse is received.
9695
- * Rejects after a certain threshold if the response is not received.
9330
+ * A flag indicating whether the client is currently migrating away
9331
+ * from this SFU.
9696
9332
  */
9697
- this.joinResponseTask = promiseWithResolvers();
9333
+ this.isMigratingAway = false;
9698
9334
  /**
9699
- * A controller to abort the current requests.
9335
+ * A flag indicating that the client connection is broken for the current
9336
+ * client and that a fast-reconnect with a new client should be attempted.
9700
9337
  */
9701
- this.abortController = new AbortController();
9702
- this.createWebSocket = () => {
9703
- this.signalWs = createWebSocketSignalChannel({
9704
- logTag: this.logTag,
9705
- endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
9706
- onMessage: (message) => {
9707
- this.lastMessageTimestamp = new Date();
9708
- this.scheduleConnectionCheck();
9709
- this.dispatcher.dispatch(message, this.logTag);
9710
- },
9711
- });
9712
- this.signalWs.addEventListener('close', this.handleWebSocketClose);
9713
- this.signalWs.addEventListener('error', this.restoreWebSocket);
9714
- this.signalReady = new Promise((resolve) => {
9715
- const onOpen = () => {
9716
- this.signalWs.removeEventListener('open', onOpen);
9717
- resolve(this.signalWs);
9718
- };
9719
- this.signalWs.addEventListener('open', onOpen);
9720
- });
9721
- };
9722
- this.cleanUpWebSocket = () => {
9723
- this.signalWs.removeEventListener('error', this.restoreWebSocket);
9724
- this.signalWs.removeEventListener('close', this.handleWebSocketClose);
9725
- };
9726
- this.restoreWebSocket = () => {
9727
- withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
9728
- this.logger('debug', 'Restoring SFU WS connection');
9729
- this.cleanUpWebSocket();
9730
- await sleep(500);
9731
- this.createWebSocket();
9732
- }).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
9733
- };
9734
- this.handleWebSocketClose = (e) => {
9735
- this.signalWs.removeEventListener('close', this.handleWebSocketClose);
9736
- clearInterval(this.keepAliveInterval);
9737
- clearTimeout(this.connectionCheckTimeout);
9738
- if (this.onSignalClose) {
9739
- this.onSignalClose(e);
9740
- }
9741
- };
9742
- this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
9743
- if (this.signalWs.readyState === WebSocket.OPEN) {
9744
- this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
9338
+ this.isFastReconnecting = false;
9339
+ this.pingIntervalInMs = 10 * 1000;
9340
+ this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
9341
+ this.close = (code, reason) => {
9342
+ this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
9343
+ if (this.signalWs.readyState !== this.signalWs.CLOSED) {
9745
9344
  this.signalWs.close(code, `js-client: ${reason}`);
9746
- this.cleanUpWebSocket();
9747
9345
  }
9748
- this.dispose();
9749
- };
9750
- this.dispose = () => {
9751
- this.logger('debug', 'Disposing SFU client');
9752
9346
  this.unsubscribeIceTrickle();
9753
9347
  clearInterval(this.keepAliveInterval);
9754
9348
  clearTimeout(this.connectionCheckTimeout);
9755
- clearTimeout(this.migrateAwayTimeout);
9756
- this.abortController.abort();
9757
- this.migrationTask?.resolve();
9758
- };
9759
- this.leaveAndClose = async (reason) => {
9760
- await this.joinResponseTask.promise;
9761
- try {
9762
- this.isLeaving = true;
9763
- await this.notifyLeave(reason);
9764
- }
9765
- catch (err) {
9766
- this.logger('debug', 'Error notifying SFU about leaving call', err);
9767
- }
9768
- this.close(StreamSfuClient.NORMAL_CLOSURE, reason.substring(0, 115));
9769
9349
  };
9770
- this.updateSubscriptions = async (tracks) => {
9771
- await this.joinResponseTask.promise;
9772
- return retryable(() => this.rpc.updateSubscriptions({ sessionId: this.sessionId, tracks }), this.abortController.signal);
9350
+ this.updateSubscriptions = async (subscriptions) => {
9351
+ return retryable(() => this.rpc.updateSubscriptions({
9352
+ sessionId: this.sessionId,
9353
+ tracks: subscriptions,
9354
+ }), this.logger, 'debug');
9773
9355
  };
9774
9356
  this.setPublisher = async (data) => {
9775
- await this.joinResponseTask.promise;
9776
- return retryable(() => this.rpc.setPublisher({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9357
+ return retryable(() => this.rpc.setPublisher({
9358
+ ...data,
9359
+ sessionId: this.sessionId,
9360
+ }), this.logger);
9777
9361
  };
9778
9362
  this.sendAnswer = async (data) => {
9779
- await this.joinResponseTask.promise;
9780
- return retryable(() => this.rpc.sendAnswer({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9363
+ return retryable(() => this.rpc.sendAnswer({
9364
+ ...data,
9365
+ sessionId: this.sessionId,
9366
+ }), this.logger);
9781
9367
  };
9782
9368
  this.iceTrickle = async (data) => {
9783
- await this.joinResponseTask.promise;
9784
- return retryable(() => this.rpc.iceTrickle({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9369
+ return retryable(() => this.rpc.iceTrickle({
9370
+ ...data,
9371
+ sessionId: this.sessionId,
9372
+ }), this.logger);
9785
9373
  };
9786
9374
  this.iceRestart = async (data) => {
9787
- await this.joinResponseTask.promise;
9788
- return retryable(() => this.rpc.iceRestart({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9375
+ return retryable(() => this.rpc.iceRestart({
9376
+ ...data,
9377
+ sessionId: this.sessionId,
9378
+ }), this.logger);
9789
9379
  };
9790
9380
  this.updateMuteState = async (trackType, muted) => {
9791
- await this.joinResponseTask.promise;
9792
- return this.updateMuteStates({ muteStates: [{ trackType, muted }] });
9381
+ return this.updateMuteStates({
9382
+ muteStates: [
9383
+ {
9384
+ trackType,
9385
+ muted,
9386
+ },
9387
+ ],
9388
+ });
9793
9389
  };
9794
9390
  this.updateMuteStates = async (data) => {
9795
- await this.joinResponseTask.promise;
9796
- return retryable(() => this.rpc.updateMuteStates({ ...data, sessionId: this.sessionId }), this.abortController.signal);
9391
+ return retryable(() => this.rpc.updateMuteStates({
9392
+ ...data,
9393
+ sessionId: this.sessionId,
9394
+ }), this.logger);
9797
9395
  };
9798
9396
  this.sendStats = async (stats) => {
9799
- await this.joinResponseTask.promise;
9800
- return retryable(() => this.rpc.sendStats({ ...stats, sessionId: this.sessionId }), this.abortController.signal);
9397
+ return retryable(() => this.rpc.sendStats({
9398
+ ...stats,
9399
+ sessionId: this.sessionId,
9400
+ }), this.logger, 'debug');
9801
9401
  };
9802
9402
  this.startNoiseCancellation = async () => {
9803
- await this.joinResponseTask.promise;
9804
- return retryable(() => this.rpc.startNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
9403
+ return retryable(() => this.rpc.startNoiseCancellation({
9404
+ sessionId: this.sessionId,
9405
+ }), this.logger);
9805
9406
  };
9806
9407
  this.stopNoiseCancellation = async () => {
9807
- await this.joinResponseTask.promise;
9808
- return retryable(() => this.rpc.stopNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
9809
- };
9810
- this.enterMigration = async (opts = {}) => {
9811
- this.isLeaving = true;
9812
- const { timeout = 10000 } = opts;
9813
- this.migrationTask?.reject(new Error('Cancelled previous migration'));
9814
- this.migrationTask = promiseWithResolvers();
9815
- const task = this.migrationTask;
9816
- const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
9817
- unsubscribe();
9818
- clearTimeout(this.migrateAwayTimeout);
9819
- task.resolve();
9820
- });
9821
- this.migrateAwayTimeout = setTimeout(() => {
9822
- unsubscribe();
9823
- // task.reject(new Error('Migration timeout'));
9824
- // FIXME OL: temporary, switch to `task.reject()` once the SFU starts sending
9825
- // the participantMigrationComplete event.
9826
- task.resolve();
9827
- }, timeout);
9828
- return this.migrationTask.promise;
9408
+ return retryable(() => this.rpc.stopNoiseCancellation({
9409
+ sessionId: this.sessionId,
9410
+ }), this.logger);
9829
9411
  };
9830
9412
  this.join = async (data) => {
9831
- // wait for the signal web socket to be ready before sending "joinRequest"
9832
- await this.signalReady;
9833
- if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
9834
- // we need to lock the RPC requests until we receive a JoinResponse.
9835
- // that's why we have this primitive lock mechanism.
9836
- // the client starts with already initialized joinResponseTask,
9837
- // and this code creates a new one for the next join request.
9838
- this.joinResponseTask = promiseWithResolvers();
9839
- }
9840
- // capture a reference to the current joinResponseTask as it might
9841
- // be replaced with a new one in case a second join request is made
9842
- const current = this.joinResponseTask;
9843
- let timeoutId;
9844
- const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
9845
- this.logger('debug', 'Received joinResponse', joinResponse);
9846
- clearTimeout(timeoutId);
9847
- unsubscribe();
9848
- this.keepAlive();
9849
- current.resolve(joinResponse);
9413
+ const joinRequest = JoinRequest.create({
9414
+ ...data,
9415
+ sessionId: this.sessionId,
9416
+ token: this.token,
9850
9417
  });
9851
- timeoutId = setTimeout(() => {
9852
- unsubscribe();
9853
- current.reject(new Error('Waiting for "joinResponse" has timed out'));
9854
- }, this.joinResponseTimeout);
9855
- await this.send(SfuRequest.create({
9856
- requestPayload: {
9857
- oneofKind: 'joinRequest',
9858
- joinRequest: JoinRequest.create({
9859
- ...data,
9860
- sessionId: this.sessionId,
9861
- token: this.credentials.token,
9862
- }),
9863
- },
9864
- }));
9865
- return current.promise;
9866
- };
9867
- this.ping = async () => {
9868
- return this.send(SfuRequest.create({
9869
- requestPayload: {
9870
- oneofKind: 'healthCheckRequest',
9871
- healthCheckRequest: {},
9872
- },
9873
- }));
9874
- };
9875
- this.notifyLeave = async (reason) => {
9876
9418
  return this.send(SfuRequest.create({
9877
9419
  requestPayload: {
9878
- oneofKind: 'leaveCallRequest',
9879
- leaveCallRequest: {
9880
- sessionId: this.sessionId,
9881
- reason,
9882
- },
9420
+ oneofKind: 'joinRequest',
9421
+ joinRequest,
9883
9422
  },
9884
9423
  }));
9885
9424
  };
9886
9425
  this.send = async (message) => {
9887
- await this.signalReady; // wait for the signal ws to be open
9888
- const msgJson = SfuRequest.toJson(message);
9889
- if (this.signalWs.readyState !== WebSocket.OPEN) {
9890
- this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
9891
- return;
9892
- }
9893
- this.logger('debug', `Sending message to: ${this.edgeName}`, msgJson);
9894
- this.signalWs.send(SfuRequest.toBinary(message));
9426
+ return this.signalReady.then((signal) => {
9427
+ if (signal.readyState !== signal.OPEN)
9428
+ return;
9429
+ this.logger('debug', `Sending message to: ${this.edgeName}`, SfuRequest.toJson(message));
9430
+ signal.send(SfuRequest.toBinary(message));
9431
+ });
9895
9432
  };
9896
9433
  this.keepAlive = () => {
9897
9434
  clearInterval(this.keepAliveInterval);
9898
9435
  this.keepAliveInterval = setInterval(() => {
9899
- this.ping().catch((e) => {
9436
+ this.logger('trace', 'Sending healthCheckRequest to SFU');
9437
+ const message = SfuRequest.create({
9438
+ requestPayload: {
9439
+ oneofKind: 'healthCheckRequest',
9440
+ healthCheckRequest: {},
9441
+ },
9442
+ });
9443
+ this.send(message).catch((e) => {
9900
9444
  this.logger('error', 'Error sending healthCheckRequest to SFU', e);
9901
9445
  });
9902
9446
  }, this.pingIntervalInMs);
@@ -9912,37 +9456,53 @@ class StreamSfuClient {
9912
9456
  }
9913
9457
  }, this.unhealthyTimeoutInMs);
9914
9458
  };
9915
- this.dispatcher = dispatcher;
9916
9459
  this.sessionId = sessionId || generateUUIDv4();
9917
- this.onSignalClose = onSignalClose;
9918
- this.credentials = credentials;
9919
- const { server, token } = credentials;
9920
- this.edgeName = server.edge_name;
9921
- this.joinResponseTimeout = joinResponseTimeout;
9922
- this.logTag = logTag;
9923
- this.logger = getLogger(['sfu-client', logTag]);
9460
+ this.sfuServer = sfuServer;
9461
+ this.edgeName = sfuServer.edge_name;
9462
+ this.token = token;
9463
+ this.logger = getLogger(['sfu-client']);
9464
+ const logInterceptor = {
9465
+ interceptUnary: (next, method, input, options) => {
9466
+ this.logger('trace', `Calling SFU RPC method ${method.name}`, {
9467
+ input,
9468
+ options,
9469
+ });
9470
+ return next(method, input, options);
9471
+ },
9472
+ };
9924
9473
  this.rpc = createSignalClient({
9925
- baseUrl: server.url,
9474
+ baseUrl: sfuServer.url,
9926
9475
  interceptors: [
9927
9476
  withHeaders({
9928
9477
  Authorization: `Bearer ${token}`,
9929
9478
  }),
9930
- getLogLevel() === 'trace' && withRequestLogger(this.logger, 'trace'),
9931
- ].filter((v) => !!v),
9479
+ logInterceptor,
9480
+ ],
9932
9481
  });
9933
9482
  // Special handling for the ICETrickle kind of events.
9934
- // The SFU might trigger these events before the initial RTC
9935
- // connection is established or "JoinResponse" received.
9936
- // In that case, those events (ICE candidates) need to be buffered
9937
- // and later added to the appropriate PeerConnection
9483
+ // These events might be triggered by the SFU before the initial RTC
9484
+ // connection is established. In that case, those events (ICE candidates)
9485
+ // need to be buffered and later added to the appropriate PeerConnection
9938
9486
  // once the remoteDescription is known and set.
9939
9487
  this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (iceTrickle) => {
9940
9488
  this.iceTrickleBuffer.push(iceTrickle);
9941
9489
  });
9942
- this.createWebSocket();
9943
- }
9944
- get isHealthy() {
9945
- return this.signalWs.readyState === WebSocket.OPEN;
9490
+ this.signalWs = createWebSocketSignalChannel({
9491
+ endpoint: sfuServer.ws_endpoint,
9492
+ onMessage: (message) => {
9493
+ this.lastMessageTimestamp = new Date();
9494
+ this.scheduleConnectionCheck();
9495
+ dispatcher.dispatch(message);
9496
+ },
9497
+ });
9498
+ this.signalReady = new Promise((resolve) => {
9499
+ const onOpen = () => {
9500
+ this.signalWs.removeEventListener('open', onOpen);
9501
+ this.keepAlive();
9502
+ resolve(this.signalWs);
9503
+ };
9504
+ this.signalWs.addEventListener('open', onOpen);
9505
+ });
9946
9506
  }
9947
9507
  }
9948
9508
  /**
@@ -9955,15 +9515,45 @@ StreamSfuClient.NORMAL_CLOSURE = 1000;
9955
9515
  * a certain amount of time (`connectionCheckTimeout`).
9956
9516
  */
9957
9517
  StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
9958
-
9959
- const toRtcConfiguration = (config) => {
9960
- return {
9961
- iceServers: config.map((ice) => ({
9962
- urls: ice.urls,
9963
- username: ice.username,
9964
- credential: ice.password,
9965
- })),
9966
- };
9518
+ /**
9519
+ * The error code used when the SFU connection is broken.
9520
+ * Usually, this means that the WS connection has been closed unexpectedly.
9521
+ * This error code is used to announce a fast-reconnect.
9522
+ */
9523
+ StreamSfuClient.ERROR_CONNECTION_BROKEN = 4002; // used in fast-reconnects
9524
+ const MAX_RETRIES = 5;
9525
+ /**
9526
+ * Creates a closure which wraps the given RPC call and retries invoking
9527
+ * the RPC until it succeeds or the maximum number of retries is reached.
9528
+ *
9529
+ * Between each retry, there would be a random delay in order to avoid
9530
+ * request bursts towards the SFU.
9531
+ *
9532
+ * @param rpc the closure around the RPC call to execute.
9533
+ * @param logger a logger instance to use.
9534
+ * @param <I> the type of the request object.
9535
+ * @param <O> the type of the response object.
9536
+ */
9537
+ const retryable = async (rpc, logger, level = 'error') => {
9538
+ let retryAttempt = 0;
9539
+ let rpcCallResult;
9540
+ do {
9541
+ // don't delay the first invocation
9542
+ if (retryAttempt > 0) {
9543
+ await sleep(retryInterval(retryAttempt));
9544
+ }
9545
+ rpcCallResult = await rpc();
9546
+ // if the RPC call failed, log the error and retry
9547
+ if (rpcCallResult.response.error) {
9548
+ logger(level, `SFU RPC Error (${rpcCallResult.method.name}):`, rpcCallResult.response.error);
9549
+ }
9550
+ retryAttempt++;
9551
+ } while (rpcCallResult.response.error?.shouldRetry &&
9552
+ retryAttempt < MAX_RETRIES);
9553
+ if (rpcCallResult.response.error) {
9554
+ throw rpcCallResult.response.error;
9555
+ }
9556
+ return rpcCallResult;
9967
9557
  };
9968
9558
 
9969
9559
  /**
@@ -10118,10 +9708,9 @@ const watchSfuErrorReports = (dispatcher) => {
10118
9708
  return dispatcher.on('error', (e) => {
10119
9709
  if (!e.error)
10120
9710
  return;
10121
- const { error, reconnectStrategy } = e;
9711
+ const { error } = e;
10122
9712
  logger$1('error', 'SFU reported error', {
10123
9713
  code: ErrorCode[error.code],
10124
- reconnectStrategy: WebsocketReconnectStrategy[reconnectStrategy],
10125
9714
  message: error.message,
10126
9715
  shouldRetry: error.shouldRetry,
10127
9716
  });
@@ -10137,17 +9726,6 @@ const watchPinsUpdated = (state) => {
10137
9726
  state.setServerSidePins(pins);
10138
9727
  };
10139
9728
  };
10140
- /**
10141
- * Watches for `callEnded` events.
10142
- */
10143
- const watchSfuCallEnded = (call) => {
10144
- return call.on('callEnded', (e) => {
10145
- const reason = CallEndedReason[e.reason];
10146
- call.leave({ reason }).catch((err) => {
10147
- logger$1('error', 'Failed to leave call after call ended by the SFU', err);
10148
- });
10149
- });
10150
- };
10151
9729
 
10152
9730
  /**
10153
9731
  * An event handler that handles soft mutes.
@@ -10169,13 +9747,12 @@ const handleRemoteSoftMute = (call) => {
10169
9747
  else if (type === TrackType.AUDIO) {
10170
9748
  await call.microphone.disable();
10171
9749
  }
10172
- else if (type === TrackType.SCREEN_SHARE ||
10173
- type === TrackType.SCREEN_SHARE_AUDIO) {
10174
- await call.screenShare.disable();
10175
- }
10176
9750
  else {
10177
9751
  logger('warn', 'Unsupported track type to soft mute', TrackType[type]);
10178
9752
  }
9753
+ if (call.publisher?.isPublishing(type)) {
9754
+ await call.stopPublish(type);
9755
+ }
10179
9756
  }
10180
9757
  catch (error) {
10181
9758
  logger('error', 'Failed to stop publishing', error);
@@ -10196,12 +9773,11 @@ const watchParticipantJoined = (state) => {
10196
9773
  // potential duplicate events from the SFU.
10197
9774
  //
10198
9775
  // Although the SFU should not send duplicate events, we have seen
10199
- // some race conditions in the past during the `join-flow`.
10200
- // The SFU would send participant info as part of the `join`
9776
+ // some race conditions in the past during the `join-flow` where
9777
+ // the SFU would send participant info as part of the `join`
10201
9778
  // response and then follow up with a `participantJoined` event for
10202
9779
  // already announced participants.
10203
- const orphanedTracks = reconcileOrphanedTracks(state, participant);
10204
- state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
9780
+ state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, {
10205
9781
  viewportVisibilityState: {
10206
9782
  videoTrack: VisibilityState.UNKNOWN,
10207
9783
  screenShareTrack: VisibilityState.UNKNOWN,
@@ -10237,14 +9813,12 @@ const watchParticipantUpdated = (state) => {
10237
9813
  */
10238
9814
  const watchTrackPublished = (state) => {
10239
9815
  return function onTrackPublished(e) {
10240
- const { type, sessionId } = e;
9816
+ const { type, sessionId, participant } = e;
10241
9817
  // An optimization for large calls.
10242
9818
  // After a certain threshold, the SFU would stop emitting `participantJoined`
10243
9819
  // events, and instead, it would only provide the participant's information
10244
9820
  // once they start publishing a track.
10245
- if (e.participant) {
10246
- const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
10247
- const participant = Object.assign(e.participant, orphanedTracks);
9821
+ if (participant) {
10248
9822
  state.updateOrAddParticipant(sessionId, participant);
10249
9823
  }
10250
9824
  else {
@@ -10260,11 +9834,9 @@ const watchTrackPublished = (state) => {
10260
9834
  */
10261
9835
  const watchTrackUnpublished = (state) => {
10262
9836
  return function onTrackUnpublished(e) {
10263
- const { type, sessionId } = e;
9837
+ const { type, sessionId, participant } = e;
10264
9838
  // An optimization for large calls. See `watchTrackPublished`.
10265
- if (e.participant) {
10266
- const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
10267
- const participant = Object.assign(e.participant, orphanedTracks);
9839
+ if (participant) {
10268
9840
  state.updateOrAddParticipant(sessionId, participant);
10269
9841
  }
10270
9842
  else {
@@ -10275,25 +9847,6 @@ const watchTrackUnpublished = (state) => {
10275
9847
  };
10276
9848
  };
10277
9849
  const unique = (v, i, arr) => arr.indexOf(v) === i;
10278
- /**
10279
- * Reconciles orphaned tracks (if any) for the given participant.
10280
- *
10281
- * @param state the call state.
10282
- * @param participant the participant.
10283
- */
10284
- const reconcileOrphanedTracks = (state, participant) => {
10285
- const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
10286
- if (!orphanTracks.length)
10287
- return;
10288
- const reconciledTracks = {};
10289
- for (const orphan of orphanTracks) {
10290
- const key = trackTypeToParticipantStreamKey(orphan.trackType);
10291
- if (!key)
10292
- continue;
10293
- reconciledTracks[key] = orphan.track;
10294
- }
10295
- return reconciledTracks;
10296
- };
10297
9850
 
10298
9851
  /**
10299
9852
  * Watches for `dominantSpeakerChanged` events.
@@ -10342,13 +9895,12 @@ const watchAudioLevelChanged = (dispatcher, state) => {
10342
9895
  * Registers the default event handlers for a call during its lifecycle.
10343
9896
  *
10344
9897
  * @param call the call to register event handlers for.
9898
+ * @param state the call state.
10345
9899
  * @param dispatcher the dispatcher.
10346
9900
  */
10347
- const registerEventHandlers = (call, dispatcher) => {
10348
- const state = call.state;
9901
+ const registerEventHandlers = (call, state, dispatcher) => {
10349
9902
  const eventHandlers = [
10350
9903
  call.on('call.ended', watchCallEnded(call)),
10351
- watchSfuCallEnded(call),
10352
9904
  watchLiveEnded(dispatcher, call),
10353
9905
  watchSfuErrorReports(dispatcher),
10354
9906
  watchChangePublishQuality(dispatcher, call),
@@ -10392,6 +9944,48 @@ const registerRingingCallEventHandlers = (call) => {
10392
9944
  };
10393
9945
  };
10394
9946
 
9947
+ /**
9948
+ * Collects all necessary information to join a call, talks to the coordinator
9949
+ * and returns the necessary information to join the call.
9950
+ *
9951
+ * @param httpClient the http client to use.
9952
+ * @param type the type of the call.
9953
+ * @param id the id of the call.
9954
+ * @param data the data for the call.
9955
+ */
9956
+ const join = async (httpClient, type, id, data) => {
9957
+ const { call, credentials, members, own_capabilities, stats_options } = await doJoin(httpClient, type, id, data);
9958
+ return {
9959
+ connectionConfig: toRtcConfiguration(credentials.ice_servers),
9960
+ sfuServer: credentials.server,
9961
+ token: credentials.token,
9962
+ metadata: call,
9963
+ members,
9964
+ ownCapabilities: own_capabilities,
9965
+ statsOptions: stats_options,
9966
+ };
9967
+ };
9968
+ const doJoin = async (httpClient, type, id, data) => {
9969
+ const location = await httpClient.getLocationHint();
9970
+ const request = {
9971
+ ...data,
9972
+ location,
9973
+ };
9974
+ return httpClient.post(`/call/${type}/${id}/join`, request);
9975
+ };
9976
+ const toRtcConfiguration = (config) => {
9977
+ if (!config || config.length === 0)
9978
+ return undefined;
9979
+ const rtcConfig = {
9980
+ iceServers: config.map((ice) => ({
9981
+ urls: ice.urls,
9982
+ username: ice.username,
9983
+ credential: ice.password,
9984
+ })),
9985
+ };
9986
+ return rtcConfig;
9987
+ };
9988
+
10395
9989
  /**
10396
9990
  * Flatten the stats report into an array of stats objects.
10397
9991
  *
@@ -10671,7 +10265,6 @@ class SfuStatsReporter {
10671
10265
  this.start = () => {
10672
10266
  if (this.options.reporting_interval_ms <= 0)
10673
10267
  return;
10674
- clearInterval(this.intervalId);
10675
10268
  this.intervalId = setInterval(() => {
10676
10269
  this.run().catch((err) => {
10677
10270
  this.logger('warn', 'Failed to report stats', err);
@@ -11244,13 +10837,9 @@ class BrowserPermission {
11244
10837
  const signal = this.disposeController.signal;
11245
10838
  this.ready = (async () => {
11246
10839
  const assumeGranted = (error) => {
11247
- this.logger('warn', "Can't query permissions, assuming granted", {
11248
- permission,
11249
- error,
11250
- });
11251
10840
  this.setState('granted');
11252
10841
  };
11253
- if (!canQueryPermissions() && !isReactNative()) {
10842
+ if (!canQueryPermissions()) {
11254
10843
  return assumeGranted();
11255
10844
  }
11256
10845
  try {
@@ -11265,7 +10854,7 @@ class BrowserPermission {
11265
10854
  }
11266
10855
  }
11267
10856
  catch (err) {
11268
- assumeGranted(err);
10857
+ assumeGranted();
11269
10858
  }
11270
10859
  })();
11271
10860
  }
@@ -11359,7 +10948,7 @@ function lazy(factory) {
11359
10948
  * Returns an Observable that emits the list of available devices
11360
10949
  * that meet the given constraints.
11361
10950
  *
11362
- * @param permission a BrowserPermission instance.
10951
+ * @param constraints the constraints to use when requesting the devices.
11363
10952
  * @param kind the kind of devices to enumerate.
11364
10953
  */
11365
10954
  const getDevices = (permission, kind) => {
@@ -11612,12 +11201,6 @@ class InputMediaDeviceManager {
11612
11201
  listDevices() {
11613
11202
  return this.getDevices();
11614
11203
  }
11615
- /**
11616
- * Returns `true` when this device is in enabled state.
11617
- */
11618
- get enabled() {
11619
- return this.state.status === 'enabled';
11620
- }
11621
11204
  /**
11622
11205
  * Starts stream.
11623
11206
  */
@@ -11740,7 +11323,7 @@ class InputMediaDeviceManager {
11740
11323
  await this.applySettingsToStream();
11741
11324
  }
11742
11325
  async applySettingsToStream() {
11743
- if (this.enabled) {
11326
+ if (this.state.status === 'enabled') {
11744
11327
  await this.muteStream();
11745
11328
  await this.unmuteStream();
11746
11329
  }
@@ -11877,22 +11460,19 @@ class InputMediaDeviceManager {
11877
11460
  return output;
11878
11461
  })
11879
11462
  .then(chainWith(parent), (error) => {
11880
- this.logger('warn', 'Filter failed to start and will be ignored', error);
11463
+ this.logger('warn', 'Fitler failed to start and will be ignored', error);
11881
11464
  return parent;
11882
11465
  }), rootStream);
11883
11466
  }
11884
11467
  if (this.call.state.callingState === CallingState.JOINED) {
11885
11468
  await this.publishStream(stream);
11886
11469
  }
11887
- else {
11888
- this.logger('debug', 'Stream is not published as the call is not joined');
11889
- }
11890
11470
  if (this.state.mediaStream !== stream) {
11891
11471
  this.state.setMediaStream(stream, await rootStream);
11892
11472
  this.getTracks().forEach((track) => {
11893
11473
  track.addEventListener('ended', async () => {
11894
11474
  await this.statusChangeSettled();
11895
- if (this.enabled) {
11475
+ if (this.state.status === 'enabled') {
11896
11476
  this.isTrackStoppedDueToTrackEnd = true;
11897
11477
  setTimeout(() => {
11898
11478
  this.isTrackStoppedDueToTrackEnd = false;
@@ -12185,7 +11765,7 @@ class CameraManager extends InputMediaDeviceManager {
12185
11765
  this.logger('warn', 'could not apply target resolution', error);
12186
11766
  }
12187
11767
  }
12188
- if (this.enabled) {
11768
+ if (this.state.status === 'enabled') {
12189
11769
  const { width, height } = this.state
12190
11770
  .mediaStream.getVideoTracks()[0]
12191
11771
  ?.getSettings();
@@ -12489,7 +12069,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
12489
12069
  });
12490
12070
  const registrationResult = this.registerFilter(noiseCancellation.toFilter());
12491
12071
  this.noiseCancellationRegistration = registrationResult.registered;
12492
- this.unregisterNoiseCancellation = registrationResult.unregister;
12072
+ this.uregisterNoiseCancellation = registrationResult.unregister;
12493
12073
  await this.noiseCancellationRegistration;
12494
12074
  // handles an edge case where a noise cancellation is enabled after
12495
12075
  // the participant as joined the call -> we immediately enable NC
@@ -12515,7 +12095,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
12515
12095
  if (isReactNative()) {
12516
12096
  throw new Error('Noise cancellation is not supported in React Native');
12517
12097
  }
12518
- await (this.unregisterNoiseCancellation?.() ?? Promise.resolve())
12098
+ await (this.uregisterNoiseCancellation?.() ?? Promise.resolve())
12519
12099
  .then(() => this.noiseCancellation?.disable())
12520
12100
  .then(() => this.noiseCancellationChangeUnsubscribe?.())
12521
12101
  .catch((err) => {
@@ -12897,17 +12477,9 @@ class Call {
12897
12477
  */
12898
12478
  this.dispatcher = new Dispatcher();
12899
12479
  this.trackSubscriptionsSubject = new BehaviorSubject({ type: DebounceType.MEDIUM, data: [] });
12900
- this.sfuClientTag = 0;
12901
- this.reconnectConcurrencyTag = Symbol('reconnectConcurrencyTag');
12902
12480
  this.reconnectAttempts = 0;
12903
- this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
12904
- this.fastReconnectDeadlineSeconds = 0;
12905
- this.lastOfflineTimestamp = 0;
12906
- // maintain the order of publishing tracks to restore them after a reconnection
12907
- // it shouldn't contain duplicates
12908
- this.trackPublishOrder = [];
12909
- this.hasJoinedOnce = false;
12910
- this.deviceSettingsAppliedOnce = false;
12481
+ this.maxReconnectAttempts = 10;
12482
+ this.isLeaving = false;
12911
12483
  this.initialized = false;
12912
12484
  this.joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
12913
12485
  /**
@@ -12917,42 +12489,6 @@ class Call {
12917
12489
  */
12918
12490
  this.leaveCallHooks = new Set();
12919
12491
  this.streamClientEventHandlers = new Map();
12920
- this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
12921
- // update the permission context.
12922
- this.permissionsContext.setPermissions(ownCapabilities);
12923
- if (!this.publisher)
12924
- return;
12925
- // check if the user still has publishing permissions and stop publishing if not.
12926
- const permissionToTrackType = {
12927
- [OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
12928
- [OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
12929
- [OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
12930
- };
12931
- for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
12932
- const hasPermission = this.permissionsContext.hasPermission(permission);
12933
- if (hasPermission)
12934
- continue;
12935
- try {
12936
- switch (trackType) {
12937
- case TrackType.AUDIO:
12938
- if (this.microphone.enabled)
12939
- await this.microphone.disable();
12940
- break;
12941
- case TrackType.VIDEO:
12942
- if (this.camera.enabled)
12943
- await this.camera.disable();
12944
- break;
12945
- case TrackType.SCREEN_SHARE:
12946
- if (this.screenShare.enabled)
12947
- await this.screenShare.disable();
12948
- break;
12949
- }
12950
- }
12951
- catch (err) {
12952
- this.logger('error', `Can't disable mic/camera/screenshare after revoked permissions`, err);
12953
- }
12954
- }
12955
- };
12956
12492
  /**
12957
12493
  * You can subscribe to WebSocket events provided by the API. To remove a subscription, call the `off` method.
12958
12494
  * Please note that subscribing to WebSocket events is an advanced use-case.
@@ -13003,8 +12539,9 @@ class Call {
13003
12539
  throw new Error('Cannot leave call that has already been left.');
13004
12540
  }
13005
12541
  if (callingState === CallingState.JOINING) {
13006
- await this.waitUntilCallJoined();
12542
+ await this.assertCallJoined();
13007
12543
  }
12544
+ this.isLeaving = true;
13008
12545
  if (this.ringing) {
13009
12546
  // I'm the one who started the call, so I should cancel it.
13010
12547
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
@@ -13026,15 +12563,14 @@ class Call {
13026
12563
  this.sfuStatsReporter = undefined;
13027
12564
  this.subscriber?.close();
13028
12565
  this.subscriber = undefined;
13029
- this.publisher?.close({ stopTracks: true });
12566
+ this.publisher?.close();
13030
12567
  this.publisher = undefined;
13031
- await this.sfuClient?.leaveAndClose(reason);
12568
+ this.sfuClient?.close(StreamSfuClient.NORMAL_CLOSURE, reason);
13032
12569
  this.sfuClient = undefined;
13033
12570
  this.state.setCallingState(CallingState.LEFT);
13034
12571
  // Call all leave call hooks, e.g. to clean up global event handlers
13035
12572
  this.leaveCallHooks.forEach((hook) => hook());
13036
12573
  this.initialized = false;
13037
- this.hasJoinedOnce = false;
13038
12574
  this.clientStore.unregisterCall(this);
13039
12575
  this.camera.dispose();
13040
12576
  this.microphone.dispose();
@@ -13074,7 +12610,7 @@ class Call {
13074
12610
  this.watching = true;
13075
12611
  this.clientStore.registerCall(this);
13076
12612
  }
13077
- await this.applyDeviceConfig(false);
12613
+ await this.applyDeviceConfig();
13078
12614
  return response;
13079
12615
  };
13080
12616
  /**
@@ -13096,7 +12632,7 @@ class Call {
13096
12632
  this.watching = true;
13097
12633
  this.clientStore.registerCall(this);
13098
12634
  }
13099
- await this.applyDeviceConfig(false);
12635
+ await this.applyDeviceConfig();
13100
12636
  return response;
13101
12637
  };
13102
12638
  /**
@@ -13152,171 +12688,253 @@ class Call {
13152
12688
  await this.setup();
13153
12689
  const callingState = this.state.callingState;
13154
12690
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
13155
- throw new Error(`Illegal State: call.join() shall be called only once`);
12691
+ this.logger('warn', 'Join method called twice, you should only call this once');
12692
+ throw new Error(`Illegal State: Already joined.`);
13156
12693
  }
13157
- this.joinCallData = data;
13158
- this.logger('debug', 'Starting join flow');
12694
+ const isMigrating = callingState === CallingState.MIGRATING;
12695
+ const isReconnecting = callingState === CallingState.RECONNECTING;
13159
12696
  this.state.setCallingState(CallingState.JOINING);
13160
- const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
13161
- const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
13162
- const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
13163
- let statsOptions = this.sfuStatsReporter?.options;
13164
- if (!this.credentials ||
13165
- !statsOptions ||
13166
- performingRejoin ||
13167
- performingMigration) {
13168
- try {
13169
- const joinResponse = await this.doJoinRequest(data);
13170
- this.credentials = joinResponse.credentials;
13171
- statsOptions = joinResponse.stats_options;
12697
+ this.logger('debug', 'Starting join flow');
12698
+ if (data?.ring && !this.ringing) {
12699
+ this.ringingSubject.next(true);
12700
+ }
12701
+ if (this.ringing && !this.isCreatedByMe) {
12702
+ // signals other users that I have accepted the incoming call.
12703
+ await this.accept();
12704
+ }
12705
+ let sfuServer;
12706
+ let sfuToken;
12707
+ let connectionConfig;
12708
+ let statsOptions;
12709
+ try {
12710
+ if (this.sfuClient?.isFastReconnecting) {
12711
+ // use previous SFU configuration and values
12712
+ connectionConfig = this.publisher?.connectionConfiguration;
12713
+ sfuServer = this.sfuClient.sfuServer;
12714
+ sfuToken = this.sfuClient.token;
12715
+ statsOptions = this.sfuStatsReporter?.options;
13172
12716
  }
13173
- catch (error) {
13174
- // restore the previous call state if the join-flow fails
13175
- this.state.setCallingState(callingState);
13176
- throw error;
12717
+ else {
12718
+ // full join flow - let the Coordinator pick a new SFU for us
12719
+ const call = await join(this.streamClient, this.type, this.id, data);
12720
+ this.state.updateFromCallResponse(call.metadata);
12721
+ this.state.setMembers(call.members);
12722
+ this.state.setOwnCapabilities(call.ownCapabilities);
12723
+ connectionConfig = call.connectionConfig;
12724
+ sfuServer = call.sfuServer;
12725
+ sfuToken = call.token;
12726
+ statsOptions = call.statsOptions;
13177
12727
  }
13178
- }
13179
- const previousSfuClient = this.sfuClient;
13180
- const previousSessionId = previousSfuClient?.sessionId;
13181
- const isWsHealthy = !!previousSfuClient?.isHealthy;
13182
- const sfuClient = performingRejoin || performingMigration || !isWsHealthy
13183
- ? new StreamSfuClient({
13184
- logTag: String(this.sfuClientTag++),
13185
- dispatcher: this.dispatcher,
13186
- credentials: this.credentials,
13187
- // a new session_id is necessary for the REJOIN strategy.
13188
- // we use the previous session_id if available
13189
- sessionId: performingRejoin ? undefined : previousSessionId,
13190
- onSignalClose: () => this.handleSfuSignalClose(sfuClient),
13191
- })
13192
- : previousSfuClient;
13193
- this.sfuClient = sfuClient;
13194
- const clientDetails = getClientDetails();
13195
- // we don't need to send JoinRequest if we are re-using an existing healthy SFU client
13196
- if (previousSfuClient !== sfuClient) {
13197
- // prepare a generic SDP and send it to the SFU.
13198
- // this is a throw-away SDP that the SFU will use to determine
13199
- // the capabilities of the client (codec support, etc.)
13200
- const receivingCapabilitiesSdp = await getGenericSdp('recvonly');
13201
- const reconnectDetails = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED
13202
- ? this.getReconnectDetails(data?.migrating_from, previousSessionId)
13203
- : undefined;
13204
- const { callState, fastReconnectDeadlineSeconds } = await sfuClient.join({
13205
- subscriberSdp: receivingCapabilitiesSdp,
13206
- clientDetails,
13207
- fastReconnect: performingFastReconnect,
13208
- reconnectDetails,
13209
- });
13210
- this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
13211
- if (callState) {
13212
- this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
12728
+ if (this.streamClient._hasConnectionID()) {
12729
+ this.watching = true;
12730
+ this.clientStore.registerCall(this);
13213
12731
  }
13214
12732
  }
13215
- this.state.setCallingState(CallingState.JOINED);
13216
- this.hasJoinedOnce = true;
13217
- // when performing fast reconnect, or when we reuse the same SFU client,
13218
- // (ws remained healthy), we just need to restore the ICE connection
13219
- if (performingFastReconnect) {
13220
- // the SFU automatically issues an ICE restart on the subscriber
13221
- // we don't have to do it ourselves
13222
- await this.restoreICE(sfuClient, { includeSubscriber: false });
12733
+ catch (error) {
12734
+ // restore the previous call state if the join-flow fails
12735
+ this.state.setCallingState(callingState);
12736
+ throw error;
13223
12737
  }
13224
- else {
13225
- const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
13226
- this.initPublisherAndSubscriber({
13227
- sfuClient,
13228
- connectionConfig,
13229
- clientDetails,
13230
- statsOptions,
13231
- closePreviousInstances: !performingMigration,
12738
+ const previousSfuClient = this.sfuClient;
12739
+ const sfuClient = (this.sfuClient = new StreamSfuClient({
12740
+ dispatcher: this.dispatcher,
12741
+ sfuServer,
12742
+ token: sfuToken,
12743
+ sessionId: previousSfuClient?.sessionId,
12744
+ }));
12745
+ /**
12746
+ * A closure which hides away the re-connection logic.
12747
+ */
12748
+ const reconnect = async (strategy, reason) => {
12749
+ const currentState = this.state.callingState;
12750
+ if (currentState === CallingState.MIGRATING ||
12751
+ currentState === CallingState.RECONNECTING) {
12752
+ // prevent parallel reconnection attempts
12753
+ return;
12754
+ }
12755
+ this.reconnectAttempts++;
12756
+ this.state.setCallingState(strategy === 'migrate'
12757
+ ? CallingState.MIGRATING
12758
+ : CallingState.RECONNECTING);
12759
+ if (strategy === 'migrate') {
12760
+ this.logger('debug', `[Migration]: migrating call ${this.cid} away from ${sfuServer.edge_name}`);
12761
+ sfuClient.isMigratingAway = true;
12762
+ }
12763
+ else {
12764
+ this.logger('debug', `[Rejoin]: ${strategy} rejoin call ${this.cid} (${this.reconnectAttempts})...`);
12765
+ }
12766
+ // take a snapshot of the current "local participant" state
12767
+ // we'll need it for restoring the previous publishing state later
12768
+ const localParticipant = this.state.localParticipant;
12769
+ if (strategy === 'fast') {
12770
+ sfuClient.close(StreamSfuClient.ERROR_CONNECTION_BROKEN, `attempting fast reconnect: ${reason}`);
12771
+ }
12772
+ else if (strategy === 'full') {
12773
+ // in migration or recovery scenarios, we don't want to
12774
+ // wait before attempting to reconnect to an SFU server
12775
+ await sleep(retryInterval(this.reconnectAttempts));
12776
+ // in full-reconnect, we need to dispose all Peer Connections
12777
+ this.subscriber?.close();
12778
+ this.subscriber = undefined;
12779
+ this.publisher?.close({ stopTracks: false });
12780
+ this.publisher = undefined;
12781
+ this.statsReporter?.stop();
12782
+ this.statsReporter = undefined;
12783
+ this.sfuStatsReporter?.stop();
12784
+ this.sfuStatsReporter = undefined;
12785
+ // clean up current connection
12786
+ sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, `attempting full reconnect: ${reason}`);
12787
+ }
12788
+ await this.join({
12789
+ ...data,
12790
+ ...(strategy === 'migrate' && { migrating_from: sfuServer.edge_name }),
13232
12791
  });
13233
- }
13234
- if (performingRejoin) {
13235
- const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
13236
- await previousSfuClient?.leaveAndClose(`Closing previous WS after reconnect with strategy: ${strategy}`);
13237
- }
13238
- else if (!isWsHealthy) {
13239
- previousSfuClient?.close(4002, 'Closing unhealthy WS after reconnect');
13240
- }
13241
- // device settings should be applied only once, we don't have to
13242
- // re-apply them on later reconnections or server-side data fetches
13243
- if (!this.deviceSettingsAppliedOnce) {
13244
- await this.applyDeviceConfig(true);
13245
- this.deviceSettingsAppliedOnce = true;
13246
- }
13247
- this.logger('info', `Joined call ${this.cid}`);
13248
- };
13249
- /**
13250
- * Prepares Reconnect Details object.
13251
- * @internal
13252
- */
13253
- this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
13254
- const strategy = this.reconnectStrategy;
13255
- const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
13256
- const announcedTracks = this.publisher?.getAnnouncedTracks() || [];
13257
- const subscribedTracks = getCurrentValue(this.trackSubscriptionsSubject);
13258
- return {
13259
- strategy,
13260
- announcedTracks,
13261
- subscriptions: subscribedTracks.data || [],
13262
- reconnectAttempt: this.reconnectAttempts,
13263
- fromSfuId: migratingFromSfuId || '',
13264
- previousSessionId: performingRejoin ? previousSessionId || '' : '',
13265
- };
13266
- };
13267
- /**
13268
- * Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
13269
- * Uses the provided SFU client to restore the ICE connection.
13270
- *
13271
- * This method can throw an error if the ICE restart fails.
13272
- * This error should be handled by the reconnect loop,
13273
- * and a new reconnection shall be attempted.
13274
- *
13275
- * @internal
13276
- */
13277
- this.restoreICE = async (nextSfuClient, opts = {}) => {
13278
- const { includeSubscriber = true, includePublisher = true } = opts;
13279
- if (this.subscriber) {
13280
- this.subscriber.setSfuClient(nextSfuClient);
13281
- if (includeSubscriber) {
13282
- await this.subscriber.restartIce();
12792
+ // clean up previous connection
12793
+ if (strategy === 'migrate') {
12794
+ sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'attempting migration');
13283
12795
  }
13284
- }
13285
- if (this.publisher) {
13286
- this.publisher.setSfuClient(nextSfuClient);
13287
- if (includePublisher) {
13288
- await this.publisher.restartIce();
12796
+ this.logger('info', `[Rejoin]: Attempt ${this.reconnectAttempts} successful!`);
12797
+ // we shouldn't be republishing the streams if we're migrating
12798
+ // as the underlying peer connection will take care of it as part
12799
+ // of the ice-restart process
12800
+ if (localParticipant && strategy === 'full') {
12801
+ const { audioStream, videoStream, screenShareStream, screenShareAudioStream, } = localParticipant;
12802
+ let screenShare;
12803
+ if (screenShareStream || screenShareAudioStream) {
12804
+ screenShare = new MediaStream();
12805
+ screenShareStream?.getVideoTracks().forEach((track) => {
12806
+ screenShare?.addTrack(track);
12807
+ });
12808
+ screenShareAudioStream?.getAudioTracks().forEach((track) => {
12809
+ screenShare?.addTrack(track);
12810
+ });
12811
+ }
12812
+ // restore previous publishing state
12813
+ if (audioStream)
12814
+ await this.publishAudioStream(audioStream);
12815
+ if (videoStream) {
12816
+ await this.publishVideoStream(videoStream, {
12817
+ preferredCodec: this.camera.preferredCodec,
12818
+ });
12819
+ }
12820
+ if (screenShare)
12821
+ await this.publishScreenShareStream(screenShare);
12822
+ this.logger('info', `[Rejoin]: State restored. Attempt: ${this.reconnectAttempts}`);
13289
12823
  }
13290
- }
13291
- };
13292
- /**
13293
- * Initializes the Publisher and Subscriber Peer Connections.
13294
- * @internal
13295
- */
13296
- this.initPublisherAndSubscriber = (opts) => {
13297
- const { sfuClient, connectionConfig, clientDetails, statsOptions, closePreviousInstances, } = opts;
13298
- if (closePreviousInstances && this.subscriber) {
13299
- this.subscriber.close();
13300
- }
13301
- this.subscriber = new Subscriber({
13302
- sfuClient,
13303
- dispatcher: this.dispatcher,
13304
- state: this.state,
13305
- connectionConfig,
13306
- logTag: String(this.reconnectAttempts),
13307
- onUnrecoverableError: () => {
13308
- this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
13309
- this.logger('warn', '[Reconnect] Error reconnecting after a subscriber error', err);
12824
+ };
12825
+ // reconnect if the connection was closed unexpectedly. example:
12826
+ // - SFU crash or restart
12827
+ // - network change
12828
+ sfuClient.signalReady.then(() => {
12829
+ // register a handler for the "goAway" event
12830
+ const unregisterGoAway = this.dispatcher.on('goAway', (event) => {
12831
+ const { reason } = event;
12832
+ this.logger('info', `[Migration]: Going away from SFU... Reason: ${GoAwayReason[reason]}`);
12833
+ reconnect('migrate', GoAwayReason[reason]).catch((err) => {
12834
+ this.logger('warn', `[Migration]: Failed to migrate to another SFU.`, err);
13310
12835
  });
13311
- },
12836
+ });
12837
+ sfuClient.signalWs.addEventListener('close', (e) => {
12838
+ // unregister the "goAway" handler, as we won't need it anymore for this connection.
12839
+ // the upcoming re-join will register a new handler anyway
12840
+ unregisterGoAway();
12841
+ // when the user has initiated "call.leave()" operation, we shouldn't
12842
+ // care for the WS close code and we shouldn't ever attempt to reconnect
12843
+ if (this.isLeaving)
12844
+ return;
12845
+ // do nothing if the connection was closed on purpose
12846
+ if (e.code === StreamSfuClient.NORMAL_CLOSURE)
12847
+ return;
12848
+ // do nothing if the connection was closed because of a policy violation
12849
+ // e.g., the user has been blocked by an admin or moderator
12850
+ if (e.code === KnownCodes.WS_POLICY_VIOLATION)
12851
+ return;
12852
+ // When the SFU is being shut down, it sends a goAway message.
12853
+ // While we migrate to another SFU, we might have the WS connection
12854
+ // to the old SFU closed abruptly. In this case, we don't want
12855
+ // to reconnect to the old SFU, but rather to the new one.
12856
+ const isMigratingAway = e.code === KnownCodes.WS_CLOSED_ABRUPTLY && sfuClient.isMigratingAway;
12857
+ const isFastReconnecting = e.code === KnownCodes.WS_CLOSED_ABRUPTLY &&
12858
+ sfuClient.isFastReconnecting;
12859
+ if (isMigratingAway || isFastReconnecting)
12860
+ return;
12861
+ // do nothing if the connection was closed because of a fast reconnect
12862
+ if (e.code === StreamSfuClient.ERROR_CONNECTION_BROKEN)
12863
+ return;
12864
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
12865
+ sfuClient.isFastReconnecting = this.reconnectAttempts === 0;
12866
+ const strategy = sfuClient.isFastReconnecting ? 'fast' : 'full';
12867
+ reconnect(strategy, `SFU closed the WS with code: ${e.code}`).catch((err) => {
12868
+ this.logger('error', `[Rejoin]: ${strategy} rejoin failed for ${this.reconnectAttempts} times. Giving up.`, err);
12869
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
12870
+ });
12871
+ }
12872
+ else {
12873
+ this.logger('error', '[Rejoin]: Reconnect attempts exceeded. Giving up...');
12874
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
12875
+ }
12876
+ });
12877
+ });
12878
+ // handlers for connection online/offline events
12879
+ const unsubscribeOnlineEvent = this.streamClient.on('connection.changed', async (e) => {
12880
+ if (e.type !== 'connection.changed')
12881
+ return;
12882
+ if (!e.online)
12883
+ return;
12884
+ unsubscribeOnlineEvent();
12885
+ const currentCallingState = this.state.callingState;
12886
+ const shouldReconnect = currentCallingState === CallingState.OFFLINE ||
12887
+ currentCallingState === CallingState.RECONNECTING_FAILED;
12888
+ if (!shouldReconnect)
12889
+ return;
12890
+ this.logger('info', '[Rejoin]: Going online...');
12891
+ let isFirstReconnectAttempt = true;
12892
+ do {
12893
+ try {
12894
+ sfuClient.isFastReconnecting = isFirstReconnectAttempt;
12895
+ await reconnect(isFirstReconnectAttempt ? 'fast' : 'full', 'Network: online');
12896
+ return; // break the loop if rejoin is successful
12897
+ }
12898
+ catch (err) {
12899
+ this.logger('error', `[Rejoin][Network]: Rejoin failed for attempt ${this.reconnectAttempts}`, err);
12900
+ }
12901
+ // wait for a bit before trying to reconnect again
12902
+ await sleep(retryInterval(this.reconnectAttempts));
12903
+ isFirstReconnectAttempt = false;
12904
+ } while (this.reconnectAttempts < this.maxReconnectAttempts);
12905
+ // if we're here, it means that we've exhausted all the reconnect attempts
12906
+ this.logger('error', `[Rejoin][Network]: Rejoin failed. Giving up.`);
12907
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
12908
+ });
12909
+ const unsubscribeOfflineEvent = this.streamClient.on('connection.changed', (e) => {
12910
+ if (e.type !== 'connection.changed')
12911
+ return;
12912
+ if (e.online)
12913
+ return;
12914
+ unsubscribeOfflineEvent();
12915
+ this.state.setCallingState(CallingState.OFFLINE);
13312
12916
  });
12917
+ this.leaveCallHooks.add(() => {
12918
+ unsubscribeOnlineEvent();
12919
+ unsubscribeOfflineEvent();
12920
+ });
12921
+ if (!this.subscriber) {
12922
+ this.subscriber = new Subscriber({
12923
+ sfuClient,
12924
+ dispatcher: this.dispatcher,
12925
+ state: this.state,
12926
+ connectionConfig,
12927
+ onUnrecoverableError: () => {
12928
+ reconnect('full', 'unrecoverable subscriber error').catch((err) => {
12929
+ this.logger('debug', '[Rejoin]: Rejoin failed', err);
12930
+ });
12931
+ },
12932
+ });
12933
+ }
13313
12934
  // anonymous users can't publish anything hence, there is no need
13314
12935
  // to create Publisher Peer Connection for them
13315
12936
  const isAnonymous = this.streamClient.user?.type === 'anonymous';
13316
- if (!isAnonymous) {
13317
- if (closePreviousInstances && this.publisher) {
13318
- this.publisher.close({ stopTracks: false });
13319
- }
12937
+ if (!this.publisher && !isAnonymous) {
13320
12938
  const audioSettings = this.state.settings?.audio;
13321
12939
  const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
13322
12940
  const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
@@ -13327,23 +12945,23 @@ class Call {
13327
12945
  connectionConfig,
13328
12946
  isDtxEnabled,
13329
12947
  isRedEnabled,
13330
- logTag: String(this.reconnectAttempts),
13331
12948
  onUnrecoverableError: () => {
13332
- this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
13333
- this.logger('warn', '[Reconnect] Error reconnecting after a publisher error', err);
12949
+ reconnect('full', 'unrecoverable publisher error').catch((err) => {
12950
+ this.logger('debug', '[Rejoin]: Rejoin failed', err);
13334
12951
  });
13335
12952
  },
13336
12953
  });
13337
12954
  }
13338
- this.statsReporter?.stop();
13339
- this.statsReporter = createStatsReporter({
13340
- subscriber: this.subscriber,
13341
- publisher: this.publisher,
13342
- state: this.state,
13343
- datacenter: sfuClient.edgeName,
13344
- });
13345
- this.sfuStatsReporter?.stop();
13346
- if (statsOptions?.reporting_interval_ms > 0) {
12955
+ if (!this.statsReporter) {
12956
+ this.statsReporter = createStatsReporter({
12957
+ subscriber: this.subscriber,
12958
+ publisher: this.publisher,
12959
+ state: this.state,
12960
+ datacenter: this.sfuClient.edgeName,
12961
+ });
12962
+ }
12963
+ const clientDetails = getClientDetails();
12964
+ if (!this.sfuStatsReporter && statsOptions) {
13347
12965
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
13348
12966
  clientDetails,
13349
12967
  options: statsOptions,
@@ -13352,258 +12970,129 @@ class Call {
13352
12970
  });
13353
12971
  this.sfuStatsReporter.start();
13354
12972
  }
13355
- };
13356
- /**
13357
- * Retrieves credentials for joining the call.
13358
- *
13359
- * @internal
13360
- *
13361
- * @param data the join call data.
13362
- */
13363
- this.doJoinRequest = async (data) => {
13364
- const location = await this.streamClient.getLocationHint();
13365
- const request = { ...data, location };
13366
- const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
13367
- this.state.updateFromCallResponse(joinResponse.call);
13368
- this.state.setMembers(joinResponse.members);
13369
- this.state.setOwnCapabilities(joinResponse.own_capabilities);
13370
- if (data?.ring && !this.ringing) {
13371
- this.ringingSubject.next(true);
13372
- }
13373
- if (this.ringing && !this.isCreatedByMe) {
13374
- // signals other users that I have accepted the incoming call.
13375
- await this.accept();
13376
- }
13377
- if (this.streamClient._hasConnectionID()) {
13378
- this.watching = true;
13379
- this.clientStore.registerCall(this);
13380
- }
13381
- return joinResponse;
13382
- };
13383
- /**
13384
- * Handles the reconnection flow.
13385
- *
13386
- * @internal
13387
- *
13388
- * @param strategy the reconnection strategy to use.
13389
- */
13390
- this.reconnect = async (strategy) => {
13391
- return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
13392
- this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
13393
- this.reconnectStrategy = strategy;
13394
- do {
13395
- const current = WebsocketReconnectStrategy[this.reconnectStrategy];
13396
- try {
13397
- // wait until the network is available
13398
- await this.networkAvailableTask?.promise;
13399
- switch (this.reconnectStrategy) {
13400
- case WebsocketReconnectStrategy.UNSPECIFIED:
13401
- case WebsocketReconnectStrategy.DISCONNECT:
13402
- this.logger('debug', `[Reconnect] No-op strategy ${current}`);
13403
- break;
13404
- case WebsocketReconnectStrategy.FAST:
13405
- await this.reconnectFast();
13406
- break;
13407
- case WebsocketReconnectStrategy.REJOIN:
13408
- await this.reconnectRejoin();
13409
- break;
13410
- case WebsocketReconnectStrategy.MIGRATE:
13411
- await this.reconnectMigrate();
13412
- break;
13413
- default:
13414
- ensureExhausted(this.reconnectStrategy, 'Unknown reconnection strategy');
13415
- break;
13416
- }
13417
- break; // do-while loop, reconnection worked, exit the loop
13418
- }
13419
- catch (error) {
13420
- this.logger('warn', `[Reconnect] ${current}(${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
13421
- await sleep(retryInterval(this.reconnectAttempts));
13422
- this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
13423
- this.reconnectAttempts++;
13424
- }
13425
- } while (this.state.callingState !== CallingState.JOINED &&
13426
- this.state.callingState !== CallingState.LEFT);
13427
- });
13428
- };
13429
- /**
13430
- * Initiates the reconnection flow with the "fast" strategy.
13431
- * @internal
13432
- */
13433
- this.reconnectFast = async () => {
13434
- this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
13435
- this.state.setCallingState(CallingState.RECONNECTING);
13436
- return this.join(this.joinCallData);
13437
- };
13438
- /**
13439
- * Initiates the reconnection flow with the "rejoin" strategy.
13440
- * @internal
13441
- */
13442
- this.reconnectRejoin = async () => {
13443
- this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
13444
- this.state.setCallingState(CallingState.RECONNECTING);
13445
- await this.join(this.joinCallData);
13446
- await this.restorePublishedTracks();
13447
- this.restoreSubscribedTracks();
13448
- };
13449
- /**
13450
- * Initiates the reconnection flow with the "migrate" strategy.
13451
- * @internal
13452
- */
13453
- this.reconnectMigrate = async () => {
13454
- const currentSfuClient = this.sfuClient;
13455
- if (!currentSfuClient) {
13456
- throw new Error('Cannot migrate without an active SFU client');
13457
- }
13458
- this.reconnectStrategy = WebsocketReconnectStrategy.MIGRATE;
13459
- this.state.setCallingState(CallingState.MIGRATING);
13460
- const currentSubscriber = this.subscriber;
13461
- const currentPublisher = this.publisher;
13462
- currentSubscriber?.detachEventHandlers();
13463
- currentPublisher?.detachEventHandlers();
13464
- const migrationTask = currentSfuClient.enterMigration();
13465
- try {
13466
- const currentSfu = currentSfuClient.edgeName;
13467
- await this.join({ ...this.joinCallData, migrating_from: currentSfu });
13468
- }
13469
- finally {
13470
- // cleanup the migration_from field after the migration is complete or failed
13471
- // as we don't want to keep dirty data in the join call data
13472
- delete this.joinCallData?.migrating_from;
13473
- }
13474
- await this.restorePublishedTracks();
13475
- this.restoreSubscribedTracks();
13476
12973
  try {
13477
- // Wait for the migration to complete, then close the previous SFU client
13478
- // and the peer connection instances. In case of failure, the migration
13479
- // task would throw an error and REJOIN would be attempted.
13480
- await migrationTask;
13481
- }
13482
- finally {
13483
- currentSubscriber?.close();
13484
- currentPublisher?.close({ stopTracks: false });
13485
- // and close the previous SFU client, without specifying close code
13486
- currentSfuClient.close();
13487
- }
13488
- };
13489
- /**
13490
- * Registers the various event handlers for reconnection.
13491
- *
13492
- * @internal
13493
- */
13494
- this.registerReconnectHandlers = () => {
13495
- // handles the legacy "goAway" event
13496
- const unregisterGoAway = this.on('goAway', () => {
13497
- this.reconnect(WebsocketReconnectStrategy.MIGRATE).catch((err) => {
13498
- this.logger('warn', '[Reconnect] Error reconnecting', err);
13499
- });
13500
- });
13501
- // handles the "error" event, through which the SFU can request a reconnect
13502
- const unregisterOnError = this.on('error', (e) => {
13503
- const { reconnectStrategy: strategy } = e;
13504
- if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
13505
- return;
13506
- if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
13507
- this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
13508
- this.logger('warn', `Can't leave call after disconnect request`, err);
12974
+ // 1. wait for the signal server to be ready before sending "joinRequest"
12975
+ sfuClient.signalReady
12976
+ .catch((err) => this.logger('error', 'Signal ready failed', err))
12977
+ // prepare a generic SDP and send it to the SFU.
12978
+ // this is a throw-away SDP that the SFU will use to determine
12979
+ // the capabilities of the client (codec support, etc.)
12980
+ .then(() => getGenericSdp('recvonly'))
12981
+ .then((sdp) => {
12982
+ const subscriptions = getCurrentValue(this.trackSubscriptionsSubject);
12983
+ const migration = isMigrating
12984
+ ? {
12985
+ fromSfuId: data?.migrating_from || '',
12986
+ subscriptions: subscriptions.data || [],
12987
+ announcedTracks: this.publisher?.getCurrentTrackInfos() || [],
12988
+ }
12989
+ : undefined;
12990
+ return sfuClient.join({
12991
+ subscriberSdp: sdp || '',
12992
+ clientDetails,
12993
+ migration,
12994
+ fastReconnect: previousSfuClient?.isFastReconnecting ?? false,
13509
12995
  });
12996
+ });
12997
+ // 2. in parallel, wait for the SFU to send us the "joinResponse"
12998
+ // this will throw an error if the SFU rejects the join request or
12999
+ // fails to respond in time
13000
+ const { callState, reconnected } = await this.waitForJoinResponse();
13001
+ if (isReconnecting) {
13002
+ this.logger('debug', '[Rejoin] fast reconnected:', reconnected);
13510
13003
  }
13511
- else {
13512
- this.reconnect(strategy).catch((err) => {
13513
- this.logger('warn', '[Reconnect] Error reconnecting', err);
13514
- });
13004
+ if (isMigrating) {
13005
+ await this.subscriber.migrateTo(sfuClient, connectionConfig);
13006
+ await this.publisher?.migrateTo(sfuClient, connectionConfig);
13515
13007
  }
13516
- });
13517
- const unregisterNetworkChanged = this.streamClient.on('network.changed', (e) => {
13518
- if (!e.online) {
13519
- this.logger('debug', '[Reconnect] Going offline');
13520
- if (!this.hasJoinedOnce)
13521
- return;
13522
- this.lastOfflineTimestamp = Date.now();
13523
- // create a new task that would resolve when the network is available
13524
- const networkAvailableTask = promiseWithResolvers();
13525
- networkAvailableTask.promise.then(() => {
13526
- let strategy = WebsocketReconnectStrategy.FAST;
13527
- if (this.lastOfflineTimestamp) {
13528
- const offline = (Date.now() - this.lastOfflineTimestamp) / 1000;
13529
- if (offline > this.fastReconnectDeadlineSeconds) {
13530
- // We shouldn't attempt FAST if we have exceeded the deadline.
13531
- // The SFU would have already wiped out the session.
13532
- strategy = WebsocketReconnectStrategy.REJOIN;
13533
- }
13008
+ else if (isReconnecting) {
13009
+ if (reconnected) {
13010
+ // update the SFU client instance on the subscriber and publisher
13011
+ this.subscriber.setSfuClient(sfuClient);
13012
+ // publisher might not be there (anonymous users)
13013
+ if (this.publisher) {
13014
+ this.publisher.setSfuClient(sfuClient);
13015
+ // and perform a full ICE restart on the publisher
13016
+ await this.publisher.restartIce();
13534
13017
  }
13535
- this.reconnect(strategy).catch((err) => {
13536
- this.logger('warn', '[Reconnect] Error restoring connection after going online', err);
13018
+ }
13019
+ else if (previousSfuClient?.isFastReconnecting) {
13020
+ // reconnection wasn't possible, so we need to do a full rejoin
13021
+ return await reconnect('full', 're-attempting').catch((err) => {
13022
+ this.logger('error', `[Rejoin]: Rejoin failed forced full rejoin.`, err);
13023
+ });
13024
+ }
13025
+ }
13026
+ const currentParticipants = callState?.participants || [];
13027
+ const participantCount = callState?.participantCount;
13028
+ const startedAt = callState?.startedAt
13029
+ ? Timestamp.toDate(callState.startedAt)
13030
+ : new Date();
13031
+ const pins = callState?.pins ?? [];
13032
+ this.state.setParticipants(() => {
13033
+ const participantLookup = this.state.getParticipantLookupBySessionId();
13034
+ return currentParticipants.map((p) => {
13035
+ // We need to preserve the local state of the participant
13036
+ // (e.g. videoDimension, visibilityState, pinnedAt, etc.)
13037
+ // as it doesn't exist on the server.
13038
+ const existingParticipant = participantLookup[p.sessionId];
13039
+ return Object.assign(p, existingParticipant, {
13040
+ isLocalParticipant: p.sessionId === sfuClient.sessionId,
13041
+ viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
13042
+ videoTrack: VisibilityState.UNKNOWN,
13043
+ screenShareTrack: VisibilityState.UNKNOWN,
13044
+ },
13537
13045
  });
13538
13046
  });
13539
- this.networkAvailableTask = networkAvailableTask;
13540
- this.sfuStatsReporter?.stop();
13541
- this.state.setCallingState(CallingState.OFFLINE);
13047
+ });
13048
+ this.state.setParticipantCount(participantCount?.total || 0);
13049
+ this.state.setAnonymousParticipantCount(participantCount?.anonymous || 0);
13050
+ this.state.setStartedAt(startedAt);
13051
+ this.state.setServerSidePins(pins);
13052
+ this.reconnectAttempts = 0; // reset the reconnect attempts counter
13053
+ this.state.setCallingState(CallingState.JOINED);
13054
+ try {
13055
+ await this.initCamera({ setStatus: true });
13056
+ await this.initMic({ setStatus: true });
13542
13057
  }
13543
- else {
13544
- this.logger('debug', '[Reconnect] Going online');
13545
- // TODO try to remove this .close call
13546
- this.sfuClient?.close(4002, 'Closing WS to reconnect after going online');
13547
- // we went online, release the previous waiters and reset the state
13548
- this.networkAvailableTask?.resolve();
13549
- this.networkAvailableTask = undefined;
13550
- this.sfuStatsReporter?.start();
13058
+ catch (error) {
13059
+ this.logger('warn', 'Camera and/or mic init failed during join call', error);
13551
13060
  }
13552
- });
13553
- this.leaveCallHooks.add(unregisterGoAway);
13554
- this.leaveCallHooks.add(unregisterOnError);
13555
- this.leaveCallHooks.add(unregisterNetworkChanged);
13556
- };
13557
- /**
13558
- * Restores the published tracks after a reconnection.
13559
- * @internal
13560
- */
13561
- this.restorePublishedTracks = async () => {
13562
- // the tracks need to be restored in their original order of publishing
13563
- // otherwise, we might get `m-lines order mismatch` errors
13564
- for (const trackType of this.trackPublishOrder) {
13565
- switch (trackType) {
13566
- case TrackType.AUDIO:
13567
- const audioStream = this.microphone.state.mediaStream;
13568
- if (audioStream) {
13569
- await this.publishAudioStream(audioStream);
13570
- }
13571
- break;
13572
- case TrackType.VIDEO:
13573
- const videoStream = this.camera.state.mediaStream;
13574
- if (videoStream) {
13575
- await this.publishVideoStream(videoStream, {
13576
- preferredCodec: this.camera.preferredCodec,
13577
- });
13578
- }
13579
- break;
13580
- case TrackType.SCREEN_SHARE:
13581
- const screenShareStream = this.screenShare.state.mediaStream;
13582
- if (screenShareStream) {
13583
- await this.publishScreenShareStream(screenShareStream, {
13584
- screenShareSettings: this.screenShare.getSettings(),
13585
- });
13586
- }
13587
- break;
13588
- // screen share audio can't exist without a screen share, so we handle it there
13589
- case TrackType.SCREEN_SHARE_AUDIO:
13590
- case TrackType.UNSPECIFIED:
13591
- break;
13592
- default:
13593
- ensureExhausted(trackType, 'Unknown track type');
13594
- break;
13061
+ // 3. once we have the "joinResponse", and possibly reconciled the local state
13062
+ // we schedule a fast subscription update for all remote participants
13063
+ // that were visible before we reconnected or migrated to a new SFU.
13064
+ const { remoteParticipants } = this.state;
13065
+ if (remoteParticipants.length > 0) {
13066
+ this.updateSubscriptions(remoteParticipants, DebounceType.FAST);
13067
+ }
13068
+ this.logger('info', `Joined call ${this.cid}`);
13069
+ }
13070
+ catch (err) {
13071
+ // join failed, try to rejoin
13072
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
13073
+ this.logger('error', `[Rejoin]: Rejoin ${this.reconnectAttempts} failed.`, err);
13074
+ await reconnect('full', 'previous attempt failed');
13075
+ this.logger('info', `[Rejoin]: Rejoin ${this.reconnectAttempts} successful!`);
13076
+ }
13077
+ else {
13078
+ this.logger('error', `[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`);
13079
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
13080
+ throw new Error('Join failed');
13595
13081
  }
13596
13082
  }
13597
13083
  };
13598
- /**
13599
- * Restores the subscribed tracks after a reconnection.
13600
- * @internal
13601
- */
13602
- this.restoreSubscribedTracks = () => {
13603
- const { remoteParticipants } = this.state;
13604
- if (remoteParticipants.length <= 0)
13605
- return;
13606
- this.updateSubscriptions(remoteParticipants, DebounceType.FAST);
13084
+ this.waitForJoinResponse = (timeout = 5000) => {
13085
+ return new Promise((resolve, reject) => {
13086
+ const unsubscribe = this.on('joinResponse', (event) => {
13087
+ clearTimeout(timeoutId);
13088
+ unsubscribe();
13089
+ resolve(event);
13090
+ });
13091
+ const timeoutId = setTimeout(() => {
13092
+ unsubscribe();
13093
+ reject(new Error('Waiting for "joinResponse" has timed out'));
13094
+ }, timeout);
13095
+ });
13607
13096
  };
13608
13097
  /**
13609
13098
  * Starts publishing the given video stream to the call.
@@ -13618,7 +13107,7 @@ class Call {
13618
13107
  this.publishVideoStream = async (videoStream, opts = {}) => {
13619
13108
  // we should wait until we get a JoinResponse from the SFU,
13620
13109
  // otherwise we risk breaking the ICETrickle flow.
13621
- await this.waitUntilCallJoined();
13110
+ await this.assertCallJoined();
13622
13111
  if (!this.publisher) {
13623
13112
  this.logger('error', 'Trying to publish video before join is completed');
13624
13113
  throw new Error(`Call not joined yet.`);
@@ -13628,9 +13117,6 @@ class Call {
13628
13117
  this.logger('error', `There is no video track to publish in the stream.`);
13629
13118
  return;
13630
13119
  }
13631
- if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
13632
- this.trackPublishOrder.push(TrackType.VIDEO);
13633
- }
13634
13120
  await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, opts);
13635
13121
  };
13636
13122
  /**
@@ -13645,7 +13131,7 @@ class Call {
13645
13131
  this.publishAudioStream = async (audioStream) => {
13646
13132
  // we should wait until we get a JoinResponse from the SFU,
13647
13133
  // otherwise we risk breaking the ICETrickle flow.
13648
- await this.waitUntilCallJoined();
13134
+ await this.assertCallJoined();
13649
13135
  if (!this.publisher) {
13650
13136
  this.logger('error', 'Trying to publish audio before join is completed');
13651
13137
  throw new Error(`Call not joined yet.`);
@@ -13655,9 +13141,6 @@ class Call {
13655
13141
  this.logger('error', `There is no audio track in the stream to publish`);
13656
13142
  return;
13657
13143
  }
13658
- if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
13659
- this.trackPublishOrder.push(TrackType.AUDIO);
13660
- }
13661
13144
  await this.publisher.publishStream(audioStream, audioTrack, TrackType.AUDIO);
13662
13145
  };
13663
13146
  /**
@@ -13672,7 +13155,7 @@ class Call {
13672
13155
  this.publishScreenShareStream = async (screenShareStream, opts = {}) => {
13673
13156
  // we should wait until we get a JoinResponse from the SFU,
13674
13157
  // otherwise we risk breaking the ICETrickle flow.
13675
- await this.waitUntilCallJoined();
13158
+ await this.assertCallJoined();
13676
13159
  if (!this.publisher) {
13677
13160
  this.logger('error', 'Trying to publish screen share before join is completed');
13678
13161
  throw new Error(`Call not joined yet.`);
@@ -13682,15 +13165,9 @@ class Call {
13682
13165
  this.logger('error', `There is no video track in the screen share stream to publish`);
13683
13166
  return;
13684
13167
  }
13685
- if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
13686
- this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
13687
- }
13688
13168
  await this.publisher.publishStream(screenShareStream, screenShareTrack, TrackType.SCREEN_SHARE, opts);
13689
13169
  const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
13690
13170
  if (screenShareAudioTrack) {
13691
- if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE_AUDIO)) {
13692
- this.trackPublishOrder.push(TrackType.SCREEN_SHARE_AUDIO);
13693
- }
13694
13171
  await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
13695
13172
  }
13696
13173
  };
@@ -13735,9 +13212,19 @@ class Call {
13735
13212
  * @param type the debounce type to use for the update.
13736
13213
  */
13737
13214
  this.updateSubscriptionsPartial = (trackType, changes, type = DebounceType.SLOW) => {
13215
+ if (trackType === 'video') {
13216
+ this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'videoTrack'`);
13217
+ trackType = 'videoTrack';
13218
+ }
13219
+ else if (trackType === 'screen') {
13220
+ this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'screenShareTrack'`);
13221
+ trackType = 'screenShareTrack';
13222
+ }
13738
13223
  const participants = this.state.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
13739
- if (change.dimension) {
13224
+ if (change.dimension?.height) {
13740
13225
  change.dimension.height = Math.ceil(change.dimension.height);
13226
+ }
13227
+ if (change.dimension?.width) {
13741
13228
  change.dimension.width = Math.ceil(change.dimension.width);
13742
13229
  }
13743
13230
  const prop = trackType === 'videoTrack'
@@ -13752,7 +13239,9 @@ class Call {
13752
13239
  }
13753
13240
  return acc;
13754
13241
  }, {}));
13755
- this.updateSubscriptions(participants, type);
13242
+ if (participants) {
13243
+ this.updateSubscriptions(participants, type);
13244
+ }
13756
13245
  };
13757
13246
  this.updateSubscriptions = (participants, type = DebounceType.SLOW) => {
13758
13247
  const subscriptions = [];
@@ -13835,10 +13324,10 @@ class Call {
13835
13324
  this.updatePublishQuality = async (enabledLayers) => {
13836
13325
  return this.publisher?.updateVideoPublishQuality(enabledLayers);
13837
13326
  };
13838
- this.waitUntilCallJoined = () => {
13327
+ this.assertCallJoined = () => {
13839
13328
  return new Promise((resolve) => {
13840
13329
  this.state.callingState$
13841
- .pipe(takeWhile((state) => state !== CallingState.JOINED, true), filter((state) => state === CallingState.JOINED))
13330
+ .pipe(takeWhile((state) => state !== CallingState.JOINED, true), filter((s) => s === CallingState.JOINED))
13842
13331
  .subscribe(() => resolve());
13843
13332
  });
13844
13333
  };
@@ -14222,72 +13711,14 @@ class Call {
14222
13711
  *
14223
13712
  * @internal
14224
13713
  */
14225
- this.applyDeviceConfig = async (status) => {
14226
- await this.initCamera({ setStatus: status }).catch((err) => {
13714
+ this.applyDeviceConfig = async () => {
13715
+ await this.initCamera({ setStatus: false }).catch((err) => {
14227
13716
  this.logger('warn', 'Camera init failed', err);
14228
13717
  });
14229
- await this.initMic({ setStatus: status }).catch((err) => {
13718
+ await this.initMic({ setStatus: false }).catch((err) => {
14230
13719
  this.logger('warn', 'Mic init failed', err);
14231
13720
  });
14232
13721
  };
14233
- this.initCamera = async (options) => {
14234
- // Wait for any in progress camera operation
14235
- await this.camera.statusChangeSettled();
14236
- if (this.state.localParticipant?.videoStream ||
14237
- !this.permissionsContext.hasPermission('send-video')) {
14238
- return;
14239
- }
14240
- // Set camera direction if it's not yet set
14241
- if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
14242
- let defaultDirection = 'front';
14243
- const backendSetting = this.state.settings?.video.camera_facing;
14244
- if (backendSetting) {
14245
- defaultDirection = backendSetting === 'front' ? 'front' : 'back';
14246
- }
14247
- this.camera.state.setDirection(defaultDirection);
14248
- }
14249
- // Set target resolution
14250
- const targetResolution = this.state.settings?.video.target_resolution;
14251
- if (targetResolution) {
14252
- await this.camera.selectTargetResolution(targetResolution);
14253
- }
14254
- if (options.setStatus) {
14255
- // Publish already that was set before we joined
14256
- if (this.camera.enabled &&
14257
- this.camera.state.mediaStream &&
14258
- !this.publisher?.isPublishing(TrackType.VIDEO)) {
14259
- await this.publishVideoStream(this.camera.state.mediaStream, {
14260
- preferredCodec: this.camera.preferredCodec,
14261
- });
14262
- }
14263
- // Start camera if backend config specifies, and there is no local setting
14264
- if (this.camera.state.status === undefined &&
14265
- this.state.settings?.video.camera_default_on) {
14266
- await this.camera.enable();
14267
- }
14268
- }
14269
- };
14270
- this.initMic = async (options) => {
14271
- // Wait for any in progress mic operation
14272
- await this.microphone.statusChangeSettled();
14273
- if (this.state.localParticipant?.audioStream ||
14274
- !this.permissionsContext.hasPermission('send-audio')) {
14275
- return;
14276
- }
14277
- if (options.setStatus) {
14278
- // Publish media stream that was set before we joined
14279
- if (this.microphone.enabled &&
14280
- this.microphone.state.mediaStream &&
14281
- !this.publisher?.isPublishing(TrackType.AUDIO)) {
14282
- await this.publishAudioStream(this.microphone.state.mediaStream);
14283
- }
14284
- // Start mic if backend config specifies, and there is no local setting
14285
- if (this.microphone.state.status === undefined &&
14286
- this.state.settings?.audio.mic_default_on) {
14287
- await this.microphone.enable();
14288
- }
14289
- }
14290
- };
14291
13722
  /**
14292
13723
  * Will begin tracking the given element for visibility changes within the
14293
13724
  * configured viewport element (`call.setViewport`).
@@ -14402,15 +13833,15 @@ class Call {
14402
13833
  }
14403
13834
  async setup() {
14404
13835
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
14405
- if (this.initialized)
13836
+ if (this.initialized) {
14406
13837
  return;
13838
+ }
14407
13839
  this.leaveCallHooks.add(this.on('all', (event) => {
14408
13840
  // update state with the latest event data
14409
13841
  this.state.updateFromEvent(event);
14410
13842
  }));
14411
- this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
13843
+ this.leaveCallHooks.add(registerEventHandlers(this, this.state, this.dispatcher));
14412
13844
  this.registerEffects();
14413
- this.registerReconnectHandlers();
14414
13845
  this.leaveCallHooks.add(createSubscription(this.trackSubscriptionsSubject.pipe(debounce((v) => timer(v.type)), map$1((v) => v.data)), (subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions).catch((err) => {
14415
13846
  this.logger('debug', `Failed to update track subscriptions`, err);
14416
13847
  })));
@@ -14430,7 +13861,44 @@ class Call {
14430
13861
  }));
14431
13862
  this.leaveCallHooks.add(
14432
13863
  // handle the case when the user permissions are modified.
14433
- createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
13864
+ createSubscription(this.state.ownCapabilities$, (ownCapabilities) => {
13865
+ // update the permission context.
13866
+ this.permissionsContext.setPermissions(ownCapabilities);
13867
+ if (!this.publisher)
13868
+ return;
13869
+ // check if the user still has publishing permissions and stop publishing if not.
13870
+ const permissionToTrackType = {
13871
+ [OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
13872
+ [OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
13873
+ [OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
13874
+ };
13875
+ for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
13876
+ const hasPermission = this.permissionsContext.hasPermission(permission);
13877
+ if (!hasPermission &&
13878
+ (this.publisher.isPublishing(trackType) ||
13879
+ this.publisher.isLive(trackType))) {
13880
+ // Stop tracks, then notify device manager
13881
+ this.stopPublish(trackType)
13882
+ .catch((err) => {
13883
+ this.logger('error', `Error stopping publish ${trackType}`, err);
13884
+ })
13885
+ .then(() => {
13886
+ if (trackType === TrackType.VIDEO &&
13887
+ this.camera.state.status === 'enabled') {
13888
+ this.camera
13889
+ .disable()
13890
+ .catch((err) => this.logger('error', `Error disabling camera after permission revoked`, err));
13891
+ }
13892
+ if (trackType === TrackType.AUDIO &&
13893
+ this.microphone.state.status === 'enabled') {
13894
+ this.microphone
13895
+ .disable()
13896
+ .catch((err) => this.logger('error', `Error disabling microphone after permission revoked`, err));
13897
+ }
13898
+ });
13899
+ }
13900
+ }
13901
+ }));
14434
13902
  this.leaveCallHooks.add(
14435
13903
  // handles the case when the user is blocked by the call owner.
14436
13904
  createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
@@ -14522,20 +13990,63 @@ class Call {
14522
13990
  get isCreatedByMe() {
14523
13991
  return this.state.createdBy?.id === this.currentUserId;
14524
13992
  }
14525
- /**
14526
- * Handles the closing of the SFU signal connection.
14527
- *
14528
- * @internal
14529
- * @param sfuClient the SFU client instance that was closed.
14530
- */
14531
- handleSfuSignalClose(sfuClient) {
14532
- this.logger('debug', '[Reconnect] SFU signal connection closed');
14533
- // normal close, no need to reconnect
14534
- if (sfuClient.isLeaving)
13993
+ async initCamera(options) {
13994
+ // Wait for any in progress camera operation
13995
+ await this.camera.statusChangeSettled();
13996
+ if (this.state.localParticipant?.videoStream ||
13997
+ !this.permissionsContext.hasPermission('send-video')) {
14535
13998
  return;
14536
- this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
14537
- this.logger('warn', '[Reconnect] Error reconnecting', err);
14538
- });
13999
+ }
14000
+ // Set camera direction if it's not yet set
14001
+ if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
14002
+ let defaultDirection = 'front';
14003
+ const backendSetting = this.state.settings?.video.camera_facing;
14004
+ if (backendSetting) {
14005
+ defaultDirection = backendSetting === 'front' ? 'front' : 'back';
14006
+ }
14007
+ this.camera.state.setDirection(defaultDirection);
14008
+ }
14009
+ // Set target resolution
14010
+ const targetResolution = this.state.settings?.video.target_resolution;
14011
+ if (targetResolution) {
14012
+ await this.camera.selectTargetResolution(targetResolution);
14013
+ }
14014
+ if (options.setStatus) {
14015
+ // Publish already that was set before we joined
14016
+ if (this.camera.state.status === 'enabled' &&
14017
+ this.camera.state.mediaStream &&
14018
+ !this.publisher?.isPublishing(TrackType.VIDEO)) {
14019
+ await this.publishVideoStream(this.camera.state.mediaStream, {
14020
+ preferredCodec: this.camera.preferredCodec,
14021
+ });
14022
+ }
14023
+ // Start camera if backend config specifies, and there is no local setting
14024
+ if (this.camera.state.status === undefined &&
14025
+ this.state.settings?.video.camera_default_on) {
14026
+ await this.camera.enable();
14027
+ }
14028
+ }
14029
+ }
14030
+ async initMic(options) {
14031
+ // Wait for any in progress mic operation
14032
+ await this.microphone.statusChangeSettled();
14033
+ if (this.state.localParticipant?.audioStream ||
14034
+ !this.permissionsContext.hasPermission('send-audio')) {
14035
+ return;
14036
+ }
14037
+ if (options.setStatus) {
14038
+ // Publish media stream that was set before we joined
14039
+ if (this.microphone.state.status === 'enabled' &&
14040
+ this.microphone.state.mediaStream &&
14041
+ !this.publisher?.isPublishing(TrackType.AUDIO)) {
14042
+ await this.publishAudioStream(this.microphone.state.mediaStream);
14043
+ }
14044
+ // Start mic if backend config specifies, and there is no local setting
14045
+ if (this.microphone.state.status === undefined &&
14046
+ this.state.settings?.audio.mic_default_on) {
14047
+ await this.microphone.enable();
14048
+ }
14049
+ }
14539
14050
  }
14540
14051
  }
14541
14052
 
@@ -14740,7 +14251,6 @@ class StableWSConnection {
14740
14251
  }
14741
14252
  }
14742
14253
  if (data) {
14743
- data.received_at = new Date();
14744
14254
  this.client.dispatchEvent(data);
14745
14255
  }
14746
14256
  this.scheduleConnectionCheck();
@@ -15081,7 +14591,7 @@ class StableWSConnection {
15081
14591
  wsURL,
15082
14592
  requestID: this.requestID,
15083
14593
  });
15084
- this.ws = new WebSocket$1(wsURL);
14594
+ this.ws = new WebSocket(wsURL);
15085
14595
  this.ws.onopen = this.onopen.bind(this, this.wsID);
15086
14596
  this.ws.onclose = this.onclose.bind(this, this.wsID);
15087
14597
  this.ws.onerror = this.onerror.bind(this, this.wsID);
@@ -15367,7 +14877,7 @@ class TokenManager {
15367
14877
  if (this.user && !this.token) {
15368
14878
  return this.token;
15369
14879
  }
15370
- throw new Error(`Both secret and user tokens are not set. Either client.connectUser wasn't called or client.disconnect was called`);
14880
+ throw new Error(`User token is not set. Either client.connectUser wasn't called or client.disconnect was called`);
15371
14881
  };
15372
14882
  this.isStatic = () => this.type === 'static';
15373
14883
  this.loadTokenPromise = null;
@@ -15697,7 +15207,6 @@ class StreamClient {
15697
15207
  const wsPromise = this.openConnection();
15698
15208
  this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then((result) => result[1]);
15699
15209
  try {
15700
- addConnectionEventListeners(this.updateNetworkConnectionStatus);
15701
15210
  return await this.setUserPromise;
15702
15211
  }
15703
15212
  catch (err) {
@@ -15793,7 +15302,6 @@ class StreamClient {
15793
15302
  delete this.userID;
15794
15303
  this.anonymous = false;
15795
15304
  await this.closeConnection(timeout);
15796
- removeConnectionEventListeners(this.updateNetworkConnectionStatus);
15797
15305
  this.tokenManager.reset();
15798
15306
  this.connectionIdPromise = undefined;
15799
15307
  this.rejectConnectionId = undefined;
@@ -15813,7 +15321,6 @@ class StreamClient {
15813
15321
  * connectAnonymousUser - Set an anonymous user and open a WebSocket connection
15814
15322
  */
15815
15323
  this.connectAnonymousUser = async (user, tokenOrProvider) => {
15816
- addConnectionEventListeners(this.updateNetworkConnectionStatus);
15817
15324
  this.connectionIdPromise = new Promise((resolve, reject) => {
15818
15325
  this.resolveConnectionId = resolve;
15819
15326
  this.rejectConnectionId = reject;
@@ -15973,6 +15480,8 @@ class StreamClient {
15973
15480
  return data;
15974
15481
  };
15975
15482
  this.dispatchEvent = (event) => {
15483
+ if (!event.received_at)
15484
+ event.received_at = new Date();
15976
15485
  this.logger('debug', `Dispatching event: ${event.type}`, event);
15977
15486
  if (!this.listeners)
15978
15487
  return;
@@ -16063,7 +15572,7 @@ class StreamClient {
16063
15572
  });
16064
15573
  };
16065
15574
  this.getUserAgent = () => {
16066
- const version = "1.5.0-0" ;
15575
+ const version = "1.5.0" ;
16067
15576
  return (this.userAgent ||
16068
15577
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
16069
15578
  };
@@ -16129,15 +15638,11 @@ class StreamClient {
16129
15638
  client_request_id,
16130
15639
  });
16131
15640
  };
16132
- this.updateNetworkConnectionStatus = (event) => {
16133
- if (event.type === 'offline') {
16134
- this.logger('debug', 'device went offline');
16135
- this.dispatchEvent({ type: 'network.changed', online: false });
16136
- }
16137
- else if (event.type === 'online') {
16138
- this.logger('debug', 'device went online');
16139
- this.dispatchEvent({ type: 'network.changed', online: true });
16140
- }
15641
+ /**
15642
+ * creates an abort controller that will be used by the next HTTP Request.
15643
+ */
15644
+ this.createAbortControllerForNextRequest = () => {
15645
+ return (this.nextRequestAbortController = new AbortController());
16141
15646
  };
16142
15647
  // set the key
16143
15648
  this.key = key;
@@ -16167,14 +15672,10 @@ class StreamClient {
16167
15672
  });
16168
15673
  }
16169
15674
  this.setBaseURL(this.options.baseURL || 'https://video.stream-io-api.com/video');
16170
- if (typeof process !== 'undefined' &&
16171
- 'env' in process &&
16172
- process.env.STREAM_LOCAL_TEST_RUN) {
15675
+ if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_RUN) {
16173
15676
  this.setBaseURL('http://localhost:3030/video');
16174
15677
  }
16175
- if (typeof process !== 'undefined' &&
16176
- 'env' in process &&
16177
- process.env.STREAM_LOCAL_TEST_HOST) {
15678
+ if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_HOST) {
16178
15679
  this.setBaseURL(`http://${process.env.STREAM_LOCAL_TEST_HOST}/video`);
16179
15680
  }
16180
15681
  this.axiosInstance = axios.create({
@@ -16209,99 +15710,6 @@ class StreamVideoClient {
16209
15710
  constructor(apiKeyOrArgs, opts) {
16210
15711
  this.logLevel = 'warn';
16211
15712
  this.eventHandlersToUnregister = [];
16212
- /**
16213
- * Connects the given user to the client.
16214
- * Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
16215
- * If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
16216
- *
16217
- * @param user the user to connect.
16218
- * @param token a token or a function that returns a token.
16219
- */
16220
- this.connectUser = async (user, token) => {
16221
- if (user.type === 'anonymous') {
16222
- user.id = '!anon';
16223
- return this.connectAnonymousUser(user, token);
16224
- }
16225
- let connectUser = () => {
16226
- return this.streamClient.connectUser(user, token);
16227
- };
16228
- if (user.type === 'guest') {
16229
- connectUser = async () => {
16230
- return this.streamClient.connectGuestUser(user);
16231
- };
16232
- }
16233
- this.connectionPromise = this.disconnectionPromise
16234
- ? this.disconnectionPromise.then(() => connectUser())
16235
- : connectUser();
16236
- this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
16237
- const connectUserResponse = await this.connectionPromise;
16238
- // connectUserResponse will be void if connectUser called twice for the same user
16239
- if (connectUserResponse?.me) {
16240
- this.writeableStateStore.setConnectedUser(connectUserResponse.me);
16241
- }
16242
- this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
16243
- if (event.online) {
16244
- const callsToReWatch = this.writeableStateStore.calls
16245
- .filter((call) => call.watching)
16246
- .map((call) => call.cid);
16247
- this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
16248
- if (callsToReWatch.length > 0) {
16249
- this.queryCalls({
16250
- watch: true,
16251
- filter_conditions: {
16252
- cid: { $in: callsToReWatch },
16253
- },
16254
- sort: [{ field: 'cid', direction: 1 }],
16255
- }).catch((err) => {
16256
- this.logger('error', 'Failed to re-watch calls', err);
16257
- });
16258
- }
16259
- }
16260
- }));
16261
- this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
16262
- const { call, members } = event;
16263
- if (user.id === call.created_by.id) {
16264
- this.logger('warn', 'Received `call.created` sent by the current user');
16265
- return;
16266
- }
16267
- this.logger('info', `New call created and registered: ${call.cid}`);
16268
- const newCall = new Call({
16269
- streamClient: this.streamClient,
16270
- type: call.type,
16271
- id: call.id,
16272
- members,
16273
- clientStore: this.writeableStateStore,
16274
- });
16275
- newCall.state.updateFromCallResponse(call);
16276
- this.writeableStateStore.registerCall(newCall);
16277
- }));
16278
- this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
16279
- const { call, members } = event;
16280
- if (user.id === call.created_by.id) {
16281
- this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
16282
- return;
16283
- }
16284
- // The call might already be tracked by the client,
16285
- // if `call.created` was received before `call.ring`.
16286
- // In that case, we cleanup the already tracked call.
16287
- const prevCall = this.writeableStateStore.findCall(call.type, call.id);
16288
- await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
16289
- // we create a new call
16290
- const theCall = new Call({
16291
- streamClient: this.streamClient,
16292
- type: call.type,
16293
- id: call.id,
16294
- members,
16295
- clientStore: this.writeableStateStore,
16296
- ringing: true,
16297
- });
16298
- theCall.state.updateFromCallResponse(call);
16299
- // we fetch the latest metadata for the call from the server
16300
- await theCall.get();
16301
- this.writeableStateStore.registerCall(theCall);
16302
- }));
16303
- return connectUserResponse;
16304
- };
16305
15713
  /**
16306
15714
  * Disconnects the currently connected user from the client.
16307
15715
  *
@@ -16314,12 +15722,16 @@ class StreamVideoClient {
16314
15722
  if (!this.streamClient.user && !this.connectionPromise) {
16315
15723
  return;
16316
15724
  }
15725
+ const userId = this.streamClient.user?.id;
16317
15726
  const disconnectUser = () => this.streamClient.disconnectUser(timeout);
16318
15727
  this.disconnectionPromise = this.connectionPromise
16319
15728
  ? this.connectionPromise.then(() => disconnectUser())
16320
15729
  : disconnectUser();
16321
15730
  this.disconnectionPromise.finally(() => (this.disconnectionPromise = undefined));
16322
15731
  await this.disconnectionPromise;
15732
+ if (userId) {
15733
+ StreamVideoClient._instanceMap.delete(userId);
15734
+ }
16323
15735
  this.eventHandlersToUnregister.forEach((unregister) => unregister());
16324
15736
  this.eventHandlersToUnregister = [];
16325
15737
  this.writeableStateStore.setConnectedUser(undefined);
@@ -16386,7 +15798,7 @@ class StreamVideoClient {
16386
15798
  clientStore: this.writeableStateStore,
16387
15799
  });
16388
15800
  call.state.updateFromCallResponse(c.call);
16389
- await call.applyDeviceConfig(false);
15801
+ await call.applyDeviceConfig();
16390
15802
  if (data.watch) {
16391
15803
  this.writeableStateStore.registerCall(call);
16392
15804
  }
@@ -16430,17 +15842,6 @@ class StreamVideoClient {
16430
15842
  ...(push_provider_name != null ? { push_provider_name } : {}),
16431
15843
  });
16432
15844
  };
16433
- /**
16434
- * addDevice - Adds a push device for a user.
16435
- *
16436
- * @param {string} id the device id
16437
- * @param {string} push_provider the push provider name (eg. apn, firebase)
16438
- * @param {string} push_provider_name user provided push provider name
16439
- * @param {string} [userID] the user id (defaults to current user)
16440
- */
16441
- this.addVoipDevice = async (id, push_provider, push_provider_name, userID) => {
16442
- return await this.addDevice(id, push_provider, push_provider_name, userID, true);
16443
- };
16444
15845
  /**
16445
15846
  * getDevices - Returns the devices associated with a current user
16446
15847
  * @param {string} [userID] User ID. Only works on serverside
@@ -16468,7 +15869,7 @@ class StreamVideoClient {
16468
15869
  this.onRingingCall = async (call_cid) => {
16469
15870
  // if we find the call and is already ringing, we don't need to create a new call
16470
15871
  // as client would have received the call.ring state because the app had WS alive when receiving push notifications
16471
- let call = this.state.calls.find((c) => c.cid === call_cid && c.ringing);
15872
+ let call = this.readOnlyStateStore.calls.find((c) => c.cid === call_cid && c.ringing);
16472
15873
  if (!call) {
16473
15874
  // if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
16474
15875
  const [callType, callId] = call_cid.split(':');
@@ -16509,13 +15910,12 @@ class StreamVideoClient {
16509
15910
  }
16510
15911
  setLogger(logger, logLevel);
16511
15912
  this.logger = getLogger(['client']);
16512
- const coordinatorLogger = getLogger(['coordinator']);
16513
15913
  if (typeof apiKeyOrArgs === 'string') {
16514
15914
  this.streamClient = new StreamClient(apiKeyOrArgs, {
16515
15915
  persistUserOnConnectionFailure: true,
16516
15916
  ...opts,
16517
15917
  logLevel,
16518
- logger: coordinatorLogger,
15918
+ logger: this.logger,
16519
15919
  });
16520
15920
  }
16521
15921
  else {
@@ -16523,14 +15923,12 @@ class StreamVideoClient {
16523
15923
  persistUserOnConnectionFailure: true,
16524
15924
  ...apiKeyOrArgs.options,
16525
15925
  logLevel,
16526
- logger: coordinatorLogger,
15926
+ logger: this.logger,
16527
15927
  });
16528
15928
  const sdkInfo = getSdkInfo();
16529
15929
  if (sdkInfo) {
16530
- const sdkName = SdkType[sdkInfo.type].toLowerCase();
16531
- const sdkVersion = `${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`;
16532
- const userAgent = this.streamClient.getUserAgent();
16533
- this.streamClient.setUserAgent(`${userAgent}-video-${sdkName}-sdk-${sdkVersion}`);
15930
+ this.streamClient.setUserAgent(this.streamClient.getUserAgent() +
15931
+ `-video-${SdkType[sdkInfo.type].toLowerCase()}-sdk-${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`);
16534
15932
  }
16535
15933
  }
16536
15934
  this.writeableStateStore = new StreamVideoWriteableStateStore();
@@ -16539,19 +15937,156 @@ class StreamVideoClient {
16539
15937
  const user = apiKeyOrArgs.user;
16540
15938
  const token = apiKeyOrArgs.token || apiKeyOrArgs.tokenProvider;
16541
15939
  if (user) {
15940
+ let id = user.id;
15941
+ if (user.type === 'anonymous') {
15942
+ id = '!anon';
15943
+ }
15944
+ if (id) {
15945
+ if (StreamVideoClient._instanceMap.has(apiKeyOrArgs.apiKey + id)) {
15946
+ this.logger('warn', `A StreamVideoClient already exists for ${user.type === 'anonymous' ? 'an anyonymous user' : id}; Prefer using getOrCreateInstance method`);
15947
+ }
15948
+ user.id = id;
15949
+ StreamVideoClient._instanceMap.set(apiKeyOrArgs.apiKey + id, this);
15950
+ }
16542
15951
  this.connectUser(user, token).catch((err) => {
16543
15952
  this.logger('error', 'Failed to connect', err);
16544
15953
  });
16545
15954
  }
16546
15955
  }
16547
15956
  }
15957
+ static getOrCreateInstance(args) {
15958
+ const user = args.user;
15959
+ if (!user.id) {
15960
+ if (args.user.type === 'anonymous') {
15961
+ user.id = '!anon';
15962
+ }
15963
+ else {
15964
+ throw new Error('User ID is required for a non-anonymous user');
15965
+ }
15966
+ }
15967
+ if (!args.token && !args.tokenProvider) {
15968
+ if (args.user.type !== 'anonymous' && args.user.type !== 'guest') {
15969
+ throw new Error('TokenProvider or token is required for a user that is not a guest or anonymous');
15970
+ }
15971
+ }
15972
+ let instance = StreamVideoClient._instanceMap.get(args.apiKey + user.id);
15973
+ if (!instance) {
15974
+ instance = new StreamVideoClient({ ...args, user });
15975
+ }
15976
+ return instance;
15977
+ }
16548
15978
  /**
16549
15979
  * Return the reactive state store, use this if you want to be notified about changes to the client state
16550
15980
  */
16551
15981
  get state() {
16552
15982
  return this.readOnlyStateStore;
16553
15983
  }
15984
+ /**
15985
+ * Connects the given user to the client.
15986
+ * Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
15987
+ * If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
15988
+ *
15989
+ * @param user the user to connect.
15990
+ * @param token a token or a function that returns a token.
15991
+ */
15992
+ async connectUser(user, token) {
15993
+ if (user.type === 'anonymous') {
15994
+ user.id = '!anon';
15995
+ return this.connectAnonymousUser(user, token);
15996
+ }
15997
+ let connectUser = () => {
15998
+ return this.streamClient.connectUser(user, token);
15999
+ };
16000
+ if (user.type === 'guest') {
16001
+ connectUser = async () => {
16002
+ return this.streamClient.connectGuestUser(user);
16003
+ };
16004
+ }
16005
+ this.connectionPromise = this.disconnectionPromise
16006
+ ? this.disconnectionPromise.then(() => connectUser())
16007
+ : connectUser();
16008
+ this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
16009
+ const connectUserResponse = await this.connectionPromise;
16010
+ // connectUserResponse will be void if connectUser called twice for the same user
16011
+ if (connectUserResponse?.me) {
16012
+ this.writeableStateStore.setConnectedUser(connectUserResponse.me);
16013
+ }
16014
+ this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
16015
+ if (event.online) {
16016
+ const callsToReWatch = this.writeableStateStore.calls
16017
+ .filter((call) => call.watching)
16018
+ .map((call) => call.cid);
16019
+ this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
16020
+ if (callsToReWatch.length > 0) {
16021
+ this.queryCalls({
16022
+ watch: true,
16023
+ filter_conditions: {
16024
+ cid: { $in: callsToReWatch },
16025
+ },
16026
+ sort: [{ field: 'cid', direction: 1 }],
16027
+ }).catch((err) => {
16028
+ this.logger('error', 'Failed to re-watch calls', err);
16029
+ });
16030
+ }
16031
+ }
16032
+ }));
16033
+ this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
16034
+ const { call, members } = event;
16035
+ if (user.id === call.created_by.id) {
16036
+ this.logger('warn', 'Received `call.created` sent by the current user');
16037
+ return;
16038
+ }
16039
+ this.logger('info', `New call created and registered: ${call.cid}`);
16040
+ const newCall = new Call({
16041
+ streamClient: this.streamClient,
16042
+ type: call.type,
16043
+ id: call.id,
16044
+ members,
16045
+ clientStore: this.writeableStateStore,
16046
+ });
16047
+ newCall.state.updateFromCallResponse(call);
16048
+ this.writeableStateStore.registerCall(newCall);
16049
+ }));
16050
+ this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
16051
+ const { call, members } = event;
16052
+ if (user.id === call.created_by.id) {
16053
+ this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
16054
+ return;
16055
+ }
16056
+ // The call might already be tracked by the client,
16057
+ // if `call.created` was received before `call.ring`.
16058
+ // In that case, we cleanup the already tracked call.
16059
+ const prevCall = this.writeableStateStore.findCall(call.type, call.id);
16060
+ await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
16061
+ // we create a new call
16062
+ const theCall = new Call({
16063
+ streamClient: this.streamClient,
16064
+ type: call.type,
16065
+ id: call.id,
16066
+ members,
16067
+ clientStore: this.writeableStateStore,
16068
+ ringing: true,
16069
+ });
16070
+ theCall.state.updateFromCallResponse(call);
16071
+ // we fetch the latest metadata for the call from the server
16072
+ await theCall.get();
16073
+ this.writeableStateStore.registerCall(theCall);
16074
+ }));
16075
+ return connectUserResponse;
16076
+ }
16077
+ /**
16078
+ * addDevice - Adds a push device for a user.
16079
+ *
16080
+ * @param {string} id the device id
16081
+ * @param {string} push_provider the push provider name (eg. apn, firebase)
16082
+ * @param {string} push_provider_name user provided push provider name
16083
+ * @param {string} [userID] the user id (defaults to current user)
16084
+ */
16085
+ async addVoipDevice(id, push_provider, push_provider_name, userID) {
16086
+ return await this.addDevice(id, push_provider, push_provider_name, userID, true);
16087
+ }
16554
16088
  }
16089
+ StreamVideoClient._instanceMap = new Map();
16555
16090
 
16556
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, BlockListOptionsBehaviorEnum, browsers as Browsers, Call, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, ChannelConfigWithInfoAutomodBehaviorEnum, ChannelConfigWithInfoAutomodEnum, ChannelConfigWithInfoBlocklistBehaviorEnum, CreateDeviceRequestPushProviderEnum, DebounceType, DynascaleManager, ErrorFromResponse, InputMediaDeviceManager, InputMediaDeviceManagerState, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, models as SfuModels, SpeakerManager, SpeakerState, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceInfo, getLogLevel, getLogger, getOSInfo, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio, hasScreenShare, hasScreenShareAudio, hasVideo, isPinned, livestreamOrAudioRoomSortPreset, logLevels, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, role, screenSharing, setDeviceInfo, setLogLevel, setLogger, setOSInfo, setSdkInfo, setWebRTCInfo, speakerLayoutSortPreset, speaking };
16091
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, BlockListOptionsBehaviorEnum, browsers as Browsers, Call, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, ChannelConfigWithInfoAutomodBehaviorEnum, ChannelConfigWithInfoAutomodEnum, ChannelConfigWithInfoBlocklistBehaviorEnum, CreateDeviceRequestPushProviderEnum, DebounceType, DynascaleManager, ErrorFromResponse, InputMediaDeviceManager, InputMediaDeviceManagerState, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, models as SfuModels, SpeakerManager, SpeakerState, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceInfo, getLogger, getOSInfo, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio, hasScreenShare, hasScreenShareAudio, hasVideo, isPinned, livestreamOrAudioRoomSortPreset, logLevels, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, role, screenSharing, setDeviceInfo, setLogLevel, setLogger, setOSInfo, setSdkInfo, setWebRTCInfo, speakerLayoutSortPreset, speaking };
16557
16092
  //# sourceMappingURL=index.browser.es.js.map