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