livekit-client 1.9.0 → 1.9.2

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.
package/README.md CHANGED
@@ -45,13 +45,12 @@ await room.connect(...);
45
45
 
46
46
  ```typescript
47
47
  import {
48
- connect,
49
- Room,
50
- RoomEvent,
48
+ Participant,
51
49
  RemoteParticipant,
52
- RemoteTrackPublication,
53
50
  RemoteTrack,
54
- Participant,
51
+ RemoteTrackPublication,
52
+ Room,
53
+ RoomEvent,
55
54
  } from 'livekit-client';
56
55
 
57
56
  // creates a new room with options
@@ -296,7 +295,9 @@ You can have a look at the `"browerslist"` section of `package.json` for more de
296
295
  If you are targeting legacy browsers, but still want adaptiveStream functionality you'll likely need to use polyfills for [ResizeObserver](https://www.npmjs.com/package/resize-observer-polyfill) and [IntersectionObserver](https://www.npmjs.com/package/intersection-observer).
297
296
 
298
297
  <!--BEGIN_REPO_NAV-->
298
+
299
299
  <br/><table>
300
+
300
301
  <thead><tr><th colspan="2">LiveKit Ecosystem</th></tr></thead>
301
302
  <tbody>
302
303
  <tr><td>Client SDKs</td><td><a href="https://github.com/livekit/components-js">Components</a> · <b>JavaScript</b> · <a href="https://github.com/livekit/client-sdk-rust">Rust</a> · <a href="https://github.com/livekit/client-sdk-swift">iOS/macOS</a> · <a href="https://github.com/livekit/client-sdk-android">Android</a> · <a href="https://github.com/livekit/client-sdk-flutter">Flutter</a> · <a href="https://github.com/livekit/client-sdk-unity-web">Unity (web)</a> · <a href="https://github.com/livekit/client-sdk-react-native">React Native (beta)</a></td></tr><tr></tr>
@@ -14940,7 +14940,7 @@ var uaParser = {exports: {}};
14940
14940
  var uaParserExports = uaParser.exports;
14941
14941
  var UAParser = /*@__PURE__*/getDefaultExportFromCjs(uaParserExports);
14942
14942
 
14943
- var version$1 = "1.9.0";
14943
+ var version$1 = "1.9.2";
14944
14944
 
14945
14945
  const version = version$1;
14946
14946
  const protocolVersion = 9;
@@ -15037,6 +15037,7 @@ function getNewAudioContext() {
15037
15037
  }
15038
15038
 
15039
15039
  const separator = '|';
15040
+ const ddExtensionURI = 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension';
15040
15041
  function unpackStreamId(packed) {
15041
15042
  const parts = packed.split(separator);
15042
15043
  if (parts.length > 1) {
@@ -15064,7 +15065,6 @@ function supportsDynacast() {
15064
15065
  function supportsAV1() {
15065
15066
  const capabilities = RTCRtpReceiver.getCapabilities('video');
15066
15067
  let hasAV1 = false;
15067
- let hasDDExt = false;
15068
15068
  if (capabilities) {
15069
15069
  for (const codec of capabilities.codecs) {
15070
15070
  if (codec.mimeType === 'video/AV1') {
@@ -15072,14 +15072,24 @@ function supportsAV1() {
15072
15072
  break;
15073
15073
  }
15074
15074
  }
15075
- for (const ext of capabilities.headerExtensions) {
15076
- if (ext.uri === 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension') {
15077
- hasDDExt = true;
15075
+ }
15076
+ return hasAV1;
15077
+ }
15078
+ function supportsVP9() {
15079
+ const capabilities = RTCRtpReceiver.getCapabilities('video');
15080
+ let hasVP9 = false;
15081
+ if (capabilities) {
15082
+ for (const codec of capabilities.codecs) {
15083
+ if (codec.mimeType === 'video/VP9') {
15084
+ hasVP9 = true;
15078
15085
  break;
15079
15086
  }
15080
15087
  }
15081
15088
  }
15082
- return hasAV1 && hasDDExt;
15089
+ return hasVP9;
15090
+ }
15091
+ function isSVCCodec(codec) {
15092
+ return codec === 'av1' || codec === 'vp9';
15083
15093
  }
15084
15094
  function supportsSetSinkId(elm) {
15085
15095
  if (!document) {
@@ -15499,13 +15509,7 @@ class SignalClient {
15499
15509
  this.handleSignalResponse(resp);
15500
15510
  };
15501
15511
  this.ws.onclose = ev => {
15502
- if (!this.isConnected) return;
15503
- livekitLogger.debug("websocket connection closed: ".concat(ev.reason));
15504
- this.isConnected = false;
15505
- if (this.onClose) {
15506
- this.onClose(ev.reason);
15507
- }
15508
- this.ws = undefined;
15512
+ this.handleOnClose(ev.reason);
15509
15513
  };
15510
15514
  });
15511
15515
  }
@@ -15774,6 +15778,17 @@ class SignalClient {
15774
15778
  }
15775
15779
  this.isReconnecting = false;
15776
15780
  }
15781
+ handleOnClose(reason) {
15782
+ if (!this.isConnected) return;
15783
+ this.clearPingInterval();
15784
+ this.clearPingTimeout();
15785
+ livekitLogger.debug("websocket connection closed: ".concat(reason));
15786
+ this.isConnected = false;
15787
+ if (this.onClose) {
15788
+ this.onClose(reason);
15789
+ }
15790
+ this.ws = undefined;
15791
+ }
15777
15792
  handleWSError(ev) {
15778
15793
  livekitLogger.error('websocket error', ev);
15779
15794
  }
@@ -15789,9 +15804,7 @@ class SignalClient {
15789
15804
  }
15790
15805
  this.pingTimeout = CriticalTimers.setTimeout(() => {
15791
15806
  livekitLogger.warn("ping timeout triggered. last pong received at: ".concat(new Date(Date.now() - this.pingTimeoutDuration * 1000).toUTCString()));
15792
- if (this.onClose) {
15793
- this.onClose('ping timeout');
15794
- }
15807
+ this.handleOnClose('ping timeout');
15795
15808
  }, this.pingTimeoutDuration * 1000);
15796
15809
  }
15797
15810
  /**
@@ -16763,6 +16776,7 @@ class PCTransport extends EventEmitter$1 {
16763
16776
  if (media.type === 'audio') {
16764
16777
  ensureAudioNackAndStereo(media, [], []);
16765
16778
  } else if (media.type === 'video') {
16779
+ ensureVideoDDExtensionForSVC(media);
16766
16780
  // mung sdp for codec bitrate setting that can't apply by sendEncoding
16767
16781
  this.trackBitrates.some(trackbr => {
16768
16782
  if (!media.msid || !media.msid.includes(trackbr.sid)) {
@@ -16780,6 +16794,9 @@ class PCTransport extends EventEmitter$1 {
16780
16794
  if (codecPayload > 0) {
16781
16795
  if (!media.fmtp.some(fmtp => {
16782
16796
  if (fmtp.payload === codecPayload) {
16797
+ if (!fmtp.config.includes('x-google-start-bitrate')) {
16798
+ fmtp.config += ";x-google-start-bitrate=".concat(trackbr.maxbr * 0.7);
16799
+ }
16783
16800
  if (!fmtp.config.includes('x-google-max-bitrate')) {
16784
16801
  fmtp.config += ";x-google-max-bitrate=".concat(trackbr.maxbr);
16785
16802
  }
@@ -16789,7 +16806,7 @@ class PCTransport extends EventEmitter$1 {
16789
16806
  })) {
16790
16807
  media.fmtp.push({
16791
16808
  payload: codecPayload,
16792
- config: "x-google-max-bitrate=".concat(trackbr.maxbr)
16809
+ config: "x-google-start-bitrate=".concat(trackbr.maxbr * 0.7, ";x-google-max-bitrate=").concat(trackbr.maxbr)
16793
16810
  });
16794
16811
  }
16795
16812
  }
@@ -16887,6 +16904,29 @@ function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
16887
16904
  }
16888
16905
  }
16889
16906
  }
16907
+ function ensureVideoDDExtensionForSVC(media) {
16908
+ var _a, _b, _c, _d;
16909
+ const codec = (_b = (_a = media.rtp.at(0)) === null || _a === void 0 ? void 0 : _a.codec) === null || _b === void 0 ? void 0 : _b.toLowerCase();
16910
+ if (!isSVCCodec(codec)) {
16911
+ return;
16912
+ }
16913
+ let maxID = 0;
16914
+ const ddFound = (_c = media.ext) === null || _c === void 0 ? void 0 : _c.some(ext => {
16915
+ if (ext.uri === ddExtensionURI) {
16916
+ return true;
16917
+ }
16918
+ if (ext.value > maxID) {
16919
+ maxID = ext.value;
16920
+ }
16921
+ return false;
16922
+ });
16923
+ if (!ddFound) {
16924
+ (_d = media.ext) === null || _d === void 0 ? void 0 : _d.push({
16925
+ value: maxID + 1,
16926
+ uri: ddExtensionURI
16927
+ });
16928
+ }
16929
+ }
16890
16930
  function extractStereoAndNackAudioFromOffer(offer) {
16891
16931
  var _a;
16892
16932
  const stereoMids = [];
@@ -16996,6 +17036,9 @@ const backupCodecs = ['vp8', 'h264'];
16996
17036
  function isBackupCodec(codec) {
16997
17037
  return !!backupCodecs.find(backup => backup === codec);
16998
17038
  }
17039
+ function isCodecEqual(c1, c2) {
17040
+ return (c1 === null || c1 === void 0 ? void 0 : c1.toLowerCase().replace(/audio\/|video\//y, '')) === (c2 === null || c2 === void 0 ? void 0 : c2.toLowerCase().replace(/audio\/|video\//y, ''));
17041
+ }
16999
17042
  var AudioPresets;
17000
17043
  (function (AudioPresets) {
17001
17044
  AudioPresets.telephone = {
@@ -18531,7 +18574,7 @@ class RTCEngine extends eventsExports.EventEmitter {
18531
18574
  } else {
18532
18575
  livekitLogger.info("could not recover connection after ".concat(this.reconnectAttempts, " attempts, ").concat(Date.now() - this.reconnectStart, "ms. giving up"));
18533
18576
  this.emit(EngineEvent.Disconnected);
18534
- this.close();
18577
+ await this.close();
18535
18578
  }
18536
18579
  } finally {
18537
18580
  this.attemptingReconnect = false;
@@ -18752,6 +18795,30 @@ class RTCEngine extends eventsExports.EventEmitter {
18752
18795
  async ensurePublisherConnected(kind) {
18753
18796
  await this.ensureDataTransportConnected(kind, false);
18754
18797
  }
18798
+ /* @internal */
18799
+ verifyTransport() {
18800
+ // primary connection
18801
+ if (!this.primaryPC) {
18802
+ return false;
18803
+ }
18804
+ if (this.primaryPC.connectionState === 'closed' || this.primaryPC.connectionState === 'failed') {
18805
+ return false;
18806
+ }
18807
+ // also verify publisher connection if it's needed or different
18808
+ if (this.hasPublished && this.subscriberPrimary) {
18809
+ if (!this.publisher) {
18810
+ return false;
18811
+ }
18812
+ if (this.publisher.pc.connectionState === 'closed' || this.publisher.pc.connectionState === 'failed') {
18813
+ return false;
18814
+ }
18815
+ }
18816
+ // ensure signal is connected
18817
+ if (!this.client.ws || this.client.ws.readyState === WebSocket.CLOSED) {
18818
+ return false;
18819
+ }
18820
+ return true;
18821
+ }
18755
18822
  /** @internal */
18756
18823
  negotiate() {
18757
18824
  // observe signal state
@@ -21064,7 +21131,7 @@ function computeVideoEncodings(isScreenShare, width, height, options) {
21064
21131
  livekitLogger.debug('using video encoding', videoEncoding);
21065
21132
  }
21066
21133
  const original = new VideoPreset(width, height, videoEncoding.maxBitrate, videoEncoding.maxFramerate);
21067
- if (scalabilityMode && videoCodec === 'av1') {
21134
+ if (scalabilityMode && isSVCCodec(videoCodec)) {
21068
21135
  livekitLogger.debug("using svc with scalabilityMode ".concat(scalabilityMode));
21069
21136
  const encodings = [];
21070
21137
  // svc use first encoding as the original, so we sort encoding from high to low
@@ -21164,8 +21231,13 @@ function determineAppropriateEncoding(isScreenShare, width, height, codec) {
21164
21231
  if (codec) {
21165
21232
  switch (codec) {
21166
21233
  case 'av1':
21234
+ encoding = _objectSpread2({}, encoding);
21167
21235
  encoding.maxBitrate = encoding.maxBitrate * 0.7;
21168
21236
  break;
21237
+ case 'vp9':
21238
+ encoding = _objectSpread2({}, encoding);
21239
+ encoding.maxBitrate = encoding.maxBitrate * 0.85;
21240
+ break;
21169
21241
  }
21170
21242
  }
21171
21243
  return encoding;
@@ -21205,13 +21277,15 @@ function encodingsFromPresets(width, height, presets) {
21205
21277
  }
21206
21278
  const size = Math.min(width, height);
21207
21279
  const rid = videoRids[idx];
21208
- encodings.push({
21280
+ const encoding = {
21209
21281
  rid,
21210
21282
  scaleResolutionDownBy: Math.max(1, size / Math.min(preset.width, preset.height)),
21211
- maxBitrate: preset.encoding.maxBitrate,
21212
- /* @ts-ignore */
21213
- maxFramerate: preset.encoding.maxFramerate
21214
- });
21283
+ maxBitrate: preset.encoding.maxBitrate
21284
+ };
21285
+ if (preset.encoding.maxFramerate) {
21286
+ encoding.maxFramerate = preset.encoding.maxFramerate;
21287
+ }
21288
+ encodings.push(encoding);
21215
21289
  });
21216
21290
  return encodings;
21217
21291
  }
@@ -21784,10 +21858,13 @@ class LocalParticipant extends Participant {
21784
21858
  // we frequently get no data on layer 0 when enabled
21785
21859
  opts.simulcast = false;
21786
21860
  }
21787
- // require full AV1 SVC support prior to using it
21861
+ // require full AV1/VP9 SVC support prior to using it
21788
21862
  if (opts.videoCodec === 'av1' && !supportsAV1()) {
21789
21863
  opts.videoCodec = undefined;
21790
21864
  }
21865
+ if (opts.videoCodec === 'vp9' && !supportsVP9()) {
21866
+ opts.videoCodec = undefined;
21867
+ }
21791
21868
  // handle track actions
21792
21869
  track.on(TrackEvent.Muted, this.onTrackMuted);
21793
21870
  track.on(TrackEvent.Unmuted, this.onTrackUnmuted);
@@ -21825,7 +21902,7 @@ class LocalParticipant extends Participant {
21825
21902
  req.height = dims.height;
21826
21903
  // for svc codecs, disable simulcast and use vp8 for backup codec
21827
21904
  if (track instanceof LocalVideoTrack) {
21828
- if ((opts === null || opts === void 0 ? void 0 : opts.videoCodec) === 'av1') {
21905
+ if (isSVCCodec(opts.videoCodec)) {
21829
21906
  // set scalabilityMode to 'L3T3' by default
21830
21907
  opts.scalabilityMode = (_c = opts.scalabilityMode) !== null && _c !== void 0 ? _c : 'L3T3';
21831
21908
  }
@@ -21856,6 +21933,28 @@ class LocalParticipant extends Participant {
21856
21933
  throw new UnexpectedConnectionState('cannot publish track when not connected');
21857
21934
  }
21858
21935
  const ti = await this.engine.addTrack(req);
21936
+ let primaryCodecSupported = false;
21937
+ let backupCodecSupported = false;
21938
+ ti.codecs.forEach(c => {
21939
+ if (isCodecEqual(c.mimeType, opts.videoCodec)) {
21940
+ primaryCodecSupported = true;
21941
+ } else if (opts.backupCodec && isCodecEqual(c.mimeType, opts.backupCodec.codec)) {
21942
+ backupCodecSupported = true;
21943
+ }
21944
+ });
21945
+ if (req.simulcastCodecs.length > 0) {
21946
+ if (!primaryCodecSupported && !backupCodecSupported) {
21947
+ throw Error('cannot publish track, codec not supported by server');
21948
+ }
21949
+ if (!primaryCodecSupported && opts.backupCodec) {
21950
+ const backupCodec = opts.backupCodec;
21951
+ opts = _objectSpread2({}, opts);
21952
+ livekitLogger.debug("primary codec ".concat(opts.videoCodec, " not supported, fallback to ").concat(backupCodec.codec));
21953
+ opts.videoCodec = backupCodec.codec;
21954
+ opts.videoEncoding = backupCodec.encoding;
21955
+ encodings = simEncodings;
21956
+ }
21957
+ }
21859
21958
  const publication = new LocalTrackPublication(track.kind, ti, track);
21860
21959
  // save options for when it needs to be republished again
21861
21960
  publication.options = opts;
@@ -21869,7 +21968,7 @@ class LocalParticipant extends Participant {
21869
21968
  });
21870
21969
  // store RTPSender
21871
21970
  track.sender = await this.engine.createSender(track, opts, encodings);
21872
- if (track.codec === 'av1' && encodings && ((_d = encodings[0]) === null || _d === void 0 ? void 0 : _d.maxBitrate)) {
21971
+ if (track.codec && isSVCCodec(track.codec) && encodings && ((_d = encodings[0]) === null || _d === void 0 ? void 0 : _d.maxBitrate)) {
21873
21972
  this.engine.publisher.setTrackCodecBitrate(req.cid, track.codec, encodings[0].maxBitrate / 1000);
21874
21973
  }
21875
21974
  this.engine.negotiate();
@@ -22170,6 +22269,7 @@ var ConnectionState;
22170
22269
  ConnectionState["Connected"] = "connected";
22171
22270
  ConnectionState["Reconnecting"] = "reconnecting";
22172
22271
  })(ConnectionState || (ConnectionState = {}));
22272
+ const connectionReconcileFrequency = 2 * 1000;
22173
22273
  /** @deprecated RoomState has been renamed to [[ConnectionState]] */
22174
22274
  const RoomState = ConnectionState;
22175
22275
  /**
@@ -22349,6 +22449,7 @@ class Room extends eventsExports.EventEmitter {
22349
22449
  }
22350
22450
  this.setAndEmitConnectionState(ConnectionState.Connected);
22351
22451
  this.emit(RoomEvent.Connected);
22452
+ this.registerConnectionReconcile();
22352
22453
  };
22353
22454
  /**
22354
22455
  * disconnects the room, emits [[RoomEvent.Disconnected]]
@@ -22392,6 +22493,7 @@ class Room extends eventsExports.EventEmitter {
22392
22493
  await this.disconnect();
22393
22494
  };
22394
22495
  this.handleRestarting = () => {
22496
+ this.clearConnectionReconcile();
22395
22497
  // also unwind existing participants & existing subscriptions
22396
22498
  for (const p of this.participants.values()) {
22397
22499
  this.handleParticipantDisconnected(p.sid, p);
@@ -22404,6 +22506,7 @@ class Room extends eventsExports.EventEmitter {
22404
22506
  livekitLogger.debug("signal reconnected to server", {
22405
22507
  region: joinResponse.serverRegion
22406
22508
  });
22509
+ this.cachedParticipantSids = [];
22407
22510
  this.applyJoinResponse(joinResponse);
22408
22511
  try {
22409
22512
  // unpublish & republish tracks
@@ -22447,6 +22550,7 @@ class Room extends eventsExports.EventEmitter {
22447
22550
  }
22448
22551
  this.setAndEmitConnectionState(ConnectionState.Connected);
22449
22552
  this.emit(RoomEvent.Reconnected);
22553
+ this.registerConnectionReconcile();
22450
22554
  // emit participant connected events after connection has been re-established
22451
22555
  this.participants.forEach(participant => {
22452
22556
  this.emit(RoomEvent.ParticipantConnected, participant);
@@ -22647,6 +22751,7 @@ class Room extends eventsExports.EventEmitter {
22647
22751
  };
22648
22752
  this.setMaxListeners(100);
22649
22753
  this.participants = new Map();
22754
+ this.cachedParticipantSids = [];
22650
22755
  this.identityToSid = new Map();
22651
22756
  this.options = _objectSpread2(_objectSpread2({}, roomOptionDefaults), options);
22652
22757
  this.options.audioCaptureDefaults = _objectSpread2(_objectSpread2({}, audioDefaults), options === null || options === void 0 ? void 0 : options.audioCaptureDefaults);
@@ -22687,7 +22792,7 @@ class Room extends eventsExports.EventEmitter {
22687
22792
  return (_b = (_a = this.roomInfo) === null || _a === void 0 ? void 0 : _a.numPublishers) !== null && _b !== void 0 ? _b : 0;
22688
22793
  }
22689
22794
  maybeCreateEngine() {
22690
- if (this.engine) {
22795
+ if (this.engine && !this.engine.isClosed) {
22691
22796
  return;
22692
22797
  }
22693
22798
  this.engine = new RTCEngine(this.options);
@@ -22702,13 +22807,20 @@ class Room extends eventsExports.EventEmitter {
22702
22807
  }).on(EngineEvent.Disconnected, reason => {
22703
22808
  this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, reason);
22704
22809
  }).on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate).on(EngineEvent.DataPacketReceived, this.handleDataPacket).on(EngineEvent.Resuming, () => {
22810
+ this.clearConnectionReconcile();
22705
22811
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
22706
22812
  this.emit(RoomEvent.Reconnecting);
22707
22813
  }
22814
+ this.cachedParticipantSids = Array.from(this.participants.keys());
22708
22815
  }).on(EngineEvent.Resumed, () => {
22709
22816
  this.setAndEmitConnectionState(ConnectionState.Connected);
22710
22817
  this.emit(RoomEvent.Reconnected);
22818
+ this.registerConnectionReconcile();
22711
22819
  this.updateSubscriptions();
22820
+ // once reconnected, figure out if any participants connected during reconnect and emit events for it
22821
+ const diffParticipants = Array.from(this.participants.values()).filter(p => !this.cachedParticipantSids.includes(p.sid));
22822
+ diffParticipants.forEach(p => this.emit(RoomEvent.ParticipantConnected, p));
22823
+ this.cachedParticipantSids = [];
22712
22824
  }).on(EngineEvent.SignalResumed, () => {
22713
22825
  if (this.state === ConnectionState.Reconnecting) {
22714
22826
  this.sendSyncState();
@@ -23014,41 +23126,45 @@ class Room extends eventsExports.EventEmitter {
23014
23126
  let shouldStopTracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
23015
23127
  let reason = arguments.length > 1 ? arguments[1] : undefined;
23016
23128
  var _a;
23129
+ this.clearConnectionReconcile();
23017
23130
  if (this.state === ConnectionState.Disconnected) {
23018
23131
  return;
23019
23132
  }
23020
- this.participants.forEach(p => {
23021
- p.tracks.forEach(pub => {
23022
- p.unpublishTrack(pub.trackSid);
23133
+ try {
23134
+ this.participants.forEach(p => {
23135
+ p.tracks.forEach(pub => {
23136
+ p.unpublishTrack(pub.trackSid);
23137
+ });
23023
23138
  });
23024
- });
23025
- this.localParticipant.tracks.forEach(pub => {
23026
- var _a, _b;
23027
- if (pub.track) {
23028
- this.localParticipant.unpublishTrack(pub.track, shouldStopTracks);
23029
- }
23030
- if (shouldStopTracks) {
23031
- (_a = pub.track) === null || _a === void 0 ? void 0 : _a.detach();
23032
- (_b = pub.track) === null || _b === void 0 ? void 0 : _b.stop();
23139
+ this.localParticipant.tracks.forEach(pub => {
23140
+ var _a, _b;
23141
+ if (pub.track) {
23142
+ this.localParticipant.unpublishTrack(pub.track, shouldStopTracks);
23143
+ }
23144
+ if (shouldStopTracks) {
23145
+ (_a = pub.track) === null || _a === void 0 ? void 0 : _a.detach();
23146
+ (_b = pub.track) === null || _b === void 0 ? void 0 : _b.stop();
23147
+ }
23148
+ });
23149
+ this.localParticipant.off(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged).off(ParticipantEvent.ParticipantNameChanged, this.onLocalParticipantNameChanged).off(ParticipantEvent.TrackMuted, this.onLocalTrackMuted).off(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted).off(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished).off(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished).off(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged).off(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError).off(ParticipantEvent.ParticipantPermissionsChanged, this.onLocalParticipantPermissionsChanged);
23150
+ this.localParticipant.tracks.clear();
23151
+ this.localParticipant.videoTracks.clear();
23152
+ this.localParticipant.audioTracks.clear();
23153
+ this.participants.clear();
23154
+ this.activeSpeakers = [];
23155
+ if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
23156
+ this.audioContext.close();
23157
+ this.audioContext = undefined;
23158
+ }
23159
+ if (isWeb()) {
23160
+ window.removeEventListener('beforeunload', this.onPageLeave);
23161
+ window.removeEventListener('pagehide', this.onPageLeave);
23162
+ (_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.removeEventListener('devicechange', this.handleDeviceChange);
23033
23163
  }
23034
- });
23035
- this.localParticipant.off(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged).off(ParticipantEvent.ParticipantNameChanged, this.onLocalParticipantNameChanged).off(ParticipantEvent.TrackMuted, this.onLocalTrackMuted).off(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted).off(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished).off(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished).off(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged).off(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError).off(ParticipantEvent.ParticipantPermissionsChanged, this.onLocalParticipantPermissionsChanged);
23036
- this.localParticipant.tracks.clear();
23037
- this.localParticipant.videoTracks.clear();
23038
- this.localParticipant.audioTracks.clear();
23039
- this.participants.clear();
23040
- this.activeSpeakers = [];
23041
- if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
23042
- this.audioContext.close();
23043
- this.audioContext = undefined;
23044
- }
23045
- if (isWeb()) {
23046
- window.removeEventListener('beforeunload', this.onPageLeave);
23047
- window.removeEventListener('pagehide', this.onPageLeave);
23048
- (_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.removeEventListener('devicechange', this.handleDeviceChange);
23164
+ } finally {
23165
+ this.setAndEmitConnectionState(ConnectionState.Disconnected);
23166
+ this.emit(RoomEvent.Disconnected, reason);
23049
23167
  }
23050
- this.setAndEmitConnectionState(ConnectionState.Disconnected);
23051
- this.emit(RoomEvent.Disconnected, reason);
23052
23168
  }
23053
23169
  handleParticipantDisconnected(sid, participant) {
23054
23170
  // remove and send event
@@ -23201,6 +23317,32 @@ class Room extends eventsExports.EventEmitter {
23201
23317
  }
23202
23318
  }
23203
23319
  }
23320
+ registerConnectionReconcile() {
23321
+ this.clearConnectionReconcile();
23322
+ let consecutiveFailures = 0;
23323
+ this.connectionReconcileInterval = CriticalTimers.setInterval(() => {
23324
+ if (
23325
+ // ensure we didn't tear it down
23326
+ !this.engine ||
23327
+ // engine detected close, but Room missed it
23328
+ this.engine.isClosed ||
23329
+ // transports failed without notifying engine
23330
+ !this.engine.verifyTransport()) {
23331
+ consecutiveFailures++;
23332
+ livekitLogger.warn('detected connection state mismatch', {
23333
+ numFailures: consecutiveFailures
23334
+ });
23335
+ if (consecutiveFailures >= 3) this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, DisconnectReason.UNKNOWN_REASON);
23336
+ } else {
23337
+ consecutiveFailures = 0;
23338
+ }
23339
+ }, connectionReconcileFrequency);
23340
+ }
23341
+ clearConnectionReconcile() {
23342
+ if (this.connectionReconcileInterval) {
23343
+ CriticalTimers.clearInterval(this.connectionReconcileInterval);
23344
+ }
23345
+ }
23204
23346
  setAndEmitConnectionState(state) {
23205
23347
  if (state === this.state) {
23206
23348
  // unchanged
@@ -23784,5 +23926,5 @@ class ConnectionCheck extends EventEmitter$1 {
23784
23926
  }
23785
23927
  }
23786
23928
 
23787
- export { AudioPresets, ConnectionCheck, ConnectionError, ConnectionQuality, ConnectionState, CriticalTimers, DataPacket_Kind, DefaultReconnectPolicy, DeviceUnsupportedError, DisconnectReason, EngineEvent, LivekitError, LocalAudioTrack, LocalParticipant, LocalTrack, LocalTrackPublication, LocalVideoTrack, LogLevel, MediaDeviceFailure, NegotiationError, Participant, ParticipantEvent, PublishDataError, RemoteAudioTrack, RemoteParticipant, RemoteTrack, RemoteTrackPublication, RemoteVideoTrack, Room, RoomEvent, RoomState, ScreenSharePresets, Track, TrackEvent, TrackInvalidError, TrackPublication, UnexpectedConnectionState, UnsupportedServer, VideoPreset, VideoPresets, VideoPresets43, VideoQuality, attachToElement, createAudioAnalyser, createLocalAudioTrack, createLocalScreenTracks, createLocalTracks, createLocalVideoTrack, detachTrack, getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isBackupCodec, isBrowserSupported, protocolVersion, setLogExtension, setLogLevel, supportsAV1, supportsAdaptiveStream, supportsDynacast, version };
23929
+ export { AudioPresets, ConnectionCheck, ConnectionError, ConnectionQuality, ConnectionState, CriticalTimers, DataPacket_Kind, DefaultReconnectPolicy, DeviceUnsupportedError, DisconnectReason, EngineEvent, LivekitError, LocalAudioTrack, LocalParticipant, LocalTrack, LocalTrackPublication, LocalVideoTrack, LogLevel, MediaDeviceFailure, NegotiationError, Participant, ParticipantEvent, PublishDataError, RemoteAudioTrack, RemoteParticipant, RemoteTrack, RemoteTrackPublication, RemoteVideoTrack, Room, RoomEvent, RoomState, ScreenSharePresets, Track, TrackEvent, TrackInvalidError, TrackPublication, UnexpectedConnectionState, UnsupportedServer, VideoPreset, VideoPresets, VideoPresets43, VideoQuality, attachToElement, createAudioAnalyser, createLocalAudioTrack, createLocalScreenTracks, createLocalTracks, createLocalVideoTrack, detachTrack, getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isBackupCodec, isBrowserSupported, isCodecEqual, protocolVersion, setLogExtension, setLogLevel, supportsAV1, supportsAdaptiveStream, supportsDynacast, version };
23788
23930
  //# sourceMappingURL=livekit-client.esm.mjs.map