livekit-client 2.18.8 → 2.18.10

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 (193) 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 +5609 -644
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2898 -2431
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.pt.worker.js +2 -0
  8. package/dist/livekit-client.pt.worker.js.map +1 -0
  9. package/dist/livekit-client.pt.worker.mjs +5834 -0
  10. package/dist/livekit-client.pt.worker.mjs.map +1 -0
  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 +2 -1
  14. package/dist/src/api/SignalClient.d.ts.map +1 -1
  15. package/dist/src/e2ee/E2eeManager.d.ts +8 -7
  16. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  17. package/dist/src/e2ee/types.d.ts +35 -8
  18. package/dist/src/e2ee/types.d.ts.map +1 -1
  19. package/dist/src/e2ee/utils.d.ts +5 -5
  20. package/dist/src/e2ee/utils.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/DataCryptor.d.ts +5 -5
  22. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/FrameCryptor.d.ts +21 -4
  24. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  25. package/dist/src/e2ee/worker/naluUtils.d.ts +1 -1
  26. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  27. package/dist/src/e2ee/worker/sifPayload.d.ts +7 -7
  28. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  29. package/dist/src/index.d.ts +4 -1
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/options.d.ts +7 -0
  32. package/dist/src/options.d.ts.map +1 -1
  33. package/dist/src/packetTrailer/PacketTrailerManager.d.ts +49 -0
  34. package/dist/src/packetTrailer/PacketTrailerManager.d.ts.map +1 -0
  35. package/dist/src/packetTrailer/packetTrailer.d.ts +32 -0
  36. package/dist/src/packetTrailer/packetTrailer.d.ts.map +1 -0
  37. package/dist/src/packetTrailer/types.d.ts +57 -0
  38. package/dist/src/packetTrailer/types.d.ts.map +1 -0
  39. package/dist/src/packetTrailer/utils.d.ts +9 -0
  40. package/dist/src/packetTrailer/utils.d.ts.map +1 -0
  41. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  42. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +1 -0
  43. package/dist/src/room/RTCEngine.d.ts +2 -1
  44. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  45. package/dist/src/room/Room.d.ts +3 -1
  46. package/dist/src/room/Room.d.ts.map +1 -1
  47. package/dist/src/room/data-track/LocalDataTrack.d.ts +2 -1
  48. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  49. package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -1
  50. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  51. package/dist/src/room/data-track/depacketizer.d.ts +12 -4
  52. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  53. package/dist/src/room/data-track/frame.d.ts +3 -3
  54. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  55. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  56. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  57. package/dist/src/room/data-track/incoming/pipeline.d.ts +4 -1
  58. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  59. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +2 -2
  60. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  61. package/dist/src/room/data-track/outgoing/types.d.ts +4 -3
  62. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  63. package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
  64. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  65. package/dist/src/room/data-track/packet/index.d.ts +5 -5
  66. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  67. package/dist/src/room/data-track/packet/serializable.d.ts +1 -1
  68. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  69. package/dist/src/room/data-track/types.d.ts +7 -0
  70. package/dist/src/room/data-track/types.d.ts.map +1 -1
  71. package/dist/src/room/events.d.ts +2 -2
  72. package/dist/src/room/participant/LocalParticipant.d.ts +3 -1
  73. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  74. package/dist/src/room/participant/Participant.d.ts +1 -1
  75. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  76. package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
  77. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
  78. package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
  79. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  80. package/dist/src/room/track/Track.d.ts +1 -1
  81. package/dist/src/room/track/Track.d.ts.map +1 -1
  82. package/dist/src/room/track/create.d.ts.map +1 -1
  83. package/dist/src/room/track/options.d.ts +10 -0
  84. package/dist/src/room/track/options.d.ts.map +1 -1
  85. package/dist/src/room/track/utils.d.ts.map +1 -1
  86. package/dist/src/room/utils.d.ts +4 -3
  87. package/dist/src/room/utils.d.ts.map +1 -1
  88. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  89. package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
  90. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  91. package/dist/src/version.d.ts +1 -1
  92. package/dist/ts4.2/api/SignalClient.d.ts +2 -1
  93. package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
  94. package/dist/ts4.2/e2ee/types.d.ts +35 -8
  95. package/dist/ts4.2/e2ee/utils.d.ts +5 -5
  96. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
  97. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
  98. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
  99. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
  100. package/dist/ts4.2/index.d.ts +5 -1
  101. package/dist/ts4.2/options.d.ts +7 -0
  102. package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
  103. package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
  104. package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
  105. package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
  106. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  107. package/dist/ts4.2/room/RTCEngine.d.ts +2 -1
  108. package/dist/ts4.2/room/Room.d.ts +3 -1
  109. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +2 -1
  110. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
  111. package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
  112. package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
  113. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  114. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
  115. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +2 -2
  116. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +4 -3
  117. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
  118. package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
  119. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
  120. package/dist/ts4.2/room/data-track/types.d.ts +7 -0
  121. package/dist/ts4.2/room/events.d.ts +2 -2
  122. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +3 -1
  123. package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
  124. package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
  125. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
  126. package/dist/ts4.2/room/track/Track.d.ts +1 -1
  127. package/dist/ts4.2/room/track/options.d.ts +10 -0
  128. package/dist/ts4.2/room/utils.d.ts +4 -3
  129. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
  130. package/dist/ts4.2/version.d.ts +1 -1
  131. package/package.json +24 -16
  132. package/src/api/SignalClient.test.ts +102 -10
  133. package/src/api/SignalClient.ts +4 -2
  134. package/src/api/WebSocketStream.test.ts +0 -1
  135. package/src/e2ee/E2eeManager.ts +82 -30
  136. package/src/e2ee/types.ts +37 -8
  137. package/src/e2ee/utils.ts +7 -6
  138. package/src/e2ee/worker/DataCryptor.ts +6 -6
  139. package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
  140. package/src/e2ee/worker/FrameCryptor.ts +94 -14
  141. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
  142. package/src/e2ee/worker/e2ee.worker.ts +13 -5
  143. package/src/e2ee/worker/naluUtils.ts +4 -4
  144. package/src/e2ee/worker/sifPayload.ts +10 -8
  145. package/src/index.ts +7 -0
  146. package/src/options.ts +8 -0
  147. package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
  148. package/src/packetTrailer/PacketTrailerManager.ts +250 -0
  149. package/src/packetTrailer/packetTrailer.test.ts +174 -0
  150. package/src/packetTrailer/packetTrailer.ts +276 -0
  151. package/src/packetTrailer/types.ts +75 -0
  152. package/src/packetTrailer/utils.test.ts +105 -0
  153. package/src/packetTrailer/utils.ts +50 -0
  154. package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
  155. package/src/packetTrailer/worker/tsconfig.json +14 -0
  156. package/src/room/BackOffStrategy.test.ts +1 -1
  157. package/src/room/RTCEngine.test.ts +219 -0
  158. package/src/room/RTCEngine.ts +86 -20
  159. package/src/room/Room.test.ts +62 -1
  160. package/src/room/Room.ts +28 -5
  161. package/src/room/data-track/LocalDataTrack.ts +15 -7
  162. package/src/room/data-track/RemoteDataTrack.ts +8 -1
  163. package/src/room/data-track/depacketizer.test.ts +433 -1
  164. package/src/room/data-track/depacketizer.ts +79 -61
  165. package/src/room/data-track/frame.ts +2 -2
  166. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
  167. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
  168. package/src/room/data-track/incoming/pipeline.ts +13 -2
  169. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +350 -198
  170. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +9 -3
  171. package/src/room/data-track/outgoing/types.ts +4 -3
  172. package/src/room/data-track/packet/extensions.ts +2 -2
  173. package/src/room/data-track/packet/index.ts +6 -6
  174. package/src/room/data-track/packet/serializable.ts +1 -1
  175. package/src/room/data-track/types.ts +8 -0
  176. package/src/room/events.ts +2 -2
  177. package/src/room/participant/LocalParticipant.test.ts +81 -0
  178. package/src/room/participant/LocalParticipant.ts +48 -7
  179. package/src/room/participant/Participant.ts +1 -1
  180. package/src/room/participant/publishUtils.ts +1 -1
  181. package/src/room/track/PacketTrailerExtractor.ts +43 -0
  182. package/src/room/track/RemoteVideoTrack.ts +23 -2
  183. package/src/room/track/Track.ts +1 -1
  184. package/src/room/track/create.ts +0 -4
  185. package/src/room/track/options.ts +11 -0
  186. package/src/room/track/record.ts +1 -1
  187. package/src/room/track/utils.ts +4 -1
  188. package/src/room/utils.test.ts +14 -1
  189. package/src/room/utils.ts +17 -3
  190. package/src/test/MockMediaStreamTrack.ts +0 -1
  191. package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
  192. package/src/utils/dataPacketBuffer.ts +1 -1
  193. package/src/version.ts +1 -1
