livekit-client 2.20.1 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) 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 +37 -7
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2279 -651
  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 +37 -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/index.d.ts +1 -1
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/logger.d.ts +3 -1
  17. package/dist/src/logger.d.ts.map +1 -1
  18. package/dist/src/room/PCTransport.d.ts +2 -0
  19. package/dist/src/room/PCTransport.d.ts.map +1 -1
  20. package/dist/src/room/PCTransportManager.d.ts +1 -0
  21. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  22. package/dist/src/room/RTCEngine.d.ts +31 -26
  23. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  24. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  25. package/dist/src/room/Room.d.ts +4 -0
  26. package/dist/src/room/Room.d.ts.map +1 -1
  27. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  28. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  29. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  30. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  31. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  32. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  33. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  34. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  35. package/dist/src/room/data-channel/types.d.ts +12 -0
  36. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  37. package/dist/src/room/data-stream/compression.d.ts +49 -0
  38. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  39. package/dist/src/room/data-stream/constants.d.ts +18 -0
  40. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  41. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  42. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  43. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  44. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  45. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  46. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  47. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  48. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  49. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  50. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  51. package/dist/src/room/errors.d.ts +3 -1
  52. package/dist/src/room/errors.d.ts.map +1 -1
  53. package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
  54. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  55. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  56. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  57. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  58. package/dist/src/room/participant/publishUtils.d.ts +7 -0
  59. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  60. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  61. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  62. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
  63. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  64. package/dist/src/room/track/Track.d.ts.map +1 -1
  65. package/dist/src/room/track/options.d.ts +11 -1
  66. package/dist/src/room/track/options.d.ts.map +1 -1
  67. package/dist/src/room/types.d.ts +17 -0
  68. package/dist/src/room/types.d.ts.map +1 -1
  69. package/dist/src/room/utils.d.ts +13 -0
  70. package/dist/src/room/utils.d.ts.map +1 -1
  71. package/dist/src/test/signalToken.d.ts +26 -0
  72. package/dist/src/test/signalToken.d.ts.map +1 -0
  73. package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
  74. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  75. package/dist/src/version.d.ts +5 -1
  76. package/dist/src/version.d.ts.map +1 -1
  77. package/dist/ts4.2/index.d.ts +1 -1
  78. package/dist/ts4.2/logger.d.ts +3 -1
  79. package/dist/ts4.2/room/PCTransport.d.ts +2 -0
  80. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  81. package/dist/ts4.2/room/RTCEngine.d.ts +31 -26
  82. package/dist/ts4.2/room/Room.d.ts +4 -0
  83. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  84. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  85. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  86. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  87. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  88. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  89. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  90. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  91. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  92. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  93. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  94. package/dist/ts4.2/room/errors.d.ts +3 -1
  95. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
  96. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  97. package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
  98. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
  99. package/dist/ts4.2/room/track/options.d.ts +11 -1
  100. package/dist/ts4.2/room/types.d.ts +17 -0
  101. package/dist/ts4.2/room/utils.d.ts +13 -0
  102. package/dist/ts4.2/test/signalToken.d.ts +26 -0
  103. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
  104. package/dist/ts4.2/version.d.ts +5 -1
  105. package/package.json +8 -4
  106. package/src/api/SignalClient.e2e.test.ts +264 -0
  107. package/src/api/SignalClient.ts +34 -2
  108. package/src/index.ts +2 -0
  109. package/src/logger.ts +2 -0
  110. package/src/room/PCTransport.ts +66 -31
  111. package/src/room/PCTransportManager.ts +10 -6
  112. package/src/room/RTCEngine.test.ts +238 -6
  113. package/src/room/RTCEngine.ts +101 -338
  114. package/src/room/RegionUrlProvider.ts +4 -2
  115. package/src/room/Room.test.ts +4 -1
  116. package/src/room/Room.ts +47 -9
  117. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  118. package/src/room/data-channel/DataChannelManager.ts +237 -0
  119. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  120. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  121. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  122. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  123. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  124. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  125. package/src/room/data-channel/types.ts +30 -0
  126. package/src/room/data-stream/compression.ts +117 -0
  127. package/src/room/data-stream/constants.ts +18 -0
  128. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  129. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  130. package/src/room/data-stream/incoming/StreamReader.ts +8 -2
  131. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  132. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  133. package/src/room/data-stream/outgoing/header-utils.ts +87 -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/errors.ts +10 -4
  138. package/src/room/participant/LocalParticipant.ts +29 -6
  139. package/src/room/participant/Participant.ts +5 -0
  140. package/src/room/participant/RemoteParticipant.ts +19 -5
  141. package/src/room/participant/publishUtils.ts +14 -11
  142. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  143. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  144. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  145. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  146. package/src/room/track/LocalAudioTrack.ts +16 -0
  147. package/src/room/track/Track.ts +10 -6
  148. package/src/room/track/options.ts +11 -1
  149. package/src/room/types.ts +18 -0
  150. package/src/room/utils.ts +67 -0
  151. package/src/test/signalServerSetup.ts +151 -0
  152. package/src/test/signalToken.ts +59 -0
  153. package/src/test/vitest-context.d.ts +10 -0
  154. package/src/utils/dataPacketBuffer.test.ts +100 -0
  155. package/src/utils/dataPacketBuffer.ts +46 -1
  156. package/src/version.ts +5 -1
