livekit-client 2.20.1 → 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 (74) 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 +33 -7
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +589 -163
  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 +33 -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/logger.d.ts +3 -1
  15. package/dist/src/logger.d.ts.map +1 -1
  16. package/dist/src/room/PCTransport.d.ts +2 -0
  17. package/dist/src/room/PCTransport.d.ts.map +1 -1
  18. package/dist/src/room/PCTransportManager.d.ts +1 -0
  19. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  20. package/dist/src/room/RTCEngine.d.ts +45 -4
  21. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  22. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  23. package/dist/src/room/Room.d.ts.map +1 -1
  24. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  25. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  26. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  27. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  28. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  29. package/dist/src/room/participant/publishUtils.d.ts +7 -0
  30. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  31. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
  32. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/Track.d.ts.map +1 -1
  34. package/dist/src/room/track/options.d.ts +11 -1
  35. package/dist/src/room/track/options.d.ts.map +1 -1
  36. package/dist/src/test/signalToken.d.ts +26 -0
  37. package/dist/src/test/signalToken.d.ts.map +1 -0
  38. package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
  39. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  40. package/dist/ts4.2/logger.d.ts +3 -1
  41. package/dist/ts4.2/room/PCTransport.d.ts +2 -0
  42. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  43. package/dist/ts4.2/room/RTCEngine.d.ts +45 -4
  44. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  45. package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
  46. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
  47. package/dist/ts4.2/room/track/options.d.ts +11 -1
  48. package/dist/ts4.2/test/signalToken.d.ts +26 -0
  49. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
  50. package/package.json +7 -3
  51. package/src/api/SignalClient.e2e.test.ts +264 -0
  52. package/src/api/SignalClient.ts +34 -2
  53. package/src/logger.ts +2 -0
  54. package/src/room/PCTransport.ts +66 -31
  55. package/src/room/PCTransportManager.ts +10 -6
  56. package/src/room/RTCEngine.test.ts +275 -2
  57. package/src/room/RTCEngine.ts +276 -71
  58. package/src/room/RegionUrlProvider.ts +4 -2
  59. package/src/room/Room.ts +7 -1
  60. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
  61. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
  62. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
  63. package/src/room/errors.ts +4 -4
  64. package/src/room/participant/LocalParticipant.ts +16 -6
  65. package/src/room/participant/Participant.ts +5 -0
  66. package/src/room/participant/publishUtils.ts +14 -11
  67. package/src/room/track/LocalAudioTrack.ts +16 -0
  68. package/src/room/track/Track.ts +10 -6
  69. package/src/room/track/options.ts +11 -1
  70. package/src/test/signalServerSetup.ts +151 -0
  71. package/src/test/signalToken.ts +59 -0
  72. package/src/test/vitest-context.d.ts +10 -0
  73. package/src/utils/dataPacketBuffer.test.ts +100 -0
  74. package/src/utils/dataPacketBuffer.ts +46 -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;
@@ -9052,7 +9054,7 @@ function filterStats(result, track, outbound) {
9052
9054
  * tree.
9053
9055
  */
9054
9056
  /* eslint-env node */
9055
- const logging = log$4;
9057
+ const logging = log$5;
9056
9058
  function shimGetUserMedia$2(window, browserDetails) {
9057
9059
  if (browserDetails.version >= 64) {
9058
9060
  return;
@@ -11679,7 +11681,7 @@ function adapterFactory() {
11679
11681
  shimSafari: true
11680
11682
  };
11681
11683
  // Utils.
11682
- const logging = log$4;
11684
+ const logging = log$5;
11683
11685
  const browserDetails = detectBrowser(window);
11684
11686
  const adapter = {
11685
11687
  browserDetails,
@@ -11881,7 +11883,7 @@ function getMatch(exp, ua) {
11881
11883
  }
11882
11884
  function getOSVersion(ua) {
11883
11885
  return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
11884
- }var version$1 = "2.20.1";const version = version$1;
11886
+ }var version$1 = "2.20.2";const version = version$1;
11885
11887
  const protocolVersion = 17;
11886
11888
  /** Initial client protocol. */
11887
11889
  const CLIENT_PROTOCOL_DEFAULT = 0;
@@ -11972,25 +11974,25 @@ class TrackInvalidError extends LivekitError {
11972
11974
  }
11973
11975
  class UnsupportedServer extends LivekitError {
11974
11976
  constructor(message) {
11975
- super(10, message !== null && message !== void 0 ? message : 'unsupported server');
11977
+ super(10, message || 'unsupported server');
11976
11978
  this.name = 'UnsupportedServer';
11977
11979
  }
11978
11980
  }
11979
11981
  class UnexpectedConnectionState extends LivekitError {
11980
11982
  constructor(message) {
11981
- super(12, message !== null && message !== void 0 ? message : 'unexpected connection state');
11983
+ super(12, message || 'unexpected connection state');
11982
11984
  this.name = 'UnexpectedConnectionState';
11983
11985
  }
11984
11986
  }
11985
11987
  class NegotiationError extends LivekitError {
11986
11988
  constructor(message) {
11987
- super(13, message !== null && message !== void 0 ? message : 'unable to negotiate');
11989
+ super(13, message || 'unable to negotiate');
11988
11990
  this.name = 'NegotiationError';
11989
11991
  }
11990
11992
  }
11991
11993
  class PublishDataError extends LivekitError {
11992
11994
  constructor(message) {
11993
- super(14, message !== null && message !== void 0 ? message : 'unable to publish data');
11995
+ super(14, message || 'unable to publish data');
11994
11996
  this.name = 'PublishDataError';
11995
11997
  }
11996
11998
  }
@@ -13195,6 +13197,9 @@ class Track extends eventsExports.EventEmitter {
13195
13197
  }
13196
13198
  /** @internal */
13197
13199
  setStreamState(value) {
13200
+ if (this._streamState !== value) {
13201
+ this.log.debug("stream state changed: ".concat(this._streamState, " -> ").concat(value));
13202
+ }
13198
13203
  this._streamState = value;
13199
13204
  }
13200
13205
  constructor(mediaTrack, kind) {
@@ -13219,8 +13224,8 @@ class Track extends eventsExports.EventEmitter {
13219
13224
  this.handleAppVisibilityChanged();
13220
13225
  }
13221
13226
  };
13222
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track);
13223
13227
  this.loggerContextCb = loggerOptions.loggerContextCb;
13228
+ this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track, () => this.logContext);
13224
13229
  this.setMaxListeners(100);
13225
13230
  this.kind = kind;
13226
13231
  this._mediaStreamTrack = mediaTrack;
@@ -13288,9 +13293,11 @@ class Track extends eventsExports.EventEmitter {
13288
13293
  this.emit(hasAudio ? TrackEvent.AudioPlaybackFailed : TrackEvent.VideoPlaybackFailed, e);
13289
13294
  } else if (e.name === 'AbortError') {
13290
13295
  // commonly triggered by another `play` request, only log for debugging purposes
13291
- 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"));
13292
13297
  } else {
13293
- 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
+ });
13294
13301
  }