@@ -1,13 +1,24 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  // TODO code inspired by https://github.com/webrtc/samples/blob/gh-pages/src/content/insertable-streams/endtoend-encryption/js/worker.js
3
2
  import { EventEmitter } from 'events';
4
3
  import type TypedEventEmitter from 'typed-emitter';
5
4
  import { workerLogger } from '../../logger';
5
+ import {
6
+ appendPacketTrailerToEncodedFrame,
7
+ processPacketTrailer,
8
+ } from '../../packetTrailer/packetTrailer';
9
+ import type { PacketTrailerPublishOptions } from '../../packetTrailer/types';
10
+ import { hasPacketTrailerPublishOptions } from '../../packetTrailer/utils';
6
11
  import type { VideoCodec } from '../../room/track/options';
7
12
  import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
8
13
  import { CryptorError, CryptorErrorReason } from '../errors';
9
14
  import { type CryptorCallbacks, CryptorEvent } from '../events';
10
- import type { DecodeRatchetOptions, KeyProviderOptions, KeySet, RatchetResult } from '../types';
15
+ import type {
16
+ DecodeRatchetOptions,
17
+ KeyProviderOptions,
18
+ KeySet,
19
+ PTMetadataFromE2EEMessage,
20
+ RatchetResult,
21
+ } from '../types';
11
22
  import { deriveKeys, isVideoFrame, needsRbspUnescaping, parseRbsp, writeRbsp } from '../utils';
