livekit-client 1.9.0 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
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