livekit-client 2.20.2 → 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 (105) 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 +4 -0
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +1993 -791
  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 +4 -0
  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/index.d.ts +1 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +28 -64
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts +4 -0
  18. package/dist/src/room/Room.d.ts.map +1 -1
  19. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  20. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  21. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  22. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  23. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  24. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  25. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  26. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  27. package/dist/src/room/data-channel/types.d.ts +12 -0
  28. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  29. package/dist/src/room/data-stream/compression.d.ts +49 -0
  30. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  31. package/dist/src/room/data-stream/constants.d.ts +18 -0
  32. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  33. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  34. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  35. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  36. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  37. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  38. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  39. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  40. package/dist/src/room/errors.d.ts +3 -1
  41. package/dist/src/room/errors.d.ts.map +1 -1
  42. package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
  43. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  44. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  45. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  46. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  47. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  48. package/dist/src/room/types.d.ts +17 -0
  49. package/dist/src/room/types.d.ts.map +1 -1
  50. package/dist/src/room/utils.d.ts +13 -0
  51. package/dist/src/room/utils.d.ts.map +1 -1
  52. package/dist/src/version.d.ts +5 -1
  53. package/dist/src/version.d.ts.map +1 -1
  54. package/dist/ts4.2/index.d.ts +1 -1
  55. package/dist/ts4.2/room/RTCEngine.d.ts +28 -64
  56. package/dist/ts4.2/room/Room.d.ts +4 -0
  57. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  58. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  59. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  60. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  61. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  62. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  63. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  64. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  65. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  66. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  67. package/dist/ts4.2/room/errors.d.ts +3 -1
  68. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
  69. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  70. package/dist/ts4.2/room/types.d.ts +17 -0
  71. package/dist/ts4.2/room/utils.d.ts +13 -0
  72. package/dist/ts4.2/version.d.ts +5 -1
  73. package/package.json +2 -2
  74. package/src/index.ts +2 -0
  75. package/src/room/RTCEngine.test.ts +58 -99
  76. package/src/room/RTCEngine.ts +92 -534
  77. package/src/room/Room.test.ts +4 -1
  78. package/src/room/Room.ts +40 -8
  79. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  80. package/src/room/data-channel/DataChannelManager.ts +237 -0
  81. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  82. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  83. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  84. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  85. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  86. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  87. package/src/room/data-channel/types.ts +30 -0
  88. package/src/room/data-stream/compression.ts +117 -0
  89. package/src/room/data-stream/constants.ts +18 -0
  90. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  91. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  92. package/src/room/data-stream/incoming/StreamReader.ts +8 -2
  93. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  94. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  95. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  96. package/src/room/errors.ts +6 -0
  97. package/src/room/participant/LocalParticipant.ts +13 -0
  98. package/src/room/participant/RemoteParticipant.ts +19 -5
  99. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  100. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  101. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  102. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  103. package/src/room/types.ts +18 -0
  104. package/src/room/utils.ts +67 -0
  105. package/src/version.ts +5 -1
@@ -44,7 +44,6 @@ import {
44
44
  type UserPacket,
45
45
  } from '@livekit/protocol';
46
46
  import { EventEmitter } from 'events';
47
- import type { Throws } from '@livekit/throws-transformer/throws';
48
47
  import type { MediaAttributes } from 'sdp-transform';
49
48
  import type TypedEventEmitter from 'typed-emitter';
50
49
  import type { SignalOptions } from '../api/SignalClient';
@@ -64,11 +63,15 @@ import log, { LoggerNames, getLogger } from '../logger';
64
63
  import type { InternalRoomOptions } from '../options';
65
64
  import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
66
65
  import TypedPromise from '../utils/TypedPromise';
67
- import { DataPacketBuffer } from '../utils/dataPacketBuffer';
68
66
  import { TTLMap } from '../utils/ttlmap';
69
67
  import PCTransport, { PCEvents } from './PCTransport';
70
68
  import { PCTransportManager, PCTransportState } from './PCTransportManager';
71
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';
72
75
  import { DataTrackInfo } from './data-track/types';
73
76
  import { roomConnectOptionDefaults } from './defaults';