@@ -63,11 +63,15 @@ import log, { LoggerNames, getLogger } from '../logger';
63
63
  import type { InternalRoomOptions } from '../options';
64
64
  import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
65
65
  import TypedPromise from '../utils/TypedPromise';
66
- import { DataPacketBuffer } from '../utils/dataPacketBuffer';
67
66
  import { TTLMap } from '../utils/ttlmap';
68
67
  import PCTransport, { PCEvents } from './PCTransport';
69
68
  import { PCTransportManager, PCTransportState } from './PCTransportManager';
70
69
  import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
70
+ import { DataChannelManager } from './data-channel/DataChannelManager';
71
+ import type { FlowControlledDataChannel } from './data-channel/FlowControlledDataChannel';
72
+ import type { LossyDataChannel } from './data-channel/LossyDataChannel';
73
+ import type { ReliableDataChannel } from './data-channel/ReliableDataChannel';
74
+ import { DataChannelKind } from './data-channel/types';
71
75
  import { DataTrackInfo } from './data-track/types';
72
76
  import { roomConnectOptionDefaults } from './defaults';
73
77
  import {
@@ -91,7 +95,6 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
91
95
  import { getTrackPublicationInfo } from './track/utils';
92
96
  import type { LoggerOptions } from './types';
93
97
  import {
94
- Future,
95
98
  isPublisherOfferWithJoinSupported,
96
99
  isReactNative,
97
100
  isVideoCodec,
@@ -103,14 +106,9 @@ import {
103
106
  toHttpUrl,
104
107
  } from './utils';
105
108
 
106
- const lossyDataChannel = '_lossy';
107
- const reliableDataChannel = '_reliable';
108
- const dataTrackDataChannel = '_data_track';
109
109
  const minReconnectWait = 2 * 1000;
110
110
  const leaveReconnect = 'leave-reconnect';
111
111
  const reliabeReceiveStateTTL = 30_000;
112
- const lossyDataChannelBufferThresholdMin = 8 * 1024;
113
- const lossyDataChannelBufferThresholdMax = 256 * 1024;
114
112
 
115
113
  const initialMediaSectionsAudio = 3;
116
114
  const initialMediaSectionsVideo = 3;
@@ -123,11 +121,7 @@ enum PCState {
123
121
  Closed,
124
122
  }
125
123
 
126
- export enum DataChannelKind {
127
- RELIABLE = DataPacket_Kind.RELIABLE,
128
- LOSSY = DataPacket_Kind.LOSSY,
129
- DATA_TRACK_LOSSY = 2,
130
- }
124
+ export { DataChannelKind };
131
125
 
132
126
  // Default data-channel max message size (bytes), used when the remote SDP
133
127
  // answer does not advertise an `a=max-message-size` attribute (RFC 8841).
@@ -171,22 +165,24 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
171
165
  return !!this.reconnectTimeout;
172
166
  }
173
167
 
174
- private lossyDC?: RTCDataChannel;
175
-
176
- // @ts-ignore noUnusedLocals
177
- private lossyDCSub?: RTCDataChannel;
178
-
179
- private reliableDC?: RTCDataChannel;
180
-
181
- // @ts-ignore noUnusedLocals
182
- private reliableDCSub?: RTCDataChannel;
168
+ /**
169
+ * Owns the data channels: the three flow-controlled publisher wrappers (engine-lifetime; the
170
+ * RTCDataChannel handles underneath are attached/detached as peer connections come and go, with
171
+ * waiter invalidation built into the turnover) plus the subscriber-side receive handles.
172
+ */
173
+ private dataChannels: DataChannelManager;
183
174
 
184
- private dataTrackDC?: RTCDataChannel;
175
+ private get reliableChannel(): ReliableDataChannel {
176
+ return this.dataChannels.reliable;
177
+ }
185
178
 
186
- // @ts-ignore noUnusedLocals
187
- private dataTrackDCSub?: RTCDataChannel;
179
+ private get lossyChannel(): LossyDataChannel {
180
+ return this.dataChannels.lossy;
181
+ }
188
182
 
189
- private dcBufferStatus: Map<DataChannelKind, boolean>;
183
+ private get dataTrackChannel(): LossyDataChannel {
184
+ return this.dataChannels.dataTrack;
185
+ }
190
186
 
191
187
  private subscriberPrimary: boolean = false;
192
188
 
@@ -243,27 +239,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
243
239
 
244
240
  private publisherConnectionPromise: Promise<void> | undefined;
245
241
 
246
- private reliableDataSequence: number = 1;
247
-
248
- private reliableMessageBuffer = new DataPacketBuffer();
249
-
250
242
  private reliableReceivedState: TTLMap<string, number> = new TTLMap(reliabeReceiveStateTTL);
251
243
 
252
- private lossyDataStatCurrentBytes: number = 0;
253
-
254
- private lossyDataStatByterate: number = 0;
255
-
256
- private lossyDataStatInterval: ReturnType<typeof setInterval> | undefined;
257
-
258
- private lossyDataDropCount: number = 0;
259
-
260
244
  private midToTrackId: { [key: string]: string } = {};
261
245
 
262
246
  /** used to indicate whether the browser is currently waiting to reconnect */
263
247
  private isWaitingForNetworkReconnect: boolean = false;
264
248
 
265
- private bufferStatusLowClosingFuture = new Future<never, UnexpectedConnectionState>();
266
-
267
249
  constructor(private options: InternalRoomOptions) {
268
250
  super();
269
251
  this.log = getLogger(options.loggerName ?? LoggerNames.Engine, () => this.logContext);
@@ -276,11 +258,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
276
258
  this.reconnectPolicy = this.options.reconnectPolicy;
277
259
  this.closingLock = new Mutex();
278
260
  this.dataProcessLock = new Mutex();
279
- this.dcBufferStatus = new Map([
280
- [DataChannelKind.RELIABLE, true],
281
- [DataChannelKind.LOSSY, true],
282
- [DataChannelKind.DATA_TRACK_LOSSY, true],
283
- ]);
261
+ this.dataChannels = new DataChannelManager({
262
+ isEngineClosed: () => this.isClosed,
263
+ isReconnecting: () => this.attemptingReconnect,
264
+ onDataMessage: (message) => this.handleDataMessage(message),
265
+ onDataTrackMessage: (message) => this.handleDataTrackMessage(message),
266
+ onDataError: (event) => this.handleDataError(event),
267
+ onChannelClose: (kind) => this.handleDataChannelClose(kind)(),
268
+ onBufferStatusChanged: (kind, isLow) =>
269
+ this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind),
270
+ });
284
271
 
285
272
  this.client.onParticipantUpdate = (updates) =>
286
273
  this.emit(EngineEvent.ParticipantUpdate, updates);
@@ -297,13 +284,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
297
284
  this.client.onParticipantUpdate = (updates) =>
298
285
  this.emit(EngineEvent.ParticipantUpdate, updates);
299
286
  this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
300
-
301
- this.on(EngineEvent.Closing, () => {
302
- this.bufferStatusLowClosingFuture.reject?.(new UnexpectedConnectionState('engine closed'));
303
- });
304
- // Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
305
- // when no waitForBufferStatusLow callers are attached.
306
- this.bufferStatusLowClosingFuture.promise.catch(() => {});
307
287
  }
