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
|
@@ -6694,6 +6694,8 @@ class FrameCryptor extends BaseFrameCryptor {
|
|
|
6694
6694
|
this.ERROR_THROTTLE_MS = 1000; // Emit error at most once per second
|
|
6695
6695
|
this.MAX_ERRORS_PER_MINUTE = 5; // Maximum errors to emit per minute per key
|
|
6696
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;
|
|
6697
6699
|
/**
|
|
6698
6700
|
* Tracks (participant, trackId, payloadType) tuples for which we've already logged a NALU
|
|
6699
6701
|
* fallback, so a persistent bad state doesn't flood the console (Firefox doesn't filter debug).
|
|
@@ -6738,9 +6740,13 @@ class FrameCryptor extends BaseFrameCryptor {
|
|
|
6738
6740
|
}
|
|
6739
6741
|
unsetParticipant() {
|
|
6740
6742
|
workerLogger.debug('unsetting participant', this.logContext);
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
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;
|
|
6744
6750
|
this.participantIdentity = undefined;
|
|
6745
6751
|
this.lastErrorTimestamp = new Map();
|
|
6746
6752
|
this.errorCounts = new Map();
|
|
@@ -6758,6 +6764,72 @@ class FrameCryptor extends BaseFrameCryptor {
|
|
|
6758
6764
|
getTrackId() {
|
|
6759
6765
|
return this.trackId;
|
|
6760
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
|
+
}
|
|
6761
6833
|
/**
|
|
6762
6834
|
* Update the video codec used by the mediaStreamTrack
|
|
6763
6835
|
* @param codec
|
|
@@ -6784,7 +6856,8 @@ class FrameCryptor extends BaseFrameCryptor {
|
|
|
6784
6856
|
this.frameMetadataOpts = frameMetadata;
|
|
6785
6857
|
this.frameMetadataFrameId = 0;
|
|
6786
6858
|
}
|
|
6787
|
-
setupTransform(operation, readable, writable, trackId,
|
|
6859
|
+
setupTransform(operation, readable, writable, trackId, codec, frameMetadata) {
|
|
6860
|
+
var _a;
|
|
6788
6861
|
if (codec) {
|
|
6789
6862
|
workerLogger.info('setting codec on cryptor to', {
|
|
6790
6863
|
codec
|
|
@@ -6798,18 +6871,17 @@ class FrameCryptor extends BaseFrameCryptor {
|
|
|
6798
6871
|
operation,
|
|
6799
6872
|
passedTrackId: trackId,
|
|
6800
6873
|
codec,
|
|
6801
|
-
isReuse,
|
|
6802
6874
|
hasCurrentTransform: !!this.currentTransform
|
|
6803
6875
|
}, this.logContext));
|
|
6804
|
-
// Always update trackId, even on reuse
|
|
6805
6876
|
this.trackId = trackId;
|
|
6806
|
-
//
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
}
|
|
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);
|
|
6813
6885
|
const symbol = Symbol('transform');
|
|
6814
6886
|
const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
|
|
6815
6887
|
const transformStream = new TransformStream({
|
|
@@ -6823,26 +6895,43 @@ class FrameCryptor extends BaseFrameCryptor {
|
|
|
6823
6895
|
trackId,
|
|
6824
6896
|
symbol
|
|
6825
6897
|
};
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
}
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
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) {
|
|
6839
6925
|
if (((_a = this.currentTransform) === null || _a === void 0 ? void 0 : _a.symbol) === symbol) {
|
|
6840
|
-
workerLogger.debug('transform completed', Object.assign(Object.assign({}, this.logContext), {
|
|
6841
|
-
trackId
|
|
6842
|
-
}));
|
|
6843
6926
|
this.currentTransform = undefined;
|
|
6844
6927
|
}
|
|
6845
|
-
|
|
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;
|
|
6846
6935
|
}
|
|
6847
6936
|
setSifTrailer(trailer) {
|
|
6848
6937
|
workerLogger.debug('setting SIF trailer', Object.assign(Object.assign({}, this.logContext), {
|
|
@@ -7023,9 +7112,29 @@ class FrameCryptor extends BaseFrameCryptor {
|
|
|
7023
7112
|
// best-effort: never break the media pipeline if trailer parsing fails
|
|
7024
7113
|
}
|
|
7025
7114
|
}
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
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
|
|
7029
7138
|
return controller.enqueue(encodedFrame);
|
|
7030
7139
|
}
|
|
7031
7140
|
if (isFrameServerInjected(encodedFrame.data, this.sifTrailer)) {
|
|
@@ -7527,12 +7636,12 @@ onmessage = ev => {
|
|
|
7527
7636
|
case 'decode':
|
|
7528
7637
|
let cryptor = getTrackCryptor(data.participantIdentity, data.trackId);
|
|
7529
7638
|
cryptor.setHasFrameMetadata(data.hasPacketTrailer);
|
|
7530
|
-
cryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.
|
|
7639
|
+
cryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.codec);
|
|
7531
7640
|
break;
|
|
7532
7641
|
case 'encode':
|
|
7533
7642
|
let pubCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
|
|
7534
7643
|
pubCryptor.setHasFrameMetadata(data.hasPacketTrailer);
|
|
7535
|
-
pubCryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.
|
|
7644
|
+
pubCryptor.setupTransform(kind, data.readableStream, data.writableStream, data.trackId, data.codec, data.packetTrailer);
|
|
7536
7645
|
break;
|
|
7537
7646
|
case 'encryptDataRequest':
|
|
7538
7647
|
const _yield$DataCryptor$en = yield DataCryptor.encrypt(data.payload, getParticipantKeyHandler(data.participantIdentity)),
|
|
@@ -7595,15 +7704,28 @@ onmessage = ev => {
|
|
|
7595
7704
|
unsetCryptorParticipant(data.trackId, data.participantIdentity);
|
|
7596
7705
|
break;
|
|
7597
7706
|
case 'updateCodec':
|
|
7598
|
-
const trackCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
|
|
7599
|
-
|
|
7707
|
+
const trackCryptor = getTrackCryptor(data.participantIdentity, data.trackId, data.previousTrackId);
|
|
7708
|
+
if (data.codec) {
|
|
7709
|
+
trackCryptor.setVideoCodec(data.codec);
|
|
7710
|
+
}
|
|
7600
7711
|
trackCryptor.setHasFrameMetadata(data.hasPacketTrailer);
|
|
7601
7712
|
workerLogger.info('updated codec', {
|
|
7602
7713
|
participantIdentity: data.participantIdentity,
|
|
7603
7714
|
trackId: data.trackId,
|
|
7715
|
+
previousTrackId: data.previousTrackId,
|
|
7604
7716
|
codec: data.codec,
|
|
7605
7717
|
hasPacketTrailer: data.hasPacketTrailer
|
|
7606
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
|
+
}
|
|
7607
7729
|
break;
|
|
7608
7730
|
case 'setRTPMap':
|
|
7609
7731
|
// this is only used for the local participant
|
|
@@ -7638,8 +7760,24 @@ function handleRatchetRequest(data) {
|
|
|
7638
7760
|
}
|
|
7639
7761
|
});
|
|
7640
7762
|
}
|
|
7641
|
-
function getTrackCryptor(participantIdentity, trackId) {
|
|
7763
|
+
function getTrackCryptor(participantIdentity, trackId, previousTrackId) {
|
|
7642
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
|
+
}
|
|
7643
7781
|
if (cryptors.length > 1) {
|
|
7644
7782
|
const debugInfo = cryptors.map(c => {
|
|
7645
7783
|
return {
|
|
@@ -7775,7 +7913,7 @@ if (self.RTCTransformEvent) {
|
|
|
7775
7913
|
trackId,
|
|
7776
7914
|
codec
|
|
7777
7915
|
});
|
|
7778
|
-
cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId,
|
|
7916
|
+
cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, codec, kind === 'encode' ? options.packetTrailer : undefined);
|
|
7779
7917
|
}));
|
|
7780
7918
|
};
|
|
7781
7919
|
}
|