livekit-client 2.21.0 → 2.22.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 (70) hide show
  1. package/README.md +10 -10
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +2 -1
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +480 -150
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +2 -1
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/room/PCTransport.d.ts +49 -2
  15. package/dist/src/room/PCTransport.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts +20 -1
  17. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  18. package/dist/src/room/Room.d.ts +2 -0
  19. package/dist/src/room/Room.d.ts.map +1 -1
  20. package/dist/src/room/errors.d.ts.map +1 -1
  21. package/dist/src/room/participant/LocalParticipant.d.ts +1 -1
  22. package/dist/src/room/token-source/TokenSource.d.ts +19 -10
  23. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  24. package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
  25. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  26. package/dist/src/room/track/RemoteTrack.d.ts +3 -0
  27. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  28. package/dist/src/room/track/Track.d.ts.map +1 -1
  29. package/dist/src/room/utils.d.ts +15 -0
  30. package/dist/src/room/utils.d.ts.map +1 -1
  31. package/dist/src/test/promiseState.d.ts +12 -0
  32. package/dist/src/test/promiseState.d.ts.map +1 -0
  33. package/dist/src/test/signalToken.d.ts.map +1 -1
  34. package/dist/src/utils/AsyncQueue.d.ts +3 -3
  35. package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
  36. package/dist/ts4.2/room/PCTransport.d.ts +49 -2
  37. package/dist/ts4.2/room/RTCEngine.d.ts +20 -1
  38. package/dist/ts4.2/room/Room.d.ts +2 -0
  39. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
  40. package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
  41. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
  42. package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
  43. package/dist/ts4.2/room/utils.d.ts +15 -0
  44. package/dist/ts4.2/test/promiseState.d.ts +12 -0
  45. package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
  46. package/package.json +13 -12
  47. package/src/api/SignalClient.e2e.test.ts +16 -9
  48. package/src/e2ee/utils.ts +1 -1
  49. package/src/room/PCTransport.test.ts +243 -1
  50. package/src/room/PCTransport.ts +181 -80
  51. package/src/room/RTCEngine.test.ts +339 -2
  52. package/src/room/RTCEngine.ts +152 -11
  53. package/src/room/Room.test.ts +134 -3
  54. package/src/room/Room.ts +52 -17
  55. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +1 -1
  56. package/src/room/data-stream/incoming/StreamReader.ts +1 -1
  57. package/src/room/errors.ts +1 -2
  58. package/src/room/token-source/TokenSource.ts +25 -12
  59. package/src/room/track/LocalVideoTrack.test.ts +105 -2
  60. package/src/room/track/LocalVideoTrack.ts +36 -10
  61. package/src/room/track/RemoteTrack.test.ts +144 -0
  62. package/src/room/track/RemoteTrack.ts +39 -12
  63. package/src/room/track/Track.ts +2 -1
  64. package/src/room/utils.test.ts +71 -2
  65. package/src/room/utils.ts +52 -0
  66. package/src/test/promiseState.ts +23 -0
  67. package/src/test/signalServerSetup.ts +2 -1
  68. package/src/test/signalToken.ts +17 -13
  69. package/src/type-polyfills/header-extensions.d.ts +13 -0
  70. package/src/utils/AsyncQueue.ts +3 -3
@@ -12160,7 +12160,7 @@ function getMatch(exp, ua) {
12160
12160
  }
12161
12161
  function getOSVersion(ua) {
12162
12162
  return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
12163
- }var version$1 = "2.21.0";const version = version$1;
12163
+ }var version$1 = "2.22.0";const version = version$1;
12164
12164
  const protocolVersion = 17;
12165
12165
  /** Initial client protocol. */
12166
12166
  const CLIENT_PROTOCOL_DEFAULT = 0;