308
288
 
309
289
  /** @internal */
@@ -458,55 +438,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
458
438
  }
459
439
 
460
440
  async cleanupPeerConnections() {
461
- const dcCleanup = (dc: RTCDataChannel | undefined) => {
462
- if (!dc) {
463
- return;
464
- }
465
-
466
- // Detach the data channel handlers before closing anything. Closing a peer connection tears
467
- // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
468
- // channels; if our handlers are still attached at that point, handleDataError logs a spurious
469
- // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
470
- // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
471
- // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
472
- dc.onbufferedamountlow = null;
473
- dc.onclose = null;
474
- dc.onclosing = null;
475
- dc.onerror = null;
476
- dc.onmessage = null;
477
- dc.onopen = null;
478
-
479
- dc.close();
480
- };
481
- dcCleanup(this.lossyDC);
482
- dcCleanup(this.lossyDCSub);
483
- dcCleanup(this.reliableDC);
484
- dcCleanup(this.reliableDCSub);
485
- dcCleanup(this.dataTrackDC);
486
- dcCleanup(this.dataTrackDCSub);
441
+ this.dataChannels.teardown();
487
442
 
488
443
  await this.pcManager?.close();
489
444
  this.pcManager = undefined;
490
445
 
491
- this.lossyDC = undefined;
492
- this.lossyDCSub = undefined;
493
- this.reliableDC = undefined;
494
- this.reliableDCSub = undefined;
495
- this.dataTrackDC = undefined;
496
- this.dataTrackDCSub = undefined;
497
- this.reliableMessageBuffer = new DataPacketBuffer();
498
- this.reliableDataSequence = 1;
499
446
  this.reliableReceivedState.clear();
500
447
  }
