@stream-io/video-client 1.5.0 → 1.6.0-0

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