12
23
  import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
13
24
  import { processNALUsForEncryption } from './naluUtils';
@@ -65,12 +76,23 @@ export class FrameCryptor extends BaseFrameCryptor {
65
76
  /**
66
77
  * used for detecting server injected unencrypted frames
67
78
  */
68
- private sifTrailer: Uint8Array;
79
+ private sifTrailer: NonSharedUint8Array;
69
80
 
70
81
  private detectedCodec?: VideoCodec;
71
82
 
72
83
  private currentTransform?: TransformerInfo;
73
84
 
85
+ /**
86
+ * Whether the subscribed track advertises packet trailer features.
87
+ * When false, we skip the per-frame trailer extraction path entirely
88
+ * on decode to avoid unnecessary work on tracks that don't use it.
89
+ */
90
+ private hasPacketTrailer: boolean = false;
91
+
92
+ private packetTrailer?: PacketTrailerPublishOptions;
93
+
94
+ private packetTrailerFrameId = 0;
95
+
74
96
  /**
75
97
  * Throttling mechanism for decryption errors to prevent memory leaks
76
98
  */
@@ -88,7 +110,7 @@ export class FrameCryptor extends BaseFrameCryptor {
88
110
  keys: ParticipantKeyHandler;
89
111
  participantIdentity: string;
90
112
  keyProviderOptions: KeyProviderOptions;
91
- sifTrailer?: Uint8Array;
113
+ sifTrailer?: NonSharedUint8Array;
92
114
  }) {
93
115
  super();
94
116
  this.sendCounts = new Map();
@@ -178,6 +200,20 @@ export class FrameCryptor extends BaseFrameCryptor {
178
200
  this.rtpMap = map;
179
201
  }
180
202
 
203
+ /**
204
+ * Sets whether the track associated with this cryptor carries packet
205
+ * trailer data. When false, {@link decodeFunction} skips the per-frame
206
+ * trailer extraction branch entirely.
207
+ */
208
+ setHasPacketTrailer(hasPacketTrailer: boolean) {
209
+ this.hasPacketTrailer = hasPacketTrailer;
210
+ }
211
+
212
+ setPacketTrailer(packetTrailer?: PacketTrailerPublishOptions) {
213
+ this.packetTrailer = packetTrailer;
214
+ this.packetTrailerFrameId = 0;
215
+ }
216
+
181
217
  setupTransform(
182
218
  operation: 'encode' | 'decode',
183
219
  readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
@@ -185,11 +221,15 @@ export class FrameCryptor extends BaseFrameCryptor {
185
221
  trackId: string,
186
222
  isReuse: boolean,
187
223
  codec?: VideoCodec,
224
+ packetTrailer?: PacketTrailerPublishOptions,
188
225
  ) {
189
226
  if (codec) {
190
227
  workerLogger.info('setting codec on cryptor to', { codec });
191
228
  this.videoCodec = codec;
192
229
  }
230
+ if (operation === 'encode') {
231
+ this.setPacketTrailer(packetTrailer);
232
+ }
193
233
 
194
234
  workerLogger.debug('Setting up frame cryptor transform', {
195
235
  operation,
@@ -262,7 +302,7 @@ export class FrameCryptor extends BaseFrameCryptor {
262
302
  });
263
303
  }
264
304
 
265
- setSifTrailer(trailer: Uint8Array) {
305
+ setSifTrailer(trailer: NonSharedUint8Array) {
266
306
  workerLogger.debug('setting SIF trailer', { ...this.logContext, trailer });
267
307
  this.sifTrailer = trailer;
268
308
  }
@@ -353,11 +393,13 @@ export class FrameCryptor extends BaseFrameCryptor {
353
393
  encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
354
394
  controller: TransformStreamDefaultController,
355
395
  ) {
356
- if (
357
- !this.isEnabled() ||
358
- // skip for encryption for empty dtx frames
359
- encodedFrame.data.byteLength === 0
360
- ) {
396
+ // skip for encryption and packet trailer writes for empty dtx frames
397
+ if (encodedFrame.data.byteLength === 0) {
398
+ return controller.enqueue(encodedFrame);
399
+ }
400
+
401
+ if (!this.isEnabled()) {
402
+ this.appendPacketTrailer(encodedFrame);
361
403
  return controller.enqueue(encodedFrame);
362
404
  }
363
405
  const keySet = this.keys.getKeySet();
@@ -410,7 +452,7 @@ export class FrameCryptor extends BaseFrameCryptor {
410
452
  new Uint8Array(encodedFrame.data, frameInfo.unencryptedBytes),
411
453
  );
412
454
 
413
- let newDataWithoutHeader = new Uint8Array(
455
+ let newDataWithoutHeader: NonSharedUint8Array = new Uint8Array(
414
456
  cipherText.byteLength + iv.byteLength + frameTrailer.byteLength,
415
457
  );
416
458
  newDataWithoutHeader.set(new Uint8Array(cipherText)); // add ciphertext.
@@ -426,6 +468,7 @@ export class FrameCryptor extends BaseFrameCryptor {
426
468
  newData.set(newDataWithoutHeader, frameHeader.byteLength);
427
469
 
428
470
  encodedFrame.data = newData.buffer;
471
+ this.appendPacketTrailer(encodedFrame);
429
472
 
430
473
  return controller.enqueue(encodedFrame);
431
474
  } catch (e: any) {
@@ -444,6 +487,18 @@ export class FrameCryptor extends BaseFrameCryptor {
444
487
  }
445
488
  }
446
489
 
490
+ private appendPacketTrailer(encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame) {
491
+ if (!hasPacketTrailerPublishOptions(this.packetTrailer) || !isVideoFrame(encodedFrame)) {
492
+ return;
493
+ }
494
+
495
+ if (this.packetTrailer?.frameId) {
496
+ this.packetTrailerFrameId =
497
+ this.packetTrailerFrameId === 0xffffffff ? 1 : this.packetTrailerFrameId + 1;
498
+ }
499
+ appendPacketTrailerToEncodedFrame(encodedFrame, this.packetTrailer, this.packetTrailerFrameId);
500
+ }
501
+
447
502
  /**
448
503
  * Function that will be injected in a stream and will decrypt the given encoded frames.
449
504
  *
@@ -454,6 +509,24 @@ export class FrameCryptor extends BaseFrameCryptor {
454
509
  encodedFrame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
455
510
  controller: TransformStreamDefaultController,
456
511
  ) {
512
+ if (this.hasPacketTrailer && isVideoFrame(encodedFrame)) {
513
+ try {
514
+ const ptResult = processPacketTrailer(encodedFrame, this.trackId);
515
+ if (ptResult.data) {
516
+ encodedFrame.data = ptResult.data;
517
+ }
518
+ if (ptResult.payload && this.participantIdentity) {
519
+ const msg: PTMetadataFromE2EEMessage = {
520
+ kind: 'packetTrailerMetadata',
521
+ data: ptResult.payload,
522
+ };
523
+ postMessage(msg);
524
+ }
525
+ } catch {
526
+ // best-effort: never break the media pipeline if trailer parsing fails
527
+ }
528
+ }
529
+
457
530
  if (
458
531
  !this.isEnabled() ||
459
532
  // skip for decryption for empty dtx frames
@@ -540,8 +613,12 @@ export class FrameCryptor extends BaseFrameCryptor {
540
613
  // ---------+-------------------------+-+---------+----
541
614
 
542
615
  try {
543
- const frameHeader = new Uint8Array(encodedFrame.data, 0, frameInfo.unencryptedBytes);
544
- var encryptedData = new Uint8Array(
616
+ const frameHeader: NonSharedUint8Array = new Uint8Array(
617
+ encodedFrame.data,
618
+ 0,
619
+ frameInfo.unencryptedBytes,
620
+ );
621
+ var encryptedData: NonSharedUint8Array = new Uint8Array(
545
622
  encodedFrame.data,
546
623
  frameHeader.length,
547
624
  encodedFrame.data.byteLength - frameHeader.length,
@@ -758,7 +835,10 @@ export class FrameCryptor extends BaseFrameCryptor {
758
835
  * by the livekit server and thus to be treated as unencrypted
759
836
  * @internal
760
837
  */
761
- export function isFrameServerInjected(frameData: ArrayBuffer, trailerBytes: Uint8Array): boolean {
838
+ export function isFrameServerInjected(
839
+ frameData: ArrayBuffer,
840
+ trailerBytes: NonSharedUint8Array,
841
+ ): boolean {
762
842
  if (trailerBytes.byteLength === 0) {
763
843
  return false;
764
844
  }
@@ -261,7 +261,7 @@ describe('ParticipantKeyHandler', () => {
261
261
 
262
262
  await keyHandler.setKey(originalMaterial);
263
263
 
264
- const ciphertexts: Uint8Array[] = [];
264
+ const ciphertexts: NonSharedUint8Array[] = [];
265
265
 
266
266
  const plaintext = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
267
267
 
@@ -354,8 +354,8 @@ describe('ParticipantKeyHandler', () => {
354
354
  async function encrypt(
355
355
  participantKeyHandler: ParticipantKeyHandler,
356
356
  keyIndex: number,
357
- iv: Uint8Array,
358
- data: Uint8Array,
357
+ iv: NonSharedUint8Array,
358
+ data: NonSharedUint8Array,
359
359
  ): Promise<ArrayBuffer> {
360
360
  return crypto.subtle.encrypt(
361
361
  {
@@ -370,7 +370,7 @@ describe('ParticipantKeyHandler', () => {
370
370
  async function decrypt(
371
371
  participantKeyHandler: ParticipantKeyHandler,
372
372
  keyIndex: number,
373
- iv: Uint8Array,
373
+ iv: NonSharedUint8Array,
374
374
  cipherText: ArrayBuffer,
375
375
  ): Promise<ArrayBuffer> {
376
376
  return crypto.subtle.decrypt(
@@ -29,7 +29,7 @@ let isEncryptionEnabled: boolean = false;
29
29
 
30
30
  let useSharedKey: boolean = false;
31
31
 
32
- let sifTrailer: Uint8Array | undefined;
32
+ let sifTrailer: NonSharedUint8Array | undefined;
33
33
 
34
34
  let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS;
35
35
 
@@ -64,6 +64,7 @@ onmessage = (ev) => {
64
64
  break;
65
65
  case 'decode':
66
66
  let cryptor = getTrackCryptor(data.participantIdentity, data.trackId);
67
+ cryptor.setHasPacketTrailer(data.hasPacketTrailer);
67
68
  cryptor.setupTransform(
68
69
  kind,
69
70
  data.readableStream,
@@ -75,6 +76,7 @@ onmessage = (ev) => {
75
76
  break;
76
77
  case 'encode':
77
78
  let pubCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
79
+ pubCryptor.setHasPacketTrailer(data.hasPacketTrailer);
78
80
  pubCryptor.setupTransform(
79
81
  kind,
80
82
  data.readableStream,
@@ -82,6 +84,7 @@ onmessage = (ev) => {
82
84
  data.trackId,
83
85
  data.isReuse,
84
86
  data.codec,
87
+ data.packetTrailer,
85
88
  );
86
89
  break;
87
90
 
@@ -159,11 +162,14 @@ onmessage = (ev) => {
159
162
  unsetCryptorParticipant(data.trackId, data.participantIdentity);
160
163
  break;
161
164
  case 'updateCodec':
162
- getTrackCryptor(data.participantIdentity, data.trackId).setVideoCodec(data.codec);
165
+ const trackCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
166
+ trackCryptor.setVideoCodec(data.codec);
167
+ trackCryptor.setHasPacketTrailer(data.hasPacketTrailer);
163
168
  workerLogger.info('updated codec', {
164
169
  participantIdentity: data.participantIdentity,
165
170
  trackId: data.trackId,
166
171
  codec: data.codec,
172
+ hasPacketTrailer: data.hasPacketTrailer,
167
173
  });
168
174
  break;
169
175
  case 'setRTPMap':
@@ -319,7 +325,7 @@ function emitRatchetedKeys(
319
325
  postMessage(msg);
320
326
  }
321
327
 
322
- function handleSifTrailer(trailer: Uint8Array) {
328
+ function handleSifTrailer(trailer: NonSharedUint8Array) {
323
329
  sifTrailer = trailer;
324
330
  participantCryptors.forEach((c) => {
325
331
  c.setSifTrailer(trailer);
@@ -333,10 +339,11 @@ if (self.RTCTransformEvent) {
333
339
  self.onrtctransform = (event: RTCTransformEvent) => {
334
340
  // @ts-ignore
335
341
  const transformer = event.transformer;
336
- const { kind, participantIdentity, trackId, codec } =
337
- transformer.options as ScriptTransformOptions;
342
+ const options = transformer.options as ScriptTransformOptions;
343
+ const { kind, participantIdentity, trackId, codec, hasPacketTrailer } = options;
338
344
  messageQueue.run(async () => {
339
345
  const cryptor = getTrackCryptor(participantIdentity, trackId);
346
+ cryptor.setHasPacketTrailer(hasPacketTrailer);
340
347
  workerLogger.debug('onrtctransform setup', { participantIdentity, trackId, codec });
341
348
  cryptor.setupTransform(
342
349
  kind,
@@ -345,6 +352,7 @@ if (self.RTCTransformEvent) {
345
352
  trackId,
346
353
  false,
347
354
  codec,
355
+ kind === 'encode' ? options.packetTrailer : undefined,
348
356
  );
349
357
  });
350
358
  };
@@ -200,7 +200,7 @@ export interface NALUProcessingResult {
200
200
  * @param naluIndices Indices where NALUs start
201
201
  * @returns Detected codec type
202
202
  */
203
- function detectCodecFromNALUs(data: Uint8Array, naluIndices: number[]): DetectedCodec {
203
+ function detectCodecFromNALUs(data: NonSharedUint8Array, naluIndices: number[]): DetectedCodec {
204
204
  for (const naluIndex of naluIndices) {
205
205
  if (isH264SliceNALU(parseH264NALUType(data[naluIndex]))) return 'h264';
206
206
  if (isH265SliceNALU(parseH265NALUType(data[naluIndex]))) return 'h265';
@@ -216,7 +216,7 @@ function detectCodecFromNALUs(data: Uint8Array, naluIndices: number[]): Detected
216
216
  * @returns Number of unencrypted bytes (index + 2) or null if no slice found
217
217
  */
218
218
  function findSliceNALUUnencryptedBytes(
219
- data: Uint8Array,
219
+ data: NonSharedUint8Array,
220
220
  naluIndices: number[],
221
221
  codec: 'h264' | 'h265',
222
222
  ): number | null {
@@ -246,7 +246,7 @@ function findSliceNALUUnencryptedBytes(
246
246
  * @param stream Byte stream containing NALUs
247
247
  * @returns Array of indices where NALUs start (after the start code)
248
248
  */
249
- function findNALUIndices(stream: Uint8Array): number[] {
249
+ function findNALUIndices(stream: NonSharedUint8Array): number[] {
250
250
  const result: number[] = [];
251
251
  let start = 0,
252
252
  pos = 0,
@@ -309,7 +309,7 @@ function findNALUIndices(stream: Uint8Array): number[] {
309
309
  * @returns NALU processing result
310
310
  */
311
311
  export function processNALUsForEncryption(
312
- data: Uint8Array,
312
+ data: NonSharedUint8Array,
313
313
  knownCodec?: 'h264' | 'h265',
314
314
  ): NALUProcessingResult {
315
315
  const naluIndices = findNALUIndices(data);
@@ -2,29 +2,31 @@ import type { VideoCodec } from '../..';
2
2
 
3
3
  // Payload definitions taken from https://github.com/livekit/livekit/blob/master/pkg/sfu/downtrack.go#L104
4
4
 
5
- export const VP8KeyFrame8x8: Uint8Array = new Uint8Array([
5
+ export const VP8KeyFrame8x8: NonSharedUint8Array = new Uint8Array([
6
6
  0x10, 0x02, 0x00, 0x9d, 0x01, 0x2a, 0x08, 0x00, 0x08, 0x00, 0x00, 0x47, 0x08, 0x85, 0x85, 0x88,
7
7
  0x85, 0x84, 0x88, 0x02, 0x02, 0x00, 0x0c, 0x0d, 0x60, 0x00, 0xfe, 0xff, 0xab, 0x50, 0x80,
8
8
  ]);
9
9
 
10
- export const H264KeyFrame2x2SPS: Uint8Array = new Uint8Array([
10
+ export const H264KeyFrame2x2SPS: NonSharedUint8Array = new Uint8Array([
11
11
  0x67, 0x42, 0xc0, 0x1f, 0x0f, 0xd9, 0x1f, 0x88, 0x88, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00,
12
12
  0x00, 0x03, 0x00, 0xc8, 0x3c, 0x60, 0xc9, 0x20,
13
13
  ]);
14
14
 
15
- export const H264KeyFrame2x2PPS: Uint8Array = new Uint8Array([0x68, 0x87, 0xcb, 0x83, 0xcb, 0x20]);
15
+ export const H264KeyFrame2x2PPS: NonSharedUint8Array = new Uint8Array([
16
+ 0x68, 0x87, 0xcb, 0x83, 0xcb, 0x20,
17
+ ]);
16
18
 
17
- export const H264KeyFrame2x2IDR: Uint8Array = new Uint8Array([
19
+ export const H264KeyFrame2x2IDR: NonSharedUint8Array = new Uint8Array([
18
20
  0x65, 0x88, 0x84, 0x0a, 0xf2, 0x62, 0x80, 0x00, 0xa7, 0xbe,
19
21
  ]);
20
22
 
21
- export const H264KeyFrame2x2: Uint8Array[] = [
23
+ export const H264KeyFrame2x2: NonSharedUint8Array[] = [
22
24
  H264KeyFrame2x2SPS,
23
25
  H264KeyFrame2x2PPS,
24
26
  H264KeyFrame2x2IDR,
25
27
  ];
26
28
 
27
- export const OpusSilenceFrame: Uint8Array = new Uint8Array([
29
+ export const OpusSilenceFrame: NonSharedUint8Array = new Uint8Array([
28
30
  0xf8, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
29
31
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30
32
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -35,7 +37,7 @@ export const OpusSilenceFrame: Uint8Array = new Uint8Array([
35
37
  /**
36
38
  * Create a crypto hash using Web Crypto API for secure comparison operations
37
39
  */
38
- async function cryptoHash(data: Uint8Array | ArrayBuffer): Promise<string> {
40
+ async function cryptoHash(data: NonSharedUint8Array | ArrayBuffer): Promise<string> {
39
41
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
40
42
  const hashArray = new Uint8Array(hashBuffer);
41
43
  return Array.from(hashArray)
@@ -58,7 +60,7 @@ export const CryptoHashes = {
58
60
  * Check if a byte array matches any of the known SIF payload frame types using secure crypto hashes
59
61
  */
60
62
  export async function identifySifPayload(
61
- data: Uint8Array | ArrayBuffer,
63
+ data: NonSharedUint8Array | ArrayBuffer,
62
64
  ): Promise<VideoCodec | 'opus' | null> {
63
65
  const hash = await cryptoHash(data);
64
66
 
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import Room, { ConnectionState, type RoomEventCallbacks } from './room/Room';
13
13
  import * as attributes from './room/attribute-typings';
14
14
  import LocalDataTrack from './room/data-track/LocalDataTrack';
15
15
  import RemoteDataTrack, { type DataTrackSubscribeOptions } from './room/data-track/RemoteDataTrack';
16
+ import { type RemoteDataTrackPipelineOptions } from './room/data-track/types';
16
17
  import LocalParticipant from './room/participant/LocalParticipant';
17
18
  import Participant, {
18
19
  ConnectionQuality,
@@ -63,6 +64,11 @@ import {
63
64
  import { getBrowser } from './utils/browserParser';
64
65
 
65
66
  export { RpcError, type RpcInvocationData, type PerformRpcParams } from './room/rpc';
67
+ export type { PacketTrailerMetadata, PacketTrailerPublishOptions } from './packetTrailer/types';
68
+ export {
69
+ PacketTrailerManager,
70
+ type PacketTrailerOptions,
71
+ } from './packetTrailer/PacketTrailerManager';
66
72
 
67
73
  export * from './connectionHelper/ConnectionCheck';
68
74
  export * from './connectionHelper/checks/Checker';
@@ -159,6 +165,7 @@ export type {
159
165
  ParticipantEventCallbacks,
160
166
  PublicationEventCallbacks,
161
167
  DataTrackSubscribeOptions,
168
+ RemoteDataTrackPipelineOptions,
162
169
  };
163
170
  export { DataTrackPacket, type DataTrackPacketHeader } from './room/data-track/packet';
164
171
  export {
package/src/options.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { E2EEOptions } from './e2ee/types';
2
+ import type { PacketTrailerOptions } from './packetTrailer/PacketTrailerManager';
2
3
  import type { ReconnectPolicy } from './room/ReconnectPolicy';
3
4
  import type {
4
5
  AudioCaptureOptions,
@@ -100,6 +101,13 @@ export interface InternalRoomOptions {
100
101
 
101
102
  loggerName?: string;
102
103
 
104
+ /**
105
+ * @experimental
106
+ * Options for enabling packet trailers on video tracks.
107
+ * Packet trailers carry frame-level metadata such as user timestamps and frame IDs.
108
+ */
109
+ packetTrailer?: PacketTrailerOptions;
110
+
103
111
  /**
104
112
  * will attempt to connect via single peer connection mode.
105
113
  * falls back to dual peer connection mode if not available.
@@ -0,0 +1,172 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import type { TrackInfo } from '@livekit/protocol';
3
+ import { PacketTrailerManager } from './PacketTrailerManager';
4
+
5
+ describe('PacketTrailerManager', () => {
6
+ const originalRTCRtpSender = window.RTCRtpSender;
7
+ const originalUserAgent = navigator.userAgent;
8
+ const originalRTCRtpScriptTransform = (window as unknown as { RTCRtpScriptTransform?: unknown })
9
+ .RTCRtpScriptTransform;
10
+
11
+ afterEach(() => {
12
+ Object.defineProperty(window, 'RTCRtpSender', {
13
+ configurable: true,
14
+ value: originalRTCRtpSender,
15
+ writable: true,
16
+ });
17
+ Object.defineProperty(window.navigator, 'userAgent', {
18
+ configurable: true,
19
+ value: originalUserAgent,
20
+ });
21
+ Object.defineProperty(window, 'RTCRtpScriptTransform', {
22
+ configurable: true,
23
+ value: originalRTCRtpScriptTransform,
24
+ writable: true,
25
+ });
26
+ Object.defineProperty(globalThis, 'RTCRtpScriptTransform', {
27
+ configurable: true,
28
+ value: originalRTCRtpScriptTransform,
29
+ writable: true,
30
+ });
31
+ });
32
+
33
+ function stubInsertableStreamsSupport() {
34
+ class MockRTCRtpSender {
35
+ createEncodedStreams() {}
36
+ }
37
+
38
+ Object.defineProperty(window, 'RTCRtpSender', {
39
+ configurable: true,
40
+ value: MockRTCRtpSender,
41
+ writable: true,
42
+ });
43
+ }
44
+
45
+ function useSafariUserAgent() {
46
+ Object.defineProperty(window.navigator, 'userAgent', {
47
+ configurable: true,
48
+ value:
49
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
50
+ });
51
+ }
52
+
53
+ function setScriptTransform(mock: unknown) {
54
+ Object.defineProperty(window, 'RTCRtpScriptTransform', {
55
+ configurable: true,
56
+ value: mock,
57
+ writable: true,
58
+ });
59
+ Object.defineProperty(globalThis, 'RTCRtpScriptTransform', {
60
+ configurable: true,
61
+ value: mock,
62
+ writable: true,
63
+ });
64
+ }
65
+
66
+ function setupWorkerReceiver(manager: PacketTrailerManager, receiver: RTCRtpReceiver) {
67
+ (
68
+ manager as unknown as {
69
+ setupWorkerReceiver: (receiver: RTCRtpReceiver, newTrackId: string) => void;
70
+ }
71
+ ).setupWorkerReceiver(receiver, 'track-id');
72
+ }
73
+
74
+ function setupReceiver(
75
+ manager: PacketTrailerManager,
76
+ receiver: RTCRtpReceiver,
77
+ trackId: string,
78
+ trackInfo?: TrackInfo,
79
+ ) {
80
+ (
81
+ manager as unknown as {
82
+ setupReceiver: (
83
+ track: { receiver: RTCRtpReceiver; mediaStreamID: string },
84
+ trackInfo?: TrackInfo,
85
+ ) => void;
86
+ }
87
+ ).setupReceiver({ receiver, mediaStreamID: trackId }, trackInfo);
88
+ }
89
+
90
+ function makeReceiver() {
91
+ const readable = {} as ReadableStream;
92
+ const writable = {} as WritableStream;
93
+ const createEncodedStreams = vi.fn(() => ({ readable, writable }));
94
+
95
+ return {
96
+ receiver: { createEncodedStreams } as unknown as RTCRtpReceiver,
97
+ readable,
98
+ writable,
99
+ createEncodedStreams,
100
+ };
101
+ }
102
+
103
+ it('uses RTCRtpScriptTransform for packet trailer extraction when supported', () => {
104
+ useSafariUserAgent();
105
+ const transform = {};
106
+ const RTCRtpScriptTransform = vi.fn(() => transform);
107
+ setScriptTransform(RTCRtpScriptTransform);
108
+
109
+ const worker = {} as Worker;
110
+ const manager = new PacketTrailerManager({ worker });
111
+ const receiver = {
112
+ createEncodedStreams: vi.fn(),
113
+ } as unknown as RTCRtpReceiver;
114
+
115
+ setupWorkerReceiver(manager, receiver);
116
+
117
+ expect(RTCRtpScriptTransform).toHaveBeenCalledWith(worker, {
118
+ kind: 'decode',
119
+ trackId: 'track-id',
120
+ });
121
+ expect((receiver as unknown as { transform: unknown }).transform).toBe(transform);
122
+ expect(
123
+ (receiver as unknown as { createEncodedStreams: ReturnType<typeof vi.fn> })
124
+ .createEncodedStreams,
125
+ ).not.toHaveBeenCalled();
126
+ });
127
+
128
+ it('sets up a passthrough receiver pipeline when a subscribed track has no packet trailer features', () => {
129
+ stubInsertableStreamsSupport();
130
+
131
+ const worker = { postMessage: vi.fn() } as unknown as Worker;
132
+ const manager = new PacketTrailerManager({ worker });
133
+ const { receiver, readable, writable, createEncodedStreams } = makeReceiver();
134
+
135
+ setupReceiver(manager, receiver, 'track-without-trailer');
136
+
137
+ expect(createEncodedStreams).toHaveBeenCalledTimes(1);
138
+ expect(worker.postMessage).toHaveBeenCalledWith(
139
+ {
140
+ kind: 'decode',
141
+ data: {
142
+ readableStream: readable,
143
+ writableStream: writable,
144
+ trackId: 'track-without-trailer',
145
+ hasPacketTrailer: false,
146
+ },
147
+ },
148
+ [readable, writable],
149
+ );
150
+ });
151
+
152
+ it('updates a reused receiver from trailer extraction to passthrough for tracks without packet trailer features', () => {
153
+ stubInsertableStreamsSupport();
154
+
155
+ const worker = { postMessage: vi.fn() } as unknown as Worker;
156
+ const manager = new PacketTrailerManager({ worker });
157
+ const { receiver } = makeReceiver();
158
+ const trackInfo = { packetTrailerFeatures: [1] } as unknown as TrackInfo;
159
+
160
+ setupReceiver(manager, receiver, 'track-with-trailer', trackInfo);
161
+ setupReceiver(manager, receiver, 'track-without-trailer');
162
+
163
+ expect(worker.postMessage).toHaveBeenLastCalledWith({
164
+ kind: 'updateTrackId',
165
+ data: {
166
+ oldTrackId: 'track-with-trailer',
167
+ newTrackId: 'track-without-trailer',
168
+ hasPacketTrailer: false,
169
+ },
170
+ });
171
+ });
172
+ });