501
448
 
502
449
  cleanupLossyDataStats() {
503
- this.lossyDataStatByterate = 0;
504
- this.lossyDataStatCurrentBytes = 0;
505
- if (this.lossyDataStatInterval) {
506
- clearInterval(this.lossyDataStatInterval);
507
- this.lossyDataStatInterval = undefined;
508
- }
509
- this.lossyDataDropCount = 0;
450
+ this.lossyChannel.stopThresholdTuning();
510
451
  }
511
452
 
512
453
  async cleanupClient() {
@@ -575,7 +516,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
575
516
  }
576
517
 
577
518
  get dataSubscriberReadyState(): string | undefined {
578
- return this.reliableDCSub?.readyState;
519
+ return this.dataChannelForKind(DataChannelKind.RELIABLE, true)?.readyState;
579
520
  }
580
521
 
581
522
  async getConnectedServerAddress(): Promise<string | undefined> {
@@ -885,116 +826,39 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
885
826
  return;
886
827
  }
887
828
 
888
- // clear old data channel callbacks if recreate
889
- if (this.lossyDC) {
890
- this.lossyDC.onmessage = null;
891
- this.lossyDC.onerror = null;
892
- this.lossyDC.onclose = null;
893
- }
894
- if (this.reliableDC) {
895
- this.reliableDC.onmessage = null;
896
- this.reliableDC.onerror = null;
897
- this.reliableDC.onclose = null;
898
- }
899
- if (this.dataTrackDC) {
900
- this.dataTrackDC.onmessage = null;
901
- this.dataTrackDC.onerror = null;
902
- this.dataTrackDC.onclose = null;
903
- }
904
-
905
- // create data channels
906
- this.lossyDC = this.pcManager.createPublisherDataChannel(lossyDataChannel, {
907
- ordered: false,
908
- maxRetransmits: 0,
909
- });
910
- this.reliableDC = this.pcManager.createPublisherDataChannel(reliableDataChannel, {
911
- ordered: true,
912
- });
913
- this.dataTrackDC = this.pcManager.createPublisherDataChannel(dataTrackDataChannel, {
914
- ordered: false,
915
- maxRetransmits: 0,
916
- });
917
-
918
- // also handle messages over the pub channel, for backwards compatibility
919
- this.lossyDC.onmessage = this.handleDataMessage;
920
- this.reliableDC.onmessage = this.handleDataMessage;
921
- this.dataTrackDC.onmessage = this.handleDataTrackMessage;
922
-
923
- // handle datachannel errors
924
- this.lossyDC.onerror = this.handleDataError;
925
- this.reliableDC.onerror = this.handleDataError;
926
- this.dataTrackDC.onerror = this.handleDataError;
927
-
928
- // detect unexpected publisher data channel closes
929
- this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
930
- this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
931
- this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
932
-
933
- // set up dc buffer threshold, set to 64kB (otherwise 0 by default)
934
- this.lossyDC.bufferedAmountLowThreshold = 65535;
935
- this.reliableDC.bufferedAmountLowThreshold = 65535;
936
- this.dataTrackDC.bufferedAmountLowThreshold = 65535;
937
-
938
- // handle buffer amount low events
939
- this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
940
- this.reliableDC.onbufferedamountlow = () =>
941
- this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
942
- this.dataTrackDC.onbufferedamountlow = () =>
943
- this.handleBufferedAmountLow(DataChannelKind.DATA_TRACK_LOSSY);
944
-
945
- this.cleanupLossyDataStats();
946
- this.lossyDataStatInterval = setInterval(() => {
947
- this.lossyDataStatByterate = this.lossyDataStatCurrentBytes;
948
- this.lossyDataStatCurrentBytes = 0;
949
-
950
- const dc = this.dataChannelForKind(DataChannelKind.LOSSY);
951
- if (dc) {
952
- // control buffered latency to ~100ms
953
- const threshold = this.lossyDataStatByterate / 10;
954
- dc.bufferedAmountLowThreshold = Math.min(
955
- Math.max(threshold, lossyDataChannelBufferThresholdMin),
956
- lossyDataChannelBufferThresholdMax,
957
- );
958
- }
959
- }, 1000);
829
+ this.dataChannels.createPublisherChannels(this.pcManager);
960
830
  }
