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
@@ -0,0 +1,153 @@
1
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
2
+ import { DataPacketBuffer } from '../../utils/dataPacketBuffer';
3
+ import {
4
+ FlowControlledDataChannel,
5
+ type FlowControlledDataChannelOptions,
6
+ } from './FlowControlledDataChannel';
7
+
8
+ export interface ReliableDataChannelOptions extends FlowControlledDataChannelOptions {
9
+ /**
10
+ * Whether sends should currently be deferred to the resume replay instead of hitting the wire
11
+ * (i.e. a reconnect attempt is underway). Read at send time so the reliable channel matches the
12
+ * engine's reconnect state without owning it.
13
+ */
14
+ isDeferringSends: () => boolean;
15
+ }
16
+
17
+ /**
18
+ * The reliable channel: flow control plus delivery-across-resume semantics.
19
+ *
20
+ * Every packet gets a monotonic sequence (stamped into the protobuf by the caller before
21
+ * serialization, via {@link nextSequence}) and is retained in a replay buffer until the channel's
22
+ * `bufferedAmount` confirms it has been handed to the transport. Sends that land in a reconnect
23
+ * window — or whose headroom wait is torn down transiently — are queued unsent and resolve;
24
+ * {@link replay} delivers them (plus any unacked packets) after a resume. Only an engine close
25
+ * rejects, because no replay is coming after that.
26
+ */
27
+ export class ReliableDataChannel extends FlowControlledDataChannel {
28
+ private messageBuffer = new DataPacketBuffer();
29
+
30
+ private sequence = 1;
31
+
32
+ private isDeferringSends: () => boolean;
33
+
34
+ constructor(opts: ReliableDataChannelOptions) {
35
+ super(opts);
36
+ this.isDeferringSends = opts.isDeferringSends;
37
+ }
38
+
39
+ /**
40
+ * Claims the next packet sequence. The caller stamps it into the packet before serialization,
41
+ * then passes it back to {@link send} so the replay buffer stays keyed by wire sequence.
42
+ */
43
+ nextSequence(): number {
44
+ const sequence = this.sequence;
45
+ this.sequence += 1;
46
+ return sequence;
47
+ }
48
+
49
+ /**
50
+ * Sends prepared bytes with reliable semantics. Resolves once the packet has either been handed
51
+ * to the channel or queued for the resume replay; throws only when the engine is closed.
52
+ */
53
+ async send(msg: NonSharedUint8Array, sequence: number) {
54
+ if (this.isDeferringSends()) {
55
+ // A reconnect is already underway — queue for the resume replay instead of parking on a
56
+ // channel that is being torn down. The send resolves; delivery is deferred to the replay.
57
+ this.messageBuffer.push({ data: msg, sequence, sent: false });
58
+ return;
59
+ }
60
+
61
+ const dc = this.getChannel();
62
+ if (!dc) {
63
+ return;
64
+ }
65
+
66
+ try {
67
+ await this.waitForHeadroomWithLock();
68
+ } catch (error) {
69
+ if (this.isEngineClosed()) {
70
+ // No replay is coming after an engine close — surface the failure.
71
+ throw error;
72
+ }
73
+ // Transient teardown (the channel closed or was replaced while we waited): the reliable
74
+ // channel promises delivery across resume, so queue the packet for the replay instead of
75
+ // rejecting a send the app can't meaningfully retry.
76
+ this.messageBuffer.push({ data: msg, sequence, sent: false });
77
+ return;
78
+ }
79
+
80
+ if (this.isDeferringSends()) {
81
+ // A reconnect began while we waited for headroom — same deal as above.
82
+ this.messageBuffer.push({ data: msg, sequence, sent: false });
83
+ return;
84
+ }
85
+
86
+ this.messageBuffer.push({ data: msg, sequence, sent: true });
87
+ dc.send(msg);
88
+ this.refreshBufferStatus();
89
+ }
90
+
91
+ /**
92
+ * Replays the buffered backlog after a resume: drops everything the server acked
93
+ * (`lastMessageSeq`), then re-sends the rest in order. The headroom lock is held across the
94
+ * whole replay — releasing it between messages would let a concurrent send (whose newer
95
+ * sequence was already assigned before it queued on the lock) hit the wire mid-replay, and
96
+ * receivers would then discard the remaining lower-sequence resent messages as duplicates.
97
+ */
98
+ async replay(lastMessageSeq: number) {
99
+ const dc = this.getChannel();
100
+ if (!dc) {
101
+ return;
102
+ }
103
+ this.messageBuffer.popToSequence(lastMessageSeq);
104
+ const unlock = await this.lockHeadroom();
105
+ try {
106
+ // Everything left after the ack cutoff must be re-handed to the current channel.
107
+ this.messageBuffer.markAllUnsent();
108
+ // Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
109
+ // sent:false) during our own awaits appends after this pass started, so we pick it up on
110
+ // the next one. Mark each packet only once we've actually handed it to the channel — a
111
+ // blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
112
+ // and a later alignBufferedAmount would then drop it for good. If the loop throws
113
+ // mid-drain, unsent entries keep their flag and the next replay picks them up.
114
+ for (
115
+ let batch = this.messageBuffer.getUnsent();
116
+ batch.length > 0;
117
+ batch = this.messageBuffer.getUnsent()
118
+ ) {
119
+ for (const item of batch) {
120
+ // Respect flow control on resume too, so a large resend doesn't overflow the buffer.
121
+ await this.waitForHeadroomWithoutLock();
122
+ dc.send(item.data);
123
+ this.messageBuffer.markSent(item);
124
+ }
125
+ }
126
+ } finally {
127
+ unlock();
128
+ }
129
+ this.refreshBufferStatus();
130
+ }
131
+
132
+ /**
133
+ * Before recomputing status, trim packets the transport has now delivered — a send or a drain
134
+ * may have acked buffered packets, and the replay buffer is keyed off the channel's buffered
135
+ * bytes.
136
+ */
137
+ override refreshBufferStatus() {
138
+ const dc = this.channelHandle;
139
+ if (dc) {
140
+ this.messageBuffer.alignBufferedAmount(dc.bufferedAmount);
141
+ }
142
+ super.refreshBufferStatus();
143
+ }
144
+
145
+ /**
146
+ * Drops all replay state and restarts sequencing. Only valid on a full reconnect, where the
147
+ * session (and the receivers' sequence tracking) starts over.
148
+ */
149
+ reset() {
150
+ this.messageBuffer = new DataPacketBuffer();
151
+ this.sequence = 1;
152
+ }
153
+ }
@@ -0,0 +1,30 @@
1
+ import { DataPacket_Kind } from '@livekit/protocol';
2
+
3
+ export enum DataChannelKind {
4
+ RELIABLE = DataPacket_Kind.RELIABLE,
5
+ LOSSY = DataPacket_Kind.LOSSY,
6
+ DATA_TRACK_LOSSY = 2,
7
+ }
8
+
9
+ // Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
10
+ // freely up to the high-water mark; once it's exceeded they block until the browser's
11
+ // `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
12
+ // The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
13
+ // isn't starved, while the high-water mark bounds the buffer well below the level that would abort
14
+ // the channel (see livekit/client-sdk-js#1995).
15
+ export const reliableDataChannelWaterMarkLow = 64 * 1024;
16
+ export const reliableDataChannelWaterMarkHigh = 1024 * 1024;
17
+ export const lossyDataChannelWaterMarkLow = 8 * 1024;
18
+ export const lossyDataChannelWaterMarkHigh = 256 * 1024;
19
+
20
+ export function dataChannelLowWaterMark(kind: DataChannelKind): number {
21
+ return kind === DataChannelKind.RELIABLE
22
+ ? reliableDataChannelWaterMarkLow
23
+ : lossyDataChannelWaterMarkLow;
24
+ }
25
+
26
+ export function dataChannelHighWaterMark(kind: DataChannelKind): number {
27
+ return kind === DataChannelKind.RELIABLE
28
+ ? reliableDataChannelWaterMarkHigh
29
+ : lossyDataChannelWaterMarkHigh;
30
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Compression helpers for data streams. The buffered deflate-raw variant ({@link deflateRawCompress})
3
+ * is for the inline (single-packet) case where the payload is small and bounded;
4
+ * {@link deflateRawTransform} / {@link inflateRawTransform} serve the chunked (multi-packet)
5
+ * `sendText`/`sendBytes`/`sendFile` paths, streaming the bytes through without buffering the whole
6
+ * payload.
7
+ *
8
+ * These operate on bytes (not strings) so a single set of helpers serves both text and byte streams;
9
+ * the `TextEncoder`/`TextDecoder` boundary lives at the manager/reader edges.
10
+ *
11
+ * Both streaming variants are exposed as `ReadableWritablePair`s so they drop straight into a
12
+ * `pipeThrough` chain. Each needs one localized cast to bridge a DOM lib-type mismatch: the platform
13
+ * `CompressionStream`/`DecompressionStream` type their `writable` as `WritableStream<BufferSource>`
14
+ * (a wider element type than `Uint8Array`), and `WritableStream<W>` is covariant in `W`, so neither
15
+ * is structurally a `ReadableWritablePair<Uint8Array, Uint8Array>` without help.
16
+ *
17
+ * @internal
18
+ */
19
+ import { type NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
20
+ import { DataStreamError, DataStreamErrorReason } from '../errors';
21
+
22
+ /**
23
+ * A `deflate-raw` compression transform (inverse of {@link inflateRawTransform}): pipe a byte stream
24
+ * through it to get the compressed bytes without buffering the whole payload. Used for the chunked
25
+ * `sendText`/`sendBytes`/`sendFile` paths, where the full payload is known up front but is streamed
26
+ * (e.g. from `file.stream()`) rather than held in memory.
27
+ */
28
+ export function deflateRawTransform(): ReadableWritablePair<
29
+ NonSharedUint8Array,
30
+ NonSharedUint8Array
31
+ > {
32
+ return new CompressionStream('deflate-raw') as unknown as ReadableWritablePair<
33
+ NonSharedUint8Array,
34
+ NonSharedUint8Array
35
+ >;
36
+ }
37
+
38
+ /**
39
+ * A `deflate-raw` decompression transform (inverse of {@link deflateRawTransform}): pipe a
40
+ * stream of compressed bytes through it to get the decompressed bytes. Inflate emits output greedily,
41
+ * so as long as the sender flushed at write boundaries each write's content is produced as soon as
42
+ * its compressed bytes arrive.
43
+ */
44
+ export function inflateRawTransform(): ReadableWritablePair<
45
+ NonSharedUint8Array,
46
+ NonSharedUint8Array
47
+ > {
48
+ return new DecompressionStream('deflate-raw') as unknown as ReadableWritablePair<
49
+ NonSharedUint8Array,
50
+ NonSharedUint8Array
51
+ >;
52
+ }
53
+
54
+ /** deflate-raw compresses a byte array in full. Use for inline payloads; prefer the streaming
55
+ * path for the chunked case. */
56
+ export async function deflateRawCompress(data: NonSharedUint8Array): Promise<NonSharedUint8Array> {
57
+ const cs = new CompressionStream('deflate-raw');
58
+ const writer = cs.writable.getWriter();
59
+ writer.write(data as NonSharedUint8Array);
60
+ writer.close();
61
+ return collect(cs.readable);
62
+ }
63
+
64
+ /**
65
+ * Decompresses a raw-deflate byte array in full (inverse of {@link deflateRawCompress}).
66
+ * `maxByteLength` bounds the decompressed output (decompression-bomb guard); exceeding it rejects
67
+ * with a `PayloadTooLarge` error.
68
+ */
69
+ export async function deflateRawDecompress(
70
+ data: NonSharedUint8Array,
71
+ maxByteLength?: number,
72
+ ): Promise<NonSharedUint8Array> {
73
+ const ds = new DecompressionStream('deflate-raw');
74
+ const writer = ds.writable.getWriter();
75
+ // The writer promises are intentionally not awaited (output is consumed via `collect`), but
76
+ // they reject when the byte cap cancels the readable mid-stream — swallow that so an enforced
77
+ // cap doesn't surface as an unhandled rejection.
78
+ writer.write(data as NonSharedUint8Array).catch(() => {});
79
+ writer.close().catch(() => {});
80
+ return collect(ds.readable, maxByteLength);
81
+ }
82
+
83
+ /**
84
+ * Drains a byte stream, concatenating all of its chunks into a single array. When
85
+ * `maxByteLength` is given, drops the stream and throws `PayloadTooLarge` as soon as the
86
+ * accumulated output exceeds it.
87
+ */
88
+ export async function collect(
89
+ stream: ReadableStream<NonSharedUint8Array>,
90
+ maxByteLength?: number,
91
+ ): Promise<NonSharedUint8Array> {
92
+ const reader = stream.getReader();
93
+ const chunks: Array<NonSharedUint8Array> = [];
94
+ let total = 0;
95
+ while (true) {
96
+ const { done, value } = await reader.read();
97
+ if (done) {
98
+ break;
99
+ }
100
+ chunks.push(value);
101
+ total += value.byteLength;
102
+ if (typeof maxByteLength === 'number' && total > maxByteLength) {
103
+ await reader.cancel();
104
+ throw new DataStreamError(
105
+ `Decompressed payload exceeds the maximum payload size of ${maxByteLength} bytes`,
106
+ DataStreamErrorReason.PayloadTooLarge,
107
+ );
108
+ }
109
+ }
110
+ const result = new Uint8Array(total);
111
+ let offset = 0;
112
+ for (const chunk of chunks) {
113
+ result.set(chunk, offset);
114
+ offset += chunk.byteLength;
115
+ }
116
+ return result;
117
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Maximum size of a single data-stream chunk in bytes, and the budget used to decide whether a
3
+ * payload can be sent inline as a single header packet. Kept below the ~16k data-channel MTU to
4
+ * leave headroom for protocol framing and E2EE overhead.
5
+ *
6
+ * @internal
7
+ */
8
+ export const STREAM_CHUNK_SIZE_BYTES = 15_000;
9
+
10
+ /**
11
+ * Default cap on the number of decompressed bytes a single incoming compressed data stream may
12
+ * produce (5 GB). A tiny compressed payload can inflate to an arbitrarily large output
13
+ * (decompression bomb), so the decompressor's output is bounded rather than trusting the wire
14
+ * size; streams exceeding the cap error with `PayloadTooLarge`.
15
+ *
16
+ * @internal
17
+ */
18
+ export const DEFAULT_MAX_PAYLOAD_BYTE_LENGTH = 5_000_000_000;