13295
13302
  // If audio playback isn't allowed make sure we still play back the video
13296
13303
  if (hasAudio && element && allMediaStreamTracks.some(tr => tr.kind === 'video') && e.name === 'NotAllowedError') {
@@ -13333,6 +13340,7 @@ class Track extends eventsExports.EventEmitter {
13333
13340
  }
13334
13341
  }
13335
13342
  stop() {
13343
+ this.log.debug('stopping track');
13336
13344
  this.stopMonitor();
13337
13345
  this._mediaStreamTrack.stop();
13338
13346
  }
@@ -13353,12 +13361,12 @@ class Track extends eventsExports.EventEmitter {
13353
13361
  }
13354
13362
  /** @internal */
13355
13363
  updateLoggerOptions(loggerOptions) {
13356
- if (loggerOptions.loggerName) {
13357
- this.log = getLogger(loggerOptions.loggerName);
13358
- }
13359
13364
  if (loggerOptions.loggerContextCb) {
13360
13365
  this.loggerContextCb = loggerOptions.loggerContextCb;
13361
13366
  }
13367
+ if (loggerOptions.loggerName) {
13368
+ this.log = getLogger(loggerOptions.loggerName, () => this.logContext);
13369
+ }
13362
13370
  }
13363
13371
  recycleElement(element) {
13364
13372
  if (element instanceof HTMLAudioElement) {
@@ -16317,6 +16325,10 @@ var SignalConnectionState;
16317
16325
  })(SignalConnectionState || (SignalConnectionState = {}));
16318
16326
  /** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
16319
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;
16320
16332
  /** @internal */