961
831
 
962
832
  private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
963
833
  if (!channel) {
964
834
  return;
965
835
  }
966
- let handler;
967
- if (channel.label === reliableDataChannel) {
968
- this.reliableDCSub = channel;
969
- handler = this.handleDataMessage;
970
- } else if (channel.label === lossyDataChannel) {
971
- this.lossyDCSub = channel;
972
- handler = this.handleDataMessage;
973
- } else if (channel.label === dataTrackDataChannel) {
974
- this.dataTrackDCSub = channel;
975
- handler = this.handleDataTrackMessage;
976
- } else {
977
- return;
836
+ if (this.dataChannels.adoptSubscriberChannel(channel)) {
837
+ this.log.debug(`on data channel ${channel.id}, ${channel.label}`);
978
838
  }
979
- this.log.debug(`on data channel ${channel.id}, ${channel.label}`);
980
- channel.onmessage = handler;
981
839
  };
982
840
 
841
+ /** Normalizes an incoming data-channel message into bytes, or logs and returns undefined. */
842
+ private async decodeDataMessage(message: MessageEvent): Promise<Uint8Array | undefined> {
843
+ if (message.data instanceof ArrayBuffer) {
844
+ return new Uint8Array(message.data);
845
+ }
846
+ if (message.data instanceof Blob) {
847
+ return new Uint8Array(await message.data.arrayBuffer());
848
+ }
849
+ this.log.error('unsupported data type', { data: message.data });
850
+ return undefined;
851
+ }
852
+
983
853
  private handleDataMessage = async (message: MessageEvent) => {
984
854
  // make sure to respect incoming data message order by processing message events one after the other
985
855
  const unlock = await this.dataProcessLock.lock();
986
856
  try {
987
- // decode
988
- let buffer: ArrayBuffer | undefined;
989
- if (message.data instanceof ArrayBuffer) {
990
- buffer = message.data;
991
- } else if (message.data instanceof Blob) {
992
- buffer = await message.data.arrayBuffer();
993
- } else {
994
- this.log.error('unsupported data type', { data: message.data });
857
+ const bytes = await this.decodeDataMessage(message);
858
+ if (!bytes) {
995
859
  return;
996
860
  }
997
- const dp = DataPacket.fromBinary(new Uint8Array(buffer));
861
+ const dp = DataPacket.fromBinary(bytes);
998
862
 
999
863
  if (dp.sequence > 0 && dp.participantSid !== '') {
1000
864
  const lastSeq = this.reliableReceivedState.get(dp.participantSid);
@@ -1043,18 +907,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1043
907
  };
1044
908
 
1045
909
  private handleDataTrackMessage = async (message: MessageEvent) => {
1046
- // Decode / normalize into a common format
1047
- let buffer: ArrayBuffer | undefined;
1048
- if (message.data instanceof ArrayBuffer) {
1049
- buffer = message.data;
1050
- } else if (message.data instanceof Blob) {
1051
- buffer = await message.data.arrayBuffer();
1052
- } else {
1053
- this.log.error('unsupported data type', { data: message.data });
910
+ const bytes = await this.decodeDataMessage(message);
911
+ if (!bytes) {
1054
912
  return;
1055
913
  }
1056
-
1057
- this.emit('dataTrackPacketReceived', new Uint8Array(buffer));
914
+ this.emit('dataTrackPacketReceived', bytes);
1058
915
  };
1059
916
 
1060
917
  private handleDataError = (event: Event) => {
@@ -1093,10 +950,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1093
950
  }
1094
951
  };
1095
952
 
1096
- private handleBufferedAmountLow = (channelKind: DataChannelKind) => {
1097
- this.updateAndEmitDCBufferStatus(channelKind);
1098
- };
1099
-
1100
953
  async createSender(
1101
954
  track: LocalTrack,
1102
955
  opts: TrackPublishOptions,
@@ -1500,12 +1353,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1500
1353
 
1501
1354
  // recreate publish datachannel if it's id is null
1502
1355
  // (for safari https://bugs.webkit.org/show_bug.cgi?id=184688)
1503
- if (this.reliableDC?.readyState === 'open' && this.reliableDC.id === null) {
1356
+ const reliableDC = this.dataChannelForKind(DataChannelKind.RELIABLE);
1357
+ if (reliableDC?.readyState === 'open' && reliableDC.id === null) {
1504
1358
  this.createDataChannels();
1505
1359
  }
1506
1360
 
1507
1361
  if (res?.lastMessageSeq) {
1508
- this.resendReliableMessagesForResume(res.lastMessageSeq);
1362
+ this.resendReliableMessagesForResume(res.lastMessageSeq).catch((error) => {
1363
+ this.log.warn('failed to resend reliable messages after resume', {
1364
+ ...this.logContext,
1365
+ error,
1366
+ });
1367
+ });
1509
1368
  }
1510
1369
 
1511
1370
  // resume success
@@ -1571,7 +1430,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1571
1430
  }
1572
1431
 
1573
1432
  /* @internal */
1574
- async sendDataPacket(packet: DataPacket, kind: DataChannelKind) {
1433
+ async sendDataPacket(
1434
+ packet: DataPacket,
1435
+ /** Data-track frames don't come through here — they're sent pre-serialized via {@link sendDataTrackFrame }. */
1436
+ kind: Exclude<DataChannelKind, DataChannelKind.DATA_TRACK_LOSSY>,
1437
+ ) {
1575
1438
  // make sure we do have a data connection
1576
1439
  await this.ensurePublisherConnected(kind);
1577
1440
 
@@ -1593,8 +1456,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1593
1456
  }
1594
1457
 
1595
1458
  if (kind === DataChannelKind.RELIABLE) {
1596
- packet.sequence = this.reliableDataSequence;
1597
- this.reliableDataSequence += 1;
1459
+ packet.sequence = this.reliableChannel.nextSequence();
1598
1460
  }
1599
1461
 
1600
1462
  const msg = packet.toBinary() as Uint8Array<ArrayBuffer>;
@@ -1616,124 +1478,46 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1616
1478
  );
1617
1479
  }
1618
1480
 
1619
- switch (kind) {
1620
- case DataChannelKind.LOSSY:
1621
- case DataChannelKind.DATA_TRACK_LOSSY:
1622
- return this.sendLossyBytes(msg, kind);
1623
-
1624
- case DataChannelKind.RELIABLE:
1625
- const dc = this.dataChannelForKind(kind);
1626
- if (dc) {
1627
- await this.waitForBufferStatusLow(kind);
1628
- this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence });
1629
-
1630
- if (this.attemptingReconnect) {
1631
- return;
1632
- }
1633
-
1634
- dc.send(msg);
1635
- }
1636
-
1637
- this.updateAndEmitDCBufferStatus(kind);
1638
- break;
1481
+ // The full-buffer policy (drop for lossy, wait/replay for reliable) is the channel's own, as
1482
+ // is emitting the buffer-status change once the send settles.
1483
+ if (kind === DataChannelKind.RELIABLE) {
1484
+ await this.reliableChannel.send(msg, packet.sequence);
1485
+ } else {
1486
+ await this.lossyChannel.send(msg);
1639
1487
  }
1640
1488
  }
1641
1489
 
1642
- /* @internal */
1643
- async sendLossyBytes(
1644
- bytes: NonSharedUint8Array,
1645
- kind: Exclude<DataChannelKind, DataChannelKind.RELIABLE>,
1646
- bufferStatusLowBehavior: 'drop' | 'wait' = 'drop',
1647
- ) {
1648
- // make sure we do have a data connection
1649
- await this.ensurePublisherConnected(kind);
1650
-
1651
- const dc = this.dataChannelForKind(kind);
1652
- if (dc) {
1653
- if (!this.isBufferStatusLow(kind)) {
1654
- // Depending on the exact circumstance that data is being sent, either drop or wait for the
1655
- // buffer status to not be low before continuing.
1656
- switch (bufferStatusLowBehavior) {
1657
- case 'wait':
1658
- await this.waitForBufferStatusLow(kind);
1659
- break;
1660
- case 'drop':
1661
- // this.log.warn(`dropping lossy data channel message`, this.logContext);
1662
- // Drop messages to reduce latency
1663
- this.lossyDataDropCount += 1;
1664
- if (this.lossyDataDropCount % 100 === 0) {
1665
- this.log.warn(
1666
- `dropping lossy data channel messages, total dropped: ${this.lossyDataDropCount}`,
1667
- );
1668
- }
1669
- return;
1670
- }
1671
- }
1672
- this.lossyDataStatCurrentBytes += bytes.byteLength;
1673
-
1674
- if (this.attemptingReconnect) {
1675
- return;
1676
- }
1677
-
1678
- dc.send(bytes);
1679
- }
1680
-
1681
- this.updateAndEmitDCBufferStatus(kind);
1490
+ /**
1491
+ * Sends pre-serialized bytes on the data-track channel. This is the one send path that doesn't
1492
+ * go through {@link sendDataPacket} — Room's `packetAvailable` handler calls it directly with
1493
+ * bytes the data-track pipeline already serialized.
1494
+ *
1495
+ * @internal
1496
+ */
1497
+ async sendDataTrackFrame(bytes: NonSharedUint8Array) {
1498
+ // Make sure we do have a data connection: on lazily negotiated publisher connections this is
1499
+ // what kicks negotiation off, and it waits for the channel to open. Memoized, so the
1500
+ // steady-state cost is one await on an already-resolved promise.
1501
+ await this.ensurePublisherConnected(DataChannelKind.DATA_TRACK_LOSSY);
1502
+ await this.dataTrackChannel.send(bytes);
1682
1503
  }
1683
1504
 
1684
1505
  private async resendReliableMessagesForResume(lastMessageSeq: number) {
1685
1506
  await this.ensurePublisherConnected(DataChannelKind.RELIABLE);
1686
- const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
1687
- if (dc) {
1688
- this.reliableMessageBuffer.popToSequence(lastMessageSeq);
1689
- this.reliableMessageBuffer.getAll().forEach((msg) => {
1690
- dc.send(msg.data);
1691
- });
1692
- }
1693
- this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
1507
+ await this.reliableChannel.replay(lastMessageSeq);
1694
1508
  }
1695
1509
 
1696
- private updateAndEmitDCBufferStatus = (kind: DataChannelKind) => {
1697
- if (kind === DataChannelKind.RELIABLE) {
1698
- const dc = this.dataChannelForKind(kind);
1699
- if (dc) {
1700
- this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
1701
- }
1702
- }
1703
-
1704
- const status = this.isBufferStatusLow(kind);
1705
- if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
1706
- this.dcBufferStatus.set(kind, status);
1707
- this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
1708
- }
1709
- };
1710
-
1711
- private isBufferStatusLow = (kind: DataChannelKind): boolean | undefined => {
1712
- const dc = this.dataChannelForKind(kind);
1713
- if (dc) {
1714
- return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
1715
- }
1716
- };
1510
+ /** The flow-control gate for `kind` see {@link FlowControlledDataChannel}. */
1511
+ private flowControlFor(kind: DataChannelKind): FlowControlledDataChannel {
1512
+ return this.dataChannels.channelFor(kind);
1513
+ }
1717
1514
 
1718
- async waitForBufferStatusLow(kind: DataChannelKind) {
1719
- return new TypedPromise<void, UnexpectedConnectionState>(async (resolve, reject) => {
1720
- if (this.isClosed) {
1721
- reject(new UnexpectedConnectionState('engine closed'));
1722
- }
1723
- if (this.isBufferStatusLow(kind)) {
1724
- resolve();
1725
- } else {
1726
- const dc = this.dataChannelForKind(kind);
1727
- if (!dc) {
1728
- reject(new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`));
1729
- return;
1730
- }
1731
- this.bufferStatusLowClosingFuture.promise.catch((e) => reject(e));
1732
- dc.addEventListener('bufferedamountlow', () => resolve(), {
1733
- once: true,
1734
- });
1735
- }
1736
- });
1515
+ /**
1516
+ * Resolves once the caller may send on the `kind` channel — see
1517
+ * {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
1518
+ */
1519
+ async waitForBufferHeadroom(kind: DataChannelKind) {
1520
+ return this.flowControlFor(kind).waitForHeadroomWithLock();
1737
1521
  }
1738
1522
 
1739
1523
  /**
@@ -1835,9 +1619,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1835
1619
  // don't negotiate without any transceivers or data channel, it will generate sdp without ice frag then negotiate failed
1836
1620
  if (
1837
1621
  this.pcManager.publisher.getTransceivers().length == 0 &&
1838
- !this.lossyDC &&
1839
- !this.reliableDC &&
1840
- !this.dataTrackDC
1622
+ !this.dataChannels.hasPublisherChannels
1841
1623
  ) {
1842
1624
  this.createDataChannels();
1843
1625
  }
@@ -1886,26 +1668,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1886
1668
  }
1887
1669
 
1888
1670
  dataChannelForKind(kind: DataChannelKind, sub?: boolean): RTCDataChannel | undefined {
1889
- switch (kind) {
1890
- case DataChannelKind.RELIABLE:
1891
- if (!sub) {
1892
- return this.reliableDC;
1893
- } else {
1894
- return this.reliableDCSub;
1895
- }
1896
- case DataChannelKind.LOSSY:
1897
- if (!sub) {
1898
- return this.lossyDC;
1899
- } else {
1900
- return this.lossyDCSub;
1901
- }
1902
- case DataChannelKind.DATA_TRACK_LOSSY:
1903
- if (!sub) {
1904
- return this.dataTrackDC;
1905
- } else {
1906
- return this.dataTrackDCSub;
1907
- }
1908
- }
1671
+ return this.dataChannels.getHandle(kind, sub);
1909
1672
  }
1910
1673
 
1911
1674
  /** @internal */
@@ -1,9 +1,11 @@
1
1
  import { Mutex } from '@livekit/mutex';
2
2
  import type { RegionInfo, RegionSettings } from '@livekit/protocol';
3
- import log from '../logger';
3
+ import { LoggerNames, getLogger } from '../logger';
4
4
  import { ConnectionError, ConnectionErrorReason } from './errors';
5
5
  import { extractMaxAgeFromRequestHeaders, isCloud } from './utils';
6
6
 
7
+ const log = getLogger(LoggerNames.Region);
8
+
7
9
  export const DEFAULT_MAX_AGE_MS = 5_000;
8
10
  export const STOP_REFETCH_DELAY_MS = 30_000;
9
11
 
@@ -229,7 +231,7 @@ export class RegionUrlProvider {
229
231
  if (regionsLeft.length > 0) {
230
232
  const nextRegion = regionsLeft[0];
231
233
  this.attemptedRegions.push(nextRegion);
232
- log.debug(`next region: ${nextRegion.region}`);
234
+ log.info(`switching to region: ${nextRegion.region}`, { region: nextRegion.region });
233
235
  return nextRegion.url;
234
236
  } else {
235
237
  return null;
@@ -81,7 +81,10 @@ describe('Room signaling options', () => {
81
81
  'wss://test.livekit.io',
82
82
  'test-token',
83
83
  expect.objectContaining({
84
- clientInfoCapabilities: [ClientInfo_Capability.CAP_PACKET_TRAILER],
84
+ clientInfoCapabilities: [
85
+ ClientInfo_Capability.CAP_PACKET_TRAILER,
86
+ ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW,
87
+ ],
85
88
  e2eeEnabled: true,
86
89
  }),
87
90
  expect.any(AbortSignal),