livekit-client 2.20.0 → 2.20.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.
Files changed (155) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +41 -11
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +744 -178
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.fm.worker.js +1 -1
  8. package/dist/livekit-client.fm.worker.js.map +1 -1
  9. package/dist/livekit-client.fm.worker.mjs +41 -7
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/SignalClient.d.ts.map +1 -1
  14. package/dist/src/e2ee/E2eeManager.d.ts +1 -0
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  16. package/dist/src/e2ee/types.d.ts +1 -0
  17. package/dist/src/e2ee/types.d.ts.map +1 -1
  18. package/dist/src/e2ee/utils.d.ts +1 -0
  19. package/dist/src/e2ee/utils.d.ts.map +1 -1
  20. package/dist/src/e2ee/worker/DataCryptor.d.ts +1 -0
  21. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  22. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
  23. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/naluUtils.d.ts +1 -0
  25. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  26. package/dist/src/e2ee/worker/sifPayload.d.ts +1 -0
  27. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  28. package/dist/src/frameMetadata/frameMetadata.d.ts +1 -0
  29. package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -1
  30. package/dist/src/frameMetadata/types.d.ts +1 -0
  31. package/dist/src/frameMetadata/types.d.ts.map +1 -1
  32. package/dist/src/logger.d.ts +3 -1
  33. package/dist/src/logger.d.ts.map +1 -1
  34. package/dist/src/room/PCTransport.d.ts +42 -1
  35. package/dist/src/room/PCTransport.d.ts.map +1 -1
  36. package/dist/src/room/PCTransportManager.d.ts +1 -0
  37. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  38. package/dist/src/room/RTCEngine.d.ts +46 -4
  39. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  40. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  41. package/dist/src/room/Room.d.ts +1 -0
  42. package/dist/src/room/Room.d.ts.map +1 -1
  43. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  44. package/dist/src/room/data-track/frame.d.ts +1 -0
  45. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  46. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  47. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  48. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  49. package/dist/src/room/data-track/outgoing/types.d.ts +1 -0
  50. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  51. package/dist/src/room/data-track/packet/extensions.d.ts +1 -0
  52. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  53. package/dist/src/room/data-track/packet/index.d.ts +1 -0
  54. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  55. package/dist/src/room/data-track/packet/serializable.d.ts +1 -0
  56. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  57. package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
  58. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  59. package/dist/src/room/participant/Participant.d.ts +1 -0
  60. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  61. package/dist/src/room/participant/publishUtils.d.ts +7 -0
  62. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  63. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
  64. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  65. package/dist/src/room/track/Track.d.ts +1 -0
  66. package/dist/src/room/track/Track.d.ts.map +1 -1
  67. package/dist/src/room/track/options.d.ts +11 -1
  68. package/dist/src/room/track/options.d.ts.map +1 -1
  69. package/dist/src/room/track/record.d.ts.map +1 -1
  70. package/dist/src/room/utils.d.ts +1 -0
  71. package/dist/src/room/utils.d.ts.map +1 -1
  72. package/dist/src/test/signalToken.d.ts +26 -0
  73. package/dist/src/test/signalToken.d.ts.map +1 -0
  74. package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
  75. package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts.map +1 -0
  76. package/dist/src/utils/dataPacketBuffer.d.ts +19 -0
  77. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  78. package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -0
  79. package/dist/ts4.2/e2ee/types.d.ts +1 -0
  80. package/dist/ts4.2/e2ee/utils.d.ts +1 -0
  81. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +1 -0
  82. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +1 -0
  83. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -0
  84. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +1 -0
  85. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +1 -0
  86. package/dist/ts4.2/frameMetadata/types.d.ts +1 -0
  87. package/dist/ts4.2/logger.d.ts +3 -1
  88. package/dist/ts4.2/room/PCTransport.d.ts +42 -1
  89. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  90. package/dist/ts4.2/room/RTCEngine.d.ts +46 -4
  91. package/dist/ts4.2/room/Room.d.ts +1 -0
  92. package/dist/ts4.2/room/data-track/frame.d.ts +1 -0
  93. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  94. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +1 -0
  95. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +1 -0
  96. package/dist/ts4.2/room/data-track/packet/index.d.ts +1 -0
  97. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -0
  98. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -0
  99. package/dist/ts4.2/room/participant/Participant.d.ts +1 -0
  100. package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
  101. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
  102. package/dist/ts4.2/room/track/Track.d.ts +1 -0
  103. package/dist/ts4.2/room/track/options.d.ts +11 -1
  104. package/dist/ts4.2/room/utils.d.ts +1 -0
  105. package/dist/ts4.2/test/signalToken.d.ts +26 -0
  106. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
  107. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +19 -0
  108. package/package.json +8 -4
  109. package/src/api/SignalClient.e2e.test.ts +264 -0
  110. package/src/api/SignalClient.ts +35 -2
  111. package/src/e2ee/E2eeManager.ts +3 -1
  112. package/src/e2ee/types.ts +1 -0
  113. package/src/e2ee/utils.ts +1 -0
  114. package/src/e2ee/worker/DataCryptor.ts +1 -0
  115. package/src/e2ee/worker/FrameCryptor.test.ts +1 -0
  116. package/src/e2ee/worker/FrameCryptor.ts +1 -0
  117. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +1 -0
  118. package/src/e2ee/worker/e2ee.worker.ts +1 -0
  119. package/src/e2ee/worker/naluUtils.ts +1 -0
  120. package/src/e2ee/worker/sifPayload.ts +1 -0
  121. package/src/frameMetadata/frameMetadata.test.ts +83 -0
  122. package/src/frameMetadata/frameMetadata.ts +8 -0
  123. package/src/frameMetadata/types.ts +1 -0
  124. package/src/logger.ts +2 -0
  125. package/src/room/PCTransport.test.ts +150 -0
  126. package/src/room/PCTransport.ts +179 -31
  127. package/src/room/PCTransportManager.ts +10 -6
  128. package/src/room/RTCEngine.test.ts +275 -2
  129. package/src/room/RTCEngine.ts +277 -71
  130. package/src/room/RegionUrlProvider.ts +4 -2
  131. package/src/room/Room.ts +8 -1
  132. package/src/room/data-track/depacketizer.ts +1 -0
  133. package/src/room/data-track/frame.ts +1 -0
  134. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
  135. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
  136. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
  137. package/src/room/data-track/outgoing/types.ts +1 -0
  138. package/src/room/data-track/packet/extensions.ts +1 -0
  139. package/src/room/data-track/packet/index.ts +1 -0
  140. package/src/room/data-track/packet/serializable.ts +1 -0
  141. package/src/room/errors.ts +4 -4
  142. package/src/room/participant/LocalParticipant.ts +17 -6
  143. package/src/room/participant/Participant.ts +6 -0
  144. package/src/room/participant/publishUtils.ts +14 -11
  145. package/src/room/track/LocalAudioTrack.ts +16 -0
  146. package/src/room/track/Track.ts +11 -6
  147. package/src/room/track/options.ts +11 -1
  148. package/src/room/track/record.ts +1 -0
  149. package/src/room/utils.ts +1 -0
  150. package/src/test/signalServerSetup.ts +151 -0
  151. package/src/test/signalToken.ts +59 -0
  152. package/src/test/vitest-context.d.ts +10 -0
  153. package/src/type-polyfills/{non-shared-typed-arrays.d.ts → non-shared-typed-arrays.ts} +6 -1
  154. package/src/utils/dataPacketBuffer.test.ts +100 -0
  155. package/src/utils/dataPacketBuffer.ts +48 -1
@@ -8226,6 +8226,8 @@ var LoggerNames;
8226
8226
  LoggerNames["PCTransport"] = "livekit-pc-transport";
8227
8227
  LoggerNames["E2EE"] = "lk-e2ee";
8228
8228
  LoggerNames["DataTracks"] = "livekit-data-tracks";
8229
+ LoggerNames["Region"] = "livekit-region";
8230
+ LoggerNames["ICE"] = "livekit-ice";
8229
8231
  })(LoggerNames || (LoggerNames = {}));
8230
8232
  let livekitLogger = loglevelExports.getLogger(LoggerNames.Default);
8231
8233
  const livekitLoggers = Object.values(LoggerNames).map(name => loglevelExports.getLogger(name));
@@ -8810,7 +8812,7 @@ function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
8810
8812
  }
8811
8813
  const addEventListener = Object.getOwnPropertyDescriptor(EventTarget.prototype, 'addEventListener');
8812
8814
  if (!addEventListener.writable) {
8813
- log$4('Unable to polyfill events');
8815
+ log$5('Unable to polyfill events');
8814
8816
  return;
8815
8817
  }
8816
8818
  const proto = window.RTCPeerConnection.prototype;
@@ -8890,7 +8892,7 @@ function disableWarnings(bool) {
8890
8892
  deprecationWarnings_ = !bool;
8891
8893
  return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
8892
8894
  }