74
77
  import {
@@ -103,24 +106,10 @@ 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
112
 
113
- // Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
114
- // freely up to the high-water mark; once it's exceeded they block until the browser's
115
- // `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
116
- // The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
117
- // isn't starved, while the high-water mark bounds the buffer well below the level that would abort
118
- // the channel (see livekit/client-sdk-js#1995).
119
- const reliableDataChannelWaterMarkLow = 64 * 1024;
120
- const reliableDataChannelWaterMarkHigh = 1024 * 1024;
121
- const lossyDataChannelWaterMarkLow = 8 * 1024;
122
- const lossyDataChannelWaterMarkHigh = 256 * 1024;
123
-
124
113
  const initialMediaSectionsAudio = 3;
125
114
  const initialMediaSectionsVideo = 3;
126
115
 
@@ -132,11 +121,7 @@ enum PCState {
132
121
  Closed,
133
122
  }
134
123
 
135
- export enum DataChannelKind {
136
- RELIABLE = DataPacket_Kind.RELIABLE,
137
- LOSSY = DataPacket_Kind.LOSSY,
138
- DATA_TRACK_LOSSY = 2,
139
- }
124
+ export { DataChannelKind };
140
125
 
141
126
  // Default data-channel max message size (bytes), used when the remote SDP
142
127
  // answer does not advertise an `a=max-message-size` attribute (RFC 8841).
@@ -180,22 +165,24 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
180
165
  return !!this.reconnectTimeout;
181
166
  }
182
167
 
183
- private lossyDC?: RTCDataChannel;
184
-
185
- // @ts-ignore noUnusedLocals
186
- private lossyDCSub?: RTCDataChannel;
187
-
188
- private reliableDC?: RTCDataChannel;
189
-
190
- // @ts-ignore noUnusedLocals
191
- 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;
192
174
 
193
- private dataTrackDC?: RTCDataChannel;
175
+ private get reliableChannel(): ReliableDataChannel {
176
+ return this.dataChannels.reliable;
177
+ }
194
178
 
195
- // @ts-ignore noUnusedLocals
196
- private dataTrackDCSub?: RTCDataChannel;
179
+ private get lossyChannel(): LossyDataChannel {
180
+ return this.dataChannels.lossy;
181
+ }
197
182
 
198
- private dcBufferStatus: Map<DataChannelKind, boolean>;
183
+ private get dataTrackChannel(): LossyDataChannel {
184
+ return this.dataChannels.dataTrack;
185
+ }
199
186
 
200
187
  private subscriberPrimary: boolean = false;
201
188
 
@@ -252,20 +239,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
252
239
 
253
240
  private publisherConnectionPromise: Promise<void> | undefined;
254
241
 
255
- private reliableDataSequence: number = 1;
256
-
257
- private reliableMessageBuffer = new DataPacketBuffer();
258
-
259
242
  private reliableReceivedState: TTLMap<string, number> = new TTLMap(reliabeReceiveStateTTL);
260
243
 
261
- private lossyDataStatCurrentBytes: number = 0;
262
-
263
- private lossyDataStatByterate: number = 0;
264
-
265
- private lossyDataStatInterval: ReturnType<typeof setInterval> | undefined;
266
-
267
- private lossyDataDropCount: number = 0;
268
-
269
244
  private midToTrackId: { [key: string]: string } = {};
270
245
 
271
246
  /** used to indicate whether the browser is currently waiting to reconnect */
@@ -283,11 +258,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
283
258
  this.reconnectPolicy = this.options.reconnectPolicy;
284
259
  this.closingLock = new Mutex();
285
260
  this.dataProcessLock = new Mutex();
286
- this.dcBufferStatus = new Map([
287
- [DataChannelKind.RELIABLE, true],
288
- [DataChannelKind.LOSSY, true],
289
- [DataChannelKind.DATA_TRACK_LOSSY, true],
290
- ]);
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
+ });
291
271
 
292
272
  this.client.onParticipantUpdate = (updates) =>
293
273
  this.emit(EngineEvent.ParticipantUpdate, updates);
@@ -458,60 +438,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
458
438
  }
459
439
 
