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