livekit-client 2.18.9 → 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 (184) 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 +2870 -2420
  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/RemoteDataTrack.d.ts +5 -1
  48. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  49. package/dist/src/room/data-track/depacketizer.d.ts +12 -4
  50. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  51. package/dist/src/room/data-track/frame.d.ts +3 -3
  52. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  53. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  54. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  55. package/dist/src/room/data-track/incoming/pipeline.d.ts +4 -1
  56. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  57. package/dist/src/room/data-track/outgoing/types.d.ts +2 -2
  58. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  59. package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
  60. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  61. package/dist/src/room/data-track/packet/index.d.ts +5 -5
  62. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  63. package/dist/src/room/data-track/packet/serializable.d.ts +1 -1
  64. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  65. package/dist/src/room/data-track/types.d.ts +7 -0
  66. package/dist/src/room/data-track/types.d.ts.map +1 -1
  67. package/dist/src/room/events.d.ts +2 -2
  68. package/dist/src/room/participant/LocalParticipant.d.ts +3 -1
  69. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  70. package/dist/src/room/participant/Participant.d.ts +1 -1
  71. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  72. package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
  73. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
  74. package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
  75. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  76. package/dist/src/room/track/Track.d.ts +1 -1
  77. package/dist/src/room/track/Track.d.ts.map +1 -1
  78. package/dist/src/room/track/create.d.ts.map +1 -1
  79. package/dist/src/room/track/options.d.ts +10 -0
  80. package/dist/src/room/track/options.d.ts.map +1 -1
  81. package/dist/src/room/track/utils.d.ts.map +1 -1
  82. package/dist/src/room/utils.d.ts +4 -3
  83. package/dist/src/room/utils.d.ts.map +1 -1
  84. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  85. package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
  86. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  87. package/dist/src/version.d.ts +1 -1
  88. package/dist/ts4.2/api/SignalClient.d.ts +2 -1
  89. package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
  90. package/dist/ts4.2/e2ee/types.d.ts +35 -8
  91. package/dist/ts4.2/e2ee/utils.d.ts +5 -5
  92. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
  93. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
  94. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
  95. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
  96. package/dist/ts4.2/index.d.ts +5 -1
  97. package/dist/ts4.2/options.d.ts +7 -0
  98. package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
  99. package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
  100. package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
  101. package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
  102. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  103. package/dist/ts4.2/room/RTCEngine.d.ts +2 -1
  104. package/dist/ts4.2/room/Room.d.ts +3 -1
  105. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
  106. package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
  107. package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
  108. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  109. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
  110. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +2 -2
  111. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
  112. package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
  113. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
  114. package/dist/ts4.2/room/data-track/types.d.ts +7 -0
  115. package/dist/ts4.2/room/events.d.ts +2 -2
  116. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +3 -1
  117. package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
  118. package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
  119. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
  120. package/dist/ts4.2/room/track/Track.d.ts +1 -1
  121. package/dist/ts4.2/room/track/options.d.ts +10 -0
  122. package/dist/ts4.2/room/utils.d.ts +4 -3
  123. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
  124. package/dist/ts4.2/version.d.ts +1 -1
  125. package/package.json +24 -16
  126. package/src/api/SignalClient.test.ts +102 -10
  127. package/src/api/SignalClient.ts +4 -2
  128. package/src/api/WebSocketStream.test.ts +0 -1
  129. package/src/e2ee/E2eeManager.ts +82 -30
  130. package/src/e2ee/types.ts +37 -8
  131. package/src/e2ee/utils.ts +7 -6
  132. package/src/e2ee/worker/DataCryptor.ts +6 -6
  133. package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
  134. package/src/e2ee/worker/FrameCryptor.ts +94 -14
  135. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
  136. package/src/e2ee/worker/e2ee.worker.ts +13 -5
  137. package/src/e2ee/worker/naluUtils.ts +4 -4
  138. package/src/e2ee/worker/sifPayload.ts +10 -8
  139. package/src/index.ts +7 -0
  140. package/src/options.ts +8 -0
  141. package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
  142. package/src/packetTrailer/PacketTrailerManager.ts +250 -0
  143. package/src/packetTrailer/packetTrailer.test.ts +174 -0
  144. package/src/packetTrailer/packetTrailer.ts +276 -0
  145. package/src/packetTrailer/types.ts +75 -0
  146. package/src/packetTrailer/utils.test.ts +105 -0
  147. package/src/packetTrailer/utils.ts +50 -0
  148. package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
  149. package/src/packetTrailer/worker/tsconfig.json +14 -0
  150. package/src/room/BackOffStrategy.test.ts +1 -1
  151. package/src/room/RTCEngine.test.ts +219 -0
  152. package/src/room/RTCEngine.ts +86 -20
  153. package/src/room/Room.test.ts +62 -1
  154. package/src/room/Room.ts +28 -5
  155. package/src/room/data-track/RemoteDataTrack.ts +8 -1
  156. package/src/room/data-track/depacketizer.test.ts +433 -1
  157. package/src/room/data-track/depacketizer.ts +79 -61
  158. package/src/room/data-track/frame.ts +2 -2
  159. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
  160. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
  161. package/src/room/data-track/incoming/pipeline.ts +13 -2
  162. package/src/room/data-track/outgoing/types.ts +3 -2
  163. package/src/room/data-track/packet/extensions.ts +2 -2
  164. package/src/room/data-track/packet/index.ts +6 -6
  165. package/src/room/data-track/packet/serializable.ts +1 -1
  166. package/src/room/data-track/types.ts +8 -0
  167. package/src/room/events.ts +2 -2
  168. package/src/room/participant/LocalParticipant.test.ts +81 -0
  169. package/src/room/participant/LocalParticipant.ts +48 -7
  170. package/src/room/participant/Participant.ts +1 -1
  171. package/src/room/participant/publishUtils.ts +1 -1
  172. package/src/room/track/PacketTrailerExtractor.ts +43 -0
  173. package/src/room/track/RemoteVideoTrack.ts +23 -2
  174. package/src/room/track/Track.ts +1 -1
  175. package/src/room/track/create.ts +0 -4
  176. package/src/room/track/options.ts +11 -0
  177. package/src/room/track/record.ts +1 -1
  178. package/src/room/track/utils.ts +4 -1
  179. package/src/room/utils.test.ts +14 -1
  180. package/src/room/utils.ts +17 -3
  181. package/src/test/MockMediaStreamTrack.ts +0 -1
  182. package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
  183. package/src/utils/dataPacketBuffer.ts +1 -1
  184. package/src/version.ts +1 -1
