livekit-client 2.22.0 → 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 (106) 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 +364 -40
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +1847 -179
  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 +187 -1
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/SignalClient.d.ts +25 -1
  14. package/dist/src/api/SignalClient.d.ts.map +1 -1
  15. package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
  16. package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
  17. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  18. package/dist/src/api/utils.d.ts.map +1 -1
  19. package/dist/src/e2ee/E2eeManager.d.ts +7 -0
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/constants.d.ts +5 -0
  22. package/dist/src/e2ee/constants.d.ts.map +1 -1
  23. package/dist/src/e2ee/types.d.ts +9 -2
  24. package/dist/src/e2ee/types.d.ts.map +1 -1
  25. package/dist/src/e2ee/worker/FrameCryptor.d.ts +42 -1
  26. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  27. package/dist/src/index.d.ts +3 -2
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/logger.d.ts +2 -1
  30. package/dist/src/logger.d.ts.map +1 -1
  31. package/dist/src/options.d.ts +22 -0
  32. package/dist/src/options.d.ts.map +1 -1
  33. package/dist/src/room/PCTransport.d.ts +2 -1
  34. package/dist/src/room/PCTransport.d.ts.map +1 -1
  35. package/dist/src/room/PCTransportManager.d.ts +12 -0
  36. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  37. package/dist/src/room/RTCEngine.d.ts +7 -2
  38. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  39. package/dist/src/room/Room.d.ts +8 -1
  40. package/dist/src/room/Room.d.ts.map +1 -1
  41. package/dist/src/room/data-stream/incoming/StreamReader.d.ts +17 -17
  42. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  43. package/dist/src/room/events.d.ts +1 -1
  44. package/dist/src/room/statsSummary.d.ts +13 -0
  45. package/dist/src/room/statsSummary.d.ts.map +1 -0
  46. package/dist/src/room/token-source/utils.d.ts.map +1 -1
  47. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  48. package/dist/src/room/track/Track.d.ts +10 -0
  49. package/dist/src/room/track/Track.d.ts.map +1 -1
  50. package/dist/src/utils/machineInspector.d.ts +54 -0
  51. package/dist/src/utils/machineInspector.d.ts.map +1 -0
  52. package/dist/ts4.2/api/SignalClient.d.ts +25 -1
  53. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
  54. package/dist/ts4.2/e2ee/E2eeManager.d.ts +7 -0
  55. package/dist/ts4.2/e2ee/constants.d.ts +5 -0
  56. package/dist/ts4.2/e2ee/types.d.ts +9 -2
  57. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +42 -1
  58. package/dist/ts4.2/index.d.ts +3 -2
  59. package/dist/ts4.2/logger.d.ts +2 -1
  60. package/dist/ts4.2/options.d.ts +22 -0
  61. package/dist/ts4.2/room/PCTransport.d.ts +2 -1
  62. package/dist/ts4.2/room/PCTransportManager.d.ts +12 -0
  63. package/dist/ts4.2/room/RTCEngine.d.ts +7 -2
  64. package/dist/ts4.2/room/Room.d.ts +8 -1
  65. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +17 -17
  66. package/dist/ts4.2/room/events.d.ts +1 -1
  67. package/dist/ts4.2/room/statsSummary.d.ts +13 -0
  68. package/dist/ts4.2/room/track/Track.d.ts +10 -0
  69. package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
  70. package/package.json +7 -1
  71. package/src/api/SignalClient.test.ts +320 -8
  72. package/src/api/SignalClient.ts +261 -82
  73. package/src/api/SignalClientStateMachine.test.ts +472 -0
  74. package/src/api/SignalClientStateMachine.ts +180 -0
  75. package/src/api/WebSocketStream.ts +19 -3
  76. package/src/api/utils.test.ts +20 -1
  77. package/src/api/utils.ts +5 -0
  78. package/src/e2ee/E2eeManager.ts +38 -14
  79. package/src/e2ee/constants.ts +6 -0
  80. package/src/e2ee/subscriberBlackScreen.test.ts +544 -0
  81. package/src/e2ee/types.ts +9 -2
  82. package/src/e2ee/worker/FrameCryptor.race.test.ts +9 -26
  83. package/src/e2ee/worker/FrameCryptor.test.ts +0 -1
  84. package/src/e2ee/worker/FrameCryptor.ts +185 -52
  85. package/src/e2ee/worker/e2ee.worker.ts +38 -6
  86. package/src/index.ts +5 -0
  87. package/src/logger.ts +1 -0
  88. package/src/options.ts +24 -0
  89. package/src/room/PCTransport.ts +2 -1
  90. package/src/room/PCTransportManager.test.ts +35 -0
  91. package/src/room/PCTransportManager.ts +12 -4
  92. package/src/room/RTCEngine.ts +16 -7
  93. package/src/room/Room.ts +70 -5
  94. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +171 -0
  95. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +17 -18
  96. package/src/room/data-stream/incoming/StreamReader.ts +20 -50
  97. package/src/room/events.ts +1 -1
  98. package/src/room/statsSummary.ts +187 -0
  99. package/src/room/token-source/test-tokens.ts +20 -0
  100. package/src/room/token-source/utils.test.ts +27 -0
  101. package/src/room/token-source/utils.ts +12 -5
  102. package/src/room/track/LocalAudioTrack.ts +9 -3
  103. package/src/room/track/Track.ts +27 -0
  104. package/src/room/utils.test.ts +24 -1
  105. package/src/room/utils.ts +1 -1
  106. package/src/utils/machineInspector.ts +90 -0