460
440
  async cleanupPeerConnections() {
461
- // Reject parked headroom waiters up front: closing the peer connection is allowed (per spec)
462
- // to transition data channels to 'closed' without firing events, which would otherwise strand
463
- // a waiter holding the headroom lock.
464
- this.invalidateDataChannelWaiters('peer connections cleaned up');
465
-
466
- const dcCleanup = (dc: RTCDataChannel | undefined) => {
467
- if (!dc) {
468
- return;
469
- }
470
-
471
- // Detach the data channel handlers before closing anything. Closing a peer connection tears
472
- // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
473
- // channels; if our handlers are still attached at that point, handleDataError logs a spurious
474
- // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
475
- // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
476
- // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
477
- dc.onbufferedamountlow = null;
478
- dc.onclose = null;
479
- dc.onclosing = null;
480
- dc.onerror = null;
481
- dc.onmessage = null;
482
- dc.onopen = null;
483
-
484
- dc.close();
485
- };
486
- dcCleanup(this.lossyDC);
487
- dcCleanup(this.lossyDCSub);
488
- dcCleanup(this.reliableDC);
489
- dcCleanup(this.reliableDCSub);
490
- dcCleanup(this.dataTrackDC);
491
- dcCleanup(this.dataTrackDCSub);
441
+ this.dataChannels.teardown();
492
442
 
493
443
  await this.pcManager?.close();
494
444
  this.pcManager = undefined;
495
445
 
496
- this.lossyDC = undefined;
497
- this.lossyDCSub = undefined;
498
- this.reliableDC = undefined;
499
- this.reliableDCSub = undefined;
500
- this.dataTrackDC = undefined;
501
- this.dataTrackDCSub = undefined;
502
- this.reliableMessageBuffer = new DataPacketBuffer();
503
- this.reliableDataSequence = 1;
504
446
  this.reliableReceivedState.clear();
505
447
  }
506
448
 
507
449
  cleanupLossyDataStats() {
508
- this.lossyDataStatByterate = 0;
509
- this.lossyDataStatCurrentBytes = 0;
510
- if (this.lossyDataStatInterval) {
511
- clearInterval(this.lossyDataStatInterval);
512
- this.lossyDataStatInterval = undefined;
513
- }
514
- this.lossyDataDropCount = 0;
450
+ this.lossyChannel.stopThresholdTuning();
515
451
  }
516
452
 
517
453
  async cleanupClient() {
@@ -580,7 +516,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
580
516
  }
581
517
 
582
518
  get dataSubscriberReadyState(): string | undefined {
583
- return this.reliableDCSub?.readyState;
519
+ return this.dataChannelForKind(DataChannelKind.RELIABLE, true)?.readyState;
584
520
  }
585
521
 
586
522
  async getConnectedServerAddress(): Promise<string | undefined> {
@@ -890,121 +826,39 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
890
826
  return;
891
827
  }
892
828
 
893
- // Waiters parked on the old channel objects would never see another event from them once they
894
- // are replaced below — reject them so the headroom lock is released and queued senders
895
- // re-check against the new channels.
896
- this.invalidateDataChannelWaiters('data channels recreated');
897
-
898
- // clear old data channel callbacks if recreate
899
- if (this.lossyDC) {
900
- this.lossyDC.onmessage = null;
901
- this.lossyDC.onerror = null;
902
- this.lossyDC.onclose = null;
903
- }
904
- if (this.reliableDC) {
905
- this.reliableDC.onmessage = null;
906
- this.reliableDC.onerror = null;
907
- this.reliableDC.onclose = null;
908
- }
909
- if (this.dataTrackDC) {
910
- this.dataTrackDC.onmessage = null;
911
- this.dataTrackDC.onerror = null;
912
- this.dataTrackDC.onclose = null;
913
- }
914
-
915
- // create data channels
916
- this.lossyDC = this.pcManager.createPublisherDataChannel(lossyDataChannel, {
917
- ordered: false,
918
- maxRetransmits: 0,
919
- });
920
- this.reliableDC = this.pcManager.createPublisherDataChannel(reliableDataChannel, {
921
- ordered: true,
922
- });
923
- this.dataTrackDC = this.pcManager.createPublisherDataChannel(dataTrackDataChannel, {
924
- ordered: false,
925
- maxRetransmits: 0,
926
- });
927
-
928
- // also handle messages over the pub channel, for backwards compatibility
929
- this.lossyDC.onmessage = this.handleDataMessage;
930
- this.reliableDC.onmessage = this.handleDataMessage;
931
- this.dataTrackDC.onmessage = this.handleDataTrackMessage;
932
-
933
- // handle datachannel errors
934
- this.lossyDC.onerror = this.handleDataError;
935
- this.reliableDC.onerror = this.handleDataError;
936
- this.dataTrackDC.onerror = this.handleDataError;
937
-
938
- // detect unexpected publisher data channel closes
939
- this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
940
- this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
941
- this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
942
-
943
- // set up dc buffer threshold - if this is not set, it will default to 0
944
- this.lossyDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
945
- this.reliableDC.bufferedAmountLowThreshold = reliableDataChannelWaterMarkLow;
946
- this.dataTrackDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
947
-
948
- // handle buffer amount low events
949
- this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
950
- this.reliableDC.onbufferedamountlow = () =>
951
- this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
952
- this.dataTrackDC.onbufferedamountlow = () =>
953
- this.handleBufferedAmountLow(DataChannelKind.DATA_TRACK_LOSSY);
954
-
955
- this.cleanupLossyDataStats();
956
- this.lossyDataStatInterval = setInterval(() => {
957
- this.lossyDataStatByterate = this.lossyDataStatCurrentBytes;
958
- this.lossyDataStatCurrentBytes = 0;
959
-
960
- const dc = this.dataChannelForKind(DataChannelKind.LOSSY);
961
- if (dc) {
962
- // control buffered latency to ~100ms
963
- const threshold = this.lossyDataStatByterate / 10;
964
- dc.bufferedAmountLowThreshold = Math.min(
965
- Math.max(threshold, lossyDataChannelWaterMarkLow),
966
- lossyDataChannelWaterMarkHigh,
967
- );
968
- }
969
- }, 1000);
829
+ this.dataChannels.createPublisherChannels(this.pcManager);
970
830
  }