@@ -13640,8 +13640,9 @@ class Track extends eventsExports.EventEmitter {
13640
13640
  if (this.monitorInterval) {
13641
13641
  clearInterval(this.monitorInterval);
13642
13642
  }
13643
- if (this.timeSyncHandle) {
13643
+ if (this.timeSyncHandle !== undefined) {
13644
13644
  cancelAnimationFrame(this.timeSyncHandle);
13645
+ this.timeSyncHandle = undefined;
13645
13646
  }
13646
13647
  }
13647
13648
  /** @internal */
@@ -13929,6 +13930,45 @@ function supportsVP9() {
13929
13930
  function isSVCCodec(codec) {
13930
13931
  return codec === 'av1' || codec === 'vp9';
13931
13932
  }
13933
+ /**
13934
+ * Opts `transceiver` into negotiating the AV1 dependency descriptor, reporting whether it will be.
13935
+ *
13936
+ * Chrome only offers the extension on transceivers that can send, so one we create to receive on
13937
+ * never negotiates it — and Chrome 152 stopped decoding AV1 that arrives without it: frames get
13938
+ * assembled, none ever decode, and the receiver asks for a keyframe forever. Asking through the
13939
+ * transceiver rather than munging the extension into the SDP leaves the browser owning the
13940
+ * extension id, which is what keeps that id consistent across the bundle and across
13941
+ * renegotiations.
13942
+ *
13943
+ * A no-op where the browser offers no such control, or does not know the extension at all.
13944
+ * @internal
13945
+ */
13946
+ function negotiateDependencyDescriptor(transceiver) {
13947
+ var _a;
13948
+ const extensions = (_a = transceiver.getHeaderExtensionsToNegotiate) === null || _a === void 0 ? void 0 : _a.call(transceiver);
13949
+ if (!extensions || !transceiver.setHeaderExtensionsToNegotiate) {
13950
+ return false;
13951
+ }
13952
+ const dd = extensions.find(ext => ext.uri === ddExtensionURI);
13953
+ if (!dd) {
13954
+ return false;
13955
+ }
13956
+ if (dd.direction !== 'stopped') {
13957
+ return true;
13958
+ }
13959
+ // sendrecv rather than recvonly, so that the extension is written as a plain `a=extmap` line —
13960
+ // the form the server already emits where it is the one offering — rather than one carrying a
13961
+ // `/recvonly` suffix that its parser may not expect
13962
+ dd.direction = 'sendrecv';
13963
+ try {
13964
+ transceiver.setHeaderExtensionsToNegotiate(extensions);
13965
+ return true;
13966
+ } catch (e) {
13967
+ // a rejected direction throws. Negotiating without the extension is what happened before this
13968
+ // existed, so it is not worth failing the connection over
13969
+ return false;
13970
+ }
13971
+ }
13932
13972
  function supportsSetSinkId(elm) {
13933
13973
  if (!document || isSafariBased()) {
13934
13974
  return false;
@@ -14470,6 +14510,17 @@ function isCompressionStreamSupported() {
14470
14510
  function isPublisherOfferWithJoinSupported() {
14471
14511
  // we have connectivity issue about publisher offer with join on firefox #1919
14472
14512
  return isCompressionStreamSupported() && !isFireFox();
14513
+ }
14514
+ function extractTrackSid(mediaTrack, stream) {
14515
+ const _unpackStreamId = unpackStreamId(stream.id),
14516
+ _unpackStreamId2 = _slicedToArray(_unpackStreamId, 2),
14517
+ streamId = _unpackStreamId2[1];
14518
+ if (streamId === null || streamId === void 0 ? void 0 : streamId.startsWith('TR')) {
14519
+ return streamId;
14520
+ }
14521
+ if (mediaTrack.id.startsWith('TR')) {
14522
+ return mediaTrack.id;
14523
+ }
14473
14524
  }function createRtcUrl(url, searchParams) {
14474
14525
  let useV0Path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
14475
14526
  const v0Url = createV0RtcUrl(url, searchParams);
@@ -14968,6 +15019,34 @@ function computeBitrate(currentStats, prevStats) {
14968
15019
  }class RemoteTrack extends Track {
14969
15020
  constructor(mediaTrack, sid, kind, receiver, loggerOptions) {
14970
15021
  super(mediaTrack, kind, loggerOptions);
15022
+ this.timeSyncLoop = () => {
15023
+ var _a;
15024
+ if (this.listenerCount(TrackEvent.TimeSyncUpdate) === 0) {
15025
+ // nobody is listening anymore, pause the loop until a new listener subscribes
15026
+ this.timeSyncHandle = undefined;
15027
+ return;
15028
+ }
15029
+ this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
15030
+ const sources = (_a = this.receiver) === null || _a === void 0 ? void 0 : _a.getSynchronizationSources()[0];
15031
+ if (sources) {
15032
+ const timestamp = sources.timestamp,
15033
+ rtpTimestamp = sources.rtpTimestamp;
15034
+ if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
15035
+ this.emit(TrackEvent.TimeSyncUpdate, {
15036
+ timestamp,
15037
+ rtpTimestamp
15038
+ });
15039
+ this.rtpTimestamp = rtpTimestamp;
15040
+ }
15041
+ }
15042
+ };
15043
+ this.onTimeSyncListenerAdded = event => {
15044
+ if (event === TrackEvent.TimeSyncUpdate && this.timeSyncHandle === undefined) {
15045
+ // `newListener` fires before the listener is registered, so schedule the
15046
+ // next frame instead of entering the loop (which would see a count of 0)
15047
+ this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
15048
+ }
15049
+ };
14971
15050
  this.sid = sid;
14972
15051
  this.receiver = receiver;
14973
15052
  }
@@ -15065,24 +15144,19 @@ function computeBitrate(currentStats, prevStats) {
15065
15144
  this.registerTimeSyncUpdate();
15066
15145
  }
15067
15146
  }
15147
+ /* @internal */
15148
+ stopMonitor() {
15149
+ super.stopMonitor();
15150
+ this.off('newListener', this.onTimeSyncListenerAdded);
15151
+ }
15068
15152
  registerTimeSyncUpdate() {
15069
- const loop = () => {
15070
- var _a;
15071
- this.timeSyncHandle = requestAnimationFrame(() => loop());
15072
- const sources = (_a = this.receiver) === null || _a === void 0 ? void 0 : _a.getSynchronizationSources()[0];
15073
- if (sources) {
15074
- const timestamp = sources.timestamp,
15075
- rtpTimestamp = sources.rtpTimestamp;
15076
- if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
15077
- this.emit(TrackEvent.TimeSyncUpdate, {
15078
- timestamp,
15079
- rtpTimestamp
15080
- });
15081
- this.rtpTimestamp = rtpTimestamp;
15082
- }
15083
- }
15084
- };
15085
- loop();
15153
+ // `newListener` isn't part of the typed event map, hence the cast
15154
+ const emitter = this;
15155
+ emitter.off('newListener', this.onTimeSyncListenerAdded);
15156
+ emitter.on('newListener', this.onTimeSyncListenerAdded);
15157
+ if (this.timeSyncHandle === undefined) {
15158
+ this.timeSyncLoop();
15159
+ }
15086
15160
  }
15087
15161
  }const REACTION_DELAY = 100;
15088
15162
  class RemoteVideoTrack extends RemoteTrack {
@@ -18392,6 +18466,51 @@ const startBitrateMultiplier = 0.9;
18392
18466
  /** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
18393
18467
  const maxStartBitrateKbps = 1000;
18394
18468
  const debounceInterval = 20;
18469
+ /**
18470
+ * Applies the configured start bitrate when this media section belongs to `cid`.
18471
+ * This SDP munging is used for a bitrate setting that cannot be applied through
18472
+ * `RTCRtpEncodingParameters`.
18473
+ *
18474
+ * Returns `undefined` when the section does not belong to the track, `0` when
18475
+ * it does but does not offer the requested codec, and the codec payload when the
18476
+ * requested codec is present (whether the bitrate was added or already set).
18477
+ *
18478
+ * @internal
18479
+ */
18480
+ function applyVideoStartBitrate(media, cid, codec, maxbr) {
18481
+ let isScreenShare = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
18482
+ var _a, _b, _c;
18483
+ if (!((_a = media.msid) === null || _a === void 0 ? void 0 : _a.includes(cid))) {
18484
+ return undefined;
18485
+ }
18486
+ const codecPayload = (_c = (_b = media.rtp.find(rtp => rtp.codec.toUpperCase() === codec.toUpperCase())) === null || _b === void 0 ? void 0 : _b.payload) !== null && _c !== void 0 ? _c : 0;
18487
+ if (codecPayload === 0) {
18488
+ return 0;
18489
+ }
18490
+ // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
18491
+ // from starting too aggressively. Screen share is not capped since text/UI
18492
+ // clarity requires high bitrate from the start.
18493
+ // TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
18494
+ const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
18495
+ const startBitrate = isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
18496
+ const fmtp = media.fmtp.find(entry => entry.payload === codecPayload);
18497
+ if (fmtp) {
18498
+ // If another track's fmtp already has a start bitrate, it cannot be
18499
+ // overridden here because the payload type is shared across the bundle.
18500
+ // This forces every track sharing that payload to use the initial track's
18501
+ // start bitrate.
18502
+ if (!fmtp.config.includes('x-google-start-bitrate')) {
18503
+ fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
18504
+ }
18505
+ } else {
18506
+ // VP8 and some codecs may not have an existing fmtp line.
18507
+ media.fmtp.push({
18508
+ payload: codecPayload,
18509
+ config: "x-google-start-bitrate=".concat(startBitrate)
18510
+ });
18511
+ }
18512
+ return codecPayload;
18513
+ }
18395
18514
  const PCEvents = {
18396
18515
  NegotiationStarted: 'negotiationStarted',
18397
18516
  NegotiationComplete: 'negotiationComplete',
@@ -18693,9 +18812,13 @@ class PCTransport extends eventsExports.EventEmitter {
18693
18812
  // the only exception to this is when ICE restart is needed
18694
18813
  const currentSD = this._pc.remoteDescription;
18695
18814
  if ((options === null || options === void 0 ? void 0 : options.iceRestart) && currentSD) {
18696
- // TODO: handle when ICE restart is needed but we don't have a remote description
18697
- // the best thing to do is to recreate the peerconnection
18815
+ // roll the remote description back in so createOffer produces a valid
18816
+ // ICE-restart offer on top of the already-negotiated state
18698
18817
  yield this._pc.setRemoteDescription(currentSD);
18818
+ } else if (options === null || options === void 0 ? void 0 : options.iceRestart) {
18819
+ // ICE restart with no remote description to restart on: `renegotiate` would stall
18820
+ // (the pending offer is never answered), so throw for the caller to recreate the PC.
18821
+ throw new NegotiationError('ICE restart requested without a remote description, peer connection must be recreated');
18699
18822
  } else {
18700
18823
  this.renegotiate = true;
18701
18824
  this.log.debug('requesting renegotiation');
@@ -18721,48 +18844,15 @@ class PCTransport extends eventsExports.EventEmitter {
18721
18844
  ensureAudioNackAndStereo(media, ['all'], []);
18722
18845
  } else if (media.type === 'video') {
18723
18846
  this.trackBitrates.some(trackbr => {
18724
- if (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
18847
+ if (!trackbr.cid) {
18725
18848
  return false;
18726
18849
  }
18727
- let codecPayload = 0;
18728
- media.rtp.some(rtp => {
18729
- if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
18730
- codecPayload = rtp.payload;
18731
- return true;
18732
- }
18850
+ const codecPayload = applyVideoStartBitrate(media, trackbr.cid, trackbr.codec, trackbr.maxbr, trackbr.isScreenShare);
18851
+ if (codecPayload === undefined) {
18733
18852
  return false;
18734
- });
18735
- if (codecPayload === 0) {
18736
- return true;
18737
- }
18738
- if (isSVCCodec(trackbr.codec) && !isSafari()) {
18739
- this.ensureVideoDDExtensionForSVC(media, sdpParsed);
18740
18853
  }
18741
- // mung sdp for bitrate setting that can't apply by sendEncoding
18742
- // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE from starting too aggressively
18743
- // Screen share is not capped since text/UI clarity requires high bitrate from the start
18744
- // TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
18745
- const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
18746
- const startBitrate = trackbr.isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
18747
- let fmtpFound = false;
18748
- for (const fmtp of media.fmtp) {
18749
- if (fmtp.payload === codecPayload) {
18750
- fmtpFound = true;
18751
- // if another track's fmtp already is set, we cannot override the bitrate
18752
- // this has the unfortunate consequence of being forced to use the
18753
- // initial track's bitrate for all tracks
18754
- if (!fmtp.config.includes('x-google-start-bitrate')) {
18755
- fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
18756
- }
18757
- break;
18758
- }
18759
- }
18760
- // VP8 and some codecs may not have an existing fmtp line - create one
18761
- if (!fmtpFound) {
18762
- media.fmtp.push({
18763
- payload: codecPayload,
18764
- config: "x-google-start-bitrate=".concat(startBitrate)
18765
- });
18854
+ if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
18855
+ this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
18766
18856
  }
18767
18857
  return true;
18768
18858
  });
@@ -18975,41 +19065,106 @@ class PCTransport extends eventsExports.EventEmitter {
18975
19065
  }
18976
19066
  });
18977
19067
  }
18978
- ensureVideoDDExtensionForSVC(media, sdp) {
18979
- var _a, _b;
18980
- const ddFound = (_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => {
18981
- if (ext.uri === ddExtensionURI) {
18982
- return true;
18983
- }
18984
- return false;
19068
+ }
19069
+ /**
19070
+ * Adds the AV1 dependency descriptor extension to `media` unless it is already there, and
19071
+ * returns the id it is mapped to so callers can pass it back in as `ddExtID` (0 when no id has
19072
+ * been chosen yet).
19073
+ *
19074
+ * A bundle has to map one URI to one id, so an id already in use for the extension anywhere in
19075
+ * `sdp` wins over both the cached one and a fresh one: Chrome advertises the extension itself on
19076
+ * sections it can send on, and an earlier offer may have munged it into others.
19077
+ * @internal
19078
+ */
19079
+ function ensureVideoDDExtension(media, sdp, ddExtID) {
19080
+ var _a, _b;
19081
+ const id = ddExtensionIDFor(sdp, ddExtID);
19082
+ if (id === undefined) {
19083
+ return ddExtID;
19084
+ }
19085
+ if (!((_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.uri === ddExtensionURI))) {
19086
+ (_b = media.ext) !== null && _b !== void 0 ? _b : media.ext = [];
19087
+ media.ext.push({
19088
+ value: id,
19089
+ uri: ddExtensionURI
18985
19090
  });
18986
- if (!ddFound) {
18987
- if (this.ddExtID === 0) {
18988
- let maxID = 0;
18989
- sdp.media.forEach(m => {
18990
- var _a;
18991
- (_a = m.ext) === null || _a === void 0 ? void 0 : _a.forEach(ext => {
18992
- if (ext.value > maxID) {
18993
- maxID = ext.value;
18994
- }
18995
- });
18996
- });
18997
- this.ddExtID = maxID + 1;
18998
- }
18999
- (_b = media.ext) === null || _b === void 0 ? void 0 : _b.push({
19000
- value: this.ddExtID,
19001
- uri: ddExtensionURI
19002
- });
19091
+ }
19092
+ return id;
19093
+ }
19094
+ /**
19095
+ * The id to map the dependency descriptor to throughout `sdp`, or undefined when no id would be
19096
+ * consistent for the whole bundle and the extension therefore has to be left out.
19097
+ */
19098
+ function ddExtensionIDFor(sdp, cachedID) {
19099
+ const mapped = mappedExtensionID(sdp, ddExtensionURI);
19100
+ if (mapped !== undefined) {
19101
+ // Adopting an id that also stands for another URI is what the browser rejects the bundle
19102
+ // over, and its own half of the map is not ours to renumber, so give up on this offer.
19103
+ return usedForOtherURI(sdp, mapped, ddExtensionURI) ? undefined : mapped;
19104
+ }
19105
+ // Reusing the id from the last offer keeps the mapping stable across renegotiations, but only
19106
+ // while nothing else has taken it: the browser assigns ids to its own extensions without
19107
+ // knowing about ours, so an id that was free when we picked it can since have been claimed —
19108
+ // typically by the fuller extension set that arrives with the first section we send on.
19109
+ if (cachedID !== 0 && !usedForOtherURI(sdp, cachedID, ddExtensionURI)) {
19110
+ return cachedID;
19111
+ }
19112
+ return unusedExtensionID(sdp);
19113
+ }
19114
+ /** The id `uri` is mapped to in `sdp`, if any section maps it. */
19115
+ function mappedExtensionID(sdp, uri) {
19116
+ var _a;
19117
+ for (const media of sdp.media) {
19118
+ const ext = (_a = media.ext) === null || _a === void 0 ? void 0 : _a.find(candidate => candidate.uri === uri);
19119
+ if (ext) {
19120
+ return ext.value;
19003
19121
  }
19004
19122
  }
19123
+ return undefined;
19124
+ }
19125
+ /** Whether `id` stands for anything in `sdp` other than `uri`. */
19126
+ function usedForOtherURI(sdp, id, uri) {
19127
+ return sdp.media.some(media => {
19128
+ var _a;
19129
+ return (_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.value === id && ext.uri !== uri);
19130
+ });
19005
19131
  }
19132
+ /**
19133
+ * An id no extension in `sdp` uses. Stays above every id in use rather than filling gaps, so it
19134
+ * is less likely to be an id the browser goes on to allocate to another extension, and steps
19135
+ * over 15, which RFC 8285 reserves.
19136
+ */
19137
+ function unusedExtensionID(sdp) {
19138
+ let maxID = 0;
19139
+ sdp.media.forEach(media => {
19140
+ var _a;
19141
+ (_a = media.ext) === null || _a === void 0 ? void 0 : _a.forEach(ext => {
19142
+ if (ext.value > maxID) {
19143
+ maxID = ext.value;
19144
+ }
19145
+ });
19146
+ });
19147
+ return maxID + 1 === 15 ? 16 : maxID + 1;
19148
+ }
19149
+ /**
19150
+ * Checks whether an fmtp config declares `param` as an exact, `;`-delimited
19151
+ * token. A plain substring check conflates distinct opus parameters — e.g.
19152
+ * `stereo=1` is a substring of `sprop-stereo=1` — so `param` must match a whole
19153
+ * parameter, not appear anywhere within the config string.
19154
+ * @internal
19155
+ */
19156
+ function fmtpConfigHasParam(config, param) {
19157
+ return config.split(';').some(entry => entry.trim() === param);
19158
+ }
19159
+ /** @internal */
19006
19160
  function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
19007
19161
  // sdp-transform types don't include number however the parser outputs mids as numbers in some cases
19008
19162
  const mid = getMidString(media.mid);
19009
19163
  // found opus codec to add nack fb
19010
19164
  let opusPayload = 0;
19011
19165
  media.rtp.some(rtp => {
19012
- if (rtp.codec === 'opus') {
19166
+ // rtpmap encoding names are case-insensitive (RFC 4855)
19167
+ if (rtp.codec.toLowerCase() === 'opus') {
19013
19168
  opusPayload = rtp.payload;
19014
19169
  return true;
19015
19170
  }
@@ -19029,7 +19184,7 @@ function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
19029
19184
  if (stereoMids.includes(mid) || stereoMids.length === 1 && stereoMids[0] === 'all') {
19030
19185
  media.fmtp.some(fmtp => {
19031
19186
  if (fmtp.payload === opusPayload) {
19032
- if (!fmtp.config.includes('stereo=1')) {
19187
+ if (!fmtpConfigHasParam(fmtp.config, 'stereo=1')) {
19033
19188
  fmtp.config += ';stereo=1';
19034
19189
  }
19035
19190
  return true;
@@ -19112,6 +19267,7 @@ function conformBundledCodecFmtp(media, isPlaceholder) {
19112
19267
  }
19113
19268
  }
19114
19269
  }
19270
+ /** @internal */
19115
19271
  function extractStereoAndNackAudioFromOffer(offer) {
19116
19272
  var _a;
19117
19273
  const stereoMids = [];
@@ -19123,7 +19279,8 @@ function extractStereoAndNackAudioFromOffer(offer) {
19123
19279
  const mid = getMidString(media.mid);
19124
19280
  if (media.type === 'audio') {
19125
19281
  media.rtp.some(rtp => {
19126
- if (rtp.codec === 'opus') {
19282
+ // rtpmap encoding names are case-insensitive (RFC 4855)
19283
+ if (rtp.codec.toLowerCase() === 'opus') {
19127
19284
  opusPayload = rtp.payload;
19128
19285
  return true;
19129
19286
  }
@@ -19134,7 +19291,7 @@ function extractStereoAndNackAudioFromOffer(offer) {
19134
19291
  }
19135
19292
  media.fmtp.some(fmtp => {
19136
19293
  if (fmtp.payload === opusPayload) {
19137
- if (fmtp.config.includes('sprop-stereo=1')) {
19294
+ if (fmtpConfigHasParam(fmtp.config, 'sprop-stereo=1')) {
19138
19295
  stereoMids.push(mid);
19139
19296
  }
19140
19297
  return true;
@@ -21927,17 +22084,37 @@ class LocalVideoTrack extends LocalTrack {
21927
22084
  setDegradationPreference(preference) {
21928
22085
  return __awaiter(this, void 0, void 0, function* () {
21929
22086
  this.degradationPreference = preference;
21930
- if (this.sender) {
21931
- try {
21932
- this.log.debug("setting degradationPreference to ".concat(preference), this.logContext);
21933
- const params = this.sender.getParameters();
21934
- params.degradationPreference = preference;
21935
- this.sender.setParameters(params);
21936
- } catch (e) {
21937
- this.log.warn("failed to set degradationPreference", Object.assign({
21938
- error: e
21939
- }, this.logContext));
21940
- }
22087
+ // applied one sender at a time on purpose, see applyDegradationPreference
22088
+ yield this.applyDegradationPreference(this.sender);
22089
+ for (const sc of this.simulcastCodecs.values()) {
22090
+ yield this.applyDegradationPreference(sc.sender);
22091
+ }
22092
+ });
22093
+ }
22094
+ /**
22095
+ * Degradation preference is a property of the sender, not of the track, so every sender
22096
+ * publishing this track needs it applied separately. A backup codec publishes over its
22097
+ * own sender, which would otherwise let the browser resolve a preference implicitly and
22098
+ * diverge from the primary encoder.
22099
+ *
22100
+ * Callers apply this sequentially rather than concurrently: `setParameters` is only valid
22101
+ * against the parameters most recently returned by `getParameters`, which is why this file
22102
+ * serializes other sender parameter updates through `senderLock`.
22103
+ */
22104
+ applyDegradationPreference(sender) {
22105
+ return __awaiter(this, void 0, void 0, function* () {
22106
+ if (!sender) {
22107
+ return;
22108
+ }
22109
+ try {
22110
+ this.log.debug("setting degradationPreference to ".concat(this.degradationPreference), this.logContext);
22111
+ const params = sender.getParameters();
22112
+ params.degradationPreference = this.degradationPreference;
22113
+ yield sender.setParameters(params);
22114
+ } catch (e) {
22115
+ this.log.warn("failed to set degradationPreference", Object.assign({
22116
+ error: e
22117
+ }, this.logContext));
21941
22118
  }
21942
22119
  });
21943
22120
  }
@@ -21956,18 +22133,23 @@ class LocalVideoTrack extends LocalTrack {
21956
22133
  return simulcastCodecInfo;
21957
22134
  }
21958
22135
  setSimulcastTrackSender(codec, sender) {
21959
- const simulcastCodecInfo = this.simulcastCodecs.get(codec);
21960
- if (!simulcastCodecInfo) {
21961
- return;
21962
- }
21963
- simulcastCodecInfo.sender = sender;
21964
- // browser will reenable disabled codec/layers after new codec has been published,
21965
- // so refresh subscribedCodecs after publish a new codec
21966
- setTimeout(() => {
21967
- if (this.subscribedCodecs) {
21968
- this.setPublishingCodecs(this.subscribedCodecs);
22136
+ return __awaiter(this, void 0, void 0, function* () {
22137
+ const simulcastCodecInfo = this.simulcastCodecs.get(codec);
22138
+ if (!simulcastCodecInfo) {
22139
+ return;
21969
22140
  }
21970
- }, refreshSubscribedCodecAfterNewCodec);
22141
+ simulcastCodecInfo.sender = sender;
22142
+ // the backup codec publishes over its own sender, so it needs the same degradation
22143
+ // preference the primary sender resolved to.
22144
+ yield this.applyDegradationPreference(sender);
22145
+ // browser will reenable disabled codec/layers after new codec has been published,
22146
+ // so refresh subscribedCodecs after publish a new codec
22147
+ setTimeout(() => {
22148
+ if (this.subscribedCodecs) {
22149
+ this.setPublishingCodecs(this.subscribedCodecs);
22150
+ }
22151
+ }, refreshSubscribedCodecAfterNewCodec);
22152
+ });
21971
22153
  }
21972
22154
  /**
21973
22155
  * @internal
@@ -22239,6 +22421,11 @@ function videoLayersFromEncodings(width, height, encodings, svc) {
22239
22421
  });
22240
22422
  }const minReconnectWait = 2 * 1000;
22241
22423
  const leaveReconnect = 'leave-reconnect';
22424
+ /**
22425
+ * How long local connection quality must stay `LOST` while connected and publishing before we
22426
+ * force a full reconnect — `LOST` is the server's verdict that it isn't receiving our media.
22427
+ */
22428
+ const connectionQualityLostTimeout = 10 * 1000;
22242
22429
  const reliabeReceiveStateTTL = 30000;
22243
22430
  const initialMediaSectionsAudio = 3;
22244
22431
  const initialMediaSectionsVideo = 3;
@@ -22528,7 +22715,10 @@ class RTCEngine extends eventsExports.EventEmitter {
22528
22715
  onBufferStatusChanged: (kind, isLow) => this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind)
22529
22716
  });
22530
22717
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
22531
- this.client.onConnectionQuality = update => this.emit(EngineEvent.ConnectionQualityUpdate, update);
22718
+ this.client.onConnectionQuality = update => {
22719
+ this.handleLocalConnectionQuality(update);
22720
+ this.emit(EngineEvent.ConnectionQualityUpdate, update);
22721
+ };
22532
22722
  this.client.onRoomUpdate = update => this.emit(EngineEvent.RoomUpdate, update);
22533
22723
  this.client.onSubscriptionError = resp => this.emit(EngineEvent.SubscriptionError, resp);
22534
22724
  this.client.onSubscriptionPermissionUpdate = update => this.emit(EngineEvent.SubscriptionPermissionUpdate, update);
@@ -22671,6 +22861,7 @@ class RTCEngine extends eventsExports.EventEmitter {
22671
22861
  this.removeAllListeners();
22672
22862
  this.deregisterOnLineListener();
22673
22863
  this.clearPendingReconnect();
22864
+ this.clearLostQualityTimeout();
22674
22865
  this.cleanupLossyDataStats();
22675
22866
  yield this.cleanupPeerConnections();
22676
22867
  yield this.cleanupClient();
@@ -22685,6 +22876,8 @@ class RTCEngine extends eventsExports.EventEmitter {
22685
22876
  this.dataChannels.teardown();
22686
22877
  yield (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.close();
22687
22878
  this.pcManager = undefined;
22879
+ // the connecting timestamp belongs to the transports we just tore down
22880
+ this.transportConnectingSince = undefined;
22688
22881
  this.reliableReceivedState.clear();
22689
22882
  });
22690
22883
  }
@@ -22710,17 +22903,17 @@ class RTCEngine extends eventsExports.EventEmitter {
22710
22903
  throw new TrackInvalidError('a track with the same ID has already been published');
22711
22904
  }
22712
22905
  return new Promise((resolve, reject) => {
22713
- const publicationTimeout = setTimeout(() => {
22906
+ const publicationTimeout = CriticalTimers.setTimeout(() => {
22714
22907
  delete this.pendingTrackResolvers[req.cid];
22715
22908
  reject(ConnectionError.timeout('publication of local track timed out, no response from server'));
22716
22909
  }, 10000);
22717
22910
  this.pendingTrackResolvers[req.cid] = {
22718
22911
  resolve: info => {
22719
- clearTimeout(publicationTimeout);
22912
+ CriticalTimers.clearTimeout(publicationTimeout);
22720
22913
  resolve(info);
22721
22914
  },
22722
22915
  reject: () => {
22723
- clearTimeout(publicationTimeout);
22916
+ CriticalTimers.clearTimeout(publicationTimeout);
22724
22917
  reject(new Error('Cancelled publication by calling unpublish'));
22725
22918
  }
22726
22919
  };
@@ -22793,6 +22986,15 @@ class RTCEngine extends eventsExports.EventEmitter {
22793
22986
  this.pcManager.onDataChannel = this.handleDataChannel;
22794
22987
  this.pcManager.onStateChange = (connectionState, publisherState, subscriberState) => __awaiter(this, void 0, void 0, function* () {
22795
22988
  this.log.debug("primary PC state changed ".concat(connectionState));
22989
+ // Record when the primary transport actually entered CONNECTING so
22990
+ // verifyTransport() can bound how long we tolerate it. Deriving it from the
22991
+ // real transition (this handler only fires on state changes) rather than from
22992
+ // observation time keeps it from going stale across peer-connection rebuilds.
22993
+ if (connectionState === PCTransportState.CONNECTING) {
22994
+ this.transportConnectingSince = Date.now();
22995
+ } else {
22996
+ this.transportConnectingSince = undefined;
22997
+ }
22796
22998
  if (['closed', 'disconnected', 'failed'].includes(publisherState)) {
22797
22999
  // reset publisher connection promise
22798
23000
  this.publisherConnectionPromise = undefined;
@@ -23001,15 +23203,23 @@ class RTCEngine extends eventsExports.EventEmitter {
23001
23203
  }
23002
23204
  }
23003
23205
  addMediaSections(numAudios, numVideos) {
23004
- var _a, _b;
23206
+ var _a, _b, _c;
23005
23207
  const transceiverInit = {
23006
23208
  direction: 'recvonly'
23007
23209
  };
23008
23210
  for (let i = 0; i < numAudios; i++) {
23009
23211
  (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.addPublisherTransceiverOfKind('audio', transceiverInit);
23010
23212
  }
23213
+ // media only arrives on these sections when there is no subscriber connection to arrive on
23214
+ const receivesMedia = ((_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.mode) === 'publisher-only';
23011
23215
  for (let i = 0; i < numVideos; i++) {
23012
- (_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.addPublisherTransceiverOfKind('video', transceiverInit);
23216
+ const transceiver = (_c = this.pcManager) === null || _c === void 0 ? void 0 : _c.addPublisherTransceiverOfKind('video', transceiverInit);
23217
+ if (receivesMedia && transceiver) {
23218
+ const negotiated = negotiateDependencyDescriptor(transceiver);
23219
+ this.log.debug('dependency descriptor negotiated for received video', {
23220
+ negotiated
23221
+ });
23222
+ }
23013
23223
  }
23014
23224
  }
23015
23225
  createDataChannels() {
@@ -23151,7 +23361,7 @@ class RTCEngine extends eventsExports.EventEmitter {
23151
23361
  if (!opts.videoCodec) {
23152
23362
  return;
23153
23363
  }
23154
- track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
23364
+ yield track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
23155
23365
  return transceiver.sender;
23156
23366
  });
23157
23367
  }
@@ -23163,6 +23373,63 @@ class RTCEngine extends eventsExports.EventEmitter {
23163
23373
  return this.pcManager.addPublisherTrack(track);
23164
23374
  });
23165
23375
  }
23376
+ /**
23377
+ * A sustained local `LOST` while connected and publishing means the server isn't receiving
23378
+ * our media, so force a full reconnect; any non-`LOST` value cancels a pending trigger.
23379
+ */
23380
+ handleLocalConnectionQuality(update) {
23381
+ if (!this.participantSid) {
23382
+ return;
23383
+ }
23384
+ const localUpdate = update.updates.find(u => u.participantSid === this.participantSid);
23385
+ if (!localUpdate) {
23386
+ return;
23387
+ }
23388
+ if (localUpdate.quality === ConnectionQuality$1.LOST) {
23389
+ this.scheduleLostQualityReconnect();
23390
+ } else {
23391
+ this.clearLostQualityTimeout();
23392
+ }
23393
+ }
23394
+ scheduleLostQualityReconnect() {
23395
+ if (this.lostQualityTimeout) {
23396
+ // already counting down towards a reconnect
23397
+ return;
23398
+ }
23399
+ this.lostQualityTimeout = CriticalTimers.setTimeout(() => {
23400
+ this.lostQualityTimeout = undefined;
23401
+ if (this._isClosed || this.pcState !== PCState.Connected || this.attemptingReconnect) {
23402
+ return;
23403
+ }
23404
+ if (!this.hasActivePublisherSenders()) {
23405
+ return;
23406
+ }
23407
+ this.log.warn('local connection quality lost while publishing, triggering full reconnect', this.logContext);
23408
+ this.fullReconnectOnNext = true;
23409
+ this.handleDisconnect('connection quality lost', ReconnectReason.RR_PUBLISHER_FAILED);
23410
+ }, connectionQualityLostTimeout);
23411
+ }
23412
+ clearLostQualityTimeout() {
23413
+ if (this.lostQualityTimeout) {
23414
+ CriticalTimers.clearTimeout(this.lostQualityTimeout);
23415
+ this.lostQualityTimeout = undefined;
23416
+ }
23417
+ }
23418
+ /** Whether the publisher currently has any sender with a live track. */
23419
+ hasActivePublisherSenders() {
23420
+ var _a, _b;
23421
+ return (_b = (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.publisher.getSenders().some(sender => !!sender.track && sender.track.readyState === 'live')) !== null && _b !== void 0 ? _b : false;
23422
+ }
23423
+ /**
23424
+ * Forces a full reconnect while keeping the engine (and its saved credentials) alive. Used by
23425
+ * Room's connection-reconcile safety net when the transport silently died but we looked connected.
23426
+ * @internal
23427
+ */
23428
+ reconnect() {
23429
+ let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ReconnectReason.RR_UNKNOWN;
23430
+ this.fullReconnectOnNext = true;
23431
+ this.handleDisconnect('reconcile', reason);
23432
+ }
23166
23433
  attemptReconnect(reason) {
23167
23434
  return __awaiter(this, void 0, void 0, function* () {
23168
23435
  var _a, _b, _c;
@@ -23174,21 +23441,32 @@ class RTCEngine extends eventsExports.EventEmitter {
23174
23441
  this.log.warn('already attempting reconnect, returning early');
23175
23442
  return;
23176
23443
  }
23444
+ // A pending Lost-quality countdown belongs to the session we're now leaving; cancel it so
23445
+ // it can't fire against the reconnected session before the server has evaluated it. (A resume
23446
+ // keeps the peer connections, so cleanupPeerConnections wouldn't cover this path.)
23447
+ this.clearLostQualityTimeout();
23177
23448
  if (((_a = this.clientConfiguration) === null || _a === void 0 ? void 0 : _a.resumeConnection) === ClientConfigSetting.DISABLED ||
23178
23449
  // signaling state could change to closed due to hardware sleep
23179
23450
  // those connections cannot be resumed
23180
23451
  ((_c = (_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.currentState) !== null && _c !== void 0 ? _c : PCTransportState.NEW) === PCTransportState.NEW) {
23181
23452
  this.fullReconnectOnNext = true;
23182
23453
  }
23454
+ // Consume the flag up front: capture whether this attempt is a full reconnect, then reset
23455
+ // it. From here on a `true` value unambiguously represents a *new* full-reconnect request
23456
+ // that arrived while this attempt was running (e.g. a server RECONNECT leave), which the
23457
+ // finally block dispatches — for both the resume and full-reconnect paths.
23458
+ const fullReconnect = this.fullReconnectOnNext;
23459
+ this.fullReconnectOnNext = false;
23460
+ let succeeded = false;
23183
23461
  try {
23184
23462
  this.attemptingReconnect = true;
23185
- if (this.fullReconnectOnNext) {
23463
+ if (fullReconnect) {
23186
23464
  yield this.restartConnection();
23187
23465
  } else {
23188
23466
  yield this.resumeConnection(reason);
23189
23467
  }
23190
23468
  this.clearPendingReconnect();
23191
- this.fullReconnectOnNext = false;
23469
+ succeeded = true;
23192
23470
  } catch (e) {
23193
23471
  this.reconnectAttempts += 1;
23194
23472
  let recoverable = true;
@@ -23198,8 +23476,9 @@ class RTCEngine extends eventsExports.EventEmitter {
23198
23476
  });
23199
23477
  // unrecoverable
23200
23478
  recoverable = false;
23201
- } else if (!(e instanceof SignalReconnectError)) {
23202
- // cannot resume
23479
+ } else if (fullReconnect || !(e instanceof SignalReconnectError)) {
23480
+ // a failed full reconnect stays a full reconnect; a failed resume can only be
23481
+ // resumed again for a signal-level error, otherwise it escalates
23203
23482
  this.fullReconnectOnNext = true;
23204
23483
  }
23205
23484
  if (recoverable) {
@@ -23211,6 +23490,13 @@ class RTCEngine extends eventsExports.EventEmitter {
23211
23490
  }
23212
23491
  } finally {
23213
23492
  this.attemptingReconnect = false;
23493
+ // A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
23494
+ // during a resume or a restart) that a successful attempt didn't act on; dispatch it now
23495
+ // (the failure path already retries).
23496
+ if (succeeded && this.fullReconnectOnNext && !this._isClosed) {
23497
+ this.log.debug('full reconnect requested during in-progress attempt, dispatching');
23498
+ this.handleDisconnect('reconnect');
23499
+ }
23214
23500
  }
23215
23501
  });
23216
23502
  }
@@ -23529,14 +23815,23 @@ class RTCEngine extends eventsExports.EventEmitter {
23529
23815
  if (!this.pcManager) {
23530
23816
  return false;
23531
23817
  }
23818
+ const state = this.pcManager.currentState;
23532
23819
  const allowedConnectionStates = [PCTransportState.CONNECTING, PCTransportState.CONNECTED];
23533
- if (!allowedConnectionStates.includes(this.pcManager.currentState)) {
23820
+ if (!allowedConnectionStates.includes(state)) {
23534
23821
  return false;
23535
23822
  }
23536
23823
  // ensure signal is connected
23537
23824
  if (!this.client.ws || this.client.ws.readyState === WebSocket.CLOSED) {
23538
23825
  return false;
23539
23826
  }
23827
+ // A transport stuck in CONNECTING never reaches CONNECTED nor reports FAILED, so it would
23828
+ // otherwise look healthy forever; bound how long we tolerate it. The entry time is recorded
23829
+ // in the pcManager state-change handler (see configure()), so this is a pure read — an
23830
+ // unrecorded CONNECTING fails open rather than measuring against a stale timestamp.
23831
+ if (state === PCTransportState.CONNECTING && this.transportConnectingSince !== undefined && Date.now() - this.transportConnectingSince > this.peerConnectionTimeout) {
23832
+ this.log.warn('transport stuck in connecting state', this.logContext);
23833
+ return false;
23834
+ }
23540
23835
  return true;
23541
23836
  }
23542
23837
  /** @internal */
@@ -24178,9 +24473,7 @@ class TextStreamReader extends BaseStreamReader {
24178
24473
  // Suppress unhandled rejection on reader.closed — errors are
24179
24474
  // already propagated through reader.read() to the consumer.
24180
24475
  reader.closed.catch(() => {});
24181
- const decoder = new TextDecoder('utf-8', {
24182
- fatal: true
24183
- });
24476
+ const decoder = new TextDecoder('utf-8');
24184
24477
  const signal = this.signal;
24185
24478
  const cleanup = () => {
24186
24479
  reader.releaseLock();
@@ -24658,9 +24951,7 @@ function bytesToChunks(streamId) {
24658
24951
  * synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
24659
24952
  */
24660
24953
  function bytesToDecodedUtf8(streamId) {
24661
- const decoder = new TextDecoder('utf-8', {
24662
- fatal: true
24663
- });
24954
+ const decoder = new TextDecoder('utf-8');
24664
24955
  const encoder = new TextEncoder();
24665
24956
  let outIndex = 0;
24666
24957
  const decodeOrThrow = bytes => {
@@ -31475,6 +31766,7 @@ class Room extends eventsExports.EventEmitter {
31475
31766
  this.log = livekitLogger;
31476
31767
  this.bufferedEvents = [];
31477
31768
  this.isResuming = false;
31769
+ this.pendingTrackAddedCallbacks = new Map();
31478
31770
  this.connect = (url, token, opts) => __awaiter(this, void 0, void 0, function* () {
31479
31771
  var _a;
31480
31772
  if (!isBrowserSupported()) {
@@ -31990,8 +32282,9 @@ class Room extends eventsExports.EventEmitter {
31990
32282
  return;
31991
32283
  }
31992
32284
  const newStreamState = Track.streamStateFromProto(streamState.state);
32285
+ const prevStreamState = pub.track.streamState;
31993
32286
  pub.track.setStreamState(newStreamState);
31994
- if (newStreamState !== pub.track.streamState) {
32287
+ if (newStreamState !== prevStreamState) {
31995
32288
  participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
31996
32289
  this.emitWhenConnected(RoomEvent.TrackStreamStateChanged, pub, pub.track.streamState, participant);
31997
32290
  }
@@ -32009,6 +32302,7 @@ class Room extends eventsExports.EventEmitter {
32009
32302
  pub.setAllowed(update.allowed);
32010
32303
  };
32011
32304
  this.handleSubscriptionError = update => {
32305
+ this.cancelPendingTrackAdded(update.trackSid);
32012
32306
  const participant = Array.from(this.remoteParticipants.values()).find(p => p.trackPublications.has(update.trackSid));
32013
32307
  if (!participant) {
32014
32308
  return;
@@ -32521,10 +32815,10 @@ class Room extends eventsExports.EventEmitter {
32521
32815
  }
32522
32816
  this.emitBufferedEvents();
32523
32817
  }).on(EngineEvent.SignalResumed, () => {
32524
- this.bufferedEvents = [];
32525
32818
  if (this.state === ConnectionState.Reconnecting || this.isResuming) {
32526
32819
  this.sendSyncState();
32527
32820
  }
32821
+ this.emitBufferedEvents();
32528
32822
  }).on(EngineEvent.Restarting, this.handleRestarting).on(EngineEvent.Restarted, this.handleRestarted).on(EngineEvent.SignalRestarted, this.handleSignalRestarted).on(EngineEvent.Offline, () => {
32529
32823
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
32530
32824
  this.emit(RoomEvent.Reconnecting);
@@ -32941,29 +33235,43 @@ class Room extends eventsExports.EventEmitter {
32941
33235
  this.maybeCreateEngine();
32942
33236
  }
32943
33237
  onTrackAdded(mediaTrack, stream, receiver) {
33238
+ var _a, _b;
32944
33239
  // don't fire onSubscribed when connecting
32945
33240
  // WebRTC fires onTrack as soon as setRemoteDescription is called on the offer
32946
33241
  // at that time, ICE connectivity has not been established so the track is not
32947
33242
  // technically subscribed.
32948
33243
  // We'll defer these events until when the room is connected or eventually disconnected.
32949
- if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
33244
+ if ([ConnectionState.Connecting, ConnectionState.Reconnecting].includes(this.state)) {
33245
+ const pendingTrackSid = extractTrackSid(mediaTrack, stream);
33246
+ this.log.debug('deferring on track for later', {
33247
+ mediaTrackId: mediaTrack.id,
33248
+ mediaStreamId: stream.id,
33249
+ tracksInStream: stream.getTracks().map(track => track.id)
33250
+ });
32950
33251
  const reconnectedHandler = () => {
32951
- this.log.debug('deferring on track for later', {
32952
- mediaTrackId: mediaTrack.id,
32953
- mediaStreamId: stream.id,
32954
- tracksInStream: stream.getTracks().map(track => track.id)
32955
- });
32956
- this.onTrackAdded(mediaTrack, stream, receiver);
32957
33252
  cleanup();
33253
+ this.onTrackAdded(mediaTrack, stream, receiver);
32958
33254
  };
32959
33255
  const cleanup = () => {
32960
33256
  this.off(RoomEvent.Reconnected, reconnectedHandler);
32961
33257
  this.off(RoomEvent.Connected, reconnectedHandler);
32962
33258
  this.off(RoomEvent.Disconnected, cleanup);
33259
+ if (pendingTrackSid) {
33260
+ const pendingCallbacks = this.pendingTrackAddedCallbacks.get(pendingTrackSid);
33261
+ pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.delete(cleanup);
33262
+ if ((pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.size) === 0) {
33263
+ this.pendingTrackAddedCallbacks.delete(pendingTrackSid);
33264
+ }
33265
+ }
32963
33266
  };
32964
33267
  this.once(RoomEvent.Reconnected, reconnectedHandler);
32965
33268
  this.once(RoomEvent.Connected, reconnectedHandler);
32966
33269
  this.once(RoomEvent.Disconnected, cleanup);
33270
+ if (pendingTrackSid) {
33271
+ const pendingCallbacks = (_a = this.pendingTrackAddedCallbacks.get(pendingTrackSid)) !== null && _a !== void 0 ? _a : new Set();
33272
+ pendingCallbacks.add(cleanup);
33273
+ this.pendingTrackAddedCallbacks.set(pendingTrackSid, pendingCallbacks);
33274
+ }
32967
33275
  return;
32968
33276
  }
32969
33277
  if (this.state === ConnectionState.Disconnected) {
@@ -32976,11 +33284,10 @@ class Room extends eventsExports.EventEmitter {
32976
33284
  }
32977
33285
  const parts = unpackStreamId(stream.id);
32978
33286
  const participantSid = parts[0];
32979
- let streamId = parts[1];
32980
- let trackId = mediaTrack.id;
33287
+ const streamId = parts[1];
32981
33288
  // firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
32982
33289
  // and generates its own track id instead of infer from sdp track id.
32983
- if (streamId && streamId.startsWith('TR')) trackId = streamId;
33290
+ let trackId = (_b = extractTrackSid(mediaTrack, stream)) !== null && _b !== void 0 ? _b : mediaTrack.id;
32984
33291
  if (participantSid === this.localParticipant.sid) {
32985
33292
  this.log.warn('tried to create RemoteParticipant for local participant');
32986
33293
  return;
@@ -33024,6 +33331,10 @@ class Room extends eventsExports.EventEmitter {
33024
33331
  this.emit(RoomEvent.EncryptionError, new Error("Encrypted ".concat(publication.source, " track received from participant ").concat(participant.sid, ", but room does not have encryption enabled!")));
33025
33332
  }
33026
33333
  }
33334
+ cancelPendingTrackAdded(trackSid) {
33335
+ var _a;
33336
+ (_a = this.pendingTrackAddedCallbacks.get(trackSid)) === null || _a === void 0 ? void 0 : _a.forEach(cleanup => cleanup());
33337
+ }
33027
33338
  handleLocalTrackSubscribed(subscribedSid) {
33028
33339
  const findPublication = () => this.localParticipant.getTrackPublications().find(_ref6 => {
33029
33340
  let trackSid = _ref6.trackSid;
@@ -33287,6 +33598,7 @@ class Room extends eventsExports.EventEmitter {
33287
33598
  }
33288
33599
  this.emitWhenConnected(RoomEvent.TrackSubscribed, track, publication, participant);
33289
33600
  }).on(ParticipantEvent.TrackUnpublished, publication => {
33601
+ this.cancelPendingTrackAdded(publication.trackSid);
33290
33602
  this.emit(RoomEvent.TrackUnpublished, publication, participant);
33291
33603
  }).on(ParticipantEvent.TrackUnsubscribed, (track, publication) => {
33292
33604
  this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
@@ -33404,8 +33716,17 @@ class Room extends eventsExports.EventEmitter {
33404
33716
  } : undefined
33405
33717
  });
33406
33718
  if (consecutiveFailures >= 3) {
33407
- this.recreateEngine();
33408
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, DisconnectReason.STATE_MISMATCH);
33719
+ this.clearConnectionReconcile();
33720
+ if (this.engine && !this.engine.isClosed) {
33721
+ // The transport silently died while we still looked connected. Try a full reconnect
33722
+ // (keeps the room alive; the engine falls back to Disconnected if it ultimately fails).
33723
+ this.log.warn('detected connection state mismatch, attempting full reconnect');
33724
+ this.engine.reconnect();
33725
+ } else {
33726
+ // No usable engine to reconnect with; tear down.
33727
+ this.recreateEngine();
33728
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, DisconnectReason.STATE_MISMATCH);
33729
+ }
33409
33730
  }
33410
33731
  } else {
33411
33732
  consecutiveFailures = 0;
@@ -34802,24 +35123,26 @@ class TokenSourceEndpoint extends TokenSourceCached {
34802
35123
  const body = yield response.json();
34803
35124
  return TokenSourceResponse.fromJson(body, {
34804
35125
  // NOTE: it could be possible that the response body could contain more fields than just
34805
- // what's in TokenSourceResponse depending on the implementation (ie, SandboxTokenServer)
35126
+ // what's in TokenSourceResponse depending on the implementation (ie, DevelopmentTokenServer)
34806
35127
  ignoreUnknownFields: true
34807
35128
  });
34808
35129
  });
34809
35130
  }
34810
35131
  }
34811
- class TokenSourceSandboxTokenServer extends TokenSourceEndpoint {
34812
- constructor(sandboxId, options) {
35132
+ class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
35133
+ constructor(tokenServerId, options) {
34813
35134
  const _options$baseUrl = options.baseUrl,
34814
35135
  baseUrl = _options$baseUrl === void 0 ? 'https://cloud-api.livekit.io' : _options$baseUrl,
34815
35136
  rest = __rest(options, ["baseUrl"]);
34816
35137
  super("".concat(baseUrl, "/api/v2/sandbox/connection-details"), Object.assign(Object.assign({}, rest), {
34817
35138
  headers: {
34818
- 'X-Sandbox-ID': sandboxId
35139
+ 'X-Sandbox-ID': tokenServerId
34819
35140
  }
34820
35141
  }));
34821
35142
  }
34822
35143
  }
35144
+ /** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
35145
+ class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {}
34823
35146
  const TokenSource = {
34824
35147
  /** TokenSource.literal contains a single, literal set of {@link TokenSourceResponseObject}
34825
35148
  * credentials, either provided directly or returned from a provided function. */
@@ -34838,24 +35161,31 @@ const TokenSource = {
34838
35161
  /**
34839
35162
  * TokenSource.endpoint creates a token source that fetches credentials from a given URL using
34840
35163
  * the standard endpoint format:
34841
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
35164
+ * @see https://docs.livekit.io/frontends/build/authentication/endpoint/
34842
35165
  */
34843
35166
  endpoint(url) {
34844
35167
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
34845
35168
  return new TokenSourceEndpoint(url, options);
34846
35169
  },
34847
35170
  /**
34848
- * TokenSource.sandboxTokenServer queries a sandbox token server for credentials,
35171
+ * @deprecated Use {@link TokenSource.developmentTokenServer} instead
35172
+ */
35173
+ sandboxTokenServer(sandboxId) {
35174
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
35175
+ return new TokenSourceSandboxTokenServer(sandboxId, options);
35176
+ },
35177
+ /**
35178
+ * TokenSource.developmentTokenServer queries a development token server for credentials,
34849
35179
  * which supports quick prototyping / getting started types of use cases.
34850
35180
  *
34851
35181
  * This token provider is INSECURE and should NOT be used in production.
34852
35182
  *
34853
35183
  * For more info:
34854
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
35184
+ * @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
34855
35185
  */
34856
- sandboxTokenServer(sandboxId) {
35186
+ developmentTokenServer(tokenServerId) {
34857
35187
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
34858
- return new TokenSourceSandboxTokenServer(sandboxId, options);
35188
+ return new TokenSourceDevelopmentTokenServer(tokenServerId, options);
34859
35189
  }
34860
35190
  };/**
34861
35191
  * Try to analyze the local track to determine the facing mode of a track.