@@ -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
+ });
@@ -0,0 +1,250 @@
1
+ import type { TrackInfo } from '@livekit/protocol';
2
+ import log from '../logger';
3
+ import type Room from '../room/Room';
4
+ import { RoomEvent } from '../room/events';
5
+ import { PacketTrailerExtractor } from '../room/track/PacketTrailerExtractor';
6
+ import type RemoteTrack from '../room/track/RemoteTrack';
7
+ import RemoteVideoTrack from '../room/track/RemoteVideoTrack';
8
+ import type { PTDecodeMessage, PTUpdateTrackIdMessage, PTWorkerMessage } from './types';
9
+ import { isPacketTrailerSupported, shouldUsePacketTrailerScriptTransform } from './utils';
10
+
11
+ export interface PacketTrailerOptions {
12
+ /**
13
+ * Dedicated worker for extracting packet trailers off the main thread.
14
+ *
15
+ * Encoded video streams are transferred to the worker for processing, which
16
+ * avoids per-frame work on the main thread.
17
+ */
18
+ worker: Worker;
19
+ }
20
+
21
+ /**
22
+ * Manages packet trailer extraction for received video tracks.
23
+ *
24
+ * When a track's TrackInfo indicates packet trailer features, the manager
25
+ * wires up an encoded frame transform to strip the trailer from encoded frames
26
+ * and cache the metadata for lookup.
27
+ *
28
+ * Packet trailer extraction is worker-only. If no worker is configured, the
29
+ * SDK does not advertise packet trailer support and skips extraction.
30
+ *
31
+ * When E2EE is active, the E2EE FrameCryptor worker handles trailer
32
+ * extraction directly (before decryption), so this manager only creates
33
+ * the extractor/metadata cache — no separate pipeline is installed.
34
+ *
35
+ * @experimental
36
+ */
37
+ export class PacketTrailerManager {
38
+ private worker?: Worker;
39
+
40
+ private room?: Room;
41
+
42
+ private extractors = new Map<string, PacketTrailerExtractor>();
43
+
44
+ /**
45
+ * Tracks the trackId associated with each receiver that has had its
46
+ * encoded streams handed off to the worker. Used to detect receiver
47
+ * reuse (transceiver recycling) so we can remap trackIds instead of
48
+ * re-transferring already-consumed streams.
49
+ */
50
+ private workerPipelines = new Map<RTCRtpReceiver, string>();
51
+
52
+ constructor(options?: PacketTrailerOptions) {
53
+ this.worker = options?.worker;
54
+ }
55
+
56
+ /** @internal */
57
+ setup(room: Room) {
58
+ if (room === this.room) {
59
+ return;
60
+ }
61
+ this.room = room;
62
+
63
+ if (this.worker) {
64
+ this.worker.onmessage = this.onWorkerMessage;
65
+ this.worker.onerror = this.onWorkerError;
66
+ this.worker.postMessage({ kind: 'init' });
67
+ }
68
+
69
+ room
70
+ .on(RoomEvent.TrackSubscribed, (track, pub, _participant) => {
71
+ if (track.kind !== 'video') {
72
+ return;
73
+ }
74
+ this.setupReceiver(track as unknown as RemoteVideoTrack, pub.trackInfo);
75
+ })
76
+ .on(RoomEvent.TrackUnsubscribed, (track) => {
77
+ this.teardownTrack(track);
78
+ })
79
+ .on(RoomEvent.Disconnected, () => {
80
+ this.cleanup();
81
+ });
82
+ }
83
+
84
+ private setupReceiver(track: RemoteVideoTrack, trackInfo?: TrackInfo) {
85
+ const receiver = track.receiver;
86
+ if (!receiver) {
87
+ return;
88
+ }
89
+
90
+ // Only install a pipeline for tracks that actually advertise packet
91
+ // trailer features. This keeps us out of the way for tracks published by
92
+ // clients on older protocols or that don't opt into the feature.
93
+ const hasFeatures =
94
+ !!trackInfo?.packetTrailerFeatures && trackInfo.packetTrailerFeatures.length > 0;
95
+ if (!hasFeatures) {
96
+ if (!this.room?.hasE2EESetup) {
97
+ this.setupPassthroughReceiver(receiver, track.mediaStreamID);
98
+ }
99
+ return;
100
+ }
101
+
102
+ if (
103
+ !isPacketTrailerSupported(this.worker ? { worker: this.worker } : undefined) &&
104
+ !this.room?.hasE2EESetup
105
+ ) {
106
+ log.warn('packet trailer transform not supported; skipping extraction');
107
+ return;
108
+ }
109
+
110
+ const extractor = new PacketTrailerExtractor();
111
+ const trackId = track.mediaStreamID;
112
+
113
+ this.extractors.set(trackId, extractor);
114
+ track.packetTrailerExtractor = extractor;
115
+
116
+ if (this.room?.hasE2EESetup) {
117
+ // E2EE worker strips the trailer and injects metadata directly into
118
+ // the extractor via E2eeManager; no pipeline is needed here.
119
+ return;
120
+ }
121
+
122
+ this.setupWorkerReceiver(receiver, trackId, true);
123
+ }
124
+
125
+ private setupPassthroughReceiver(receiver: RTCRtpReceiver, trackId: string) {
126
+ if (shouldUsePacketTrailerScriptTransform()) {
127
+ if ('transform' in receiver) {
128
+ // @ts-ignore
129
+ receiver.transform = null;
130
+ }
131
+ return;
132
+ }
133
+
134
+ if (
135
+ this.worker &&
136
+ isPacketTrailerSupported({ worker: this.worker }) &&
137
+ !this.workerPipelines.has(receiver)
138
+ ) {
139
+ this.setupWorkerReceiver(receiver, trackId, false);
140
+ return;
141
+ }
142
+
143
+ if (this.worker && this.workerPipelines.has(receiver)) {
144
+ this.setupWorkerReceiver(receiver, trackId, false);
145
+ }
146
+ }
147
+
148
+ private setupWorkerReceiver(
149
+ receiver: RTCRtpReceiver,
150
+ newTrackId: string,
151
+ hasPacketTrailer = true,
152
+ ) {
153
+ const worker = this.worker;
154
+ if (!worker) {
155
+ return;
156
+ }
157
+
158
+ if (shouldUsePacketTrailerScriptTransform()) {
159
+ // @ts-ignore
160
+ receiver.transform = new RTCRtpScriptTransform(worker, {
161
+ kind: 'decode',
162
+ trackId: newTrackId,
163
+ });
164
+ return;
165
+ }
166
+
167
+ const existingTrackId = this.workerPipelines.get(receiver);
168
+
169
+ if (existingTrackId) {
170
+ // Receiver is reused (transceiver recycled). The worker already owns
171
+ // the encoded streams — just remap the trackId so metadata is keyed
172
+ // correctly and re-activate processing.
173
+ const msg: PTUpdateTrackIdMessage = {
174
+ kind: 'updateTrackId',
175
+ data: { oldTrackId: existingTrackId, newTrackId, hasPacketTrailer },
176
+ };
177
+ worker.postMessage(msg);
178
+ this.workerPipelines.set(receiver, newTrackId);
179
+ return;
180
+ }
181
+
182
+ if (!('createEncodedStreams' in receiver)) {
183
+ log.warn('createEncodedStreams not supported');
184
+ return;
185
+ }
186
+
187
+ let streams: { readable: ReadableStream; writable: WritableStream };
188
+ try {
189
+ // @ts-ignore — createEncodedStreams is not in standard typings
190
+ streams = receiver.createEncodedStreams();
191
+ } catch (err) {
192
+ log.warn('failed to create encoded streams', { error: err });
193
+ return;
194
+ }
195
+
196
+ const msg: PTDecodeMessage = {
197
+ kind: 'decode',
198
+ data: {
199
+ readableStream: streams.readable,
200
+ writableStream: streams.writable,
201
+ trackId: newTrackId,
202
+ hasPacketTrailer,
203
+ },
204
+ };
205
+ worker.postMessage(msg, [streams.readable, streams.writable]);
206
+ this.workerPipelines.set(receiver, newTrackId);
207
+ }
208
+
209
+ private teardownTrack(track: RemoteTrack) {
210
+ const trackId = track.mediaStreamID;
211
+ const extractor = this.extractors.get(trackId);
212
+ if (extractor) {
213
+ extractor.dispose();
214
+ this.extractors.delete(trackId);
215
+ }
216
+
217
+ if (track instanceof RemoteVideoTrack) {
218
+ track.packetTrailerExtractor = undefined;
219
+ }
220
+
221
+ // The receiver pipeline is intentionally left running. If the receiver is
222
+ // reused for a new track, `setupReceiver` will remap it. If the room
223
+ // disconnects, `cleanup` drops all state. Any metadata produced in the
224
+ // meantime is harmless — the extractor above has already been disposed and
225
+ // is no longer reachable from any track.
226
+ }
227
+
228
+ private cleanup() {
229
+ for (const extractor of this.extractors.values()) {
230
+ extractor.dispose();
231
+ }
232
+ this.extractors.clear();
233
+ this.workerPipelines.clear();
234
+ this.worker?.terminate();
235
+ }
236
+
237
+ private onWorkerMessage = (ev: MessageEvent<PTWorkerMessage>) => {
238
+ const msg = ev.data;
239
+ if (msg.kind === 'metadata') {
240
+ const extractor = this.extractors.get(msg.data.trackId);
241
+ if (extractor) {
242
+ extractor.storeMetadata(msg.data.rtpTimestamp, msg.data.ssrc, msg.data.metadata);
243
+ }
244
+ }
245
+ };
246
+
247
+ private onWorkerError = (ev: ErrorEvent) => {
248
+ log.error('packet trailer worker encountered an error:', { error: ev.error });
249
+ };
250
+ }