16321
16333
  class SignalClient {
16322
16334
  get currentState() {
@@ -16465,7 +16477,6 @@ class SignalClient {
16465
16477
  this.ws = new WebSocketStream(rtcUrl);
16466
16478
  try {
16467
16479
  this.ws.closed.then(closeInfo => {
16468
- var _a;
16469
16480
  if (this.isEstablishingConnection) {
16470
16481
  reject(ConnectionError.internal("Websocket got closed during a (re)connection attempt: ".concat(closeInfo.reason)));
16471
16482
  }
@@ -16477,7 +16488,7 @@ class SignalClient {
16477
16488
  state: this.state
16478
16489
  });
16479
16490
  if (this.state === SignalConnectionState.CONNECTED) {
16480
- this.handleOnClose((_a = closeInfo.reason) !== null && _a !== void 0 ? _a : 'Unexpected WS error');
16491
+ this.handleOnClose(closeInfo.reason || 'Unexpected WS error');
16481
16492
  }
16482
16493
  }
16483
16494
  return;
@@ -16505,7 +16516,26 @@ class SignalClient {
16505
16516
  }
16506
16517
  const signalReader = connection.readable.getReader();
16507
16518
  this.streamWriter = connection.writable.getWriter();
16508
- 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
+ }
16509
16539
  signalReader.releaseLock();
16510
16540
  if (!firstMessage.value) {
16511
16541
  throw ConnectionError.internal('no message received as first message');
@@ -17243,21 +17273,50 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
17243
17273
  constructor() {
17244
17274
  this.buffer = [];
17245
17275
  this._totalSize = 0;
17276
+ this._sentSize = 0;
17246
17277
  }
17247
17278
  push(item) {
17248
17279
  this.buffer.push(item);
17249
17280
  this._totalSize += item.data.byteLength;
17281
+ if (item.sent) {
17282
+ this._sentSize += item.data.byteLength;
17283
+ }
17250
17284
  }
17251
17285
  pop() {
17252
17286
  const item = this.buffer.shift();
17253
17287
  if (item) {
17254
17288
  this._totalSize -= item.data.byteLength;
17289
+ if (item.sent) {
17290
+ this._sentSize -= item.data.byteLength;
17291
+ }
17255
17292
  }
17256
17293
  return item;
17257
17294
  }
17258
17295
  getAll() {
17259
17296
  return this.buffer.slice();
17260
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
+ }
17261
17320
  popToSequence(sequence) {
17262
17321
  while (this.buffer.length > 0) {
17263
17322
  const first = this.buffer[0];
@@ -17271,7 +17330,12 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
17271
17330
  alignBufferedAmount(bufferedAmount) {
17272
17331
  while (this.buffer.length > 0) {
17273
17332
  const first = this.buffer[0];
17274
- 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) {
17275
17339
  break;
17276
17340
  }
17277
17341
  this.pop();
@@ -18056,13 +18120,20 @@ function requireLib() {
18056
18120
  lib.parseImageAttributes = parser.parseImageAttributes;
18057
18121
  lib.parseSimulcastStreamList = parser.parseSimulcastStreamList;
18058
18122
  return lib;
18059
- }var libExports = requireLib();/* The svc codec (av1/vp9) would use a very low bitrate at the begining and
18060
- increase slowly by the bandwidth estimator until it reach the target bitrate. The
18061
- process commonly cost more than 10 seconds cause subscriber will get blur video at
18062
- the first few seconds. So we use a 70% of target bitrate here as the start bitrate to
18063
- eliminate this issue.
18064
- */
18065
- 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;
18066
18137
  const debounceInterval = 20;
18067
18138
  const PCEvents = {
18068
18139
  NegotiationStarted: 'negotiationStarted',
@@ -18087,6 +18158,7 @@ class PCTransport extends eventsExports.EventEmitter {
18087
18158
  var _a;
18088
18159
  super();
18089
18160
  this.log = livekitLogger;
18161
+ this.iceLog = livekitLogger;
18090
18162
  this.ddExtID = 0;
18091
18163
  this.latestOfferId = 0;
18092
18164
  this.latestAcknowledgedOfferId = 0;
@@ -18113,6 +18185,7 @@ class PCTransport extends eventsExports.EventEmitter {
18113
18185
  if (!this._pc) {
18114
18186
  return;
18115
18187
  }
18188
+ this.log.debug('closing peer connection');
18116
18189
  this.pendingInitialOffer = undefined;
18117
18190
  this._pc.close();
18118
18191
  this._pc.onconnectionstatechange = null;
@@ -18128,8 +18201,9 @@ class PCTransport extends eventsExports.EventEmitter {
18128
18201
  this._pc.oniceconnectionstatechange = null;
18129
18202
  this._pc = null;
18130
18203
  };
18131
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCTransport);
18132
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);
18133
18207
  this.config = config;
18134
18208
  this._pc = this.createPC();
18135
18209
  this.offerLock = new _();
@@ -18139,26 +18213,39 @@ class PCTransport extends eventsExports.EventEmitter {
18139
18213
  pc.onicecandidate = ev => {
18140
18214
  var _a;
18141
18215
  if (!ev.candidate) return;
18216
+ this.iceLog.debug('local ICE candidate gathered', {
18217
+ candidate: ev.candidate.candidate
18218
+ });
18142
18219
  (_a = this.onIceCandidate) === null || _a === void 0 ? void 0 : _a.call(this, ev.candidate);
18143
18220
  };
18144
18221
  pc.onicecandidateerror = ev => {
18145
18222
  var _a;
18223
+ this.iceLog.debug('ICE candidate error', {
18224
+ event: ev
18225
+ });
18146
18226
  (_a = this.onIceCandidateError) === null || _a === void 0 ? void 0 : _a.call(this, ev);
18147
18227
  };
18148
18228
  pc.oniceconnectionstatechange = () => {
18149
18229
  var _a;
18230
+ this.iceLog.debug("ICE connection state: ".concat(pc.iceConnectionState));
18150
18231
  (_a = this.onIceConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.iceConnectionState);
18151
18232
  };
18152
18233
  pc.onsignalingstatechange = () => {
18153
18234
  var _a;
18235
+ this.log.debug("signaling state: ".concat(pc.signalingState));
18154
18236
  (_a = this.onSignalingStatechange) === null || _a === void 0 ? void 0 : _a.call(this, pc.signalingState);
18155
18237
  };
18156
18238
  pc.onconnectionstatechange = () => {
18157
18239
  var _a;
18240
+ this.log.debug("connection state: ".concat(pc.connectionState));
18158
18241
  (_a = this.onConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.connectionState);
18159
18242
  };
18160
18243
  pc.ondatachannel = ev => {
18161
18244
  var _a;
18245
+ this.log.debug('data channel opened by peer', {
18246
+ label: ev.channel.label,
18247
+ id: ev.channel.id
18248
+ });
18162
18249
  (_a = this.onDataChannel) === null || _a === void 0 ? void 0 : _a.call(this, ev);
18163
18250
  };
18164
18251
  pc.ontrack = ev => {
@@ -18179,6 +18266,9 @@ class PCTransport extends eventsExports.EventEmitter {
18179
18266
  if (this.pc.remoteDescription && !this.restartingIce) {
18180
18267
  return this.pc.addIceCandidate(candidate);
18181
18268
  }
18269
+ this.iceLog.debug('queuing remote ICE candidate until remote description applied', {
18270
+ pendingCount: this.pendingCandidates.length + 1
18271
+ });
18182
18272
  this.pendingCandidates.push(candidate);
18183
18273
  });
18184
18274
  }
@@ -18186,10 +18276,10 @@ class PCTransport extends eventsExports.EventEmitter {
18186
18276
  return __awaiter(this, void 0, void 0, function* () {
18187
18277
  var _a, _b;
18188
18278
  if (sd.type === 'answer' && this.latestOfferId > 0 && offerId > 0 && offerId !== this.latestOfferId) {
18189
- this.log.warn('ignoring answer for old offer', Object.assign(Object.assign({}, this.logContext), {
18279
+ this.log.warn('ignoring answer for old offer', {
18190
18280
  offerId,
18191
18281
  latestOfferId: this.latestOfferId
18192
- }));
18282
+ });
18193
18283
  return false;
18194
18284
  }
18195
18285
  let mungedSDP = undefined;
@@ -18207,7 +18297,7 @@ class PCTransport extends eventsExports.EventEmitter {
18207
18297
  sdpParsed.media.forEach(media => {
18208
18298
  ensureIPAddrMatchVersion(media);
18209
18299
  });
18210
- this.log.debug('setting pending initial offer before processing answer', this.logContext);
18300
+ this.log.debug('setting pending initial offer before processing answer');
18211
18301
  yield this.setMungedSDP(initialOffer, libExports.write(sdpParsed));
18212
18302
  }
18213
18303
  const sdpParsed = libExports.parse((_b = sd.sdp) !== null && _b !== void 0 ? _b : '');
@@ -18265,6 +18355,11 @@ class PCTransport extends eventsExports.EventEmitter {
18265
18355
  mungedSDP = libExports.write(sdpParsed);
18266
18356
  }
18267
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
+ }
18268
18363
  this.pendingCandidates.forEach(candidate => {
18269
18364
  this.pc.addIceCandidate(candidate);
18270
18365
  });
@@ -18300,7 +18395,7 @@ class PCTransport extends eventsExports.EventEmitter {
18300
18395
  const unlock = yield this.offerLock.lock();
18301
18396
  try {
18302
18397
  if (this.pc.signalingState !== 'stable') {
18303
- 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');
18304
18399
  return;
18305
18400
  }
18306
18401
  const offerId = this.latestOfferId + 1;
@@ -18333,7 +18428,7 @@ class PCTransport extends eventsExports.EventEmitter {
18333
18428
  return;
18334
18429
  }
18335
18430
  if (options === null || options === void 0 ? void 0 : options.iceRestart) {
18336
- this.log.debug('restarting ICE', this.logContext);
18431
+ this.iceLog.debug('restarting ICE');
18337
18432
  this.restartingIce = true;
18338
18433
  }
18339
18434
  if (this._pc && (this._pc.signalingState === 'have-local-offer' || this.pendingInitialOffer)) {
@@ -18346,22 +18441,22 @@ class PCTransport extends eventsExports.EventEmitter {
18346
18441
  yield this._pc.setRemoteDescription(currentSD);
18347
18442
  } else {
18348
18443
  this.renegotiate = true;
18349
- this.log.debug('requesting renegotiation', Object.assign({}, this.logContext));
18444
+ this.log.debug('requesting renegotiation');
18350
18445
  return;
18351
18446
  }
18352
18447
  } else if (!this._pc || this._pc.signalingState === 'closed') {
18353
- this.log.warn('could not createOffer with closed peer connection', this.logContext);
18448
+ this.log.warn('could not createOffer with closed peer connection');
18354
18449
  return;
18355
18450
  }
18356
18451
  // actually negotiate
18357
- this.log.debug('starting to negotiate', this.logContext);
18452
+ this.log.debug('starting to negotiate');
18358
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
18359
18454
  const offerId = this.latestOfferId + 1;
18360
18455
  this.latestOfferId = offerId;
18361
18456
  const offer = yield this.pc.createOffer(options);
18362
- this.log.debug('original offer', Object.assign({
18457
+ this.log.debug('original offer', {
18363
18458
  sdp: offer.sdp
18364
- }, this.logContext));
18459
+ });
18365
18460
  const sdpParsed = libExports.parse((_a = offer.sdp) !== null && _a !== void 0 ? _a : '');
18366
18461
  sdpParsed.media.forEach(media => {
18367
18462
  ensureIPAddrMatchVersion(media);
@@ -18387,12 +18482,15 @@ class PCTransport extends eventsExports.EventEmitter {
18387
18482
  this.ensureVideoDDExtensionForSVC(media, sdpParsed);
18388
18483
  }
18389
18484
  // mung sdp for bitrate setting that can't apply by sendEncoding
18390
- if (!isSVCCodec(trackbr.codec)) {
18391
- return true;
18392
- }
18393
- 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;
18394
18491
  for (const fmtp of media.fmtp) {
18395
18492
  if (fmtp.payload === codecPayload) {
18493
+ fmtpFound = true;
18396
18494
  // if another track's fmtp already is set, we cannot override the bitrate
18397
18495
  // this has the unfortunate consequence of being forced to use the
18398
18496
  // initial track's bitrate for all tracks
@@ -18402,6 +18500,13 @@ class PCTransport extends eventsExports.EventEmitter {
18402
18500
  break;
18403
18501
  }
18404
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
+ }
18405
18510
  return true;
18406
18511
  });
18407
18512
  }
@@ -18416,10 +18521,10 @@ class PCTransport extends eventsExports.EventEmitter {
18416
18521
  conformBundledCodecFmtp(sdpParsed.media, media => placeholderMids.has(getMidString(media.mid)));
18417
18522
  }
18418
18523
  if (this.latestOfferId > offerId) {
18419
- this.log.warn('latestOfferId mismatch', Object.assign(Object.assign({}, this.logContext), {
18524
+ this.log.warn('latestOfferId mismatch', {
18420
18525
  latestOfferId: this.latestOfferId,
18421
18526
  offerId
18422
- }));
18527
+ });
18423
18528
  return;
18424
18529
  }
18425
18530
  yield this.setMungedSDP(offer, libExports.write(sdpParsed));
@@ -18567,7 +18672,7 @@ class PCTransport extends eventsExports.EventEmitter {
18567
18672
  if (munged) {
18568
18673
  sd.sdp = munged;
18569
18674
  try {
18570
- this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description"), this.logContext);
18675
+ this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description"));
18571
18676
  if (remote) {
18572
18677
  yield this.pc.setRemoteDescription(sd);
18573
18678
  } else {
@@ -18575,11 +18680,11 @@ class PCTransport extends eventsExports.EventEmitter {
18575
18680
  }
18576
18681
  return;
18577
18682
  } catch (e) {
18578
- 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"), {
18579
18684
  error: e,
18580
18685
  mungedSdp: munged,
18581
18686
  originalSdp
18582
- }));
18687
+ });
18583
18688
  sd.sdp = originalSdp;
18584
18689
  }
18585
18690
  }
@@ -18606,9 +18711,9 @@ class PCTransport extends eventsExports.EventEmitter {
18606
18711
  if (!remote && this.pc.remoteDescription) {
18607
18712
  fields.remoteSdp = this.pc.remoteDescription;
18608
18713
  }
18609
- 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), {
18610
18715
  fields
18611
- }));
18716
+ });
18612
18717
  throw new NegotiationError(msg);
18613
18718
  }
18614
18719
  });
@@ -18869,6 +18974,7 @@ class PCTransportManager {
18869
18974
  var _a;
18870
18975
  this.peerConnectionTimeout = roomConnectOptionDefaults.peerConnectionTimeout;
18871
18976
  this.log = livekitLogger;
18977
+ this.iceLog = livekitLogger;
18872
18978
  this.updateState = () => {
18873
18979
  var _a, _b;
18874
18980
  const previousState = this.state;
@@ -18887,12 +18993,13 @@ class PCTransportManager {
18887
18993
  this.state = PCTransportState.NEW;
18888
18994
  }
18889
18995
  if (previousState !== this.state) {
18890
- 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]));
18891
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());
18892
18998
  }
