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.
Files changed (42) 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 +177 -39
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +85 -60
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts.map +1 -1
  10. package/dist/src/e2ee/E2eeManager.d.ts +7 -0
  11. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  12. package/dist/src/e2ee/constants.d.ts +5 -0
  13. package/dist/src/e2ee/constants.d.ts.map +1 -1
  14. package/dist/src/e2ee/types.d.ts +9 -2
  15. package/dist/src/e2ee/types.d.ts.map +1 -1
  16. package/dist/src/e2ee/worker/FrameCryptor.d.ts +42 -1
  17. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  18. package/dist/src/room/PCTransportManager.d.ts +12 -0
  19. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  20. package/dist/src/room/data-stream/incoming/StreamReader.d.ts +17 -17
  21. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  22. package/dist/ts4.2/e2ee/E2eeManager.d.ts +7 -0
  23. package/dist/ts4.2/e2ee/constants.d.ts +5 -0
  24. package/dist/ts4.2/e2ee/types.d.ts +9 -2
  25. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +42 -1
  26. package/dist/ts4.2/room/PCTransportManager.d.ts +12 -0
  27. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +17 -17
  28. package/package.json +1 -1
  29. package/src/api/SignalClient.ts +2 -1
  30. package/src/e2ee/E2eeManager.ts +38 -14
  31. package/src/e2ee/constants.ts +6 -0
  32. package/src/e2ee/subscriberBlackScreen.test.ts +544 -0
  33. package/src/e2ee/types.ts +9 -2
  34. package/src/e2ee/worker/FrameCryptor.race.test.ts +9 -26
  35. package/src/e2ee/worker/FrameCryptor.test.ts +0 -1
  36. package/src/e2ee/worker/FrameCryptor.ts +185 -52
  37. package/src/e2ee/worker/e2ee.worker.ts +38 -6
  38. package/src/room/PCTransportManager.test.ts +35 -0
  39. package/src/room/PCTransportManager.ts +12 -4
  40. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +171 -0
  41. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +17 -18
  42. 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.1";const version = version$1;
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.setParticipantCryptorEnabled(pub.trackInfo.encryption !== Encryption_Type.NONE, participant.identity));
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.setParticipantCryptorEnabled(pub.trackInfo.encryption !== Encryption_Type.NONE, participant.identity);
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 && codec) {
16143
- // update track-specific state when the transceiver is reused
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.close(false, 'error in reading loop');
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
- class ByteStreamReader extends BaseStreamReader {
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 provide them in a structured format.
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 structured chunks from the stream.
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 if (chunk.content.length > 0) {
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 if (chunk.content.length > 0) {
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
- try {
26475
- const bytes = yield content;
26476
- controller.enqueue(new DataStream_Chunk({
26477
- streamId,
26478
- chunkIndex: BigInt(0),
26479
- content: bytes
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
- * A stateful decompressor silently corrupts on duplicated or out-of-order input, so duplicates are
26492
- * dropped (with a warning - in-order delivery is expected on the reliable channel, but reconnect
26493
- * handling may replay) and a gap is a hard error. Shared by the text and byte deflate-raw decoders.
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 compressed data stream ").concat(streamId, " (last processed: ").concat(lastChunkIndex, ")"));
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 compressed data stream ").concat(streamId, " - cannot continue decompressing"), DataStreamErrorReason.Incomplete);
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
  });