@@ -381,6 +381,7 @@ var LoggerNames;
381
381
  LoggerNames["DataTracks"] = "livekit-data-tracks";
382
382
  LoggerNames["Region"] = "livekit-region";
383
383
  LoggerNames["ICE"] = "livekit-ice";
384
+ LoggerNames["Stats"] = "livekit-stats";
384
385
  })(LoggerNames || (LoggerNames = {}));
385
386
  let livekitLogger = loglevelExports.getLogger(LoggerNames.Default);
386
387
  Object.values(LoggerNames).map(name => loglevelExports.getLogger(name));
@@ -4866,7 +4867,7 @@ var RoomEvent;
4866
4867
  * When a [[RemoteParticipant]] leaves *after* the local
4867
4868
  * participant has joined.
4868
4869
  *
4869
- * args: ([[RemoteParticipant]])
4870
+ * args: ([[RemoteParticipant]], [[DisconnectReason]] | undefined)
4870
4871
  */
4871
4872
  RoomEvent["ParticipantDisconnected"] = "participantDisconnected";
4872
4873
  /**
@@ -5436,6 +5437,172 @@ var TrackEvent;
5436
5437
  TrackEvent["PreConnectBufferFlushed"] = "preConnectBufferFlushed";
5437
5438
  })(TrackEvent || (TrackEvent = {}));
5438
5439
 
5440
+ function compact(summary) {
5441
+ const compacted = {};
5442
+ for (const _ref of Object.entries(summary)) {
5443
+ var _ref2 = _slicedToArray(_ref, 2);
5444
+ const key = _ref2[0];
5445
+ const value = _ref2[1];
5446
+ if (value !== undefined) {
5447
+ compacted[key] = value;
5448
+ }
5449
+ }
5450
+ return compacted;
5451
+ }
5452
+ function resolution(width, height) {
5453
+ return width && height ? "".concat(width, "x").concat(height) : undefined;
5454
+ }
5455
+ /** keeps the derived durations readable; reported values are logged as they are */
5456
+ function round(seconds) {
5457
+ return Math.round(seconds * 10000) / 10000;
5458
+ }
5459
+ /**
5460
+ * Average time media spent in the jitter buffer, in s. Both counters are
5461
+ * cumulative, so this is the average over the lifetime of the stream.
5462
+ */
5463
+ function jitterBuffer(stat) {
5464
+ const delay = stat.jitterBufferDelay;
5465
+ const emitted = stat.jitterBufferEmittedCount;
5466
+ return delay !== undefined && emitted ? round(delay / emitted) : undefined;
5467
+ }
5468
+ /**
5469
+ * Playout delay in s. Video receive reports it directly, the audio path keeps it
5470
+ * in the linked `media-playout` stats summed over the samples played out. Pairs
5471
+ * with `RemoteTrack.setPlayoutDelay`.
5472
+ */
5473
+ function playoutDelay(stat, playout) {
5474
+ if (stat.playoutDelay !== undefined) {
5475
+ return round(stat.playoutDelay);
5476
+ }
5477
+ const total = playout === null || playout === void 0 ? void 0 : playout.totalPlayoutDelay;
5478
+ const samples = playout === null || playout === void 0 ? void 0 : playout.totalSamplesCount;
5479
+ return total !== undefined && samples ? round(total / samples) : undefined;
5480
+ }
5481
+ /**
5482
+ * Picks the interesting fields out of a `getStats()` report and groups them by
5483
+ * RTP stream, so a stats dump can be read without unfolding the raw report.
5484
+ */
5485
+ function summarizeStatsReport(report) {
5486
+ var _a, _b;
5487
+ const byId = new Map();
5488
+ const candidatePairs = [];
5489
+ const inbound = [];
5490
+ const outbound = [];
5491
+ let transport;
5492
+ report.forEach(stat => byId.set(stat.id, stat));
5493
+ const codecOf = stat => {
5494
+ var _a;
5495
+ return stat.codecId ? (_a = byId.get(stat.codecId)) === null || _a === void 0 ? void 0 : _a.mimeType : undefined;
5496
+ };
5497
+ const relatedOf = (stat, key) => stat[key] ? byId.get(stat[key]) : undefined;
5498
+ report.forEach(stat => {
5499
+ switch (stat.type) {
5500
+ case 'inbound-rtp':
5501
+ {
5502
+ const playout = relatedOf(stat, 'playoutId');
5503
+ inbound.push(compact({
5504
+ kind: stat.kind,
5505
+ ssrc: stat.ssrc,
5506
+ mid: stat.mid,
5507
+ // matches `streamTrackID` in the track's own log context
5508
+ trackId: stat.trackIdentifier,
5509
+ codec: codecOf(stat),
5510
+ decoder: stat.decoderImplementation,
5511
+ resolution: resolution(stat.frameWidth, stat.frameHeight),
5512
+ fps: stat.framesPerSecond,
5513
+ bytesReceived: stat.bytesReceived,
5514
+ packetsReceived: stat.packetsReceived,
5515
+ packetsLost: stat.packetsLost,
5516
+ packetsDiscarded: stat.packetsDiscarded,
5517
+ // frames received without frames decoded is a decode failure
5518
+ framesReceived: stat.framesReceived,
5519
+ framesDecoded: stat.framesDecoded,
5520
+ framesDropped: stat.framesDropped,
5521
+ keyFramesDecoded: stat.keyFramesDecoded,
5522
+ freezeCount: stat.freezeCount,
5523
+ totalFreezesDuration: stat.totalFreezesDuration,
5524
+ pauseCount: stat.pauseCount,
5525
+ nackCount: stat.nackCount,
5526
+ pliCount: stat.pliCount,
5527
+ firCount: stat.firCount,
5528
+ jitter: stat.jitter,
5529
+ jitterBuffer: jitterBuffer(stat),
5530
+ playoutDelay: playoutDelay(stat, playout),
5531
+ audioLevel: stat.audioLevel,
5532
+ totalSamplesReceived: stat.totalSamplesReceived,
5533
+ concealedSamples: stat.concealedSamples
5534
+ }));
5535
+ break;
5536
+ }
5537
+ case 'outbound-rtp':
5538
+ {
5539
+ const remote = relatedOf(stat, 'remoteId');
5540
+ const source = relatedOf(stat, 'mediaSourceId');
5541
+ outbound.push(compact({
5542
+ kind: stat.kind,
5543
+ ssrc: stat.ssrc,
5544
+ mid: stat.mid,
5545
+ rid: stat.rid,
5546
+ trackId: source === null || source === void 0 ? void 0 : source.trackIdentifier,
5547
+ active: stat.active,
5548
+ codec: codecOf(stat),
5549
+ encoder: stat.encoderImplementation,
5550
+ resolution: resolution(stat.frameWidth, stat.frameHeight),
5551
+ fps: stat.framesPerSecond,
5552
+ // what the source produces, to tell a stalled capture from a stalled encoder
5553
+ captureResolution: resolution(source === null || source === void 0 ? void 0 : source.width, source === null || source === void 0 ? void 0 : source.height),
5554
+ captureFps: source === null || source === void 0 ? void 0 : source.framesPerSecond,
5555
+ audioLevel: source === null || source === void 0 ? void 0 : source.audioLevel,
5556
+ targetBitrate: stat.targetBitrate,
5557
+ bytesSent: stat.bytesSent,
5558
+ packetsSent: stat.packetsSent,
5559
+ retransmittedPacketsSent: stat.retransmittedPacketsSent,
5560
+ framesEncoded: stat.framesEncoded,
5561
+ keyFramesEncoded: stat.keyFramesEncoded,
5562
+ limitedBy: stat.qualityLimitationReason === 'none' ? undefined : stat.qualityLimitationReason,
5563
+ nackCount: stat.nackCount,
5564
+ pliCount: stat.pliCount,
5565
+ firCount: stat.firCount,
5566
+ // loss, jitter and RTT are only known from what the remote reports
5567
+ remotePacketsLost: remote === null || remote === void 0 ? void 0 : remote.packetsLost,
5568
+ remoteFractionLost: remote === null || remote === void 0 ? void 0 : remote.fractionLost,
5569
+ remoteJitter: remote === null || remote === void 0 ? void 0 : remote.jitter,
5570
+ remoteRoundTripTime: remote === null || remote === void 0 ? void 0 : remote.roundTripTime
5571
+ }));
5572
+ break;
5573
+ }
5574
+ case 'transport':
5575
+ transport = stat;
5576
+ break;
5577
+ case 'candidate-pair':
5578
+ candidatePairs.push(stat);
5579
+ break;
5580
+ }
5581
+ });
5582
+ const selectedPairId = transport === null || transport === void 0 ? void 0 : transport.selectedCandidatePairId;
5583
+ const pair = (_b = (_a = selectedPairId ? byId.get(selectedPairId) : undefined) !== null && _a !== void 0 ? _a : candidatePairs.find(candidate => candidate.selected)) !== null && _b !== void 0 ? _b : candidatePairs.find(candidate => candidate.nominated);
5584
+ const local = (pair === null || pair === void 0 ? void 0 : pair.localCandidateId) ? byId.get(pair.localCandidateId) : undefined;
5585
+ const remote = (pair === null || pair === void 0 ? void 0 : pair.remoteCandidateId) ? byId.get(pair.remoteCandidateId) : undefined;
5586
+ const connection = compact({
5587
+ ice: transport === null || transport === void 0 ? void 0 : transport.iceState,
5588
+ dtls: transport === null || transport === void 0 ? void 0 : transport.dtlsState,
5589
+ route: local && remote ? "".concat(local.candidateType, "/").concat(local.protocol, " -> ").concat(remote.candidateType) : undefined,
5590
+ network: local === null || local === void 0 ? void 0 : local.networkType,
5591
+ currentRoundTripTime: pair === null || pair === void 0 ? void 0 : pair.currentRoundTripTime,
5592
+ // the send bandwidth estimate; no RTP stream reports it
5593
+ availableOutgoingBitrate: pair === null || pair === void 0 ? void 0 : pair.availableOutgoingBitrate,
5594
+ availableIncomingBitrate: pair === null || pair === void 0 ? void 0 : pair.availableIncomingBitrate,
5595
+ bytesSent: pair === null || pair === void 0 ? void 0 : pair.bytesSent,
5596
+ bytesReceived: pair === null || pair === void 0 ? void 0 : pair.bytesReceived,
5597
+ candidatePairChanges: transport === null || transport === void 0 ? void 0 : transport.selectedCandidatePairChanges
5598
+ });
5599
+ return {
5600
+ connection: Object.keys(connection).length > 0 ? connection : undefined,
5601
+ outbound: outbound.length > 0 ? outbound : undefined,
5602
+ inbound: inbound.length > 0 ? inbound : undefined
5603
+ };
5604
+ }
5605
+
5439
5606
  class VideoPreset {
5440
5607
  constructor(widthOrOptions, height, maxBitrate, maxFramerate, priority) {
5441
5608
  if (typeof widthOrOptions === 'object') {
@@ -5604,6 +5771,7 @@ class Track extends eventsExports.EventEmitter {
5604
5771
  this._streamState = Track.StreamState.Active;
5605
5772
  this.isInBackground = false;
5606
5773
  this._currentBitrate = 0;
5774
+ this.finalStatsLogged = false;
5607
5775
  this.log = livekitLogger;
5608
5776
  this.appVisibilityChangedListener = () => {
5609
5777
  if (this.backgroundTimeout) {
@@ -5752,6 +5920,24 @@ class Track extends eventsExports.EventEmitter {
5752
5920
  cancelAnimationFrame(this.timeSyncHandle);
5753
5921
  this.timeSyncHandle = undefined;
5754
5922
  }
5923
+ this.logFinalStats();
5924
+ }
5925
+ /**
5926
+ * Dumps the raw stats of the track as it ends, once: a track that stops
5927
+ * between two of the room's stats dumps is gone by the time the next one runs.
5928
+ */
5929
+ logFinalStats() {
5930
+ if (this.finalStatsLogged) {
5931
+ return;
5932
+ }
5933
+ this.finalStatsLogged = true;
5934
+ this.getRTCStatsReport().then(report => {
5935
+ if (report) {
5936
+ this.log.info('final track stats', summarizeStatsReport(report));
5937
+ }
5938
+ }).catch(error => this.log.debug('could not collect final track stats', {
5939
+ error
5940
+ }));
5755
5941
  }
5756
5942
  /** @internal */
5757
5943
  updateLoggerOptions(loggerOptions) {
@@ -6508,6 +6694,8 @@ class FrameCryptor extends BaseFrameCryptor {
6508
6694
  this.ERROR_THROTTLE_MS = 1000; // Emit error at most once per second
6509
6695
  this.MAX_ERRORS_PER_MINUTE = 5; // Maximum errors to emit per minute per key
6510
6696
  this.ERROR_WINDOW_MS = 60000; // 1 minute window
6697
+ /** grace period for a teardown or a resubscribe to land before we report a stalled track */
6698
+ this.UNDECRYPTED_TRACK_GRACE_MS = 2000;
6511
6699
  /**
6512
6700
  * Tracks (participant, trackId, payloadType) tuples for which we've already logged a NALU
6513
6701
  * fallback, so a persistent bad state doesn't flood the console (Firefox doesn't filter debug).
@@ -6552,9 +6740,13 @@ class FrameCryptor extends BaseFrameCryptor {
6552
6740
  }
6553
6741
  unsetParticipant() {
6554
6742
  workerLogger.debug('unsetting participant', this.logContext);
6555
- if (this.currentTransform) {
6556
- this.currentTransform = undefined;
6557
- }
6743
+ // NOTE: deliberately does not clear `currentTransform`. Nothing here cancels
6744
+ // the pipe, so the transform really is still running and clearing it would
6745
+ // desync our bookkeeping from reality -- which previously let a later
6746
+ // resubscribe skip setup while frames kept flowing through a cryptor with no
6747
+ // participant assigned.
6748
+ clearTimeout(this.undecryptedTrackTimeout);
6749
+ this.undecryptedTrackTimeout = undefined;
6558
6750
  this.participantIdentity = undefined;
6559
6751
  this.lastErrorTimestamp = new Map();
6560
6752
  this.errorCounts = new Map();
@@ -6572,6 +6764,72 @@ class FrameCryptor extends BaseFrameCryptor {
6572
6764
  getTrackId() {
6573
6765
  return this.trackId;
6574
6766
  }
6767
+ /**
6768
+ * Re-point this cryptor at a new trackId, keeping its (already transferred)
6769
+ * encoded streams. Used when a transceiver is reused for a new track.
6770
+ */
6771
+ setTrackId(trackId) {
6772
+ if (this.trackId === trackId) {
6773
+ return;
6774
+ }
6775
+ workerLogger.debug('re-pointing cryptor at new trackId', Object.assign(Object.assign({}, this.logContext), {
6776
+ newTrackId: trackId
6777
+ }));
6778
+ this.trackId = trackId;
6779
+ }
6780
+ hasActiveTransform() {
6781
+ return !!this.currentTransform;
6782
+ }
6783
+ /**
6784
+ * A track that is subscribed and known to be encrypted, but has no transform to
6785
+ * decrypt it, will never produce a decodable frame again. That state used to be
6786
+ * completely silent (a black tile and endless PLIs), so report it.
6787
+ *
6788
+ * Deferred, because on teardown the encoded streams routinely close before the
6789
+ * 'removeTransform' message arrives -- checking immediately would cry wolf on
6790
+ * every unsubscribe.
6791
+ *
6792
+ * Only meaningful while decoding: a sender's pipe closing on unpublish is
6793
+ * routine and has no 'removeTransform' equivalent to quiet it down.
6794
+ */
6795
+ scheduleUndecryptedTrackWatchdog(operation) {
6796
+ clearTimeout(this.undecryptedTrackTimeout);
6797
+ this.undecryptedTrackTimeout = undefined;
6798
+ if (operation !== 'decode') {
6799
+ return;
6800
+ }
6801
+ this.undecryptedTrackTimeout = setTimeout(() => {
6802
+ if (this.currentTransform || !this.isEnabled()) {
6803
+ return;
6804
+ }
6805
+ workerLogger.warn('encrypted track has no active decrypt transform', this.logContext);
6806
+ this.emitThrottledError(new CryptorError("no active decrypt transform for encrypted track ".concat(this.trackId), CryptorErrorReason.InternalError, this.participantIdentity));
6807
+ }, this.UNDECRYPTED_TRACK_GRACE_MS);
6808
+ }
6809
+ /**
6810
+ * Re-establish the transform if it is gone while we still own the encoded
6811
+ * streams, e.g. after a pipe died on its own (the swallowed
6812
+ * 'Destination stream closed') or when a reused transceiver never got a new
6813
+ * pipeline. Without this the frames pile up in a readable nobody reads and the
6814
+ * decoder starves, which shows up as a permanently black tile.
6815
+ */
6816
+ ensureTransform() {
6817
+ if (this.currentTransform) {
6818
+ return true;
6819
+ }
6820
+ if (!this.retainedStreams || this.trackId === undefined) {
6821
+ workerLogger.warn('no streams retained, cannot re-establish transform', this.logContext);
6822
+ return false;
6823
+ }
6824
+ const _this$retainedStreams = this.retainedStreams,
6825
+ readable = _this$retainedStreams.readable,
6826
+ writable = _this$retainedStreams.writable,
6827
+ operation = _this$retainedStreams.operation;
6828
+ workerLogger.info('re-establishing transform', Object.assign(Object.assign({}, this.logContext), {
6829
+ operation
6830
+ }));
6831
+ return this.setupTransform(operation, readable, writable, this.trackId);
6832
+ }
6575
6833
  /**
6576
6834
  * Update the video codec used by the mediaStreamTrack
6577
6835
  * @param codec
@@ -6598,7 +6856,8 @@ class FrameCryptor extends BaseFrameCryptor {
6598
6856
  this.frameMetadataOpts = frameMetadata;
6599
6857
  this.frameMetadataFrameId = 0;
6600
6858
  }
6601
- setupTransform(operation, readable, writable, trackId, isReuse, codec, frameMetadata) {
6859
+ setupTransform(operation, readable, writable, trackId, codec, frameMetadata) {
6860
+ var _a;
6602
6861
  if (codec) {
6603
6862
  workerLogger.info('setting codec on cryptor to', {
6604
6863
  codec
@@ -6612,18 +6871,17 @@ class FrameCryptor extends BaseFrameCryptor {
6612
6871
  operation,
6613
6872
  passedTrackId: trackId,
6614
6873
  codec,
6615
- isReuse,
6616
6874
  hasCurrentTransform: !!this.currentTransform
6617
6875
  }, this.logContext));
6618
- // Always update trackId, even on reuse
6619
6876
  this.trackId = trackId;
6620
- // If we're reusing and have an active transform skip setup
6621
- if (isReuse && this.currentTransform && readable === this.currentTransform.readable && writable === this.currentTransform.writable) {
6622
- workerLogger.debug('reusing existing transform', Object.assign(Object.assign({}, this.logContext), {
6623
- trackId
6624
- }));
6625
- return;
6626
- }
6877
+ // Retain the streams so we can rebuild the pipe later even if this cryptor
6878
+ // gets detached from its participant in between (see `ensureTransform`).
6879
+ this.retainedStreams = {
6880
+ readable,
6881
+ writable,
6882
+ operation
6883
+ };
6884
+ clearTimeout(this.undecryptedTrackTimeout);
6627
6885
  const symbol = Symbol('transform');
6628
6886
  const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
6629
6887
  const transformStream = new TransformStream({
@@ -6637,26 +6895,43 @@ class FrameCryptor extends BaseFrameCryptor {
6637
6895
  trackId,
6638
6896
  symbol
6639
6897
  };
6640
- readable.pipeThrough(transformStream).pipeTo(writable).catch(e => {
6641
- if (e instanceof TypeError && e.message === 'Destination stream closed') {
6642
- // this can happen when subscriptions happen in quick successions, but doesn't influence functionality
6643
- workerLogger.debug('destination stream closed');
6644
- } else {
6645
- workerLogger.warn('transform error', Object.assign({
6646
- error: e
6647
- }, this.logContext));
6648
- this.emit(CryptorEvent.Error, e instanceof CryptorError ? e : new CryptorError(e.message, undefined, this.participantIdentity));
6649
- }
6650
- }).finally(() => {
6651
- var _a;
6652
- // Only clear currentTransform if it's still the same one we started
6898
+ // pipeThrough/pipeTo throw synchronously on an already locked stream, which
6899
+ // would bypass the .catch below and leave us with no transform at all.
6900
+ try {
6901
+ readable.pipeThrough(transformStream).pipeTo(writable).catch(e => {
6902
+ if (e instanceof TypeError && e.message === 'Destination stream closed') {
6903
+ // this can happen when subscriptions happen in quick successions, but doesn't influence functionality
6904
+ workerLogger.debug('destination stream closed');
6905
+ } else {
6906
+ workerLogger.warn('transform error', Object.assign({
6907
+ error: e
6908
+ }, this.logContext));
6909
+ this.emit(CryptorEvent.Error, e instanceof CryptorError ? e : new CryptorError(e.message, undefined, this.participantIdentity));
6910
+ }
6911
+ }).finally(() => {
6912
+ var _a;
6913
+ // Only clear currentTransform if it's still the same one we started
6914
+ if (((_a = this.currentTransform) === null || _a === void 0 ? void 0 : _a.symbol) === symbol) {
6915
+ workerLogger.debug('transform completed', Object.assign(Object.assign({}, this.logContext), {
6916
+ trackId
6917
+ }));
6918
+ this.currentTransform = undefined;
6919
+ }
6920
+ // A pipe ending while the track is still assigned to a participant that
6921
+ // publishes encrypted media means we have silently stopped decrypting.
6922
+ this.scheduleUndecryptedTrackWatchdog(operation);
6923
+ });
6924
+ } catch (e) {
6653
6925
  if (((_a = this.currentTransform) === null || _a === void 0 ? void 0 : _a.symbol) === symbol) {
6654
- workerLogger.debug('transform completed', Object.assign(Object.assign({}, this.logContext), {
6655
- trackId
6656
- }));
6657
6926
  this.currentTransform = undefined;
6658
6927
  }
6659
- });
6928
+ workerLogger.error('failed to set up transform', Object.assign({
6929
+ error: e
6930
+ }, this.logContext));
6931
+ this.emit(CryptorEvent.Error, e instanceof CryptorError ? e : new CryptorError(e.message, CryptorErrorReason.InternalError, this.participantIdentity));
6932
+ return false;
6933
+ }
6934
+ return true;
6660
6935
  }
6661
6936
  setSifTrailer(trailer) {
6662
6937
  workerLogger.debug('setting SIF trailer', Object.assign(Object.assign({}, this.logContext), {
@@ -6837,9 +7112,29 @@ class FrameCryptor extends BaseFrameCryptor {
6837
7112
  // best-effort: never break the media pipeline if trailer parsing fails
6838
7113
  }
6839
7114
  }
6840
- if (!this.isEnabled() ||
6841
- // skip for decryption for empty dtx frames
6842
- encodedFrame.data.byteLength === 0) {
7115
+ // skip decryption for empty dtx frames
7116
+ if (encodedFrame.data.byteLength === 0) {
7117
+ return controller.enqueue(encodedFrame);
7118
+ }
7119
+ const encryptionEnabled = this.isEnabled();
7120
+ if (encryptionEnabled === undefined) {
7121
+ // Either way we drop: forwarding would hand ciphertext straight to the
7122
+ // decoder, which renders as a black frame and logs nothing.
7123
+ if (this.participantIdentity === undefined) {
7124
+ // The pipe outlived its subscription. Expected on unsubscribe/disconnect:
7125
+ // we deliberately leave the pipe running so a reused transceiver can be
7126
+ // re-pointed at a new track, so in-flight frames still arrive here for a
7127
+ // moment. Not worth reporting -- the watchdog covers a track that stays
7128
+ // subscribed without a transform.
7129
+ workerLogger.debug('dropping frame for unassigned cryptor', this.logContext);
7130
+ return;
7131
+ }
7132
+ // A live subscription whose encryption state we were never told about.
7133
+ this.emitThrottledError(new CryptorError("encryption state unknown for track ".concat(this.trackId, ", dropping frame"), CryptorErrorReason.InternalError, this.participantIdentity));
7134
+ return;
7135
+ }
7136
+ if (!encryptionEnabled) {
7137
+ // participant is known to publish unencrypted media
6843
7138
  return controller.enqueue(encodedFrame);
6844
7139
  }
6845
7140
  if (isFrameServerInjected(encodedFrame.data, this.sifTrailer)) {
@@ -7341,12 +7636,12 @@ onmessage = ev => {
7341
7636
  case 'decode':
7342
7637
  let cryptor = getTrackCryptor(data.participantIdentity, data.trackId);
7343
7638
  cryptor.setHasFrameMetadata(data.hasPacketTrailer);
7344
- cryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.isReuse, data.codec);
7639
+ cryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.codec);
7345
7640
  break;
7346
7641
  case 'encode':
7347
7642
  let pubCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
7348
7643
  pubCryptor.setHasFrameMetadata(data.hasPacketTrailer);
7349
- pubCryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.isReuse, data.codec, data.packetTrailer);
7644
+ pubCryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.codec, data.packetTrailer);
7350
7645
  break;
7351
7646
  case 'encryptDataRequest':
7352
7647
  const _yield$DataCryptor$en = yield DataCryptor.encrypt(data.payload, getParticipantKeyHandler(data.participantIdentity)),
@@ -7409,15 +7704,28 @@ onmessage = ev => {
7409
7704
  unsetCryptorParticipant(data.trackId, data.participantIdentity);
7410
7705
  break;
7411
7706
  case 'updateCodec':
7412
- const trackCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
7413
- trackCryptor.setVideoCodec(data.codec);
7707
+ const trackCryptor = getTrackCryptor(data.participantIdentity, data.trackId, data.previousTrackId);
7708
+ if (data.codec) {
7709
+ trackCryptor.setVideoCodec(data.codec);
7710
+ }
7414
7711
  trackCryptor.setHasFrameMetadata(data.hasPacketTrailer);
7415
7712
  workerLogger.info('updated codec', {
7416
7713
  participantIdentity: data.participantIdentity,
7417
7714
  trackId: data.trackId,
7715
+ previousTrackId: data.previousTrackId,
7418
7716
  codec: data.codec,
7419
7717
  hasPacketTrailer: data.hasPacketTrailer
7420
7718
  });
7719
+ // Sent on transceiver reuse, where the main thread cannot hand us a new
7720
+ // pipeline. If the previous one died in the meantime we have to rebuild it
7721
+ // here or no frame ever gets decrypted again.
7722
+ if (data.previousTrackId !== undefined && !trackCryptor.ensureTransform()) {
7723
+ workerLogger.error('could not re-establish transform for reused receiver', {
7724
+ participantIdentity: data.participantIdentity,
7725
+ trackId: data.trackId,
7726
+ previousTrackId: data.previousTrackId
7727
+ });
7728
+ }
7421
7729
  break;
7422
7730
  case 'setRTPMap':
7423
7731
  // this is only used for the local participant
@@ -7452,8 +7760,24 @@ function handleRatchetRequest(data) {
7452
7760
  }
7453
7761
  });
7454
7762
  }
7455
- function getTrackCryptor(participantIdentity, trackId) {
7763
+ function getTrackCryptor(participantIdentity, trackId, previousTrackId) {
7456
7764
  let cryptors = participantCryptors.filter(c => c.getTrackId() === trackId);
7765
+ if (cryptors.length === 0 && previousTrackId !== undefined && previousTrackId !== trackId) {
7766
+ // A reused transceiver carries a new trackId, but the cryptor that owns this
7767
+ // receiver's encoded streams is still keyed on the old one. Re-point it instead
7768
+ // of creating a fresh cryptor that has no pipeline (and leaving the old one
7769
+ // piping frames with no participant assigned).
7770
+ const previous = participantCryptors.filter(c => c.getTrackId() === previousTrackId);
7771
+ if (previous.length > 0) {
7772
+ workerLogger.info('reusing cryptor from previous trackId', {
7773
+ participantIdentity,
7774
+ trackId,
7775
+ previousTrackId
7776
+ });
7777
+ previous[0].setTrackId(trackId);
7778
+ cryptors = previous;
7779
+ }
7780
+ }
7457
7781
  if (cryptors.length > 1) {
7458
7782
  const debugInfo = cryptors.map(c => {
7459
7783
  return {
@@ -7589,7 +7913,7 @@ if (self.RTCTransformEvent) {
7589
7913
  trackId,
7590
7914
  codec
7591
7915
  });
7592
- cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, false, codec, kind === 'encode' ? options.packetTrailer : undefined);
7916
+ cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, codec, kind === 'encode' ? options.packetTrailer : undefined);
7593
7917
  }));
7594
7918
  };
7595
7919
  }