18893
18999
  };
18894
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCManager);
18895
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);
18896
19003
  this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
18897
19004
  this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
18898
19005
  this.publisher = new PCTransport(rtcConfig, loggerOptions);
@@ -18965,9 +19072,9 @@ class PCTransportManager {
18965
19072
  publisher.removeTrack(sender);
18966
19073
  }
18967
19074
  } catch (e) {
18968
- this.log.warn('could not removeTrack', Object.assign(Object.assign({}, this.logContext), {
19075
+ this.log.warn('could not removeTrack', {
18969
19076
  error: e
18970
- }));
19077
+ });
18971
19078
  }
18972
19079
  }
18973
19080
  }
@@ -18977,6 +19084,7 @@ class PCTransportManager {
18977
19084
  }
18978
19085
  triggerIceRestart() {
18979
19086
  return __awaiter(this, void 0, void 0, function* () {
19087
+ this.iceLog.warn('triggering ICE restart');
18980
19088
  if (this.subscriber) {
18981
19089
  this.subscriber.restartingIce = true;
18982
19090
  }
@@ -18991,6 +19099,10 @@ class PCTransportManager {
18991
19099
  addIceCandidate(candidate, target) {
18992
19100
  return __awaiter(this, void 0, void 0, function* () {
18993
19101
  var _a;
19102
+ this.iceLog.debug('adding remote ICE candidate', {
19103
+ target,
19104
+ candidate
19105
+ });
18994
19106
  if (target === SignalTarget.PUBLISHER) {
18995
19107
  yield this.publisher.addIceCandidate(candidate);
18996
19108
  } else {
@@ -19001,11 +19113,11 @@ class PCTransportManager {
19001
19113
  createSubscriberAnswerFromOffer(sd, offerId) {
19002
19114
  return __awaiter(this, void 0, void 0, function* () {
19003
19115
  var _a, _b, _c;
19004
- this.log.debug('received server offer', Object.assign(Object.assign({}, this.logContext), {
19116
+ this.log.debug('received server offer', {
19005
19117
  RTCSdpType: sd.type,
19006
19118
  sdp: sd.sdp,
19007
19119
  signalingState: (_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.getSignallingState().toString()
19008
- }));
19120
+ });
19009
19121
  const unlock = yield this.remoteOfferLock.lock();
19010
19122
  try {
19011
19123
  const success = yield (_b = this.subscriber) === null || _b === void 0 ? void 0 : _b.setRemoteDescription(sd, offerId);
@@ -19022,6 +19134,9 @@ class PCTransportManager {
19022
19134
  }
19023
19135
  updateConfiguration(config, iceRestart) {
19024
19136
  var _a;
19137
+ this.log.debug('updating rtc configuration', {
19138
+ iceRestart
19139
+ });
19025
19140
  this.publisher.setConfiguration(config);
19026
19141
  (_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.setConfiguration(config);
19027
19142
  if (iceRestart) {
@@ -19034,7 +19149,7 @@ class PCTransportManager {
19034
19149
  const unlock = yield this.connectionLock.lock();
19035
19150
  try {
19036
19151
  if (this.isPublisherConnectionRequired && this.publisher.getConnectionState() !== 'connected' && this.publisher.getConnectionState() !== 'connecting') {
19037
- this.log.debug('negotiation required, start negotiating', this.logContext);
19152
+ this.log.debug('negotiation required, start negotiating');
19038
19153
  this.publisher.negotiate();
19039
19154
  }
19040
19155
  yield Promise.all((_a = this.requiredTransports) === null || _a === void 0 ? void 0 : _a.map(transport => this.ensureTransportConnected(transport, abortController, timeout)));
@@ -19148,7 +19263,7 @@ class PCTransportManager {
19148
19263
  }
19149
19264
  return new Promise((resolve, reject) => __awaiter(_this, void 0, void 0, function* () {
19150
19265
  const abortHandler = () => {
19151
- this.log.warn('abort transport connection', this.logContext);
19266
+ this.log.warn('abort transport connection');
19152
19267
  CriticalTimers.clearTimeout(connectTimeout);
19153
19268
  reject(ConnectionError.cancelled('room connection has been cancelled'));
19154
19269
  };
@@ -19984,6 +20099,18 @@ class LocalTrack extends Track {
19984
20099
  yield this.restart(constraints);
19985
20100
  });
19986
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
+ }
19987
20114
  restart(constraints, isUnmuting) {
19988
20115
  const _super = Object.create(null, {
19989
20116
  restart: {
@@ -20420,14 +20547,21 @@ class ScalabilityMode {
20420
20547
  return "L".concat(this.spatial, "T").concat(this.temporal).concat((_a = this.suffix) !== null && _a !== void 0 ? _a : '');
20421
20548
  }
20422
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
+ */
20423
20557
  function getDefaultDegradationPreference(track) {
20424
- // a few of reasons we have different default paths:
20425
- // 1. without this, Chrome seems to aggressively resize the SVC video stating `quality-limitation: bandwidth` even when BW isn't an issue
20426
- // 2. since we are overriding contentHint to motion (to workaround L1T3 publishing), it overrides the default degradationPreference to `balanced`
20427
- if (track.source === Track.Source.ScreenShare || track.constraints.height && unwrapConstraint(track.constraints.height) >= 1080) {
20428
- return 'maintain-resolution';
20429
- } else {
20430
- 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';
20431
20565
  }
20432
20566
  }const refreshSubscribedCodecAfterNewCodec = 5000;
20433
20567
  class LocalVideoTrack extends LocalTrack {
@@ -21195,8 +21329,16 @@ const dataTrackDataChannel = '_data_track';
21195
21329
  const minReconnectWait = 2 * 1000;
21196
21330
  const leaveReconnect = 'leave-reconnect';
21197
21331
  const reliabeReceiveStateTTL = 30000;
21198
- const lossyDataChannelBufferThresholdMin = 8 * 1024;
21199
- 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;
21200
21342
  const initialMediaSectionsAudio = 3;
21201
21343
  const initialMediaSectionsVideo = 3;
21202
21344
  var PCState;
@@ -21263,7 +21405,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21263
21405
  this.midToTrackId = {};
21264
21406
  /** used to indicate whether the browser is currently waiting to reconnect */
21265
21407
  this.isWaitingForNetworkReconnect = false;
21266
- this.bufferStatusLowClosingFuture = new Future();
21267
21408
  this.handleDataChannel = _a => __awaiter(this, [_a], void 0, function (_ref) {
21268
21409
  var _this = this;
21269
21410
  let channel = _ref.channel;
@@ -21457,18 +21598,53 @@ class RTCEngine extends eventsExports.EventEmitter {
21457
21598
  this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
21458
21599
  }
21459
21600
  }
21460
- const status = this.isBufferStatusLow(kind);
21461
- if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
21462
- this.dcBufferStatus.set(kind, status);
21463
- 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
+ });
21464
21611
  }
21465
21612
  };
21466
- 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 => {
21467
21618
  const dc = this.dataChannelForKind(kind);
21468
- if (dc) {
21469
- return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
21619
+ if (!dc) {
21620
+ throw new TypeError("Could not get data channel for kind ".concat(kind));
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;
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));
21470
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;
21471
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();
21472
21648
  this.onRtpMapAvailable = rtpTypes => {
21473
21649
  const rtpMap = new Map();
21474
21650
  rtpTypes.forEach(rtp => {
@@ -21539,13 +21715,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21539
21715
  this.client.onRequestResponse = response => this.emit(EngineEvent.SignalRequestResponse, response);
21540
21716
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
21541
21717
  this.client.onJoined = joinResponse => this.emit(EngineEvent.Joined, joinResponse);
21542
- this.on(EngineEvent.Closing, () => {
21543
- var _a, _b;
21544
- (_b = (_a = this.bufferStatusLowClosingFuture).reject) === null || _b === void 0 ? void 0 : _b.call(_a, new UnexpectedConnectionState('engine closed'));
21545
- });
21546
- // Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
21547
- // when no waitForBufferStatusLow callers are attached.
21548
- this.bufferStatusLowClosingFuture.promise.catch(() => {});
21549
21718
  }
21550
21719
  /** @internal */
21551
21720
  get logContext() {
@@ -21691,6 +21860,10 @@ class RTCEngine extends eventsExports.EventEmitter {
21691
21860
  cleanupPeerConnections() {
21692
21861
  return __awaiter(this, void 0, void 0, function* () {
21693
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');
21694
21867
  const dcCleanup = dc => {
21695
21868
  if (!dc) {
21696
21869
  return;
@@ -22062,6 +22235,10 @@ class RTCEngine extends eventsExports.EventEmitter {
22062
22235
  if (!this.pcManager) {
22063
22236
  return;
22064
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');
22065
22242
  // clear old data channel callbacks if recreate
22066
22243
  if (this.lossyDC) {
22067
22244
  this.lossyDC.onmessage = null;
@@ -22102,10 +22279,10 @@ class RTCEngine extends eventsExports.EventEmitter {
22102
22279
  this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
22103
22280
  this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
22104
22281
  this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
22105
- // set up dc buffer threshold, set to 64kB (otherwise 0 by default)
22106
- this.lossyDC.bufferedAmountLowThreshold = 65535;
22107
- this.reliableDC.bufferedAmountLowThreshold = 65535;
22108
- 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;
22109
22286
  // handle buffer amount low events
22110
22287
  this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
22111
22288
  this.reliableDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
@@ -22118,7 +22295,7 @@ class RTCEngine extends eventsExports.EventEmitter {
22118
22295
  if (dc) {
22119
22296
  // control buffered latency to ~100ms
22120
22297
  const threshold = this.lossyDataStatByterate / 10;
22121
- dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelBufferThresholdMin), lossyDataChannelBufferThresholdMax);
22298
+ dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelWaterMarkLow), lossyDataChannelWaterMarkHigh);
22122
22299
  }
22123
22300
  }, 1000);
22124
22301
  }
@@ -22430,7 +22607,11 @@ class RTCEngine extends eventsExports.EventEmitter {
22430
22607
  this.createDataChannels();
22431
22608
  }
22432
22609
  if (res === null || res === void 0 ? void 0 : res.lastMessageSeq) {
22433
- 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
+ });
22434
22615
  }
22435
22616
  // resume success
22436
22617
  this.emit(EngineEvent.Resumed);
@@ -22515,20 +22696,55 @@ class RTCEngine extends eventsExports.EventEmitter {
22515
22696
  case DataChannelKind.DATA_TRACK_LOSSY:
22516
22697
  return this.sendLossyBytes(msg, kind);
22517
22698
  case DataChannelKind.RELIABLE:
22518
- const dc = this.dataChannelForKind(kind);
22519
- if (dc) {
22520
- yield this.waitForBufferStatusLow(kind);
22521
- this.reliableMessageBuffer.push({
22522
- data: msg,
22523
- sequence: packet.sequence
22524
- });
22699
+ {
22525
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
+ });
22526
22708
  return;
22527
22709
  }
22528
- 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;
22529
22747
  }
22530
- this.updateAndEmitDCBufferStatus(kind);
22531
- break;
22532
22748
  }
22533
22749
  });
22534
22750
  }
@@ -22536,36 +22752,56 @@ class RTCEngine extends eventsExports.EventEmitter {
22536
22752
  sendLossyBytes(bytes_1, kind_1) {
22537
22753
  return __awaiter(this, arguments, void 0, function (bytes, kind) {
22538
22754
  var _this3 = this;
22539
- let bufferStatusLowBehavior = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'drop';
22755
+ let bufferStatusFullBehavior = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'drop';
22540
22756
  return function* () {
22541
- // 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.
22542
22762
  yield _this3.ensurePublisherConnected(kind);
22543
22763
  const dc = _this3.dataChannelForKind(kind);
22544
- if (dc) {
22545
- if (!_this3.isBufferStatusLow(kind)) {
22764
+ try {
22765
+ if (dc) {
22546
22766
  // Depending on the exact circumstance that data is being sent, either drop or wait for the
22547
- // buffer status to not be low before continuing.
22548
- switch (bufferStatusLowBehavior) {
22767
+ // buffer to drain below the high-water mark before continuing.
22768
+ switch (bufferStatusFullBehavior) {
22549
22769
  case 'wait':
22550
- yield _this3.waitForBufferStatusLow(kind);
22770
+ if (!_this3.isBelowHighWaterMark(kind)) {
22771
+ yield _this3.waitForBufferHeadroomWithLock(kind);
22772
+ }
22551
22773
  break;
22552
22774
  case 'drop':
22553
- // this.log.warn(`dropping lossy data channel message`, this.logContext);
22554
- // Drop messages to reduce latency
22555
- _this3.lossyDataDropCount += 1;
22556
- if (_this3.lossyDataDropCount % 100 === 0) {
22557
- _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;
22558
22783
  }
22559
- return;
22560
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);
22561
22795
  }
22562
- _this3.lossyDataStatCurrentBytes += bytes.byteLength;
22563
- if (_this3.attemptingReconnect) {
22564
- 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;
22565
22803
  }
22566
- dc.send(bytes);
22567
22804
  }
22568
- _this3.updateAndEmitDCBufferStatus(kind);
22569
22805
  }();
22570
22806
  });
22571
22807
  }
@@ -22575,33 +22811,129 @@ class RTCEngine extends eventsExports.EventEmitter {
22575
22811
  const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
22576
22812
  if (dc) {
22577
22813
  this.reliableMessageBuffer.popToSequence(lastMessageSeq);
22578
- this.reliableMessageBuffer.getAll().forEach(msg => {
22579
- dc.send(msg.data);
22580
- });
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
+ }
22581
22840
  }
22582
22841
  this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
22583
22842
  });
22584
22843
  }
22585
- 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) {
22875
+ return __awaiter(this, void 0, void 0, function* () {
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) {
22586
22896
  return __awaiter(this, void 0, void 0, function* () {
22587
- return new TypedPromise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
22588
- if (this.isClosed) {
22589
- reject(new UnexpectedConnectionState('engine closed'));
22590
- }
22591
- if (this.isBufferStatusLow(kind)) {
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();
22592
22911
  resolve();
22593
- } else {
22594
- const dc = this.dataChannelForKind(kind);
22595
- if (!dc) {
22596
- reject(new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind)));
22597
- return;
22598
- }
22599
- this.bufferStatusLowClosingFuture.promise.catch(e => reject(e));
22600
- dc.addEventListener('bufferedamountlow', () => resolve(), {
22601
- once: true
22602
- });
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;
22603
22929
  }
22604
- }));
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
+ });
22605
22937
  });
22606
22938
  }
22607
22939
  /**
@@ -22883,7 +23215,8 @@ function applyUserDataCompat(newObj, oldObj) {
22883
23215
  const destinationIdentities = newObj.destinationIdentities.length !== 0 ? newObj.destinationIdentities : oldObj.destinationIdentities;
22884
23216
  newObj.destinationIdentities = destinationIdentities;
22885
23217
  oldObj.destinationIdentities = destinationIdentities;
22886
- }const DEFAULT_MAX_AGE_MS = 5000;
23218
+ }const log$4 = getLogger(LoggerNames.Region);
23219
+ const DEFAULT_MAX_AGE_MS = 5000;
22887
23220
  const STOP_REFETCH_DELAY_MS = 30000;
22888
23221
  class RegionUrlProvider {
22889
23222
  static fetchRegionSettings(serverUrl, token, signal) {
@@ -22937,10 +23270,10 @@ class RegionUrlProvider {
22937
23270
  RegionUrlProvider.updateCachedRegionSettings(url, token, newSettings);
22938
23271
  } catch (error) {
22939
23272
  if (error instanceof ConnectionError && error.reason === ConnectionErrorReason.NotAllowed) {
22940
- livekitLogger.debug('token is not valid, cancelling auto region refresh');
23273
+ log$4.debug('token is not valid, cancelling auto region refresh');
22941
23274
  return;
22942
23275
  }
22943
- livekitLogger.debug('auto refetching of region settings failed', {
23276
+ log$4.debug('auto refetching of region settings failed', {
22944
23277
  error
22945
23278
  });
22946
23279
  // continue retrying with the same max age
@@ -22973,7 +23306,7 @@ class RegionUrlProvider {
22973
23306
  tracker.cleanupTimeout = setTimeout(() => {
22974
23307
  const currentTracker = RegionUrlProvider.connectionTrackers.get(hostname);
22975
23308
  if (currentTracker && currentTracker.connectionCount === 0) {
22976
- livekitLogger.debug('stopping region refetch after disconnect delay', {
23309
+ log$4.debug('stopping region refetch after disconnect delay', {
22977
23310
  hostname
22978
23311
  });
22979
23312
  RegionUrlProvider.stopRefetch(hostname);
@@ -23053,7 +23386,9 @@ class RegionUrlProvider {
23053
23386
  if (regionsLeft.length > 0) {
23054
23387
  const nextRegion = regionsLeft[0];
23055
23388
  this.attemptedRegions.push(nextRegion);
23056
- livekitLogger.debug("next region: ".concat(nextRegion.region));
23389
+ log$4.info("switching to region: ".concat(nextRegion.region), {
23390
+ region: nextRegion.region
23391
+ });
23057
23392
  return nextRegion.url;
23058
23393
  } else {
23059
23394
  return null;
@@ -24829,6 +25164,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24829
25164
  let bufferSize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : READABLE_STREAM_DEFAULT_BUFFER_SIZE;
24830
25165
  let streamController = null;
24831
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);
24832
25170
  const detachSignal = () => {
24833
25171
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
24834
25172
  };
@@ -24838,8 +25176,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24838
25176
  log$1.warn("ReadableStream subscribed to ".concat(sid, " was not started."));
24839
25177
  return;
24840
25178
  }
24841
- const descriptor = this.descriptors.get(sid);
24842
- if (!descriptor) {
25179
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
24843
25180
  log$1.warn("Unknown track ".concat(sid, ", skipping cancel..."));
24844
25181
  return;
24845
25182
  }
@@ -24858,9 +25195,8 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24858
25195
  if (!streamController) {
24859
25196
  return;
24860
25197
  }
24861
- const currentDescriptor = this.descriptors.get(sid);
24862
- if ((currentDescriptor === null || currentDescriptor === void 0 ? void 0 : currentDescriptor.subscription.type) === 'active') {
24863
- currentDescriptor.subscription.streamControllers.delete(streamController);
25198
+ if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.subscription.type) === 'active') {
25199
+ descriptor.subscription.streamControllers.delete(streamController);
24864
25200
  }
24865
25201
  streamController.error(DataTrackSubscribeError.cancelled());
24866
25202
  (_a = sfuSubscriptionComplete.reject) === null || _a === void 0 ? void 0 : _a.call(sfuSubscriptionComplete, DataTrackSubscribeError.cancelled());
@@ -24871,8 +25207,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24871
25207
  streamController = controller;
24872
25208
  this.subscribeRequest(sid, signal).then(() => __awaiter(this, void 0, void 0, function* () {
24873
25209
  var _a, _b, _c;
24874
- const descriptor = this.descriptors.get(sid);
24875
- if (!descriptor) {
25210
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
24876
25211
  log$1.error("Unknown track ".concat(sid));
24877
25212
  const err = DataTrackSubscribeError.disconnected();
24878
25213
  controller.error(err);
@@ -24987,7 +25322,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24987
25322
  };
24988
25323
  // Let the SFU know that the subscribe has been cancelled
24989
25324
  this.emit('sfuUpdateSubscription', {
24990
- sid,
25325
+ sid: descriptor.info.sid,
24991
25326
  subscribe: false
24992
25327
  });
24993
25328
  if (previousDescriptorSubscription.type === 'pending') {
@@ -25101,6 +25436,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25101
25436
  if (this.descriptors.has(info.sid)) {
25102
25437
  continue;
25103
25438
  }
25439
+ if (this.handleSidReassigned(publisherIdentity, info)) {
25440
+ continue;
25441
+ }
25104
25442
  yield this.handleTrackPublished(publisherIdentity, info);
25105
25443
  }
25106
25444
  publisherParticipantToSidsInUpdate.set(publisherIdentity, sidsInUpdate);
@@ -25158,6 +25496,62 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25158
25496
  });
25159
25497
  });
25160
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
+ }
25161
25555
  handleTrackUnpublished(sid) {
25162
25556
  const descriptor = this.descriptors.get(sid);
25163
25557
  if (!descriptor) {
@@ -25179,10 +25573,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25179
25573
  /** Mapping between track handles attached to incoming packets to the
25180
25574
  * track SIDs they belong to. */
25181
25575
  mapping) {
25182
- for (const _ref1 of mapping.entries()) {
25183
- var _ref10 = _slicedToArray(_ref1, 2);
25184
- const handle = _ref10[0];
25185
- 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];
25186
25580
  this.registerSubscriberHandle(handle, sid);
25187
25581
  }
25188
25582
  }
@@ -25203,12 +25597,17 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25203
25597
  case 'active':
25204
25598
  {
25205
25599
  // Update handle for an active subscription. This can occur following a full reconnect.
25600
+ this.subscriptionHandles.delete(descriptor.subscription.subcriptionHandle);
25206
25601
  descriptor.subscription.subcriptionHandle = assignedHandle;
25207
25602
  this.subscriptionHandles.set(assignedHandle, sid);
25208
25603
  return;
25209
25604
  }
25210
25605
  case 'pending':
25211
25606
  {
25607
+ log$1.debug("data track subscription activated", {
25608
+ sid,
25609
+ handle: assignedHandle
25610
+ });
25212
25611
  const pipeline = new IncomingDataTrackPipeline({
25213
25612
  info: descriptor.info,
25214
25613
  publisherIdentity: descriptor.publisherIdentity,
@@ -25275,10 +25674,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25275
25674
  * tracks are subscribed to locally.
25276
25675
  */
25277
25676
  resendSubscriptionUpdates() {
25278
- for (const _ref11 of this.descriptors) {
25279
- var _ref12 = _slicedToArray(_ref11, 2);
25280
- const sid = _ref12[0];
25281
- 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];
25282
25681
  if (descriptor.subscription.type === 'none') {
25283
25682
  continue;
25284
25683
  }
@@ -25944,10 +26343,16 @@ class OutgoingDataTrackManager extends eventsExports.EventEmitter {
25944
26343
  {
25945
26344
  if (result.type === 'ok') {
25946
26345
  const info = result.data;
26346
+ log.debug("SFU accepted publish request for handle ".concat(handle), {
26347
+ sid: info.sid
26348
+ });
25947
26349
  const e2eeManager = info.usesE2ee ? this.e2eeManager : null;
25948
26350
  this.descriptors.set(info.pubHandle, Descriptor.active(info, e2eeManager));
25949
26351
  (_b = (_a = descriptor.completionFuture).resolve) === null || _b === void 0 ? void 0 : _b.call(_a);
25950
26352
  } else {
26353
+ log.debug("SFU rejected publish request for handle ".concat(handle), {
26354
+ error: result.error
26355
+ });
25951
26356
  (_d = (_c = descriptor.completionFuture).reject) === null || _d === void 0 ? void 0 : _d.call(_c, result.error);
25952
26357
  }
25953
26358
  return;
@@ -27403,6 +27808,11 @@ class Participant extends eventsExports.EventEmitter {
27403
27808
  this.audioTrackPublications.forEach(track => isAudioTrack(track.track) && track.track.setAudioContext(ctx));
27404
27809
  }
27405
27810
  addTrackPublication(publication) {
27811
+ this.log.debug("adding track publication", {
27812
+ trackSid: publication.trackSid,
27813
+ source: publication.source,
27814
+ kind: publication.kind
27815
+ });
27406
27816
  // forward publication driven events
27407
27817
  publication.on(TrackEvent.Muted, () => {
27408
27818
  this.emit(ParticipantEvent.TrackMuted, publication);
@@ -28447,7 +28857,7 @@ class Participant extends eventsExports.EventEmitter {
28447
28857
  throw new UnexpectedConnectionState('cannot publish track when not connected');
28448
28858
  }
28449
28859
  const negotiate = () => __awaiter(this, void 0, void 0, function* () {
28450
- var _a, _b, _c;
28860
+ var _a, _b, _c, _d;
28451
28861
  if (!this.engine.pcManager) {
28452
28862
  throw new UnexpectedConnectionState('pcManager is not ready');
28453
28863
  }
@@ -28483,12 +28893,22 @@ class Participant extends eventsExports.EventEmitter {
28483
28893
  maxbr: ((_b = encodings[0]) === null || _b === void 0 ? void 0 : _b.maxBitrate) ? encodings[0].maxBitrate / 1000 : 0
28484
28894
  });
28485
28895
  }
28486
- } else if (track.codec && isSVCCodec(track.codec) && ((_c = encodings[0]) === null || _c === void 0 ? void 0 : _c.maxBitrate)) {
28487
- this.engine.pcManager.publisher.setTrackCodecBitrate({
28488
- cid: req.cid,
28489
- codec: track.codec,
28490
- maxbr: encodings[0].maxBitrate / 1000
28491
- });
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
+ }
28492
28912
  }
28493
28913
  }
28494
28914
  yield this.engine.negotiate();
@@ -30206,7 +30626,13 @@ class Room extends eventsExports.EventEmitter {
30206
30626
  });
30207
30627
  try {
30208
30628
  yield Promise.all([this.acquireAudioContext(), ...elements.map(e => {
30209
- 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
+ }
30210
30636
  return e.play();
30211
30637
  })]);
30212
30638
  this.handleAudioPlaybackStarted();