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.
- 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 +364 -40
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1847 -179
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +187 -1
- package/dist/livekit-client.fm.worker.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 +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.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/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -1
- package/dist/src/room/PCTransport.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/RTCEngine.d.ts +7 -2
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +8 -1
- package/dist/src/room/Room.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/src/room/events.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +10 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- 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/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +2 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +12 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +7 -2
- package/dist/ts4.2/room/Room.d.ts +8 -1
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- package/dist/ts4.2/room/track/Track.d.ts +10 -0
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +7 -1
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +261 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- 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/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.ts +2 -1
- package/src/room/PCTransportManager.test.ts +35 -0
- package/src/room/PCTransportManager.ts +12 -4
- package/src/room/RTCEngine.ts +16 -7
- package/src/room/Room.ts +70 -5
- 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
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- package/src/room/track/Track.ts +27 -0
- package/src/room/utils.test.ts +24 -1
- package/src/room/utils.ts +1 -1
- package/src/utils/machineInspector.ts +90 -0
|
@@ -68,8 +68,8 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
68
68
|
vitest.useRealTimers();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
describe('Race Condition 1: setupTransform
|
|
72
|
-
it('should update trackId
|
|
71
|
+
describe('Race Condition 1: setupTransform on transceiver reuse updates trackId', () => {
|
|
72
|
+
it('should update trackId when the transform is replaced', async () => {
|
|
73
73
|
const { cryptor, keys } = createCryptor('participant1');
|
|
74
74
|
await keys.setKey(await createKeyMaterialFromString('key1'), 0);
|
|
75
75
|
|
|
@@ -82,7 +82,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
82
82
|
new ReadableStream(input1),
|
|
83
83
|
new WritableStream(output1),
|
|
84
84
|
'track1',
|
|
85
|
-
false,
|
|
86
85
|
undefined,
|
|
87
86
|
);
|
|
88
87
|
|
|
@@ -91,18 +90,16 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
91
90
|
const input2 = new TestUnderlyingSource<RTCEncodedVideoFrame>();
|
|
92
91
|
const output2 = new TestUnderlyingSink<RTCEncodedVideoFrame>();
|
|
93
92
|
|
|
94
|
-
// Second setup with
|
|
93
|
+
// Second setup with a different trackId 'track2'
|
|
95
94
|
// This simulates transceiver reuse for a new track
|
|
96
95
|
cryptor.setupTransform(
|
|
97
96
|
'encode',
|
|
98
97
|
new ReadableStream(input2),
|
|
99
98
|
new WritableStream(output2),
|
|
100
99
|
'track2',
|
|
101
|
-
true, // isReuse = true
|
|
102
100
|
undefined,
|
|
103
101
|
);
|
|
104
102
|
|
|
105
|
-
// BUG: trackId should be updated to 'track2' but remains 'track1'
|
|
106
103
|
expect(cryptor.getTrackId()).toBe('track2');
|
|
107
104
|
});
|
|
108
105
|
});
|
|
@@ -120,7 +117,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
120
117
|
new ReadableStream(input),
|
|
121
118
|
new WritableStream(output),
|
|
122
119
|
'track1',
|
|
123
|
-
false,
|
|
124
120
|
undefined,
|
|
125
121
|
);
|
|
126
122
|
|
|
@@ -147,7 +143,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
147
143
|
new ReadableStream(input2),
|
|
148
144
|
new WritableStream(output2),
|
|
149
145
|
'track2',
|
|
150
|
-
false,
|
|
151
146
|
undefined,
|
|
152
147
|
);
|
|
153
148
|
|
|
@@ -198,7 +193,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
198
193
|
new ReadableStream(input1),
|
|
199
194
|
new WritableStream(output1),
|
|
200
195
|
'track1',
|
|
201
|
-
false,
|
|
202
196
|
undefined,
|
|
203
197
|
);
|
|
204
198
|
|
|
@@ -213,7 +207,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
213
207
|
new ReadableStream(input2),
|
|
214
208
|
new WritableStream(output2),
|
|
215
209
|
'track2',
|
|
216
|
-
false,
|
|
217
210
|
undefined,
|
|
218
211
|
);
|
|
219
212
|
|
|
@@ -244,7 +237,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
244
237
|
new ReadableStream(input1),
|
|
245
238
|
new WritableStream(output1),
|
|
246
239
|
'track1',
|
|
247
|
-
false,
|
|
248
240
|
undefined,
|
|
249
241
|
);
|
|
250
242
|
|
|
@@ -257,13 +249,12 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
257
249
|
const input2 = new TestUnderlyingSource<RTCEncodedVideoFrame>();
|
|
258
250
|
const output2 = new TestUnderlyingSink<RTCEncodedVideoFrame>();
|
|
259
251
|
|
|
260
|
-
// This should create a new transform
|
|
252
|
+
// This should create a new transform
|
|
261
253
|
cryptor.setupTransform(
|
|
262
254
|
'encode',
|
|
263
255
|
new ReadableStream(input2),
|
|
264
256
|
new WritableStream(output2),
|
|
265
257
|
'track2',
|
|
266
|
-
true, // isReuse=true
|
|
267
258
|
undefined,
|
|
268
259
|
);
|
|
269
260
|
|
|
@@ -274,7 +265,7 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
274
265
|
expect(cryptor.getTrackId()).toBe('track2');
|
|
275
266
|
});
|
|
276
267
|
|
|
277
|
-
it('should handle race between pipe completion and new setupTransform
|
|
268
|
+
it('should handle race between pipe completion and a new setupTransform', async () => {
|
|
278
269
|
const { cryptor, keys } = createCryptor('participant1');
|
|
279
270
|
await keys.setKey(await createKeyMaterialFromString('key1'), 0);
|
|
280
271
|
|
|
@@ -286,11 +277,10 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
286
277
|
new ReadableStream(input1),
|
|
287
278
|
new WritableStream(output1),
|
|
288
279
|
'track1',
|
|
289
|
-
false,
|
|
290
280
|
undefined,
|
|
291
281
|
);
|
|
292
282
|
|
|
293
|
-
// Immediately
|
|
283
|
+
// Immediately set up again (simulating a quick resubscribe)
|
|
294
284
|
const input2 = new TestUnderlyingSource<RTCEncodedVideoFrame>();
|
|
295
285
|
const output2 = new TestUnderlyingSink<RTCEncodedVideoFrame>();
|
|
296
286
|
|
|
@@ -298,8 +288,7 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
298
288
|
'encode',
|
|
299
289
|
new ReadableStream(input2),
|
|
300
290
|
new WritableStream(output2),
|
|
301
|
-
'track2',
|
|
302
|
-
true, // Should return early
|
|
291
|
+
'track2', // Should return early
|
|
303
292
|
undefined,
|
|
304
293
|
);
|
|
305
294
|
|
|
@@ -308,7 +297,7 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
308
297
|
|
|
309
298
|
await vitest.advanceTimersToNextTimerAsync();
|
|
310
299
|
|
|
311
|
-
// Now
|
|
300
|
+
// Now set up again after the first pipe completed
|
|
312
301
|
const input3 = new TestUnderlyingSource<RTCEncodedVideoFrame>();
|
|
313
302
|
const output3 = new TestUnderlyingSink<RTCEncodedVideoFrame>();
|
|
314
303
|
|
|
@@ -317,7 +306,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
317
306
|
new ReadableStream(input3),
|
|
318
307
|
new WritableStream(output3),
|
|
319
308
|
'track3',
|
|
320
|
-
true,
|
|
321
309
|
undefined,
|
|
322
310
|
);
|
|
323
311
|
|
|
@@ -345,7 +333,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
345
333
|
new ReadableStream(input),
|
|
346
334
|
new WritableStream(output),
|
|
347
335
|
'track1',
|
|
348
|
-
false,
|
|
349
336
|
undefined,
|
|
350
337
|
);
|
|
351
338
|
|
|
@@ -431,7 +418,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
431
418
|
new ReadableStream(input),
|
|
432
419
|
new WritableStream(output),
|
|
433
420
|
'track1',
|
|
434
|
-
false,
|
|
435
421
|
undefined,
|
|
436
422
|
);
|
|
437
423
|
|
|
@@ -469,7 +455,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
469
455
|
new ReadableStream(input),
|
|
470
456
|
new WritableStream(output),
|
|
471
457
|
'track1',
|
|
472
|
-
false,
|
|
473
458
|
'vp8',
|
|
474
459
|
);
|
|
475
460
|
|
|
@@ -509,7 +494,6 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
509
494
|
new ReadableStream(input1),
|
|
510
495
|
new WritableStream(output1),
|
|
511
496
|
'track1',
|
|
512
|
-
false,
|
|
513
497
|
undefined,
|
|
514
498
|
);
|
|
515
499
|
|
|
@@ -534,11 +518,10 @@ describe('FrameCryptor Race Conditions', () => {
|
|
|
534
518
|
new ReadableStream(input2),
|
|
535
519
|
new WritableStream(output2),
|
|
536
520
|
'track2',
|
|
537
|
-
true, // isReuse
|
|
538
521
|
undefined,
|
|
539
522
|
);
|
|
540
523
|
|
|
541
|
-
// Track ID should be updated
|
|
524
|
+
// Track ID should be updated
|
|
542
525
|
expect(cryptor.getTrackId()).toBe('track2');
|
|
543
526
|
expect(cryptor.getParticipantIdentity()).toBe('participant2');
|
|
544
527
|
});
|
|
@@ -84,6 +84,22 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
84
84
|
|
|
85
85
|
private currentTransform?: TransformerInfo;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The encoded streams this cryptor was last set up with, retained beyond the
|
|
89
|
+
* lifetime of {@link currentTransform}.
|
|
90
|
+
*
|
|
91
|
+
* The main thread transfers a receiver's encoded streams to the worker once and
|
|
92
|
+
* cannot transfer them again (a transferred stream is locked), and
|
|
93
|
+
* `createEncodedStreams()` may only be called once per receiver. So when a
|
|
94
|
+
* transceiver is reused, or when a pipe dies underneath us, re-establishing the
|
|
95
|
+
* transform is only possible from here. See {@link ensureTransform}.
|
|
96
|
+
*/
|
|
97
|
+
private retainedStreams?: {
|
|
98
|
+
readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>;
|
|
99
|
+
writable: WritableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>;
|
|
100
|
+
operation: 'encode' | 'decode';
|
|
101
|
+
};
|
|
102
|
+
|
|
87
103
|
/**
|
|
88
104
|
* Whether the subscribed track advertises packet trailer features.
|
|
89
105
|
* When false, we skip the per-frame trailer extraction path entirely
|
|
@@ -108,6 +124,11 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
108
124
|
|
|
109
125
|
private readonly ERROR_WINDOW_MS = 60000; // 1 minute window
|
|
110
126
|
|
|
127
|
+
private undecryptedTrackTimeout?: ReturnType<typeof setTimeout>;
|
|
128
|
+
|
|
129
|
+
/** grace period for a teardown or a resubscribe to land before we report a stalled track */
|
|
130
|
+
private readonly UNDECRYPTED_TRACK_GRACE_MS = 2000;
|
|
131
|
+
|
|
111
132
|
/**
|
|
112
133
|
* Tracks (participant, trackId, payloadType) tuples for which we've already logged a NALU
|
|
113
134
|
* fallback, so a persistent bad state doesn't flood the console (Firefox doesn't filter debug).
|
|
@@ -167,10 +188,13 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
167
188
|
unsetParticipant() {
|
|
168
189
|
workerLogger.debug('unsetting participant', this.logContext);
|
|
169
190
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
// NOTE: deliberately does not clear `currentTransform`. Nothing here cancels
|
|
192
|
+
// the pipe, so the transform really is still running and clearing it would
|
|
193
|
+
// desync our bookkeeping from reality -- which previously let a later
|
|
194
|
+
// resubscribe skip setup while frames kept flowing through a cryptor with no
|
|
195
|
+
// participant assigned.
|
|
196
|
+
clearTimeout(this.undecryptedTrackTimeout);
|
|
197
|
+
this.undecryptedTrackTimeout = undefined;
|
|
174
198
|
this.participantIdentity = undefined;
|
|
175
199
|
this.lastErrorTimestamp = new Map();
|
|
176
200
|
this.errorCounts = new Map();
|
|
@@ -192,6 +216,78 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
192
216
|
return this.trackId;
|
|
193
217
|
}
|
|
194
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Re-point this cryptor at a new trackId, keeping its (already transferred)
|
|
221
|
+
* encoded streams. Used when a transceiver is reused for a new track.
|
|
222
|
+
*/
|
|
223
|
+
setTrackId(trackId: string) {
|
|
224
|
+
if (this.trackId === trackId) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
workerLogger.debug('re-pointing cryptor at new trackId', {
|
|
228
|
+
...this.logContext,
|
|
229
|
+
newTrackId: trackId,
|
|
230
|
+
});
|
|
231
|
+
this.trackId = trackId;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
hasActiveTransform() {
|
|
235
|
+
return !!this.currentTransform;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A track that is subscribed and known to be encrypted, but has no transform to
|
|
240
|
+
* decrypt it, will never produce a decodable frame again. That state used to be
|
|
241
|
+
* completely silent (a black tile and endless PLIs), so report it.
|
|
242
|
+
*
|
|
243
|
+
* Deferred, because on teardown the encoded streams routinely close before the
|
|
244
|
+
* 'removeTransform' message arrives -- checking immediately would cry wolf on
|
|
245
|
+
* every unsubscribe.
|
|
246
|
+
*
|
|
247
|
+
* Only meaningful while decoding: a sender's pipe closing on unpublish is
|
|
248
|
+
* routine and has no 'removeTransform' equivalent to quiet it down.
|
|
249
|
+
*/
|
|
250
|
+
private scheduleUndecryptedTrackWatchdog(operation: 'encode' | 'decode') {
|
|
251
|
+
clearTimeout(this.undecryptedTrackTimeout);
|
|
252
|
+
this.undecryptedTrackTimeout = undefined;
|
|
253
|
+
if (operation !== 'decode') {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.undecryptedTrackTimeout = setTimeout(() => {
|
|
257
|
+
if (this.currentTransform || !this.isEnabled()) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
workerLogger.warn('encrypted track has no active decrypt transform', this.logContext);
|
|
261
|
+
this.emitThrottledError(
|
|
262
|
+
new CryptorError(
|
|
263
|
+
`no active decrypt transform for encrypted track ${this.trackId}`,
|
|
264
|
+
CryptorErrorReason.InternalError,
|
|
265
|
+
this.participantIdentity,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}, this.UNDECRYPTED_TRACK_GRACE_MS);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Re-establish the transform if it is gone while we still own the encoded
|
|
273
|
+
* streams, e.g. after a pipe died on its own (the swallowed
|
|
274
|
+
* 'Destination stream closed') or when a reused transceiver never got a new
|
|
275
|
+
* pipeline. Without this the frames pile up in a readable nobody reads and the
|
|
276
|
+
* decoder starves, which shows up as a permanently black tile.
|
|
277
|
+
*/
|
|
278
|
+
ensureTransform() {
|
|
279
|
+
if (this.currentTransform) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (!this.retainedStreams || this.trackId === undefined) {
|
|
283
|
+
workerLogger.warn('no streams retained, cannot re-establish transform', this.logContext);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const { readable, writable, operation } = this.retainedStreams;
|
|
287
|
+
workerLogger.info('re-establishing transform', { ...this.logContext, operation });
|
|
288
|
+
return this.setupTransform(operation, readable, writable, this.trackId);
|
|
289
|
+
}
|
|
290
|
+
|
|
195
291
|
/**
|
|
196
292
|
* Update the video codec used by the mediaStreamTrack
|
|
197
293
|
* @param codec
|
|
@@ -227,7 +323,6 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
227
323
|
readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
|
228
324
|
writable: WritableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
|
229
325
|
trackId: string,
|
|
230
|
-
isReuse: boolean,
|
|
231
326
|
codec?: VideoCodec,
|
|
232
327
|
frameMetadata?: FrameMetadataPublishOptions,
|
|
233
328
|
) {
|
|
@@ -243,27 +338,17 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
243
338
|
operation,
|
|
244
339
|
passedTrackId: trackId,
|
|
245
340
|
codec,
|
|
246
|
-
isReuse,
|
|
247
341
|
hasCurrentTransform: !!this.currentTransform,
|
|
248
342
|
...this.logContext,
|
|
249
343
|
});
|
|
250
344
|
|
|
251
|
-
// Always update trackId, even on reuse
|
|
252
345
|
this.trackId = trackId;
|
|
253
346
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
writable === this.currentTransform.writable
|
|
260
|
-
) {
|
|
261
|
-
workerLogger.debug('reusing existing transform', {
|
|
262
|
-
...this.logContext,
|
|
263
|
-
trackId,
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
347
|
+
// Retain the streams so we can rebuild the pipe later even if this cryptor
|
|
348
|
+
// gets detached from its participant in between (see `ensureTransform`).
|
|
349
|
+
this.retainedStreams = { readable, writable, operation };
|
|
350
|
+
|
|
351
|
+
clearTimeout(this.undecryptedTrackTimeout);
|
|
267
352
|
|
|
268
353
|
const symbol = Symbol('transform');
|
|
269
354
|
|
|
@@ -281,33 +366,54 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
281
366
|
symbol,
|
|
282
367
|
};
|
|
283
368
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
e
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
369
|
+
// pipeThrough/pipeTo throw synchronously on an already locked stream, which
|
|
370
|
+
// would bypass the .catch below and leave us with no transform at all.
|
|
371
|
+
try {
|
|
372
|
+
readable
|
|
373
|
+
.pipeThrough(transformStream)
|
|
374
|
+
.pipeTo(writable)
|
|
375
|
+
.catch((e) => {
|
|
376
|
+
if (e instanceof TypeError && e.message === 'Destination stream closed') {
|
|
377
|
+
// this can happen when subscriptions happen in quick successions, but doesn't influence functionality
|
|
378
|
+
workerLogger.debug('destination stream closed');
|
|
379
|
+
} else {
|
|
380
|
+
workerLogger.warn('transform error', { error: e, ...this.logContext });
|
|
381
|
+
this.emit(
|
|
382
|
+
CryptorEvent.Error,
|
|
383
|
+
e instanceof CryptorError
|
|
384
|
+
? e
|
|
385
|
+
: new CryptorError(e.message, undefined, this.participantIdentity),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
.finally(() => {
|
|
390
|
+
// Only clear currentTransform if it's still the same one we started
|
|
391
|
+
if (this.currentTransform?.symbol === symbol) {
|
|
392
|
+
workerLogger.debug('transform completed', {
|
|
393
|
+
...this.logContext,
|
|
394
|
+
trackId,
|
|
395
|
+
});
|
|
396
|
+
this.currentTransform = undefined;
|
|
397
|
+
}
|
|
398
|
+
// A pipe ending while the track is still assigned to a participant that
|
|
399
|
+
// publishes encrypted media means we have silently stopped decrypting.
|
|
400
|
+
this.scheduleUndecryptedTrackWatchdog(operation);
|
|
401
|
+
});
|
|
402
|
+
} catch (e: any) {
|
|
403
|
+
if (this.currentTransform?.symbol === symbol) {
|
|
404
|
+
this.currentTransform = undefined;
|
|
405
|
+
}
|
|
406
|
+
workerLogger.error('failed to set up transform', { error: e, ...this.logContext });
|
|
407
|
+
this.emit(
|
|
408
|
+
CryptorEvent.Error,
|
|
409
|
+
e instanceof CryptorError
|
|
410
|
+
? e
|
|
411
|
+
: new CryptorError(e.message, CryptorErrorReason.InternalError, this.participantIdentity),
|
|
412
|
+
);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return true;
|
|
311
417
|
}
|
|
312
418
|
|
|
313
419
|
setSifTrailer(trailer: NonSharedUint8Array) {
|
|
@@ -539,11 +645,38 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
539
645
|
}
|
|
540
646
|
}
|
|
541
647
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
648
|
+
// skip decryption for empty dtx frames
|
|
649
|
+
if (encodedFrame.data.byteLength === 0) {
|
|
650
|
+
return controller.enqueue(encodedFrame);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const encryptionEnabled = this.isEnabled();
|
|
654
|
+
|
|
655
|
+
if (encryptionEnabled === undefined) {
|
|
656
|
+
// Either way we drop: forwarding would hand ciphertext straight to the
|
|
657
|
+
// decoder, which renders as a black frame and logs nothing.
|
|
658
|
+
if (this.participantIdentity === undefined) {
|
|
659
|
+
// The pipe outlived its subscription. Expected on unsubscribe/disconnect:
|
|
660
|
+
// we deliberately leave the pipe running so a reused transceiver can be
|
|
661
|
+
// re-pointed at a new track, so in-flight frames still arrive here for a
|
|
662
|
+
// moment. Not worth reporting -- the watchdog covers a track that stays
|
|
663
|
+
// subscribed without a transform.
|
|
664
|
+
workerLogger.debug('dropping frame for unassigned cryptor', this.logContext);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
// A live subscription whose encryption state we were never told about.
|
|
668
|
+
this.emitThrottledError(
|
|
669
|
+
new CryptorError(
|
|
670
|
+
`encryption state unknown for track ${this.trackId}, dropping frame`,
|
|
671
|
+
CryptorErrorReason.InternalError,
|
|
672
|
+
this.participantIdentity,
|
|
673
|
+
),
|
|
674
|
+
);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!encryptionEnabled) {
|
|
679
|
+
// participant is known to publish unencrypted media
|
|
547
680
|
return controller.enqueue(encodedFrame);
|
|
548
681
|
}
|
|
549
682
|
|
|
@@ -71,7 +71,6 @@ onmessage = (ev) => {
|
|
|
71
71
|
data.readableStream,
|
|
72
72
|
data.writableStream,
|
|
73
73
|
data.trackId,
|
|
74
|
-
data.isReuse,
|
|
75
74
|
data.codec,
|
|
76
75
|
);
|
|
77
76
|
break;
|
|
@@ -83,7 +82,6 @@ onmessage = (ev) => {
|
|
|
83
82
|
data.readableStream,
|
|
84
83
|
data.writableStream,
|
|
85
84
|
data.trackId,
|
|
86
|
-
data.isReuse,
|
|
87
85
|
data.codec,
|
|
88
86
|
data.packetTrailer, // wire format still uses 'packetTrailer' field name
|
|
89
87
|
);
|
|
@@ -163,15 +161,32 @@ onmessage = (ev) => {
|
|
|
163
161
|
unsetCryptorParticipant(data.trackId, data.participantIdentity);
|
|
164
162
|
break;
|
|
165
163
|
case 'updateCodec':
|
|
166
|
-
const trackCryptor = getTrackCryptor(
|
|
167
|
-
|
|
164
|
+
const trackCryptor = getTrackCryptor(
|
|
165
|
+
data.participantIdentity,
|
|
166
|
+
data.trackId,
|
|
167
|
+
data.previousTrackId,
|
|
168
|
+
);
|
|
169
|
+
if (data.codec) {
|
|
170
|
+
trackCryptor.setVideoCodec(data.codec);
|
|
171
|
+
}
|
|
168
172
|
trackCryptor.setHasFrameMetadata(data.hasPacketTrailer);
|
|
169
173
|
workerLogger.info('updated codec', {
|
|
170
174
|
participantIdentity: data.participantIdentity,
|
|
171
175
|
trackId: data.trackId,
|
|
176
|
+
previousTrackId: data.previousTrackId,
|
|
172
177
|
codec: data.codec,
|
|
173
178
|
hasPacketTrailer: data.hasPacketTrailer,
|
|
174
179
|
});
|
|
180
|
+
// Sent on transceiver reuse, where the main thread cannot hand us a new
|
|
181
|
+
// pipeline. If the previous one died in the meantime we have to rebuild it
|
|
182
|
+
// here or no frame ever gets decrypted again.
|
|
183
|
+
if (data.previousTrackId !== undefined && !trackCryptor.ensureTransform()) {
|
|
184
|
+
workerLogger.error('could not re-establish transform for reused receiver', {
|
|
185
|
+
participantIdentity: data.participantIdentity,
|
|
186
|
+
trackId: data.trackId,
|
|
187
|
+
previousTrackId: data.previousTrackId,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
175
190
|
break;
|
|
176
191
|
case 'setRTPMap':
|
|
177
192
|
// this is only used for the local participant
|
|
@@ -210,8 +225,26 @@ async function handleRatchetRequest(data: RatchetRequestMessage['data']) {
|
|
|
210
225
|
}
|
|
211
226
|
}
|
|
212
227
|
|
|
213
|
-
function getTrackCryptor(participantIdentity: string, trackId: string) {
|
|
228
|
+
function getTrackCryptor(participantIdentity: string, trackId: string, previousTrackId?: string) {
|
|
214
229
|
let cryptors = participantCryptors.filter((c) => c.getTrackId() === trackId);
|
|
230
|
+
|
|
231
|
+
if (cryptors.length === 0 && previousTrackId !== undefined && previousTrackId !== trackId) {
|
|
232
|
+
// A reused transceiver carries a new trackId, but the cryptor that owns this
|
|
233
|
+
// receiver's encoded streams is still keyed on the old one. Re-point it instead
|
|
234
|
+
// of creating a fresh cryptor that has no pipeline (and leaving the old one
|
|
235
|
+
// piping frames with no participant assigned).
|
|
236
|
+
const previous = participantCryptors.filter((c) => c.getTrackId() === previousTrackId);
|
|
237
|
+
if (previous.length > 0) {
|
|
238
|
+
workerLogger.info('reusing cryptor from previous trackId', {
|
|
239
|
+
participantIdentity,
|
|
240
|
+
trackId,
|
|
241
|
+
previousTrackId,
|
|
242
|
+
});
|
|
243
|
+
previous[0].setTrackId(trackId);
|
|
244
|
+
cryptors = previous;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
215
248
|
if (cryptors.length > 1) {
|
|
216
249
|
const debugInfo = cryptors
|
|
217
250
|
.map((c) => {
|
|
@@ -351,7 +384,6 @@ if (self.RTCTransformEvent) {
|
|
|
351
384
|
transformer.readable,
|
|
352
385
|
transformer.writable,
|
|
353
386
|
trackId,
|
|
354
|
-
false,
|
|
355
387
|
codec,
|
|
356
388
|
kind === 'encode' ? options.packetTrailer : undefined,
|
|
357
389
|
);
|
package/src/index.ts
CHANGED
|
@@ -53,12 +53,14 @@ import {
|
|
|
53
53
|
isLocalTrack,
|
|
54
54
|
isRemoteParticipant,
|
|
55
55
|
isRemoteTrack,
|
|
56
|
+
isSVCCodec,
|
|
56
57
|
isVideoCodec,
|
|
57
58
|
isVideoTrack,
|
|
58
59
|
supportsAV1,
|
|
59
60
|
supportsAdaptiveStream,
|
|
60
61
|
supportsAudioOutputSelection,
|
|
61
62
|
supportsDynacast,
|
|
63
|
+
supportsH265,
|
|
62
64
|
supportsVP9,
|
|
63
65
|
} from './room/utils';
|
|
64
66
|
import { getBrowser } from './utils/browserParser';
|
|
@@ -149,12 +151,14 @@ export {
|
|
|
149
151
|
supportsAdaptiveStream,
|
|
150
152
|
supportsAudioOutputSelection,
|
|
151
153
|
supportsDynacast,
|
|
154
|
+
supportsH265,
|
|
152
155
|
supportsVP9,
|
|
153
156
|
Mutex,
|
|
154
157
|
isAudioCodec,
|
|
155
158
|
isAudioTrack,
|
|
156
159
|
isLocalTrack,
|
|
157
160
|
isRemoteTrack,
|
|
161
|
+
isSVCCodec,
|
|
158
162
|
isVideoCodec,
|
|
159
163
|
isVideoTrack,
|
|
160
164
|
isLocalParticipant,
|
|
@@ -180,6 +184,7 @@ export type {
|
|
|
180
184
|
DataTrackSubscribeOptions,
|
|
181
185
|
RemoteDataTrackPipelineOptions,
|
|
182
186
|
};
|
|
187
|
+
export { type DataTrackFrame } from './room/data-track/frame';
|
|
183
188
|
export { DataTrackPacket, type DataTrackPacketHeader } from './room/data-track/packet';
|
|
184
189
|
export {
|
|
185
190
|
type DataTrackExtensions,
|
package/src/logger.ts
CHANGED
package/src/options.ts
CHANGED
|
@@ -120,6 +120,30 @@ export interface InternalRoomOptions {
|
|
|
120
120
|
* @default true
|
|
121
121
|
*/
|
|
122
122
|
singlePeerConnection: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Options controlling data stream behavior for this room.
|
|
126
|
+
*/
|
|
127
|
+
dataStream?: RoomDataStreamOptions;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Options controlling data stream behavior for a room.
|
|
132
|
+
*/
|
|
133
|
+
export interface RoomDataStreamOptions {
|
|
134
|
+
/**
|
|
135
|
+
* Maximum size, in bytes, of the payload this client accepts from a single incoming data stream.
|
|
136
|
+
*
|
|
137
|
+
* A compressed stream can inflate to an arbitrarily large payload, so the decompressed output is
|
|
138
|
+
* bounded rather than trusting the size declared on the wire. An incoming stream that exceeds the
|
|
139
|
+
* cap fails with a `DataStreamErrorReason.PayloadTooLarge` error on the next read instead of
|
|
140
|
+
* buffering without bound.
|
|
141
|
+
*
|
|
142
|
+
* This is enforced on the receiving side only: raising it on a sender has no effect.
|
|
143
|
+
*
|
|
144
|
+
* @default 5_000_000_000 (5 GB)
|
|
145
|
+
*/
|
|
146
|
+
maxPayloadByteLength?: number;
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
/**
|
package/src/room/PCTransport.ts
CHANGED
|
@@ -597,8 +597,9 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
597
597
|
return this.pc?.remoteDescription;
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
/** stats of the underlying connection, `undefined` when there is none */
|
|
600
601
|
getStats() {
|
|
601
|
-
return this.
|
|
602
|
+
return this._pc?.getStats();
|
|
602
603
|
}
|
|
603
604
|
|
|
604
605
|
getMaxMessageSize() {
|