971
831
 
972
832
  private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
973
833
  if (!channel) {
974
834
  return;
975
835
  }
976
- let handler;
977
- if (channel.label === reliableDataChannel) {
978
- this.reliableDCSub = channel;
979
- handler = this.handleDataMessage;
980
- } else if (channel.label === lossyDataChannel) {
981
- this.lossyDCSub = channel;
982
- handler = this.handleDataMessage;
983
- } else if (channel.label === dataTrackDataChannel) {
984
- this.dataTrackDCSub = channel;
985
- handler = this.handleDataTrackMessage;
986
- } else {
987
- return;
836
+ if (this.dataChannels.adoptSubscriberChannel(channel)) {
837
+ this.log.debug(`on data channel ${channel.id}, ${channel.label}`);
988
838
  }
989
- this.log.debug(`on data channel ${channel.id}, ${channel.label}`);
990
- channel.onmessage = handler;
991
839
  };
992
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
+
993
853
  private handleDataMessage = async (message: MessageEvent) => {
994
854
  // make sure to respect incoming data message order by processing message events one after the other
995
855
  const unlock = await this.dataProcessLock.lock();
996
856
  try {
997
- // decode
998
- let buffer: ArrayBuffer | undefined;
999
- if (message.data instanceof ArrayBuffer) {
1000
- buffer = message.data;
1001
- } else if (message.data instanceof Blob) {
1002
- buffer = await message.data.arrayBuffer();
1003
- } else {
1004
- this.log.error('unsupported data type', { data: message.data });
857
+ const bytes = await this.decodeDataMessage(message);
858
+ if (!bytes) {
1005
859
  return;
1006
860
  }
1007
- const dp = DataPacket.fromBinary(new Uint8Array(buffer));
861
+ const dp = DataPacket.fromBinary(bytes);
1008
862
 
1009
863
  if (dp.sequence > 0 && dp.participantSid !== '') {
1010
864
  const lastSeq = this.reliableReceivedState.get(dp.participantSid);
@@ -1053,18 +907,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1053
907
  };
1054
908
 
1055
909
  private handleDataTrackMessage = async (message: MessageEvent) => {
1056
- // Decode / normalize into a common format
1057
- let buffer: ArrayBuffer | undefined;
1058
- if (message.data instanceof ArrayBuffer) {
1059
- buffer = message.data;
1060
- } else if (message.data instanceof Blob) {
1061
- buffer = await message.data.arrayBuffer();
1062
- } else {
1063
- this.log.error('unsupported data type', { data: message.data });
910
+ const bytes = await this.decodeDataMessage(message);
911
+ if (!bytes) {
1064
912
  return;
1065
913
  }
1066
-
1067
- this.emit('dataTrackPacketReceived', new Uint8Array(buffer));
914
+ this.emit('dataTrackPacketReceived', bytes);
1068
915
  };
1069
916
 
1070
917
  private handleDataError = (event: Event) => {
@@ -1103,10 +950,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1103
950
  }
1104
951
  };
1105
952
 
1106
- private handleBufferedAmountLow = (channelKind: DataChannelKind) => {
1107
- this.updateAndEmitDCBufferStatus(channelKind);
1108
- };
1109
-
1110
953
  async createSender(
1111
954
  track: LocalTrack,
1112
955
  opts: TrackPublishOptions,
@@ -1510,7 +1353,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1510
1353
 
1511
1354
  // recreate publish datachannel if it's id is null
1512
1355
  // (for safari https://bugs.webkit.org/show_bug.cgi?id=184688)
1513
- if (this.reliableDC?.readyState === 'open' && this.reliableDC.id === null) {
1356
+ const reliableDC = this.dataChannelForKind(DataChannelKind.RELIABLE);
1357
+ if (reliableDC?.readyState === 'open' && reliableDC.id === null) {
1514
1358
  this.createDataChannels();
1515
1359
  }
1516
1360
 
@@ -1586,7 +1430,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1586
1430
  }
1587
1431
 
1588
1432
  /* @internal */
1589
- 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
+ ) {
1590
1438
  // make sure we do have a data connection
1591
1439
  await this.ensurePublisherConnected(kind);
1592
1440
 
@@ -1608,8 +1456,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1608
1456
  }
1609
1457
 
1610
1458
  if (kind === DataChannelKind.RELIABLE) {
1611
- packet.sequence = this.reliableDataSequence;
1612
- this.reliableDataSequence += 1;
1459
+ packet.sequence = this.reliableChannel.nextSequence();
1613
1460
  }
1614
1461
 
1615
1462
  const msg = packet.toBinary() as Uint8Array<ArrayBuffer>;
@@ -1631,314 +1478,46 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1631
1478
  );
1632
1479
  }
1633
1480
 
1634
- switch (kind) {
1635
- case DataChannelKind.LOSSY:
1636
- case DataChannelKind.DATA_TRACK_LOSSY:
1637
- return this.sendLossyBytes(msg, kind);
1638
-
1639
- case DataChannelKind.RELIABLE: {
1640
- if (this.attemptingReconnect) {
1641
- // A reconnect is already underway — queue for the resume replay instead of parking on a
1642
- // channel that is being torn down. The send resolves; delivery is deferred to the replay.
1643
- this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
1644
- return;
1645
- }
1646
-
1647
- const dc = this.dataChannelForKind(kind);
1648
- if (dc) {
1649
- try {
1650
- await this.waitForBufferHeadroomWithLock(kind);
1651
- } catch (error) {
1652
- if (this.isClosed) {
1653
- // No replay is coming after an engine close — surface the failure.
1654
- throw error;
1655
- }
1656
- // Transient teardown (the channel closed or was replaced while we waited): the
1657
- // reliable channel promises delivery across resume, so queue the packet for the
1658
- // replay instead of rejecting a send the app can't meaningfully retry.
1659
- this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
1660
- return;
1661
- }
1662
-
1663
- if (this.attemptingReconnect) {
1664
- // A reconnect began while we waited for headroom — same deal as above.
1665
- this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
1666
- return;
1667
- }
1668
-
1669
- this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: true });
1670
- dc.send(msg);
1671
- }
1672
-
1673
- this.updateAndEmitDCBufferStatus(kind);
1674
- break;
1675
- }
1676
- }
1677
- }
1678
-
1679
- /* @internal */
1680
- async sendLossyBytes(
1681
- bytes: NonSharedUint8Array,
1682
- kind: Exclude<DataChannelKind, DataChannelKind.RELIABLE>,
1683
- bufferStatusFullBehavior: 'drop' | 'wait' = 'drop',
1684
- ) {
1685
- // Make sure we do have a data connection. This matters most for the direct data-track path
1686
- // (Room's packetAvailable handler), which doesn't go through sendDataPacket: it both waits for
1687
- // the channel to be open and — on lazily negotiated publisher connections — is what kicks the
1688
- // negotiation off in the first place. The call is memoized, so the steady-state cost is one
1689
- // await on an already-resolved promise.
1690
- await this.ensurePublisherConnected(kind);
1691
-
1692
- const dc = this.dataChannelForKind(kind);
1693
- try {
1694
- if (dc) {
1695
- // Depending on the exact circumstance that data is being sent, either drop or wait for the
1696
- // buffer to drain below the high-water mark before continuing.
1697
- switch (bufferStatusFullBehavior) {
1698
- case 'wait':
1699
- if (!this.isBelowHighWaterMark(kind)) {
1700
- await this.waitForBufferHeadroomWithLock(kind);
1701
- }
1702
- break;
1703
- case 'drop':
1704
- // we check against the actual threshold on the DC here, as it is dynamic for the lossy DC
1705
- if (!this.isBelowLowWaterMark(kind)) {
1706
- // Drop messages to reduce latency
1707
- this.lossyDataDropCount += 1;
1708
- if (this.lossyDataDropCount % 100 === 0) {
1709
- this.log.warn(
1710
- `dropping lossy data channel messages, total dropped: ${this.lossyDataDropCount}`,
1711
- );
1712
- }
1713
- return;
1714
- }
1715
- }
1716
- if (kind === DataChannelKind.LOSSY) {
1717
- // The byterate stat tunes the LOSSY channel's dynamic drop threshold; counting data-track
1718
- // bytes here would inflate it and let the lossy channel buffer far more latency than the
1719
- // ~100ms the tuning targets.
1720
- this.lossyDataStatCurrentBytes += bytes.byteLength;
1721
- }
1722
-
1723
- if (this.attemptingReconnect) {
1724
- return;
1725
- }
1726
-
1727
- dc.send(bytes);
1728
- }
1729
-
1730
- this.updateAndEmitDCBufferStatus(kind);
1731
- } catch (error: unknown) {
1732
- // ensure same surface behaviour as before with missing data channel being silently ignored, just log an error message for clarity
1733
- if (error instanceof TypeError) {
1734
- this.log.error(error);
1735
- } else {
1736
- throw error;
1737
- }
1738
- }
1739
- }
1740
-
1741
- private async resendReliableMessagesForResume(lastMessageSeq: number) {
1742
- await this.ensurePublisherConnected(DataChannelKind.RELIABLE);
1743
- const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
1744
- if (dc) {
1745
- this.reliableMessageBuffer.popToSequence(lastMessageSeq);
1746
- // Hold the headroom lock across the whole replay. Releasing it between messages would let a
1747
- // concurrent send — whose (newer) sequence was already assigned in sendDataPacket before it
1748
- // queued on the lock — hit the wire mid-replay, and receivers would then discard the
1749
- // remaining lower-sequence resent messages as duplicates.
1750
- const unlock = await this.getBufferHeadroomLock(DataChannelKind.RELIABLE).lock();
1751
- try {
1752
- // Everything left after the ack cutoff must be re-handed to the current channel.
1753
- this.reliableMessageBuffer.markAllUnsent();
1754
- // Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
1755
- // sent:false) during our own awaits appends after this pass started, so we pick it up on
1756
- // the next one. We mark each packet only once we've actually handed it to the channel — a
1757
- // blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
1758
- // and a later alignBufferedAmount would then drop it for good. If the loop throws
1759
- // mid-drain, unsent entries keep their flag and the next replay picks them up.
1760
- for (
1761
- let batch = this.reliableMessageBuffer.getUnsent();
1762
- batch.length > 0;
1763
- batch = this.reliableMessageBuffer.getUnsent()
1764
- ) {
1765
- for (const item of batch) {
1766
- // Respect flow control on resume too, so a large resend doesn't overflow the buffer.
1767
- // We already hold the lock across the whole replay, so use the lock-free wait.
1768
- await this.waitForBufferHeadroomWithoutLock(DataChannelKind.RELIABLE);
1769
- dc.send(item.data);
1770
- this.reliableMessageBuffer.markSent(item);
1771
- }
1772
- }
1773
- } finally {
1774
- unlock();
1775
- }
1776
- }
1777
- this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
1778
- }
1779
-
1780
- private updateAndEmitDCBufferStatus = (kind: DataChannelKind) => {
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.
1781
1483
  if (kind === DataChannelKind.RELIABLE) {
1782
- const dc = this.dataChannelForKind(kind);
1783
- if (dc) {
1784
- this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
1785
- }
1786
- }
1787
- try {
1788
- const status = this.isBelowLowWaterMark(kind);
1789
- if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
1790
- this.dcBufferStatus.set(kind, status);
1791
- this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
1792
- }
1793
- } catch (e) {
1794
- this.log.warn('could not update buffer status', { error: e });
1795
- }
1796
- };
1797
-
1798
- /**
1799
- * Whether the send buffer has room to accept more data (the send gate). Senders proceed while
1800
- * this is true and block once it goes false.
1801
- */
1802
- private isBelowHighWaterMark = (kind: DataChannelKind): Throws<boolean, TypeError> => {
1803
- const dc = this.dataChannelForKind(kind);
1804
- if (!dc) {
1805
- throw new TypeError(`Could not get data channel for kind ${kind}`);
1806
- }
1807
- // because RTCDatachannel has no high water mark built in we read the statically defined versions as constants
1808
- const highMark =
1809
- kind === DataChannelKind.RELIABLE
1810
- ? reliableDataChannelWaterMarkHigh
1811
- : lossyDataChannelWaterMarkHigh;
1812
- return dc.bufferedAmount <= highMark;
1813
- };
1814
-
1815
- /**
1816
- * Whether the send buffer has drained to its low-water mark. Drives the public
1817
- * {@link EngineEvent.DCBufferStatusChanged} event.
1818
- */
1819
- private isBelowLowWaterMark = (kind: DataChannelKind): Throws<boolean, TypeError> => {
1820
- const dc = this.dataChannelForKind(kind);
1821
- if (!dc) {
1822
- throw new TypeError(`Could not get data channel for kind ${kind}`);
1484
+ await this.reliableChannel.send(msg, packet.sequence);
1485
+ } else {
1486
+ await this.lossyChannel.send(msg);
1823
1487
  }
1824
- // because RTCDatachannel has the threshold built in we can read it dynamically to account for changing thresholds over time
1825
- return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
1826
- };
1827
-
1828
- /** Per-kind lock serializing senders that have to wait for the buffer to drain. */
1829
- private waitForBufferHeadroomLocks = new Map<DataChannelKind, Mutex>();
1488
+ }
1830
1489
 