8893
- function log$4() {
8895
+ function log$5() {
8894
8896
  if (typeof window === 'object') {
8895
8897
  if (logDisabled_) {
8896
8898
  return;
@@ -8937,10 +8939,15 @@ function detectBrowser(window) {
8937
8939
  return brand.brand === 'Chromium';
8938
8940
  });
8939
8941
  if (chromium) {
8940
- return {
8941
- browser: 'chrome',
8942
- version: parseInt(chromium.version, 10)
8943
- };
8942
+ const version = parseInt(chromium.version, 10);
8943
+ // navigator.userAgentData was introduced in Chromium 90
8944
+ // so any Chromium brand reporting a version below 90 is invalid
8945
+ if (version >= 90) {
8946
+ return {
8947
+ browser: 'chrome',
8948
+ version
8949
+ };
8950
+ }
8944
8951
  }
8945
8952
  }
8946
8953
  if (navigator.mozGetUserMedia) {
@@ -9047,8 +9054,11 @@ function filterStats(result, track, outbound) {
9047
9054
  * tree.
9048
9055
  */
9049
9056
  /* eslint-env node */
9050
- const logging = log$4;
9057
+ const logging = log$5;
9051
9058
  function shimGetUserMedia$2(window, browserDetails) {
9059
+ if (browserDetails.version >= 64) {
9060
+ return;
9061
+ }
9052
9062
  const navigator = window && window.navigator;
9053
9063
  if (!navigator.mediaDevices) {
9054
9064
  return;
@@ -9202,6 +9212,8 @@ function shimGetUserMedia$2(window, browserDetails) {
9202
9212
  // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
9203
9213
  // function which returns a Promise, it does not accept spec-style
9204
9214
  // constraints.
9215
+ // Fixed in M64 too, see
9216
+ // https://groups.google.com/g/discuss-webrtc/c/DMPTKcXVUQ4/m/85FaASoFBwAJ?
9205
9217
  if (navigator.mediaDevices.getUserMedia) {
9206
9218
  const origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
9207
9219
  navigator.mediaDevices.getUserMedia = function (cs) {
@@ -9787,6 +9799,9 @@ function fixNegotiationNeeded(window, browserDetails) {
9787
9799
  /* eslint-env node */
9788
9800
  function shimGetUserMedia$1(window, browserDetails) {
9789
9801
  const navigator = window && window.navigator;
9802
+ if (!navigator.mediaDevices) {
9803
+ return;
9804
+ }
9790
9805
  const MediaStreamTrack = window && window.MediaStreamTrack;
9791
9806
  navigator.getUserMedia = function (constraints, onSuccess, onError) {
9792
9807
  // Replace Firefox 44+'s deprecation warning with unprefixed version.
@@ -9840,10 +9855,10 @@ function shimGetUserMedia$1(window, browserDetails) {
9840
9855
  /* eslint-env node */
9841
9856
 
9842
9857
  function shimGetDisplayMedia(window, preferredMediaSource) {
9843
- if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
9858
+ if (!window.navigator.mediaDevices) {
9844
9859
  return;
9845
9860
  }
9846
- if (!window.navigator.mediaDevices) {
9861
+ if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
9847
9862
  return;
9848
9863
  }
9849
9864
  window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) {
@@ -10023,13 +10038,17 @@ function shimRTCDataChannel(window) {
10023
10038
  window.RTCDataChannel = window.DataChannel;
10024
10039
  }
10025
10040
  }
10026
- function shimAddTransceiver(window) {
10041
+ function shimAddTransceiver(window, browserDetails) {
10027
10042
  // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
10028
10043
  // Firefox ignores the init sendEncodings options passed to addTransceiver
10029
10044
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
10030
10045
  if (!(typeof window === 'object' && window.RTCPeerConnection)) {
10031
10046
  return;
10032
10047
  }
10048
+ // Fixed by https://bugzilla.mozilla.org/show_bug.cgi?id=1401592 in FF110.
10049
+ if (browserDetails.version >= 110) {
10050
+ return;
10051
+ }
10033
10052
  const origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver;
10034
10053
  if (origAddTransceiver) {
10035
10054
  window.RTCPeerConnection.prototype.addTransceiver = function addTransceiver() {
@@ -10089,10 +10108,14 @@ function shimAddTransceiver(window) {
10089
10108
  };
10090
10109
  }
10091
10110
  }
10092
- function shimGetParameters(window) {
10111
+ function shimGetParameters(window, browserDetails) {
10093
10112
  if (!(typeof window === 'object' && window.RTCRtpSender)) {
10094
10113
  return;
10095
10114
  }
10115
+ // Fixed by https://bugzilla.mozilla.org/show_bug.cgi?id=1401592 in FF110.
10116
+ if (browserDetails.version >= 110) {
10117
+ return;
10118
+ }
10096
10119
  const origGetParameters = window.RTCRtpSender.prototype.getParameters;
10097
10120
  if (origGetParameters) {
10098
10121
  window.RTCRtpSender.prototype.getParameters = function getParameters() {
@@ -10104,13 +10127,17 @@ function shimGetParameters(window) {
10104
10127
  };
10105
10128
  }
10106
10129
  }
10107
- function shimCreateOffer(window) {
10130
+ function shimCreateOffer(window, browserDetails) {
10108
10131
  // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
10109
10132
  // Firefox ignores the init sendEncodings options passed to addTransceiver
10110
10133
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
10111
10134
  if (!(typeof window === 'object' && window.RTCPeerConnection)) {
10112
10135
  return;
10113
10136
  }
10137
+ // Fixed by https://bugzilla.mozilla.org/show_bug.cgi?id=1401592 in FF110.
10138
+ if (browserDetails.version >= 110) {
10139
+ return;
10140
+ }
10114
10141
  const origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
10115
10142
  window.RTCPeerConnection.prototype.createOffer = function createOffer() {
10116
10143
  if (this.setParametersPromises && this.setParametersPromises.length) {
@@ -10123,13 +10150,17 @@ function shimCreateOffer(window) {
10123
10150
  return origCreateOffer.apply(this, arguments);
10124
10151
  };
10125
10152
  }
10126
- function shimCreateAnswer(window) {
10153
+ function shimCreateAnswer(window, browserDetails) {
10127
10154
  // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
10128
10155
  // Firefox ignores the init sendEncodings options passed to addTransceiver
10129
10156
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
10130
10157
  if (!(typeof window === 'object' && window.RTCPeerConnection)) {
10131
10158
  return;
10132
10159
  }
10160
+ // Fixed by https://bugzilla.mozilla.org/show_bug.cgi?id=1401592 in FF110.
10161
+ if (browserDetails.version >= 110) {
10162
+ return;
10163
+ }
10133
10164
  const origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer;
10134
10165
  window.RTCPeerConnection.prototype.createAnswer = function createAnswer() {
10135
10166
  if (this.setParametersPromises && this.setParametersPromises.length) {
@@ -11291,6 +11322,15 @@ function shimMaxMessageSize(window, browserDetails) {
11291
11322
  if (!window.RTCPeerConnection) {
11292
11323
  return;
11293
11324
  }
11325
+ if (browserDetails.browser === 'chrome' && browserDetails.version > 102) {
11326
+ // Unified plan is supported so no need to do anything.
11327
+ return;
11328
+ }
11329
+ if (browserDetails.browser === 'firefox' && browserDetails.version >= 113) {
11330
+ // Native RTCSctpTransport, see
11331
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1278299
11332
+ return;
11333
+ }
11294
11334
  if (!('sctp' in window.RTCPeerConnection.prototype)) {
11295
11335
  Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', {
11296
11336
  get() {
@@ -11427,7 +11467,7 @@ function shimSendThrowTypeError(window, browserDetails) {
11427
11467
  if (!(window.RTCPeerConnection && 'createDataChannel' in window.RTCPeerConnection.prototype)) {
11428
11468
  return;
11429
11469
  }
11430
- if (browserDetails.browser === 'chrome' && browserDetails.version > 149) {
11470
+ if (browserDetails.browser === 'chrome' && browserDetails.version >= 149) {
11431
11471
  // Fixed by https://issues.chromium.org/issues/490588131
11432
11472
  return;
11433
11473
  }
@@ -11641,7 +11681,7 @@ function adapterFactory() {
11641
11681
  shimSafari: true
11642
11682
  };
11643
11683
  // Utils.
11644
- const logging = log$4;
11684
+ const logging = log$5;
11645
11685
  const browserDetails = detectBrowser(window);
11646
11686
  const adapter = {
11647
11687
  browserDetails,
@@ -11706,10 +11746,10 @@ function adapterFactory() {
11706
11746
  shimSenderGetStats(window);
11707
11747
  shimReceiverGetStats(window);
11708
11748
  shimRTCDataChannel(window);
11709
- shimAddTransceiver(window);
11710
- shimGetParameters(window);
11711
- shimCreateOffer(window);
11712
- shimCreateAnswer(window);
11749
+ shimAddTransceiver(window, browserDetails);
11750
+ shimGetParameters(window, browserDetails);
11751
+ shimCreateOffer(window, browserDetails);
11752
+ shimCreateAnswer(window, browserDetails);
11713
11753
  shimRTCIceCandidate(window);
11714
11754
  shimConnectionState(window);
11715
11755
  shimMaxMessageSize(window, browserDetails);
@@ -11843,7 +11883,7 @@ function getMatch(exp, ua) {
11843
11883
  }
11844
11884
  function getOSVersion(ua) {
11845
11885
  return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
11846
- }var version$1 = "2.20.0";const version = version$1;
11886
+ }var version$1 = "2.20.2";const version = version$1;
11847
11887
  const protocolVersion = 17;
11848
11888
  /** Initial client protocol. */
11849
11889
  const CLIENT_PROTOCOL_DEFAULT = 0;
@@ -11934,25 +11974,25 @@ class TrackInvalidError extends LivekitError {
11934
11974
  }
11935
11975
  class UnsupportedServer extends LivekitError {
11936
11976
  constructor(message) {
11937
- super(10, message !== null && message !== void 0 ? message : 'unsupported server');
11977
+ super(10, message || 'unsupported server');
11938
11978
  this.name = 'UnsupportedServer';
11939
11979
  }
11940
11980
  }
11941
11981
  class UnexpectedConnectionState extends LivekitError {
11942
11982
  constructor(message) {
11943
- super(12, message !== null && message !== void 0 ? message : 'unexpected connection state');
11983
+ super(12, message || 'unexpected connection state');
11944
11984
  this.name = 'UnexpectedConnectionState';
11945
11985
  }
11946
11986
  }
11947
11987
  class NegotiationError extends LivekitError {
11948
11988
  constructor(message) {
11949
- super(13, message !== null && message !== void 0 ? message : 'unable to negotiate');
11989
+ super(13, message || 'unable to negotiate');
11950
11990
  this.name = 'NegotiationError';
11951
11991
  }
11952
11992
  }
11953
11993
  class PublishDataError extends LivekitError {
11954
11994
  constructor(message) {
11955
- super(14, message !== null && message !== void 0 ? message : 'unable to publish data');
11995
+ super(14, message || 'unable to publish data');
11956
11996
  this.name = 'PublishDataError';
11957
11997
  }
11958
11998
  }
@@ -13157,6 +13197,9 @@ class Track extends eventsExports.EventEmitter {
13157
13197
  }
13158
13198
  /** @internal */
13159
13199
  setStreamState(value) {
13200
+ if (this._streamState !== value) {
13201
+ this.log.debug("stream state changed: ".concat(this._streamState, " -> ").concat(value));
13202
+ }
13160
13203
  this._streamState = value;
13161
13204
  }
13162
13205
  constructor(mediaTrack, kind) {
@@ -13181,8 +13224,8 @@ class Track extends eventsExports.EventEmitter {
13181
13224
  this.handleAppVisibilityChanged();
13182
13225
  }
13183
13226
  };
13184
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track);
13185
13227
  this.loggerContextCb = loggerOptions.loggerContextCb;
13228
+ this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track, () => this.logContext);
13186
13229
  this.setMaxListeners(100);
13187
13230
  this.kind = kind;
13188
13231
  this._mediaStreamTrack = mediaTrack;
@@ -13250,9 +13293,11 @@ class Track extends eventsExports.EventEmitter {
13250
13293
  this.emit(hasAudio ? TrackEvent.AudioPlaybackFailed : TrackEvent.VideoPlaybackFailed, e);
13251
13294
  } else if (e.name === 'AbortError') {
13252
13295
  // commonly triggered by another `play` request, only log for debugging purposes
13253
- livekitLogger.debug("".concat(hasAudio ? 'audio' : 'video', " playback aborted, likely due to new play request"));
13296
+ this.log.debug("".concat(hasAudio ? 'audio' : 'video', " playback aborted, likely due to new play request"));
13254
13297
  } else {
13255
- livekitLogger.warn("could not playback ".concat(hasAudio ? 'audio' : 'video'), e);
13298
+ this.log.warn("could not playback ".concat(hasAudio ? 'audio' : 'video'), {
13299
+ error: e
13300
+ });
13256
13301
  }
13257
13302
  // If audio playback isn't allowed make sure we still play back the video
13258
13303
  if (hasAudio && element && allMediaStreamTracks.some(tr => tr.kind === 'video') && e.name === 'NotAllowedError') {
@@ -13295,6 +13340,7 @@ class Track extends eventsExports.EventEmitter {
13295
13340
  }
13296
13341
  }
13297
13342
  stop() {
13343
+ this.log.debug('stopping track');
13298
13344
  this.stopMonitor();
13299
13345
  this._mediaStreamTrack.stop();
13300
13346
  }
@@ -13315,12 +13361,12 @@ class Track extends eventsExports.EventEmitter {
13315
13361
  }
13316
13362
  /** @internal */
13317
13363
  updateLoggerOptions(loggerOptions) {
13318
- if (loggerOptions.loggerName) {
13319
- this.log = getLogger(loggerOptions.loggerName);
13320
- }
13321
13364
  if (loggerOptions.loggerContextCb) {
13322
13365
  this.loggerContextCb = loggerOptions.loggerContextCb;
13323
13366
  }
13367
+ if (loggerOptions.loggerName) {
13368
+ this.log = getLogger(loggerOptions.loggerName, () => this.logContext);
13369
+ }
13324
13370
  }
13325
13371
  recycleElement(element) {
13326
13372
  if (element instanceof HTMLAudioElement) {
@@ -16279,6 +16325,10 @@ var SignalConnectionState;
16279
16325
  })(SignalConnectionState || (SignalConnectionState = {}));
16280
16326
  /** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
16281
16327
  const MAX_WS_CLOSE_TIME = 250;
16328
+ /**
16329
+ * How long (in ms) to wait for the first message after the WebSocket upgrade.
16330
+ */
16331
+ const JOIN_RESPONSE_TIMEOUT = 5000;
16282
16332
  /** @internal */
16283
16333
  class SignalClient {
16284
16334
  get currentState() {
@@ -16427,7 +16477,6 @@ class SignalClient {
16427
16477
  this.ws = new WebSocketStream(rtcUrl);
16428
16478
  try {
16429
16479
  this.ws.closed.then(closeInfo => {
16430
- var _a;
16431
16480
  if (this.isEstablishingConnection) {
16432
16481
  reject(ConnectionError.internal("Websocket got closed during a (re)connection attempt: ".concat(closeInfo.reason)));
16433
16482
  }
@@ -16439,7 +16488,7 @@ class SignalClient {
16439
16488
  state: this.state
16440
16489
  });
16441
16490
  if (this.state === SignalConnectionState.CONNECTED) {
16442
- this.handleOnClose((_a = closeInfo.reason) !== null && _a !== void 0 ? _a : 'Unexpected WS error');
16491
+ this.handleOnClose(closeInfo.reason || 'Unexpected WS error');
16443
16492
  }
16444
16493
  }
16445
16494
  return;
@@ -16467,7 +16516,26 @@ class SignalClient {
16467
16516
  }
16468
16517
  const signalReader = connection.readable.getReader();
16469
16518
  this.streamWriter = connection.writable.getWriter();
16470
- const firstMessage = yield signalReader.read();
16519
+ // wsTimeout only guarded the upgrade; guard the first-message read with
16520
+ // its own timeout so a silent server can't hang join() forever.
16521
+ let firstMessage;
16522
+ let firstMessageTimeout;
16523
+ try {
16524
+ firstMessage = yield Promise.race([signalReader.read(), new Promise((_, rejectRead) => {
16525
+ firstMessageTimeout = setTimeout(() => {
16526
+ rejectRead(ConnectionError.timeout('signal connection timed out while waiting for the first message'));
16527
+ }, JOIN_RESPONSE_TIMEOUT);
16528
+ })]);
16529
+ } catch (e) {
16530
+ // No first message in time: release the reader and tear down the ws
16531
+ // so we surface the timeout instead of leaking an open connection.
16532
+ signalReader.releaseLock();
16533
+ reject(e);
16534
+ this.close();
16535
+ return;
16536
+ } finally {
16537
+ clearTimeout(firstMessageTimeout);
16538
+ }
16471
16539
  signalReader.releaseLock();
16472
16540
  if (!firstMessage.value) {
16473
16541
  throw ConnectionError.internal('no message received as first message');
@@ -17205,21 +17273,50 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
17205
17273
  constructor() {
17206
17274
  this.buffer = [];
17207
17275
  this._totalSize = 0;
17276
+ this._sentSize = 0;
17208
17277
  }
17209
17278
  push(item) {
17210
17279
  this.buffer.push(item);
17211
17280
  this._totalSize += item.data.byteLength;
17281
+ if (item.sent) {
17282
+ this._sentSize += item.data.byteLength;
17283
+ }
17212
17284
  }
17213
17285
  pop() {
17214
17286
  const item = this.buffer.shift();
17215
17287
  if (item) {
17216
17288
  this._totalSize -= item.data.byteLength;
17289
+ if (item.sent) {
17290
+ this._sentSize -= item.data.byteLength;
17291
+ }
17217
17292
  }
17218
17293
  return item;
17219
17294
  }
17220
17295
  getAll() {
17221
17296
  return this.buffer.slice();
17222
17297
  }
17298
+ /** Every queued packet not yet handed to the channel, in sequence order. */
17299
+ getUnsent() {
17300
+ return this.buffer.filter(item => !item.sent);
17301
+ }
17302
+ /** Marks a single queued packet as handed to the channel. */
17303
+ markSent(item) {
17304
+ if (!item.sent) {
17305
+ item.sent = true;
17306
+ this._sentSize += item.data.byteLength;
17307
+ }
17308
+ }
17309
+ /**
17310
+ * Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
17311
+ * still buffered was sent on the previous channel (or deferred) and must be re-handed to the
17312
+ * current one, so none of it counts as sent until the replay actually transmits it.
17313
+ */
17314
+ markAllUnsent() {
17315
+ for (const item of this.buffer) {
17316
+ item.sent = false;
17317
+ }
17318
+ this._sentSize = 0;
17319
+ }
17223
17320
  popToSequence(sequence) {
17224
17321
  while (this.buffer.length > 0) {
17225
17322
  const first = this.buffer[0];
@@ -17233,7 +17330,12 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
17233
17330
  alignBufferedAmount(bufferedAmount) {
17234
17331
  while (this.buffer.length > 0) {
17235
17332
  const first = this.buffer[0];
17236
- if (this._totalSize - first.data.byteLength <= bufferedAmount) {
17333
+ // Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
17334
+ // resume replay — trimming them would silently lose them.
17335
+ if (!first.sent) {
17336
+ break;
17337
+ }
17338
+ if (this._sentSize - first.data.byteLength <= bufferedAmount) {
17237
17339
  break;
17238
17340
  }
17239
17341
  this.pop();
@@ -18018,13 +18120,20 @@ function requireLib() {
18018
18120
  lib.parseImageAttributes = parser.parseImageAttributes;
18019
18121
  lib.parseSimulcastStreamList = parser.parseSimulcastStreamList;
18020
18122
  return lib;
18021
- }var libExports = requireLib();/* The svc codec (av1/vp9) would use a very low bitrate at the begining and
18022
- increase slowly by the bandwidth estimator until it reach the target bitrate. The
18023
- process commonly cost more than 10 seconds cause subscriber will get blur video at
18024
- the first few seconds. So we use a 70% of target bitrate here as the start bitrate to
18025
- eliminate this issue.
18026
- */
18027
- const startBitrateForSVC = 0.7;
18123
+ }var libExports = requireLib();/*
18124
+ * Video codecs use a very low bitrate at the beginning and increase slowly by
18125
+ * the bandwidth estimator until they reach the target bitrate. The process commonly
18126
+ * costs more than 10 seconds causing subscribers to get blurry video at the first
18127
+ * few seconds. We use x-google-start-bitrate to hint the BWE to start higher.
18128
+ *
18129
+ * Why 90%: Gives ~10% headroom for bandwidth estimation while starting close to target.
18130
+ * Why same for all codecs: Target bitrate already accounts for codec efficiency
18131
+ * (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
18132
+ * Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
18133
+ */
18134
+ const startBitrateMultiplier = 0.9;
18135
+ /** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
18136
+ const maxStartBitrateKbps = 1000;
18028
18137
  const debounceInterval = 20;
18029
18138
  const PCEvents = {
18030
18139
  NegotiationStarted: 'negotiationStarted',
@@ -18049,6 +18158,7 @@ class PCTransport extends eventsExports.EventEmitter {
18049
18158
  var _a;
18050
18159
  super();
18051
18160
  this.log = livekitLogger;
18161
+ this.iceLog = livekitLogger;
18052
18162
  this.ddExtID = 0;
18053
18163
  this.latestOfferId = 0;
18054
18164
  this.latestAcknowledgedOfferId = 0;
@@ -18075,6 +18185,7 @@ class PCTransport extends eventsExports.EventEmitter {
18075
18185
  if (!this._pc) {
18076
18186
  return;
18077
18187
  }
18188
+ this.log.debug('closing peer connection');
18078
18189
  this.pendingInitialOffer = undefined;
18079
18190
  this._pc.close();
18080
18191
  this._pc.onconnectionstatechange = null;
@@ -18090,8 +18201,9 @@ class PCTransport extends eventsExports.EventEmitter {
18090
18201
  this._pc.oniceconnectionstatechange = null;
18091
18202
  this._pc = null;
18092
18203
  };
18093
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCTransport);
18094
18204
  this.loggerOptions = loggerOptions;
18205
+ this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCTransport, () => this.logContext);
18206
+ this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
18095
18207
  this.config = config;
18096
18208
  this._pc = this.createPC();
18097
18209
  this.offerLock = new _();
@@ -18101,26 +18213,39 @@ class PCTransport extends eventsExports.EventEmitter {
18101
18213
  pc.onicecandidate = ev => {
18102
18214
  var _a;
18103
18215
  if (!ev.candidate) return;
18216
+ this.iceLog.debug('local ICE candidate gathered', {
18217
+ candidate: ev.candidate.candidate
18218
+ });
18104
18219
  (_a = this.onIceCandidate) === null || _a === void 0 ? void 0 : _a.call(this, ev.candidate);
18105
18220
  };
18106
18221
  pc.onicecandidateerror = ev => {
18107
18222
  var _a;
18223
+ this.iceLog.debug('ICE candidate error', {
18224
+ event: ev
18225
+ });
18108
18226
  (_a = this.onIceCandidateError) === null || _a === void 0 ? void 0 : _a.call(this, ev);
18109
18227
  };
18110
18228
  pc.oniceconnectionstatechange = () => {
18111
18229
  var _a;
18230
+ this.iceLog.debug("ICE connection state: ".concat(pc.iceConnectionState));
18112
18231
  (_a = this.onIceConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.iceConnectionState);
18113
18232
  };
18114
18233
  pc.onsignalingstatechange = () => {
18115
18234
  var _a;
18235
+ this.log.debug("signaling state: ".concat(pc.signalingState));
18116
18236
  (_a = this.onSignalingStatechange) === null || _a === void 0 ? void 0 : _a.call(this, pc.signalingState);
18117
18237
  };
18118
18238
  pc.onconnectionstatechange = () => {
18119
18239
  var _a;
18240
+ this.log.debug("connection state: ".concat(pc.connectionState));
18120
18241
  (_a = this.onConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.connectionState);
18121
18242
  };
18122
18243
  pc.ondatachannel = ev => {
18123
18244
  var _a;
18245
+ this.log.debug('data channel opened by peer', {
18246
+ label: ev.channel.label,
18247
+ id: ev.channel.id
18248
+ });
18124
18249
  (_a = this.onDataChannel) === null || _a === void 0 ? void 0 : _a.call(this, ev);
18125
18250
  };
18126
18251
  pc.ontrack = ev => {
@@ -18141,6 +18266,9 @@ class PCTransport extends eventsExports.EventEmitter {
18141
18266
  if (this.pc.remoteDescription && !this.restartingIce) {
18142
18267
  return this.pc.addIceCandidate(candidate);
18143
18268
  }
18269
+ this.iceLog.debug('queuing remote ICE candidate until remote description applied', {
18270
+ pendingCount: this.pendingCandidates.length + 1
18271
+ });
18144
18272
  this.pendingCandidates.push(candidate);
18145
18273
  });
18146
18274
  }
@@ -18148,10 +18276,10 @@ class PCTransport extends eventsExports.EventEmitter {
18148
18276
  return __awaiter(this, void 0, void 0, function* () {
18149
18277
  var _a, _b;
18150
18278
  if (sd.type === 'answer' && this.latestOfferId > 0 && offerId > 0 && offerId !== this.latestOfferId) {
18151
- this.log.warn('ignoring answer for old offer', Object.assign(Object.assign({}, this.logContext), {
18279
+ this.log.warn('ignoring answer for old offer', {
18152
18280
  offerId,
18153
18281
  latestOfferId: this.latestOfferId
18154
- }));
18282
+ });
18155
18283
  return false;
18156
18284
  }
18157
18285
  let mungedSDP = undefined;
@@ -18169,7 +18297,7 @@ class PCTransport extends eventsExports.EventEmitter {
18169
18297
  sdpParsed.media.forEach(media => {
18170
18298
  ensureIPAddrMatchVersion(media);
18171
18299
  });
18172
- this.log.debug('setting pending initial offer before processing answer', this.logContext);
18300
+ this.log.debug('setting pending initial offer before processing answer');
18173
18301
  yield this.setMungedSDP(initialOffer, libExports.write(sdpParsed));
18174
18302
  }
18175
18303
  const sdpParsed = libExports.parse((_b = sd.sdp) !== null && _b !== void 0 ? _b : '');
@@ -18215,9 +18343,23 @@ class PCTransport extends eventsExports.EventEmitter {
18215
18343
  });
18216
18344
  }
18217
18345
  });
18346
+ // The server's answer sets per-track codec params (e.g. opus `usedtx`) on
18347
+ // the sections mapped to a published track, but leaves the pre-populated
18348
+ // recvonly placeholder sections with defaults. Conform the placeholders so
18349
+ // each shared payload type is consistent across the bundle, otherwise
18350
+ // libwebrtc flags a "bundled payload type collision".
18351
+ const placeholderMids = this.getPlaceholderMids();
18352
+ if (placeholderMids.size > 0) {
18353
+ conformBundledCodecFmtp(sdpParsed.media, media => placeholderMids.has(getMidString(media.mid)));
18354
+ }
18218
18355
  mungedSDP = libExports.write(sdpParsed);
18219
18356
  }
18220
18357
  yield this.setMungedSDP(sd, mungedSDP, true);
18358
+ if (this.pendingCandidates.length > 0) {
18359
+ this.iceLog.debug('flushing queued ICE candidates', {
18360
+ count: this.pendingCandidates.length
18361
+ });
18362
+ }
18221
18363
  this.pendingCandidates.forEach(candidate => {
18222
18364
  this.pc.addIceCandidate(candidate);
18223
18365
  });
@@ -18253,7 +18395,7 @@ class PCTransport extends eventsExports.EventEmitter {
18253
18395
  const unlock = yield this.offerLock.lock();
18254
18396
  try {
18255
18397
  if (this.pc.signalingState !== 'stable') {
18256
- this.log.warn('signaling state is not stable, cannot create initial offer', this.logContext);
18398
+ this.log.warn('signaling state is not stable, cannot create initial offer');
18257
18399
  return;
18258
18400
  }
18259
18401
  const offerId = this.latestOfferId + 1;
@@ -18286,7 +18428,7 @@ class PCTransport extends eventsExports.EventEmitter {
18286
18428
  return;
18287
18429
  }
18288
18430
  if (options === null || options === void 0 ? void 0 : options.iceRestart) {
18289
- this.log.debug('restarting ICE', this.logContext);
18431
+ this.iceLog.debug('restarting ICE');
18290
18432
  this.restartingIce = true;
18291
18433
  }
18292
18434
  if (this._pc && (this._pc.signalingState === 'have-local-offer' || this.pendingInitialOffer)) {
@@ -18299,22 +18441,22 @@ class PCTransport extends eventsExports.EventEmitter {
18299
18441
  yield this._pc.setRemoteDescription(currentSD);
18300
18442
  } else {
18301
18443
  this.renegotiate = true;
18302
- this.log.debug('requesting renegotiation', Object.assign({}, this.logContext));
18444
+ this.log.debug('requesting renegotiation');
18303
18445
  return;
18304
18446
  }
18305
18447
  } else if (!this._pc || this._pc.signalingState === 'closed') {
18306
- this.log.warn('could not createOffer with closed peer connection', this.logContext);
18448
+ this.log.warn('could not createOffer with closed peer connection');
18307
18449
  return;
18308
18450
  }
18309
18451
  // actually negotiate
18310
- this.log.debug('starting to negotiate', this.logContext);
18452
+ this.log.debug('starting to negotiate');
18311
18453
  // increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
18312
18454
  const offerId = this.latestOfferId + 1;
18313
18455
  this.latestOfferId = offerId;
18314
18456
  const offer = yield this.pc.createOffer(options);
18315
- this.log.debug('original offer', Object.assign({
18457
+ this.log.debug('original offer', {
18316
18458
  sdp: offer.sdp
18317
- }, this.logContext));
18459
+ });
18318
18460
  const sdpParsed = libExports.parse((_a = offer.sdp) !== null && _a !== void 0 ? _a : '');
18319
18461
  sdpParsed.media.forEach(media => {
18320
18462
  ensureIPAddrMatchVersion(media);
@@ -18340,12 +18482,15 @@ class PCTransport extends eventsExports.EventEmitter {
18340
18482
  this.ensureVideoDDExtensionForSVC(media, sdpParsed);
18341
18483
  }
18342
18484
  // mung sdp for bitrate setting that can't apply by sendEncoding
18343
- if (!isSVCCodec(trackbr.codec)) {
18344
- return true;
18345
- }
18346
- const startBitrate = Math.round(trackbr.maxbr * startBitrateForSVC);
18485
+ // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE from starting too aggressively
18486
+ // Screen share is not capped since text/UI clarity requires high bitrate from the start
18487
+ // TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
18488
+ const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
18489
+ const startBitrate = trackbr.isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
18490
+ let fmtpFound = false;
18347
18491
  for (const fmtp of media.fmtp) {
18348
18492
  if (fmtp.payload === codecPayload) {
18493
+ fmtpFound = true;
18349
18494
  // if another track's fmtp already is set, we cannot override the bitrate
18350
18495
  // this has the unfortunate consequence of being forced to use the
18351
18496
  // initial track's bitrate for all tracks
@@ -18355,15 +18500,31 @@ class PCTransport extends eventsExports.EventEmitter {
18355
18500
  break;
18356
18501
  }
18357
18502
  }
18503
+ // VP8 and some codecs may not have an existing fmtp line - create one
18504
+ if (!fmtpFound) {
18505
+ media.fmtp.push({
18506
+ payload: codecPayload,
18507
+ config: "x-google-start-bitrate=".concat(startBitrate)
18508
+ });
18509
+ }
18358
18510
  return true;
18359
18511
  });
18360
18512
  }
18361
18513
  });
18514
+ // Conform the placeholder sections (pre-populated, or reverted after an
18515
+ // unpublish) so every shared payload type carries identical fmtp across the
18516
+ // bundle, otherwise libwebrtc flags a "bundled payload type collision".
18517
+ // Detection is by transceiver (mids are stable across renegotiations) since
18518
+ // an unpublished section keeps its `a=msid`.
18519
+ const placeholderMids = this.getPlaceholderMids();
18520
+ if (placeholderMids.size > 0) {
18521
+ conformBundledCodecFmtp(sdpParsed.media, media => placeholderMids.has(getMidString(media.mid)));
18522
+ }
18362
18523
  if (this.latestOfferId > offerId) {
18363
- this.log.warn('latestOfferId mismatch', Object.assign(Object.assign({}, this.logContext), {
18524
+ this.log.warn('latestOfferId mismatch', {
18364
18525
  latestOfferId: this.latestOfferId,
18365
18526
  offerId
18366
- }));
18527
+ });
18367
18528
  return;
18368
18529
  }
18369
18530
  yield this.setMungedSDP(offer, libExports.write(sdpParsed));
@@ -18388,6 +18549,17 @@ class PCTransport extends eventsExports.EventEmitter {
18388
18549
  return answer;
18389
18550
  });
18390
18551
  }
18552
+ /**
18553
+ * Returns the mids of transceivers that carry no outgoing track on this
18554
+ * (publisher) connection: the pre-populated placeholders added by
18555
+ * `RTCEngine.applyInitialPublisherLayout`, plus any transceiver that was used
18556
+ * for a track and reverted on unpublish. Their codec fmtp is conformed to the
18557
+ * published tracks so a shared payload type stays consistent across the bundle.
18558
+ */
18559
+ getPlaceholderMids() {
18560
+ var _a, _b;
18561
+ return placeholderMidsFromTransceivers((_b = (_a = this._pc) === null || _a === void 0 ? void 0 : _a.getTransceivers()) !== null && _b !== void 0 ? _b : []);
18562
+ }
18391
18563
  createDataChannel(label, dataChannelDict) {
18392
18564
  return this.pc.createDataChannel(label, dataChannelDict);
18393
18565
  }
@@ -18500,7 +18672,7 @@ class PCTransport extends eventsExports.EventEmitter {
18500
18672
  if (munged) {
18501
18673
  sd.sdp = munged;
18502
18674
  try {
18503
- this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description"), this.logContext);
18675
+ this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description"));
18504
18676
  if (remote) {
18505
18677
  yield this.pc.setRemoteDescription(sd);
18506
18678
  } else {
@@ -18508,11 +18680,11 @@ class PCTransport extends eventsExports.EventEmitter {
18508
18680
  }
18509
18681
  return;
18510
18682
  } catch (e) {
18511
- this.log.warn("not able to set ".concat(sd.type, ", falling back to unmodified sdp"), Object.assign(Object.assign({}, this.logContext), {
18683
+ this.log.warn("not able to set ".concat(sd.type, ", falling back to unmodified sdp"), {
18512
18684
  error: e,
18513
18685
  mungedSdp: munged,
18514
18686
  originalSdp
18515
- }));
18687
+ });
18516
18688
  sd.sdp = originalSdp;
18517
18689
  }
18518
18690
  }
@@ -18539,9 +18711,9 @@ class PCTransport extends eventsExports.EventEmitter {
18539
18711
  if (!remote && this.pc.remoteDescription) {
18540
18712
  fields.remoteSdp = this.pc.remoteDescription;
18541
18713
  }
18542
- this.log.error("unable to set ".concat(sd.type), Object.assign(Object.assign({}, this.logContext), {
18714
+ this.log.error("unable to set ".concat(sd.type), {
18543
18715
  fields
18544
- }));
18716
+ });
18545
18717
  throw new NegotiationError(msg);
18546
18718
  }
18547
18719
  });
@@ -18610,6 +18782,79 @@ function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
18610
18782
  }
18611
18783
  }
18612
18784
  }
18785
+ /**
18786
+ * Returns the mids of transceivers that carry no outgoing track: the
18787
+ * pre-populated placeholders added by `RTCEngine.applyInitialPublisherLayout`,
18788
+ * plus any transceiver that was used for a track and reverted on unpublish. The
18789
+ * `sender.track` check is the reliable signal — an unpublished section keeps its
18790
+ * `a=msid` (and its stale send-derived fmtp), so it can't be told apart from a
18791
+ * real send by SDP alone. Transceiver mids are stable across renegotiations, so
18792
+ * this works for every offer/answer after the first.
18793
+ * @internal
18794
+ */
18795
+ function placeholderMidsFromTransceivers(transceivers) {
18796
+ const mids = new Set();
18797
+ for (const transceiver of transceivers) {
18798
+ if (transceiver.mid && !transceiver.sender.track) {
18799
+ mids.add(transceiver.mid);
18800
+ }
18801
+ }
18802
+ return mids;
18803
+ }
18804
+ /**
18805
+ * Within a BUNDLE group a payload type must map to identical codec parameters
18806
+ * across every m-line. When the same payload type carries different fmtp between
18807
+ * sections — e.g. opus `usedtx=1` on the published microphone but not on the
18808
+ * pre-populated recvonly placeholders, or H.265 with different `level-id` between
18809
+ * a published video track and a placeholder — libwebrtc flags a "bundled payload
18810
+ * type collision".
18811
+ *
18812
+ * Rewrite the placeholder sections so every shared payload type carries the
18813
+ * same fmtp. Real (non-placeholder) sections always win the canonical value, so
18814
+ * a published track's encoder parameters are never altered. When no real
18815
+ * section declares a payload type — e.g. a placeholder that was reused for a
18816
+ * track and then reverted to recvonly keeps its send-derived `level-id` while
18817
+ * fresh placeholders use the default — the placeholders still converge on the
18818
+ * first value seen, so two placeholders can't disagree either. Only placeholder
18819
+ * sections are ever rewritten, and it is codec-agnostic (opus, H.265, ...).
18820
+ * `isPlaceholder` identifies the sections to conform.
18821
+ * @internal
18822
+ */
18823
+ function conformBundledCodecFmtp(media, isPlaceholder) {
18824
+ var _a, _b;
18825
+ // Canonical fmtp per payload type. Payload types are unique within a BUNDLE
18826
+ // group, so keying by payload alone (across audio and video) is safe. A real
18827
+ // section's value always takes precedence; otherwise the first placeholder
18828
+ // value seen is used so divergent placeholders still converge.
18829
+ const canonicalByPayload = new Map();
18830
+ const fromRealSection = new Set();
18831
+ for (const m of media) {
18832
+ const placeholder = isPlaceholder(m);
18833
+ for (const fmtp of (_a = m.fmtp) !== null && _a !== void 0 ? _a : []) {
18834
+ if (!placeholder) {
18835
+ canonicalByPayload.set(fmtp.payload, fmtp.config);
18836
+ fromRealSection.add(fmtp.payload);
18837
+ } else if (!canonicalByPayload.has(fmtp.payload)) {
18838
+ canonicalByPayload.set(fmtp.payload, fmtp.config);
18839
+ }
18840
+ }
18841
+ }
18842
+ if (canonicalByPayload.size === 0) {
18843
+ return;
18844
+ }
18845
+ // Conform placeholder sections to the canonical fmtp for each shared payload.
18846
+ for (const m of media) {
18847
+ if (!isPlaceholder(m)) {
18848
+ continue;
18849
+ }
18850
+ for (const fmtp of (_b = m.fmtp) !== null && _b !== void 0 ? _b : []) {
18851
+ const config = canonicalByPayload.get(fmtp.payload);
18852
+ if (config !== undefined && fmtp.config !== config) {
18853
+ fmtp.config = config;
18854
+ }
18855
+ }
18856
+ }
18857
+ }
18613
18858
  function extractStereoAndNackAudioFromOffer(offer) {
18614
18859
  var _a;
18615
18860
  const stereoMids = [];
@@ -18729,6 +18974,7 @@ class PCTransportManager {
18729
18974
  var _a;
18730
18975
  this.peerConnectionTimeout = roomConnectOptionDefaults.peerConnectionTimeout;
18731
18976
  this.log = livekitLogger;
18977
+ this.iceLog = livekitLogger;
18732
18978
  this.updateState = () => {
18733
18979
  var _a, _b;
18734
18980
  const previousState = this.state;
@@ -18747,12 +18993,13 @@ class PCTransportManager {
18747
18993
  this.state = PCTransportState.NEW;
18748
18994
  }
18749
18995
  if (previousState !== this.state) {
18750
- this.log.debug("pc state change: from ".concat(PCTransportState[previousState], " to ").concat(PCTransportState[this.state]), this.logContext);
18996
+ this.log.debug("pc state change: from ".concat(PCTransportState[previousState], " to ").concat(PCTransportState[this.state]));
18751
18997
  (_a = this.onStateChange) === null || _a === void 0 ? void 0 : _a.call(this, this.state, this.publisher.getConnectionState(), (_b = this.subscriber) === null || _b === void 0 ? void 0 : _b.getConnectionState());
18752
18998
  }
18753
18999
  };
18754
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCManager);
18755
19000
  this.loggerOptions = loggerOptions;
19001
+ this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCManager, () => this.logContext);
19002
+ this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
18756
19003
  this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
18757
19004
  this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
18758
19005
  this.publisher = new PCTransport(rtcConfig, loggerOptions);
@@ -18825,9 +19072,9 @@ class PCTransportManager {
18825
19072
  publisher.removeTrack(sender);
18826
19073
  }
18827
19074
  } catch (e) {
18828
- this.log.warn('could not removeTrack', Object.assign(Object.assign({}, this.logContext), {
19075
+ this.log.warn('could not removeTrack', {
18829
19076
  error: e
18830
- }));
19077
+ });
18831
19078
  }
18832
19079
  }
18833
19080
  }
@@ -18837,6 +19084,7 @@ class PCTransportManager {
18837
19084
  }
18838
19085
  triggerIceRestart() {
18839
19086
  return __awaiter(this, void 0, void 0, function* () {
19087
+ this.iceLog.warn('triggering ICE restart');
18840
19088
  if (this.subscriber) {
18841
19089
  this.subscriber.restartingIce = true;
18842
19090
  }
@@ -18851,6 +19099,10 @@ class PCTransportManager {
18851
19099
  addIceCandidate(candidate, target) {
18852
19100
  return __awaiter(this, void 0, void 0, function* () {
18853
19101
  var _a;
19102
+ this.iceLog.debug('adding remote ICE candidate', {
19103
+ target,
19104
+ candidate
19105
+ });
18854
19106
  if (target === SignalTarget.PUBLISHER) {
18855
19107
  yield this.publisher.addIceCandidate(candidate);
18856
19108
  } else {
@@ -18861,11 +19113,11 @@ class PCTransportManager {
18861
19113
  createSubscriberAnswerFromOffer(sd, offerId) {
18862
19114
  return __awaiter(this, void 0, void 0, function* () {
18863
19115
  var _a, _b, _c;
18864
- this.log.debug('received server offer', Object.assign(Object.assign({}, this.logContext), {
19116
+ this.log.debug('received server offer', {
18865
19117
  RTCSdpType: sd.type,
18866
19118
  sdp: sd.sdp,
18867
19119
  signalingState: (_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.getSignallingState().toString()
18868
- }));
19120
+ });
18869
19121
  const unlock = yield this.remoteOfferLock.lock();
18870
19122
  try {
18871
19123
  const success = yield (_b = this.subscriber) === null || _b === void 0 ? void 0 : _b.setRemoteDescription(sd, offerId);
@@ -18882,6 +19134,9 @@ class PCTransportManager {
18882
19134
  }
18883
19135
  updateConfiguration(config, iceRestart) {
18884
19136
  var _a;
19137
+ this.log.debug('updating rtc configuration', {
19138
+ iceRestart
19139
+ });
18885
19140
  this.publisher.setConfiguration(config);
18886
19141
  (_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.setConfiguration(config);
18887
19142
  if (iceRestart) {
@@ -18894,7 +19149,7 @@ class PCTransportManager {
18894
19149
  const unlock = yield this.connectionLock.lock();
18895
19150
  try {
18896
19151
  if (this.isPublisherConnectionRequired && this.publisher.getConnectionState() !== 'connected' && this.publisher.getConnectionState() !== 'connecting') {
18897
- this.log.debug('negotiation required, start negotiating', this.logContext);
19152
+ this.log.debug('negotiation required, start negotiating');
18898
19153
  this.publisher.negotiate();
18899
19154
  }
18900
19155
  yield Promise.all((_a = this.requiredTransports) === null || _a === void 0 ? void 0 : _a.map(transport => this.ensureTransportConnected(transport, abortController, timeout)));
@@ -19008,7 +19263,7 @@ class PCTransportManager {
19008
19263
  }
19009
19264
  return new Promise((resolve, reject) => __awaiter(_this, void 0, void 0, function* () {
19010
19265
  const abortHandler = () => {
19011
- this.log.warn('abort transport connection', this.logContext);
19266
+ this.log.warn('abort transport connection');
19012
19267
  CriticalTimers.clearTimeout(connectTimeout);
19013
19268
  reject(ConnectionError.cancelled('room connection has been cancelled'));
19014
19269
  };
@@ -19844,6 +20099,18 @@ class LocalTrack extends Track {
19844
20099
  yield this.restart(constraints);
19845
20100
  });
19846
20101
  }
20102
+ applyConstraints(constraints) {
20103
+ return __awaiter(this, void 0, void 0, function* () {
20104
+ const unlock = yield this.trackChangeLock.lock();
20105
+ try {
20106
+ const res = yield this._mediaStreamTrack.applyConstraints(constraints);
20107
+ this._constraints = Object.assign(Object.assign({}, this._constraints), constraints);
20108
+ return res;
20109
+ } finally {
20110
+ unlock();
20111
+ }
20112
+ });
20113
+ }
19847
20114
  restart(constraints, isUnmuting) {
19848
20115
  const _super = Object.create(null, {
19849
20116
  restart: {
@@ -20280,14 +20547,21 @@ class ScalabilityMode {
20280
20547
  return "L".concat(this.spatial, "T").concat(this.temporal).concat((_a = this.suffix) !== null && _a !== void 0 ? _a : '');
20281
20548
  }
20282
20549
  }
20550
+ /**
20551
+ * Returns the appropriate degradation preference for a video track based on its source.
20552
+ *
20553
+ * - Camera: 'maintain-framerate' (smoother video for real-time communication)
20554
+ * - Screen share: 'maintain-resolution' (clarity is critical for reading text/UI)
20555
+ * - Other/unknown: 'balanced'
20556
+ */
20283
20557
  function getDefaultDegradationPreference(track) {
20284
- // a few of reasons we have different default paths:
20285
- // 1. without this, Chrome seems to aggressively resize the SVC video stating `quality-limitation: bandwidth` even when BW isn't an issue
20286
- // 2. since we are overriding contentHint to motion (to workaround L1T3 publishing), it overrides the default degradationPreference to `balanced`
20287
- if (track.source === Track.Source.ScreenShare || track.constraints.height && unwrapConstraint(track.constraints.height) >= 1080) {
20288
- return 'maintain-resolution';
20289
- } else {
20290
- return 'balanced';
20558
+ switch (track.source) {
20559
+ case Track.Source.Camera:
20560
+ return 'maintain-framerate';
20561
+ case Track.Source.ScreenShare:
20562
+ return 'maintain-resolution';
20563
+ default:
20564
+ return 'balanced';
20291
20565
  }
20292
20566
  }const refreshSubscribedCodecAfterNewCodec = 5000;
20293
20567
  class LocalVideoTrack extends LocalTrack {
@@ -21055,8 +21329,16 @@ const dataTrackDataChannel = '_data_track';
21055
21329
  const minReconnectWait = 2 * 1000;
21056
21330
  const leaveReconnect = 'leave-reconnect';
21057
21331
  const reliabeReceiveStateTTL = 30000;
21058
- const lossyDataChannelBufferThresholdMin = 8 * 1024;
21059
- const lossyDataChannelBufferThresholdMax = 256 * 1024;
21332
+ // Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
21333
+ // freely up to the high-water mark; once it's exceeded they block until the browser's
21334
+ // `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
21335
+ // The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
21336
+ // isn't starved, while the high-water mark bounds the buffer well below the level that would abort
21337
+ // the channel (see livekit/client-sdk-js#1995).
21338
+ const reliableDataChannelWaterMarkLow = 64 * 1024;
21339
+ const reliableDataChannelWaterMarkHigh = 1024 * 1024;
21340
+ const lossyDataChannelWaterMarkLow = 8 * 1024;
21341
+ const lossyDataChannelWaterMarkHigh = 256 * 1024;
21060
21342
  const initialMediaSectionsAudio = 3;
21061
21343
  const initialMediaSectionsVideo = 3;
21062
21344
  var PCState;
@@ -21123,7 +21405,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21123
21405
  this.midToTrackId = {};
21124
21406
  /** used to indicate whether the browser is currently waiting to reconnect */
21125
21407
  this.isWaitingForNetworkReconnect = false;
21126
- this.bufferStatusLowClosingFuture = new Future();
21127
21408
  this.handleDataChannel = _a => __awaiter(this, [_a], void 0, function (_ref) {
21128
21409
  var _this = this;
21129
21410
  let channel = _ref.channel;
@@ -21317,18 +21598,53 @@ class RTCEngine extends eventsExports.EventEmitter {
21317
21598
  this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
21318
21599
  }
21319
21600
  }
21320
- const status = this.isBufferStatusLow(kind);
21321
- if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
21322
- this.dcBufferStatus.set(kind, status);
21323
- this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
21601
+ try {
21602
+ const status = this.isBelowLowWaterMark(kind);
21603
+ if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
21604
+ this.dcBufferStatus.set(kind, status);
21605
+ this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
21606
+ }
21607
+ } catch (e) {
21608
+ this.log.warn('could not update buffer status', {
21609
+ error: e
21610
+ });
21324
21611
  }
21325
21612
  };
21326
- this.isBufferStatusLow = kind => {
21613
+ /**
21614
+ * Whether the send buffer has room to accept more data (the send gate). Senders proceed while
21615
+ * this is true and block once it goes false.
21616
+ */
21617
+ this.isBelowHighWaterMark = kind => {
21327
21618
  const dc = this.dataChannelForKind(kind);
21328
- if (dc) {
21329
- return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
21619
+ if (!dc) {
21620
+ throw new TypeError("Could not get data channel for kind ".concat(kind));
21330
21621
  }
21622
+ // because RTCDatachannel has no high water mark built in we read the statically defined versions as constants
21623
+ const highMark = kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkHigh : lossyDataChannelWaterMarkHigh;
21624
+ return dc.bufferedAmount <= highMark;
21331
21625
  };
21626
+ /**
21627
+ * Whether the send buffer has drained to its low-water mark. Drives the public
21628
+ * {@link EngineEvent.DCBufferStatusChanged} event.
21629
+ */
21630
+ this.isBelowLowWaterMark = kind => {
21631
+ const dc = this.dataChannelForKind(kind);
21632
+ if (!dc) {
21633
+ throw new TypeError("Could not get data channel for kind ".concat(kind));
21634
+ }
21635
+ // because RTCDatachannel has the threshold built in we can read it dynamically to account for changing thresholds over time
21636
+ return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
21637
+ };
21638
+ /** Per-kind lock serializing senders that have to wait for the buffer to drain. */
21639
+ this.waitForBufferHeadroomLocks = new Map();
21640
+ /**
21641
+ * Per-kind AbortController that cancels parked headroom waiters whenever a kind's channel object
21642
+ * stops being current (replaced by createDataChannels or torn down by cleanupPeerConnections). A
21643
+ * waiter is parked on the channel object it captured at wait entry; if that object is abandoned
21644
+ * its events may never fire again, so the abort is what rejects the waiter and releases the
21645
+ * headroom lock instead of stranding every future sender behind it.
21646
+ */
21647
+ this.waiterAbortControllers = new Map();
21332
21648
  this.onRtpMapAvailable = rtpTypes => {
21333
21649
  const rtpMap = new Map();
21334
21650
  rtpTypes.forEach(rtp => {
@@ -21399,13 +21715,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21399
21715
  this.client.onRequestResponse = response => this.emit(EngineEvent.SignalRequestResponse, response);
21400
21716
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
21401
21717
  this.client.onJoined = joinResponse => this.emit(EngineEvent.Joined, joinResponse);
21402
- this.on(EngineEvent.Closing, () => {
21403
- var _a, _b;
21404
- (_b = (_a = this.bufferStatusLowClosingFuture).reject) === null || _b === void 0 ? void 0 : _b.call(_a, new UnexpectedConnectionState('engine closed'));
21405
- });
21406
- // Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
21407
- // when no waitForBufferStatusLow callers are attached.
21408
- this.bufferStatusLowClosingFuture.promise.catch(() => {});
21409
21718
  }
21410
21719
  /** @internal */
21411
21720
  get logContext() {
@@ -21551,6 +21860,10 @@ class RTCEngine extends eventsExports.EventEmitter {
21551
21860
  cleanupPeerConnections() {
21552
21861
  return __awaiter(this, void 0, void 0, function* () {
21553
21862
  var _a;
21863
+ // Reject parked headroom waiters up front: closing the peer connection is allowed (per spec)
21864
+ // to transition data channels to 'closed' without firing events, which would otherwise strand
21865
+ // a waiter holding the headroom lock.
21866
+ this.invalidateDataChannelWaiters('peer connections cleaned up');
21554
21867
  const dcCleanup = dc => {
21555
21868
  if (!dc) {
21556
21869
  return;
@@ -21922,6 +22235,10 @@ class RTCEngine extends eventsExports.EventEmitter {
21922
22235
  if (!this.pcManager) {
21923
22236
  return;
21924
22237
  }
22238
+ // Waiters parked on the old channel objects would never see another event from them once they
22239
+ // are replaced below — reject them so the headroom lock is released and queued senders
22240
+ // re-check against the new channels.
22241
+ this.invalidateDataChannelWaiters('data channels recreated');
21925
22242
  // clear old data channel callbacks if recreate
21926
22243
  if (this.lossyDC) {
21927
22244
  this.lossyDC.onmessage = null;
@@ -21962,10 +22279,10 @@ class RTCEngine extends eventsExports.EventEmitter {
21962
22279
  this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
21963
22280
  this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
21964
22281
  this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
21965
- // set up dc buffer threshold, set to 64kB (otherwise 0 by default)
21966
- this.lossyDC.bufferedAmountLowThreshold = 65535;
21967
- this.reliableDC.bufferedAmountLowThreshold = 65535;
21968
- this.dataTrackDC.bufferedAmountLowThreshold = 65535;
22282
+ // set up dc buffer threshold - if this is not set, it will default to 0
22283
+ this.lossyDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
22284
+ this.reliableDC.bufferedAmountLowThreshold = reliableDataChannelWaterMarkLow;
22285
+ this.dataTrackDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
21969
22286
  // handle buffer amount low events
21970
22287
  this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
21971
22288
  this.reliableDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
@@ -21978,7 +22295,7 @@ class RTCEngine extends eventsExports.EventEmitter {
21978
22295
  if (dc) {
21979
22296
  // control buffered latency to ~100ms
21980
22297
  const threshold = this.lossyDataStatByterate / 10;
21981
- dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelBufferThresholdMin), lossyDataChannelBufferThresholdMax);
22298
+ dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelWaterMarkLow), lossyDataChannelWaterMarkHigh);
21982
22299
  }
21983
22300
  }, 1000);
21984
22301
  }
@@ -22290,7 +22607,11 @@ class RTCEngine extends eventsExports.EventEmitter {
22290
22607
  this.createDataChannels();
22291
22608
  }
22292
22609
  if (res === null || res === void 0 ? void 0 : res.lastMessageSeq) {
22293
- this.resendReliableMessagesForResume(res.lastMessageSeq);
22610
+ this.resendReliableMessagesForResume(res.lastMessageSeq).catch(error => {
22611
+ this.log.warn('failed to resend reliable messages after resume', Object.assign(Object.assign({}, this.logContext), {
22612
+ error
22613
+ }));
22614
+ });
22294
22615
  }
22295
22616
  // resume success
22296
22617
  this.emit(EngineEvent.Resumed);
@@ -22375,20 +22696,55 @@ class RTCEngine extends eventsExports.EventEmitter {
22375
22696
  case DataChannelKind.DATA_TRACK_LOSSY:
22376
22697
  return this.sendLossyBytes(msg, kind);
22377
22698
  case DataChannelKind.RELIABLE:
22378
- const dc = this.dataChannelForKind(kind);
22379
- if (dc) {
22380
- yield this.waitForBufferStatusLow(kind);
22381
- this.reliableMessageBuffer.push({
22382
- data: msg,
22383
- sequence: packet.sequence
22384
- });
22699
+ {
22385
22700
  if (this.attemptingReconnect) {
22701
+ // A reconnect is already underway — queue for the resume replay instead of parking on a
22702
+ // channel that is being torn down. The send resolves; delivery is deferred to the replay.
22703
+ this.reliableMessageBuffer.push({
22704
+ data: msg,
22705
+ sequence: packet.sequence,
22706
+ sent: false
22707
+ });
22386
22708
  return;
22387
22709
  }
22388
- dc.send(msg);
22710
+ const dc = this.dataChannelForKind(kind);
22711
+ if (dc) {
22712
+ try {
22713
+ yield this.waitForBufferHeadroomWithLock(kind);
22714
+ } catch (error) {
22715
+ if (this.isClosed) {
22716
+ // No replay is coming after an engine close — surface the failure.
22717
+ throw error;
22718
+ }
22719
+ // Transient teardown (the channel closed or was replaced while we waited): the
22720
+ // reliable channel promises delivery across resume, so queue the packet for the
22721
+ // replay instead of rejecting a send the app can't meaningfully retry.
22722
+ this.reliableMessageBuffer.push({
22723
+ data: msg,
22724
+ sequence: packet.sequence,
22725
+ sent: false
22726
+ });
22727
+ return;
22728
+ }
22729
+ if (this.attemptingReconnect) {
22730
+ // A reconnect began while we waited for headroom — same deal as above.
22731
+ this.reliableMessageBuffer.push({
22732
+ data: msg,
22733
+ sequence: packet.sequence,
22734
+ sent: false
22735
+ });
22736
+ return;
22737
+ }
22738
+ this.reliableMessageBuffer.push({
22739
+ data: msg,
22740
+ sequence: packet.sequence,
22741
+ sent: true
22742
+ });
22743
+ dc.send(msg);
22744
+ }
22745
+ this.updateAndEmitDCBufferStatus(kind);
22746
+ break;
22389
22747
  }
22390
- this.updateAndEmitDCBufferStatus(kind);
22391
- break;
22392
22748
  }
22393
22749
  });
22394
22750
  }
@@ -22396,36 +22752,56 @@ class RTCEngine extends eventsExports.EventEmitter {
22396
22752
  sendLossyBytes(bytes_1, kind_1) {
22397
22753
  return __awaiter(this, arguments, void 0, function (bytes, kind) {
22398
22754
  var _this3 = this;
22399
- let bufferStatusLowBehavior = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'drop';
22755
+ let bufferStatusFullBehavior = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'drop';
22400
22756
  return function* () {
22401
- // make sure we do have a data connection
22757
+ // Make sure we do have a data connection. This matters most for the direct data-track path
22758
+ // (Room's packetAvailable handler), which doesn't go through sendDataPacket: it both waits for
22759
+ // the channel to be open and — on lazily negotiated publisher connections — is what kicks the
22760
+ // negotiation off in the first place. The call is memoized, so the steady-state cost is one
22761
+ // await on an already-resolved promise.
22402
22762
  yield _this3.ensurePublisherConnected(kind);
22403
22763
  const dc = _this3.dataChannelForKind(kind);
22404
- if (dc) {
22405
- if (!_this3.isBufferStatusLow(kind)) {
22764
+ try {
22765
+ if (dc) {
22406
22766
  // Depending on the exact circumstance that data is being sent, either drop or wait for the
22407
- // buffer status to not be low before continuing.
22408
- switch (bufferStatusLowBehavior) {
22767
+ // buffer to drain below the high-water mark before continuing.
22768
+ switch (bufferStatusFullBehavior) {
22409
22769
  case 'wait':
22410
- yield _this3.waitForBufferStatusLow(kind);
22770
+ if (!_this3.isBelowHighWaterMark(kind)) {
22771
+ yield _this3.waitForBufferHeadroomWithLock(kind);
22772
+ }
22411
22773
  break;
22412
22774
  case 'drop':
22413
- // this.log.warn(`dropping lossy data channel message`, this.logContext);
22414
- // Drop messages to reduce latency
22415
- _this3.lossyDataDropCount += 1;
22416
- if (_this3.lossyDataDropCount % 100 === 0) {
22417
- _this3.log.warn("dropping lossy data channel messages, total dropped: ".concat(_this3.lossyDataDropCount));
22775
+ // we check against the actual threshold on the DC here, as it is dynamic for the lossy DC
22776
+ if (!_this3.isBelowLowWaterMark(kind)) {
22777
+ // Drop messages to reduce latency
22778
+ _this3.lossyDataDropCount += 1;
22779
+ if (_this3.lossyDataDropCount % 100 === 0) {
22780
+ _this3.log.warn("dropping lossy data channel messages, total dropped: ".concat(_this3.lossyDataDropCount));
22781
+ }
22782
+ return;
22418
22783
  }
22419
- return;
22420
22784
  }
22785
+ if (kind === DataChannelKind.LOSSY) {
22786
+ // The byterate stat tunes the LOSSY channel's dynamic drop threshold; counting data-track
22787
+ // bytes here would inflate it and let the lossy channel buffer far more latency than the
22788
+ // ~100ms the tuning targets.
22789
+ _this3.lossyDataStatCurrentBytes += bytes.byteLength;
22790
+ }
22791
+ if (_this3.attemptingReconnect) {
22792
+ return;
22793
+ }
22794
+ dc.send(bytes);
22421
22795
  }
22422
- _this3.lossyDataStatCurrentBytes += bytes.byteLength;
22423
- if (_this3.attemptingReconnect) {
22424
- return;
22796
+ _this3.updateAndEmitDCBufferStatus(kind);
22797
+ } catch (error) {
22798
+ // ensure same surface behaviour as before with missing data channel being silently ignored, just log an error message for clarity
22799
+ if (error instanceof TypeError) {
22800
+ _this3.log.error(error);
22801
+ } else {
22802
+ throw error;
22425
22803
  }
22426
- dc.send(bytes);
22427
22804
  }
22428
- _this3.updateAndEmitDCBufferStatus(kind);
22429
22805
  }();
22430
22806
  });
22431
22807
  }
@@ -22435,33 +22811,129 @@ class RTCEngine extends eventsExports.EventEmitter {
22435
22811
  const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
22436
22812
  if (dc) {
22437
22813
  this.reliableMessageBuffer.popToSequence(lastMessageSeq);
22438
- this.reliableMessageBuffer.getAll().forEach(msg => {
22439
- dc.send(msg.data);
22440
- });
22814
+ // Hold the headroom lock across the whole replay. Releasing it between messages would let a
22815
+ // concurrent send — whose (newer) sequence was already assigned in sendDataPacket before it
22816
+ // queued on the lock — hit the wire mid-replay, and receivers would then discard the
22817
+ // remaining lower-sequence resent messages as duplicates.
22818
+ const unlock = yield this.getBufferHeadroomLock(DataChannelKind.RELIABLE).lock();
22819
+ try {
22820
+ // Everything left after the ack cutoff must be re-handed to the current channel.
22821
+ this.reliableMessageBuffer.markAllUnsent();
22822
+ // Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
22823
+ // sent:false) during our own awaits appends after this pass started, so we pick it up on
22824
+ // the next one. We mark each packet only once we've actually handed it to the channel — a
22825
+ // blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
22826
+ // and a later alignBufferedAmount would then drop it for good. If the loop throws
22827
+ // mid-drain, unsent entries keep their flag and the next replay picks them up.
22828
+ for (let batch = this.reliableMessageBuffer.getUnsent(); batch.length > 0; batch = this.reliableMessageBuffer.getUnsent()) {
22829
+ for (const item of batch) {
22830
+ // Respect flow control on resume too, so a large resend doesn't overflow the buffer.
22831
+ // We already hold the lock across the whole replay, so use the lock-free wait.
22832
+ yield this.waitForBufferHeadroomWithoutLock(DataChannelKind.RELIABLE);
22833
+ dc.send(item.data);
22834
+ this.reliableMessageBuffer.markSent(item);
22835
+ }
22836
+ }
22837
+ } finally {
22838
+ unlock();
22839
+ }
22441
22840
  }
22442
22841
  this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
22443
22842
  });
22444
22843
  }
22445
- waitForBufferStatusLow(kind) {
22844
+ waiterAbortSignal(kind) {
22845
+ let controller = this.waiterAbortControllers.get(kind);
22846
+ if (!controller) {
22847
+ controller = new AbortController();
22848
+ this.waiterAbortControllers.set(kind, controller);
22849
+ }
22850
+ return controller.signal;
22851
+ }
22852
+ /** Rejects all parked headroom waiters (all kinds); the next waiter gets a fresh controller. */
22853
+ invalidateDataChannelWaiters(reason) {
22854
+ for (const controller of this.waiterAbortControllers.values()) {
22855
+ controller.abort(reason);
22856
+ }
22857
+ this.waiterAbortControllers.clear();
22858
+ }
22859
+ /** Acquires the per-kind headroom lock, resolving with the unlock function. */
22860
+ getBufferHeadroomLock(kind) {
22861
+ let lock = this.waitForBufferHeadroomLocks.get(kind);
22862
+ if (!lock) {
22863
+ lock = new _();
22864
+ this.waitForBufferHeadroomLocks.set(kind, lock);
22865
+ }
22866
+ return lock;
22867
+ }
22868
+ /**
22869
+ * Acquires the `kind` headroom lock, waits until the send buffer has room, then releases. The
22870
+ * common single-send entry point. Callers that need to hold the lock across several sends (the
22871
+ * resume replay) acquire it via {@link getBufferHeadroomLock} and call
22872
+ * {@link waitForBufferHeadroomWithoutLock} per message instead.
22873
+ */
22874
+ waitForBufferHeadroomWithLock(kind) {
22446
22875
  return __awaiter(this, void 0, void 0, function* () {
22447
- return new TypedPromise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
22448
- if (this.isClosed) {
22449
- reject(new UnexpectedConnectionState('engine closed'));
22450
- }
22451
- if (this.isBufferStatusLow(kind)) {
22876
+ const unlock = yield this.getBufferHeadroomLock(kind).lock();
22877
+ try {
22878
+ yield this.waitForBufferHeadroomWithoutLock(kind);
22879
+ } finally {
22880
+ unlock();
22881
+ }
22882
+ });
22883
+ }
22884
+ /**
22885
+ * Waits until the send buffer for `kind` is at or below the high-water mark (draining, if
22886
+ * needed, via the `bufferedamountlow` event). Does no locking of its own — the caller must
22887
+ * already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
22888
+ * holds the lock across its whole batch and calls this per message so no other sender can
22889
+ * interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
22890
+ *
22891
+ * Serializing through the per-kind lock means that, once the buffer drains, queued callers
22892
+ * refill it one at a time (up to the high-water mark) rather than all at once and overflowing
22893
+ * the SCTP send buffer (see livekit/client-sdk-js#1995).
22894
+ */
22895
+ waitForBufferHeadroomWithoutLock(kind) {
22896
+ return __awaiter(this, void 0, void 0, function* () {
22897
+ if (this.isClosed) {
22898
+ throw new UnexpectedConnectionState('engine closed');
22899
+ }
22900
+ if (this.isBelowHighWaterMark(kind)) {
22901
+ return;
22902
+ }
22903
+ const dc = this.dataChannelForKind(kind);
22904
+ if (!dc) {
22905
+ throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind));
22906
+ }
22907
+ const abortSignal = this.waiterAbortSignal(kind);
22908
+ yield new TypedPromise((resolve, reject) => {
22909
+ const onBufferedAmountLow = () => {
22910
+ cleanup();
22452
22911
  resolve();
22453
- } else {
22454
- const dc = this.dataChannelForKind(kind);
22455
- if (!dc) {
22456
- reject(new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind)));
22457
- return;
22458
- }
22459
- this.bufferStatusLowClosingFuture.promise.catch(e => reject(e));
22460
- dc.addEventListener('bufferedamountlow', () => resolve(), {
22461
- once: true
22462
- });
22912
+ };
22913
+ const onDCClose = () => {
22914
+ cleanup();
22915
+ reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " closed while draining the buffer")));
22916
+ };
22917
+ const onAbort = () => {
22918
+ cleanup();
22919
+ reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " was replaced or torn down while waiting for headroom")));
22920
+ };
22921
+ const cleanup = () => {
22922
+ dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
22923
+ dc.removeEventListener('close', onDCClose);
22924
+ abortSignal.removeEventListener('abort', onAbort);
22925
+ };
22926
+ if (abortSignal.aborted) {
22927
+ onAbort();
22928
+ return;
22463
22929
  }
22464
- }));
22930
+ dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
22931
+ // Proxy along any error caused by the data channel closing while we wait.
22932
+ dc.addEventListener('close', onDCClose);
22933
+ // The channel object we're parked on can be abandoned without ever firing another event
22934
+ // (e.g. createDataChannels replacing it); the abort is our way out.
22935
+ abortSignal.addEventListener('abort', onAbort);
22936
+ });
22465
22937
  });
22466
22938
  }
22467
22939
  /**
@@ -22743,7 +23215,8 @@ function applyUserDataCompat(newObj, oldObj) {
22743
23215
  const destinationIdentities = newObj.destinationIdentities.length !== 0 ? newObj.destinationIdentities : oldObj.destinationIdentities;
22744
23216
  newObj.destinationIdentities = destinationIdentities;
22745
23217
  oldObj.destinationIdentities = destinationIdentities;
22746
- }const DEFAULT_MAX_AGE_MS = 5000;
23218
+ }const log$4 = getLogger(LoggerNames.Region);
23219
+ const DEFAULT_MAX_AGE_MS = 5000;
22747
23220
  const STOP_REFETCH_DELAY_MS = 30000;
22748
23221
  class RegionUrlProvider {
22749
23222
  static fetchRegionSettings(serverUrl, token, signal) {
@@ -22797,10 +23270,10 @@ class RegionUrlProvider {
22797
23270
  RegionUrlProvider.updateCachedRegionSettings(url, token, newSettings);
22798
23271
  } catch (error) {
22799
23272
  if (error instanceof ConnectionError && error.reason === ConnectionErrorReason.NotAllowed) {
22800
- livekitLogger.debug('token is not valid, cancelling auto region refresh');
23273
+ log$4.debug('token is not valid, cancelling auto region refresh');
22801
23274
  return;
22802
23275
  }
22803
- livekitLogger.debug('auto refetching of region settings failed', {
23276
+ log$4.debug('auto refetching of region settings failed', {
22804
23277
  error
22805
23278
  });
22806
23279
  // continue retrying with the same max age
@@ -22833,7 +23306,7 @@ class RegionUrlProvider {
22833
23306
  tracker.cleanupTimeout = setTimeout(() => {
22834
23307
  const currentTracker = RegionUrlProvider.connectionTrackers.get(hostname);
22835
23308
  if (currentTracker && currentTracker.connectionCount === 0) {
22836
- livekitLogger.debug('stopping region refetch after disconnect delay', {
23309
+ log$4.debug('stopping region refetch after disconnect delay', {
22837
23310
  hostname
22838
23311
  });
22839
23312
  RegionUrlProvider.stopRefetch(hostname);
@@ -22913,7 +23386,9 @@ class RegionUrlProvider {
22913
23386
  if (regionsLeft.length > 0) {
22914
23387
  const nextRegion = regionsLeft[0];
22915
23388
  this.attemptedRegions.push(nextRegion);
22916
- livekitLogger.debug("next region: ".concat(nextRegion.region));
23389
+ log$4.info("switching to region: ".concat(nextRegion.region), {
23390
+ region: nextRegion.region
23391
+ });
22917
23392
  return nextRegion.url;
22918
23393
  } else {
22919
23394
  return null;
@@ -24689,6 +25164,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24689
25164
  let bufferSize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : READABLE_STREAM_DEFAULT_BUFFER_SIZE;
24690
25165
  let streamController = null;
24691
25166
  const sfuSubscriptionComplete = new Future();
25167
+ // Hold the descriptor by reference: SID reassignment re-keys the map, so lookups
25168
+ // with the SID captured at subscribe time would fail.
25169
+ const descriptor = this.descriptors.get(sid);
24692
25170
  const detachSignal = () => {
24693
25171
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
24694
25172
  };
@@ -24698,8 +25176,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24698
25176
  log$1.warn("ReadableStream subscribed to ".concat(sid, " was not started."));
24699
25177
  return;
24700
25178
  }
24701
- const descriptor = this.descriptors.get(sid);
24702
- if (!descriptor) {
25179
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
24703
25180
  log$1.warn("Unknown track ".concat(sid, ", skipping cancel..."));
24704
25181
  return;
24705
25182
  }
@@ -24718,9 +25195,8 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24718
25195
  if (!streamController) {
24719
25196
  return;
24720
25197
  }
24721
- const currentDescriptor = this.descriptors.get(sid);
24722
- if ((currentDescriptor === null || currentDescriptor === void 0 ? void 0 : currentDescriptor.subscription.type) === 'active') {
24723
- currentDescriptor.subscription.streamControllers.delete(streamController);
25198
+ if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.subscription.type) === 'active') {
25199
+ descriptor.subscription.streamControllers.delete(streamController);
24724
25200
  }
24725
25201
  streamController.error(DataTrackSubscribeError.cancelled());
24726
25202
  (_a = sfuSubscriptionComplete.reject) === null || _a === void 0 ? void 0 : _a.call(sfuSubscriptionComplete, DataTrackSubscribeError.cancelled());
@@ -24731,8 +25207,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24731
25207
  streamController = controller;
24732
25208
  this.subscribeRequest(sid, signal).then(() => __awaiter(this, void 0, void 0, function* () {
24733
25209
  var _a, _b, _c;
24734
- const descriptor = this.descriptors.get(sid);
24735
- if (!descriptor) {
25210
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
24736
25211
  log$1.error("Unknown track ".concat(sid));
24737
25212
  const err = DataTrackSubscribeError.disconnected();
24738
25213
  controller.error(err);
@@ -24847,7 +25322,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24847
25322
  };
24848
25323
  // Let the SFU know that the subscribe has been cancelled
24849
25324
  this.emit('sfuUpdateSubscription', {
24850
- sid,
25325
+ sid: descriptor.info.sid,
24851
25326
  subscribe: false
24852
25327
  });
24853
25328
  if (previousDescriptorSubscription.type === 'pending') {
@@ -24961,6 +25436,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24961
25436
  if (this.descriptors.has(info.sid)) {
24962
25437
  continue;
24963
25438
  }
25439
+ if (this.handleSidReassigned(publisherIdentity, info)) {
25440
+ continue;
25441
+ }
24964
25442
  yield this.handleTrackPublished(publisherIdentity, info);
24965
25443
  }
24966
25444
  publisherParticipantToSidsInUpdate.set(publisherIdentity, sidsInUpdate);
@@ -25018,6 +25496,62 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25018
25496
  });
25019
25497
  });
25020
25498
  }
25499
+ /**
25500
+ * Detects and handles SID reassignment, which occurs when the publisher
25501
+ * republishes its tracks after a full reconnect.
25502
+ *
25503
+ * Returns `true` if an SID reassignment occurred, `false` otherwise.
25504
+ */
25505
+ handleSidReassigned(publisherIdentity, info) {
25506
+ // Publisher identity and pub handle are stable across republications.
25507
+ const existingEntry = Array.from(this.descriptors.entries()).find(_ref1 => {
25508
+ let _ref10 = _slicedToArray(_ref1, 2);
25509
+ _ref10[0];
25510
+ let descriptor = _ref10[1];
25511
+ return descriptor.publisherIdentity === publisherIdentity && descriptor.info.pubHandle === info.pubHandle;
25512
+ });
25513
+ if (!existingEntry) {
25514
+ return false;
25515
+ }
25516
+ const _existingEntry = _slicedToArray(existingEntry, 2),
25517
+ oldSid = _existingEntry[0],
25518
+ descriptor = _existingEntry[1];
25519
+ // Invariant: other than SID, info should not have changed.
25520
+ // TODO: consider refactoring to move SID out of info to allow for direct comparison.
25521
+ const _descriptor$info = descriptor.info,
25522
+ name = _descriptor$info.name,
25523
+ usesE2ee = _descriptor$info.usesE2ee;
25524
+ if (name !== info.name || usesE2ee !== info.usesE2ee) {
25525
+ log$1.warn("Info mismatch for ".concat(oldSid, ", treating as new publication"));
25526
+ return false;
25527
+ }
25528
+ const newSid = info.sid;
25529
+ log$1.debug("SID reassigned: ".concat(oldSid, " -> ").concat(newSid));
25530
+ if (!this.descriptors.delete(oldSid)) {
25531
+ return false;
25532
+ }
25533
+ descriptor.info.sid = newSid;
25534
+ switch (descriptor.subscription.type) {
25535
+ case 'none':
25536
+ break;
25537
+ case 'pending':
25538
+ case 'active':
25539
+ // The SFU does not carry subscriptions across a publisher's full
25540
+ // reconnect; re-request the subscription under the new SID.
25541
+ this.emit('sfuUpdateSubscription', {
25542
+ sid: newSid,
25543
+ subscribe: true
25544
+ });
25545
+ break;
25546
+ }
25547
+ if (descriptor.subscription.type === 'active') {
25548
+ // Keep the routing index consistent until the SFU assigns a new handle
25549
+ // (see `registerSubscriberHandle`).
25550
+ this.subscriptionHandles.set(descriptor.subscription.subcriptionHandle, newSid);
25551
+ }
25552
+ this.descriptors.set(newSid, descriptor);
25553
+ return true;
25554
+ }
25021
25555
  handleTrackUnpublished(sid) {
25022
25556
  const descriptor = this.descriptors.get(sid);
25023
25557
  if (!descriptor) {
@@ -25039,10 +25573,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25039
25573
  /** Mapping between track handles attached to incoming packets to the
25040
25574
  * track SIDs they belong to. */
25041
25575
  mapping) {
25042
- for (const _ref1 of mapping.entries()) {
25043
- var _ref10 = _slicedToArray(_ref1, 2);
25044
- const handle = _ref10[0];
25045
- const sid = _ref10[1];
25576
+ for (const _ref11 of mapping.entries()) {
25577
+ var _ref12 = _slicedToArray(_ref11, 2);
25578
+ const handle = _ref12[0];
25579
+ const sid = _ref12[1];
25046
25580
  this.registerSubscriberHandle(handle, sid);
25047
25581
  }
25048
25582
  }
@@ -25063,12 +25597,17 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25063
25597
  case 'active':
25064
25598
  {
25065
25599
  // Update handle for an active subscription. This can occur following a full reconnect.
25600
+ this.subscriptionHandles.delete(descriptor.subscription.subcriptionHandle);
25066
25601
  descriptor.subscription.subcriptionHandle = assignedHandle;
25067
25602
  this.subscriptionHandles.set(assignedHandle, sid);
25068
25603
  return;
25069
25604
  }
25070
25605
  case 'pending':
25071
25606
  {
25607
+ log$1.debug("data track subscription activated", {
25608
+ sid,
25609
+ handle: assignedHandle
25610
+ });
25072
25611
  const pipeline = new IncomingDataTrackPipeline({
25073
25612
  info: descriptor.info,
25074
25613
  publisherIdentity: descriptor.publisherIdentity,
@@ -25135,10 +25674,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25135
25674
  * tracks are subscribed to locally.
25136
25675
  */
25137
25676
  resendSubscriptionUpdates() {
25138
- for (const _ref11 of this.descriptors) {
25139
- var _ref12 = _slicedToArray(_ref11, 2);
25140
- const sid = _ref12[0];
25141
- const descriptor = _ref12[1];
25677
+ for (const _ref13 of this.descriptors) {
25678
+ var _ref14 = _slicedToArray(_ref13, 2);
25679
+ const sid = _ref14[0];
25680
+ const descriptor = _ref14[1];
25142
25681
  if (descriptor.subscription.type === 'none') {
25143
25682
  continue;
25144
25683
  }
@@ -25804,10 +26343,16 @@ class OutgoingDataTrackManager extends eventsExports.EventEmitter {
25804
26343
  {
25805
26344
  if (result.type === 'ok') {
25806
26345
  const info = result.data;
26346
+ log.debug("SFU accepted publish request for handle ".concat(handle), {
26347
+ sid: info.sid
26348
+ });
25807
26349
  const e2eeManager = info.usesE2ee ? this.e2eeManager : null;
25808
26350
  this.descriptors.set(info.pubHandle, Descriptor.active(info, e2eeManager));
25809
26351
  (_b = (_a = descriptor.completionFuture).resolve) === null || _b === void 0 ? void 0 : _b.call(_a);
25810
26352
  } else {
26353
+ log.debug("SFU rejected publish request for handle ".concat(handle), {
26354
+ error: result.error
26355
+ });
25811
26356
  (_d = (_c = descriptor.completionFuture).reject) === null || _d === void 0 ? void 0 : _d.call(_c, result.error);
25812
26357
  }
25813
26358
  return;
@@ -27263,6 +27808,11 @@ class Participant extends eventsExports.EventEmitter {
27263
27808
  this.audioTrackPublications.forEach(track => isAudioTrack(track.track) && track.track.setAudioContext(ctx));
27264
27809
  }
27265
27810
  addTrackPublication(publication) {
27811
+ this.log.debug("adding track publication", {
27812
+ trackSid: publication.trackSid,
27813
+ source: publication.source,
27814
+ kind: publication.kind
27815
+ });
27266
27816
  // forward publication driven events
27267
27817
  publication.on(TrackEvent.Muted, () => {
27268
27818
  this.emit(ParticipantEvent.TrackMuted, publication);
@@ -28307,7 +28857,7 @@ class Participant extends eventsExports.EventEmitter {
28307
28857
  throw new UnexpectedConnectionState('cannot publish track when not connected');
28308
28858
  }
28309
28859
  const negotiate = () => __awaiter(this, void 0, void 0, function* () {
28310
- var _a, _b, _c;
28860
+ var _a, _b, _c, _d;
28311
28861
  if (!this.engine.pcManager) {
28312
28862
  throw new UnexpectedConnectionState('pcManager is not ready');
28313
28863
  }
@@ -28343,12 +28893,22 @@ class Participant extends eventsExports.EventEmitter {
28343
28893
  maxbr: ((_b = encodings[0]) === null || _b === void 0 ? void 0 : _b.maxBitrate) ? encodings[0].maxBitrate / 1000 : 0
28344
28894
  });
28345
28895
  }
28346
- } else if (track.codec && isSVCCodec(track.codec) && ((_c = encodings[0]) === null || _c === void 0 ? void 0 : _c.maxBitrate)) {
28347
- this.engine.pcManager.publisher.setTrackCodecBitrate({
28348
- cid: req.cid,
28349
- codec: track.codec,
28350
- maxbr: encodings[0].maxBitrate / 1000
28351
- });
28896
+ } else if (track.codec && isVideoCodec(track.codec)) {
28897
+ // Apply start bitrate for all video codecs to prevent initial blurriness.
28898
+ // - SVC codecs: use first encoding's bitrate (single stream with built-in layers)
28899
+ // - Simulcast: sum all encoding bitrates (independent streams, BWE needs total)
28900
+ const targetBitrate = isSVCCodec(track.codec) ? (_d = (_c = encodings[0]) === null || _c === void 0 ? void 0 : _c.maxBitrate) !== null && _d !== void 0 ? _d : 0 : encodings.reduce((sum, enc) => {
28901
+ var _a;
28902
+ return sum + ((_a = enc.maxBitrate) !== null && _a !== void 0 ? _a : 0);
28903
+ }, 0);
28904
+ if (targetBitrate > 0) {
28905
+ this.engine.pcManager.publisher.setTrackCodecBitrate({
28906
+ cid: req.cid,
28907
+ codec: track.codec,
28908
+ maxbr: targetBitrate / 1000,
28909
+ isScreenShare: track.source === Track.Source.ScreenShare
28910
+ });
28911
+ }
28352
28912
  }
28353
28913
  }
28354
28914
  yield this.engine.negotiate();
@@ -30066,7 +30626,13 @@ class Room extends eventsExports.EventEmitter {
30066
30626
  });
30067
30627
  try {
30068
30628
  yield Promise.all([this.acquireAudioContext(), ...elements.map(e => {
30069
- e.muted = false;
30629
+ // when webAudioMix is enabled, attached elements are deliberately kept muted by
30630
+ // RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
30631
+ // unmuting them here would cause double playback on platforms where element.volume
30632
+ // has no effect (e.g. iOS Safari)
30633
+ if (!this.options.webAudioMix) {
30634
+ e.muted = false;
30635
+ }
30070
30636
  return e.play();
30071
30637
  })]);
30072
30638
  this.handleAudioPlaybackStarted();