livekit-client 2.22.1 → 2.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +177 -39
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +85 -60
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +7 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/constants.d.ts +5 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +9 -2
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +42 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +12 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +7 -0
- package/dist/ts4.2/e2ee/constants.d.ts +5 -0
- package/dist/ts4.2/e2ee/types.d.ts +9 -2
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +42 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/package.json +1 -1
- package/src/api/SignalClient.ts +2 -1
- package/src/e2ee/E2eeManager.ts +38 -14
- package/src/e2ee/constants.ts +6 -0
- package/src/e2ee/subscriberBlackScreen.test.ts +544 -0
- package/src/e2ee/types.ts +9 -2
- package/src/e2ee/worker/FrameCryptor.race.test.ts +9 -26
- package/src/e2ee/worker/FrameCryptor.test.ts +0 -1
- package/src/e2ee/worker/FrameCryptor.ts +185 -52
- package/src/e2ee/worker/e2ee.worker.ts +38 -6
- package/src/room/PCTransportManager.test.ts +35 -0
- package/src/room/PCTransportManager.ts +12 -4
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +171 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +17 -18
- package/src/room/data-stream/incoming/StreamReader.ts +20 -50
|
@@ -12208,7 +12208,7 @@ function getMatch(exp, ua) {
|
|
|
12208
12208
|
}
|
|
12209
12209
|
function getOSVersion(ua) {
|
|
12210
12210
|
return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
|
|
12211
|
-
}var version$1 = "2.22.
|
|
12211
|
+
}var version$1 = "2.22.2";const version = version$1;
|
|
12212
12212
|
const protocolVersion = 17;
|
|
12213
12213
|
/** Initial client protocol. */
|
|
12214
12214
|
const CLIENT_PROTOCOL_DEFAULT = 0;
|
|
@@ -14829,6 +14829,11 @@ function getAbortReasonAsString(signal) {
|
|
|
14829
14829
|
const DECRYPTION_FAILURE_TOLERANCE = 10;
|
|
14830
14830
|
// flag set to indicate that e2ee has been setup for sender/receiver;
|
|
14831
14831
|
const E2EE_FLAG = 'lk_e2ee';
|
|
14832
|
+
/**
|
|
14833
|
+
* trackId a receiver's/sender's encoded stream pipeline was last set up for.
|
|
14834
|
+
* Needed to recover the right cryptor when a transceiver gets reused.
|
|
14835
|
+
*/
|
|
14836
|
+
const E2EE_TRACK_ID = 'lk_e2ee_track_id';
|
|
14832
14837
|
const SALT = 'LKFrameEncryptionKey';
|
|
14833
14838
|
const KEY_PROVIDER_DEFAULTS = {
|
|
14834
14839
|
sharedKey: false,
|
|
@@ -15925,12 +15930,12 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
15925
15930
|
});
|
|
15926
15931
|
}
|
|
15927
15932
|
setupEventListeners(room, keyProvider) {
|
|
15928
|
-
room.on(RoomEvent.TrackPublished, (pub, participant) => this.
|
|
15933
|
+
room.on(RoomEvent.TrackPublished, (pub, participant) => this.setParticipantCryptorEnabledForPublication(pub, participant.identity));
|
|
15929
15934
|
room.on(RoomEvent.ConnectionStateChanged, state => {
|
|
15930
15935
|
if (state === ConnectionState.Connected) {
|
|
15931
15936
|
room.remoteParticipants.forEach(participant => {
|
|
15932
15937
|
participant.trackPublications.forEach(pub => {
|
|
15933
|
-
this.
|
|
15938
|
+
this.setParticipantCryptorEnabledForPublication(pub, participant.identity);
|
|
15934
15939
|
});
|
|
15935
15940
|
});
|
|
15936
15941
|
}
|
|
@@ -16100,6 +16105,22 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
16100
16105
|
};
|
|
16101
16106
|
this.worker.postMessage(msg);
|
|
16102
16107
|
}
|
|
16108
|
+
/**
|
|
16109
|
+
* Derive the participant's encryption state from one of its publications.
|
|
16110
|
+
* Publications without trackInfo are skipped rather than thrown on: throwing
|
|
16111
|
+
* inside the forEach above would leave every remaining participant without an
|
|
16112
|
+
* encryption state, and a cryptor with unknown state can no longer decrypt.
|
|
16113
|
+
*/
|
|
16114
|
+
setParticipantCryptorEnabledForPublication(pub, participantIdentity) {
|
|
16115
|
+
if (!pub.trackInfo) {
|
|
16116
|
+
livekitLogger.warn('skipping e2ee enabled update for publication without trackInfo', {
|
|
16117
|
+
participant: participantIdentity,
|
|
16118
|
+
trackSid: pub.trackSid
|
|
16119
|
+
});
|
|
16120
|
+
return;
|
|
16121
|
+
}
|
|
16122
|
+
this.setParticipantCryptorEnabled(pub.trackInfo.encryption !== Encryption_Type.NONE, participantIdentity);
|
|
16123
|
+
}
|
|
16103
16124
|
setupE2EEReceiver(track, remoteId, trackInfo) {
|
|
16104
16125
|
if (!track.receiver) {
|
|
16105
16126
|
return;
|
|
@@ -16136,21 +16157,27 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
16136
16157
|
codec,
|
|
16137
16158
|
hasPacketTrailer
|
|
16138
16159
|
};
|
|
16139
|
-
// @ts-ignore
|
|
16140
16160
|
receiver.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
16141
16161
|
} else {
|
|
16142
|
-
if (E2EE_FLAG in receiver &&
|
|
16143
|
-
//
|
|
16162
|
+
if (E2EE_FLAG in receiver && E2EE_TRACK_ID in receiver) {
|
|
16163
|
+
// The transceiver is being reused for a new track. We cannot set up a new
|
|
16164
|
+
// pipeline from here: this receiver's encoded streams were transferred to
|
|
16165
|
+
// the worker on first setup and a transferred stream can't be transferred
|
|
16166
|
+
// again, while createEncodedStreams() may only be called once per receiver.
|
|
16167
|
+
// So hand the worker the new track/participant and let it re-point (and if
|
|
16168
|
+
// necessary rebuild) the cryptor that still owns those streams.
|
|
16144
16169
|
const msg = {
|
|
16145
16170
|
kind: 'updateCodec',
|
|
16146
16171
|
data: {
|
|
16147
16172
|
trackId,
|
|
16173
|
+
previousTrackId: receiver[E2EE_TRACK_ID],
|
|
16148
16174
|
codec,
|
|
16149
16175
|
participantIdentity,
|
|
16150
16176
|
hasPacketTrailer
|
|
16151
16177
|
}
|
|
16152
16178
|
};
|
|
16153
16179
|
this.worker.postMessage(msg);
|
|
16180
|
+
receiver[E2EE_TRACK_ID] = trackId;
|
|
16154
16181
|
return;
|
|
16155
16182
|
}
|
|
16156
16183
|
// @ts-ignore
|
|
@@ -16175,7 +16202,6 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
16175
16202
|
trackId: trackId,
|
|
16176
16203
|
codec,
|
|
16177
16204
|
participantIdentity: participantIdentity,
|
|
16178
|
-
isReuse: E2EE_FLAG in receiver,
|
|
16179
16205
|
hasPacketTrailer
|
|
16180
16206
|
}
|
|
16181
16207
|
};
|
|
@@ -16183,6 +16209,8 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
16183
16209
|
}
|
|
16184
16210
|
// @ts-ignore
|
|
16185
16211
|
receiver[E2EE_FLAG] = true;
|
|
16212
|
+
// @ts-ignore
|
|
16213
|
+
receiver[E2EE_TRACK_ID] = trackId;
|
|
16186
16214
|
});
|
|
16187
16215
|
}
|
|
16188
16216
|
/**
|
|
@@ -16222,7 +16250,6 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
16222
16250
|
codec,
|
|
16223
16251
|
trackId,
|
|
16224
16252
|
participantIdentity: this.room.localParticipant.identity,
|
|
16225
|
-
isReuse: false,
|
|
16226
16253
|
hasPacketTrailer: hasFrameMetadataPublishOptions(frameMetadata),
|
|
16227
16254
|
packetTrailer: frameMetadata
|
|
16228
16255
|
}
|
|
@@ -18500,6 +18527,7 @@ class SignalClient {
|
|
|
18500
18527
|
if (firstMessage) {
|
|
18501
18528
|
this.handleSignalResponse(firstMessage);
|
|
18502
18529
|
}
|
|
18530
|
+
const attemptId = this.attemptId;
|
|
18503
18531
|
while (true) {
|
|
18504
18532
|
if (this.signalLatency) {
|
|
18505
18533
|
yield sleep(this.signalLatency);
|
|
@@ -18517,7 +18545,7 @@ class SignalClient {
|
|
|
18517
18545
|
this.log.error("error reading from signal stream", {
|
|
18518
18546
|
error: e
|
|
18519
18547
|
});
|
|
18520
|
-
yield this.
|
|
18548
|
+
yield this.handleOnClose('error in reading loop', attemptId);
|
|
18521
18549
|
break;
|
|
18522
18550
|
}
|
|
18523
18551
|
}
|
|
@@ -21074,13 +21102,21 @@ class PCTransportManager {
|
|
|
21074
21102
|
this.updateState();
|
|
21075
21103
|
});
|
|
21076
21104
|
}
|
|
21105
|
+
/**
|
|
21106
|
+
* Restarts ICE on the transports that need it. Only the publisher: the server restarts the
|
|
21107
|
+
* subscriber's ICE itself and follows with a fresh offer.
|
|
21108
|
+
*
|
|
21109
|
+
* The subscriber deliberately does NOT enter `restartingIce` here. Queueing its remote
|
|
21110
|
+
* candidates would guard against candidates for a new generation arriving before the offer
|
|
21111
|
+
* that introduces it, but the server does not send them in that order -- on a same-node
|
|
21112
|
+
* resume it buffers them until the offer has gone out, and on a reconnect that lands on
|
|
21113
|
+
* another node it withholds subscriber candidates until immediately before creating the
|
|
21114
|
+
* offer. Setting the flag only risks withholding candidates during the window that decides
|
|
21115
|
+
* whether the reconnect succeeded.
|
|
21116
|
+
*/
|
|
21077
21117
|
triggerIceRestart() {
|
|
21078
21118
|
return __awaiter(this, void 0, void 0, function* () {
|
|
21079
21119
|
this.iceLog.warn('triggering ICE restart');
|
|
21080
|
-
if (this.subscriber) {
|
|
21081
|
-
this.subscriber.restartingIce = true;
|
|
21082
|
-
}
|
|
21083
|
-
// only restart publisher if it's needed
|
|
21084
21120
|
if (this.needsPublisher) {
|
|
21085
21121
|
yield this.createAndSendPublisherOffer({
|
|
21086
21122
|
iceRestart: true
|
|
@@ -25911,8 +25947,11 @@ const DEFAULT_MAX_PAYLOAD_BYTE_LENGTH = 5000000000;class BaseStreamReader {
|
|
|
25911
25947
|
this._info = info;
|
|
25912
25948
|
this.bytesReceived = 0;
|
|
25913
25949
|
}
|
|
25914
|
-
|
|
25915
|
-
|
|
25950
|
+
/**
|
|
25951
|
+
* Counts a chunk's bytes against `totalByteSize` and reports progress. Chunk ordering and
|
|
25952
|
+
* de-duplication happen upstream in the manager's `ensureOrderedChunks`, so every chunk reaching
|
|
25953
|
+
* here is new and in order.
|
|
25954
|
+
*/
|
|
25916
25955
|
handleChunkReceived(chunk) {
|
|
25917
25956
|
var _a;
|
|
25918
25957
|
this.bytesReceived += chunk.content.byteLength;
|
|
@@ -25920,6 +25959,8 @@ class ByteStreamReader extends BaseStreamReader {
|
|
|
25920
25959
|
const currentProgress = this.totalByteSize ? this.bytesReceived / this.totalByteSize : undefined;
|
|
25921
25960
|
(_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, currentProgress);
|
|
25922
25961
|
}
|
|
25962
|
+
}
|
|
25963
|
+
class ByteStreamReader extends BaseStreamReader {
|
|
25923
25964
|
[Symbol.asyncIterator]() {
|
|
25924
25965
|
const reader = this.reader.getReader();
|
|
25925
25966
|
// Suppress unhandled rejection on reader.closed — errors are
|
|
@@ -26027,35 +26068,18 @@ class ByteStreamReader extends BaseStreamReader {
|
|
|
26027
26068
|
}
|
|
26028
26069
|
}
|
|
26029
26070
|
/**
|
|
26030
|
-
* A class to read chunks from a ReadableStream and
|
|
26071
|
+
* A class to read chunks from a ReadableStream and decode them as UTF-8 text.
|
|
26072
|
+
*
|
|
26073
|
+
* NOTE: chunk-level `version` (resending a chunk at an already-received `chunkIndex` to supersede
|
|
26074
|
+
* it) is not supported. The reader used to rebuild the whole string from a per-index chunk map and
|
|
26075
|
+
* yield it as `TextStreamChunk.collected`, which made superseding work; 5d4a6346 (#1410, text auto
|
|
26076
|
+
* chunking) changed the iterator to yield each chunk's text as it arrives, and a streaming reader
|
|
26077
|
+
* cannot retract text it has already handed to the consumer. No sender emits a versioned chunk.
|
|
26031
26078
|
*/
|
|
26032
26079
|
class TextStreamReader extends BaseStreamReader {
|
|
26033
|
-
/**
|
|
26034
|
-
* A TextStreamReader instance can be used as an AsyncIterator that returns the entire string
|
|
26035
|
-
* that has been received up to the current point in time.
|
|
26036
|
-
*/
|
|
26037
|
-
constructor(info, stream, totalChunkCount) {
|
|
26038
|
-
super(info, stream, totalChunkCount);
|
|
26039
|
-
this.receivedChunks = new Map();
|
|
26040
|
-
}
|
|
26041
|
-
handleChunkReceived(chunk) {
|
|
26042
|
-
var _a;
|
|
26043
|
-
const index = bigIntToNumber(chunk.chunkIndex);
|
|
26044
|
-
const previousChunkAtIndex = this.receivedChunks.get(index);
|
|
26045
|
-
if (previousChunkAtIndex && previousChunkAtIndex.version > chunk.version) {
|
|
26046
|
-
// we have a newer version already, dropping the old one
|
|
26047
|
-
return;
|
|
26048
|
-
}
|
|
26049
|
-
this.receivedChunks.set(index, chunk);
|
|
26050
|
-
this.bytesReceived += chunk.content.byteLength;
|
|
26051
|
-
this.validateBytesReceived();
|
|
26052
|
-
const currentProgress = this.totalByteSize ? this.bytesReceived / this.totalByteSize : undefined;
|
|
26053
|
-
(_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, currentProgress);
|
|
26054
|
-
}
|
|
26055
26080
|
/**
|
|
26056
26081
|
* Async iterator implementation to allow usage of `for await...of` syntax.
|
|
26057
|
-
* Yields
|
|
26058
|
-
*
|
|
26082
|
+
* Yields each chunk's decoded text as it arrives - a delta, not the string accumulated so far.
|
|
26059
26083
|
*/
|
|
26060
26084
|
[Symbol.asyncIterator]() {
|
|
26061
26085
|
const reader = this.reader.getReader();
|
|
@@ -26326,7 +26350,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
26326
26350
|
});
|
|
26327
26351
|
}
|
|
26328
26352
|
});
|
|
26329
|
-
streamHandlerCallback(new ByteStreamReader(info, compressed ? inflateRawByteChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
|
|
26353
|
+
streamHandlerCallback(new ByteStreamReader(info, compressed ? inflateRawByteChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream.pipeThrough(ensureOrderedChunks(streamHeader.streamId)),
|
|
26330
26354
|
// `totalLength` is the pre-compression size, and the reader counts decompressed bytes,
|
|
26331
26355
|
// so it applies to both paths (mirrors text).
|
|
26332
26356
|
bigIntToNumber(streamHeader.totalLength)), {
|
|
@@ -26398,7 +26422,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
26398
26422
|
});
|
|
26399
26423
|
}
|
|
26400
26424
|
});
|
|
26401
|
-
streamHandlerCallback(new TextStreamReader(info, compressed ? inflateRawChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
|
|
26425
|
+
streamHandlerCallback(new TextStreamReader(info, compressed ? inflateRawChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream.pipeThrough(ensureOrderedChunks(streamHeader.streamId)),
|
|
26402
26426
|
// `totalLength` is the pre-compression size, and the reader sees decompressed bytes, so
|
|
26403
26427
|
// it applies to both paths.
|
|
26404
26428
|
bigIntToNumber(streamHeader.totalLength)), {
|
|
@@ -26414,7 +26438,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
26414
26438
|
if (fileBuffer.info.encryptionType !== encryptionType) {
|
|
26415
26439
|
fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(chunk.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
|
|
26416
26440
|
this.byteStreamControllers.delete(chunk.streamId);
|
|
26417
|
-
} else
|
|
26441
|
+
} else {
|
|
26418
26442
|
fileBuffer.controller.enqueue(chunk);
|
|
26419
26443
|
}
|
|
26420
26444
|
}
|
|
@@ -26423,7 +26447,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
26423
26447
|
if (textBuffer.info.encryptionType !== encryptionType) {
|
|
26424
26448
|
textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(chunk.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
|
|
26425
26449
|
this.textStreamControllers.delete(chunk.streamId);
|
|
26426
|
-
} else
|
|
26450
|
+
} else {
|
|
26427
26451
|
textBuffer.controller.enqueue(chunk);
|
|
26428
26452
|
}
|
|
26429
26453
|
}
|
|
@@ -26471,26 +26495,24 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
26471
26495
|
function createInlineStream(streamId, content) {
|
|
26472
26496
|
return new ReadableStream({
|
|
26473
26497
|
start: controller => __awaiter(this, void 0, void 0, function* () {
|
|
26474
|
-
|
|
26475
|
-
|
|
26476
|
-
|
|
26477
|
-
|
|
26478
|
-
|
|
26479
|
-
|
|
26480
|
-
|
|
26481
|
-
controller.close();
|
|
26482
|
-
} catch (err) {
|
|
26483
|
-
controller.error(err);
|
|
26484
|
-
}
|
|
26498
|
+
const bytes = yield content;
|
|
26499
|
+
controller.enqueue(new DataStream_Chunk({
|
|
26500
|
+
streamId,
|
|
26501
|
+
chunkIndex: BigInt(0),
|
|
26502
|
+
content: bytes
|
|
26503
|
+
}));
|
|
26504
|
+
controller.close();
|
|
26485
26505
|
})
|
|
26486
26506
|
});
|
|
26487
26507
|
}
|
|
26488
26508
|
/**
|
|
26489
26509
|
* Validates that chunks are received in order, dropping duplicates and throwing if gaps are found.
|
|
26490
26510
|
*
|
|
26491
|
-
*
|
|
26492
|
-
* dropped (with a warning - in-order delivery is expected
|
|
26493
|
-
* handling may replay) and a gap is a hard error.
|
|
26511
|
+
* Reassembly (and, for compressed streams, a stateful decompressor) silently corrupts on duplicated
|
|
26512
|
+
* or out-of-order input, so duplicates are dropped (with a warning - in-order delivery is expected
|
|
26513
|
+
* on the reliable channel, but reconnect handling may replay) and a gap is a hard error. Empty
|
|
26514
|
+
* chunks consume their index and are then dropped, so they never reach the reader. Applied to every
|
|
26515
|
+
* chunked stream, compressed or not.
|
|
26494
26516
|
*/
|
|
26495
26517
|
function ensureOrderedChunks(streamId) {
|
|
26496
26518
|
let lastChunkIndex = -1;
|
|
@@ -26498,13 +26520,16 @@ function ensureOrderedChunks(streamId) {
|
|
|
26498
26520
|
transform: (value, controller) => {
|
|
26499
26521
|
const index = bigIntToNumber(value.chunkIndex);
|
|
26500
26522
|
if (index <= lastChunkIndex) {
|
|
26501
|
-
livekitLogger.warn("ignoring duplicate chunk ".concat(index, " for
|
|
26523
|
+
livekitLogger.warn("ignoring duplicate chunk ".concat(index, " ").concat(value.version > 0 ? "(version ".concat(value.version, ")") : '', " for data stream ").concat(streamId, " (last processed: ").concat(lastChunkIndex, ")"));
|
|
26502
26524
|
return;
|
|
26503
26525
|
}
|
|
26504
26526
|
if (index > lastChunkIndex + 1) {
|
|
26505
|
-
throw new DataStreamError("Missing chunk(s) ".concat(lastChunkIndex + 1, "..").concat(index - 1, " for
|
|
26527
|
+
throw new DataStreamError("Missing chunk(s) ".concat(lastChunkIndex + 1, "..").concat(index - 1, " for data stream ").concat(streamId, " - cannot reassemble payload"), DataStreamErrorReason.Incomplete);
|
|
26506
26528
|
}
|
|
26507
26529
|
lastChunkIndex = index;
|
|
26530
|
+
if (value.content.length === 0) {
|
|
26531
|
+
return;
|
|
26532
|
+
}
|
|
26508
26533
|
controller.enqueue(value);
|
|
26509
26534
|
}
|
|
26510
26535
|
});
|