1831
1490
  /**
1832
- * Per-kind AbortController that cancels parked headroom waiters whenever a kind's channel object
1833
- * stops being current (replaced by createDataChannels or torn down by cleanupPeerConnections). A
1834
- * waiter is parked on the channel object it captured at wait entry; if that object is abandoned
1835
- * its events may never fire again, so the abort is what rejects the waiter and releases the
1836
- * headroom lock instead of stranding every future sender behind it.
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
1837
1496
  */
1838
- private waiterAbortControllers = new Map<DataChannelKind, AbortController>();
1839
-
1840
- private waiterAbortSignal(kind: DataChannelKind): AbortSignal {
1841
- let controller = this.waiterAbortControllers.get(kind);
1842
- if (!controller) {
1843
- controller = new AbortController();
1844
- this.waiterAbortControllers.set(kind, controller);
1845
- }
1846
- return controller.signal;
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);
1847
1503
  }
1848
1504
 
1849
- /** Rejects all parked headroom waiters (all kinds); the next waiter gets a fresh controller. */
1850
- private invalidateDataChannelWaiters(reason: string) {
1851
- for (const controller of this.waiterAbortControllers.values()) {
1852
- controller.abort(reason);
1853
- }
1854
- this.waiterAbortControllers.clear();
1505
+ private async resendReliableMessagesForResume(lastMessageSeq: number) {
1506
+ await this.ensurePublisherConnected(DataChannelKind.RELIABLE);
1507
+ await this.reliableChannel.replay(lastMessageSeq);
1855
1508
  }
1856
1509
 
1857
- /** Acquires the per-kind headroom lock, resolving with the unlock function. */
1858
- private getBufferHeadroomLock(kind: DataChannelKind): Mutex {
1859
- let lock = this.waitForBufferHeadroomLocks.get(kind);
1860
- if (!lock) {
1861
- lock = new Mutex();
1862
- this.waitForBufferHeadroomLocks.set(kind, lock);
1863
- }
1864
- return lock;
1510
+ /** The flow-control gate for `kind` see {@link FlowControlledDataChannel}. */
1511
+ private flowControlFor(kind: DataChannelKind): FlowControlledDataChannel {
1512
+ return this.dataChannels.channelFor(kind);
1865
1513
  }
1866
1514
 
1867
1515
  /**
1868
- * Acquires the `kind` headroom lock, waits until the send buffer has room, then releases. The
1869
- * common single-send entry point. Callers that need to hold the lock across several sends (the
1870
- * resume replay) acquire it via {@link getBufferHeadroomLock} and call
1871
- * {@link waitForBufferHeadroomWithoutLock} per message instead.
1516
+ * Resolves once the caller may send on the `kind` channel see
1517
+ * {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
1872
1518
  */
1873
- async waitForBufferHeadroomWithLock(kind: DataChannelKind) {
1874
- const unlock = await this.getBufferHeadroomLock(kind).lock();
1875
- try {
1876
- await this.waitForBufferHeadroomWithoutLock(kind);
1877
- } finally {
1878
- unlock();
1879
- }
1880
- }
1881
-
1882
- /**
1883
- * Waits until the send buffer for `kind` is at or below the high-water mark (draining, if
1884
- * needed, via the `bufferedamountlow` event). Does no locking of its own — the caller must
1885
- * already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
1886
- * holds the lock across its whole batch and calls this per message so no other sender can
1887
- * interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
1888
- *
1889
- * Serializing through the per-kind lock means that, once the buffer drains, queued callers
1890
- * refill it one at a time (up to the high-water mark) rather than all at once and overflowing
1891
- * the SCTP send buffer (see livekit/client-sdk-js#1995).
1892
- */
1893
- private async waitForBufferHeadroomWithoutLock(
1894
- kind: DataChannelKind,
1895
- ): Promise<Throws<void, UnexpectedConnectionState | TypeError>> {
1896
- if (this.isClosed) {
1897
- throw new UnexpectedConnectionState('engine closed');
1898
- }
1899
- if (this.isBelowHighWaterMark(kind)) {
1900
- return;
1901
- }
1902
- const dc = this.dataChannelForKind(kind);
1903
- if (!dc) {
1904
- throw new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`);
1905
- }
1906
- const abortSignal = this.waiterAbortSignal(kind);
1907
- await new TypedPromise<void, UnexpectedConnectionState>((resolve, reject) => {
1908
- const onBufferedAmountLow = () => {
1909
- cleanup();
1910
- resolve();
1911
- };
1912
- const onDCClose = () => {
1913
- cleanup();
1914
- reject(
1915
- new UnexpectedConnectionState(`DataChannel ${kind} closed while draining the buffer`),
1916
- );
1917
- };
1918
- const onAbort = () => {
1919
- cleanup();
1920
- reject(
1921
- new UnexpectedConnectionState(
1922
- `DataChannel ${kind} was replaced or torn down while waiting for headroom`,
1923
- ),
1924
- );
1925
- };
1926
- const cleanup = () => {
1927
- dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
1928
- dc.removeEventListener('close', onDCClose);
1929
- abortSignal.removeEventListener('abort', onAbort);
1930
- };
1931
- if (abortSignal.aborted) {
1932
- onAbort();
1933
- return;
1934
- }
1935
- dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
1936
- // Proxy along any error caused by the data channel closing while we wait.
1937
- dc.addEventListener('close', onDCClose);
1938
- // The channel object we're parked on can be abandoned without ever firing another event
1939
- // (e.g. createDataChannels replacing it); the abort is our way out.
1940
- abortSignal.addEventListener('abort', onAbort);
1941
- });
1519
+ async waitForBufferHeadroom(kind: DataChannelKind) {
1520
+ return this.flowControlFor(kind).waitForHeadroomWithLock();
1942
1521
  }
1943
1522
 
1944
1523
  /**
@@ -2040,9 +1619,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
2040
1619
  // don't negotiate without any transceivers or data channel, it will generate sdp without ice frag then negotiate failed
2041
1620
  if (
2042
1621
  this.pcManager.publisher.getTransceivers().length == 0 &&
2043
- !this.lossyDC &&
2044
- !this.reliableDC &&
2045
- !this.dataTrackDC
1622
+ !this.dataChannels.hasPublisherChannels
2046
1623
  ) {
2047
1624
  this.createDataChannels();
2048
1625
  }
@@ -2091,26 +1668,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
2091
1668
  }
2092
1669
 
2093
1670
  dataChannelForKind(kind: DataChannelKind, sub?: boolean): RTCDataChannel | undefined {
2094
- switch (kind) {
2095
- case DataChannelKind.RELIABLE:
2096
- if (!sub) {
2097
- return this.reliableDC;
2098
- } else {
2099
- return this.reliableDCSub;
2100
- }
2101
- case DataChannelKind.LOSSY:
2102
- if (!sub) {
2103
- return this.lossyDC;
2104
- } else {
2105
- return this.lossyDCSub;
2106
- }
2107
- case DataChannelKind.DATA_TRACK_LOSSY:
2108
- if (!sub) {
2109
- return this.dataTrackDC;
2110
- } else {
2111
- return this.dataTrackDCSub;
2112
- }
2113
- }
1671
+ return this.dataChannels.getHandle(kind, sub);
2114
1672
  }
2115
1673
 
2116
1674
  /** @internal */