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
@@ -2,16 +2,24 @@ import { Encryption_Type, TrackInfo } from '@livekit/protocol';
2
2
  import { EventEmitter } from 'events';
3
3
  import type TypedEventEmitter from 'typed-emitter';
4
4
  import log, { LogLevel, workerLogger } from '../logger';
5
+ import { hasPacketTrailerPublishOptions } from '../packetTrailer/utils';
5
6
  import type RTCEngine from '../room/RTCEngine';
6
7
  import type Room from '../room/Room';
7
8
  import { ConnectionState } from '../room/Room';
8
9
  import { DeviceUnsupportedError } from '../room/errors';
9
10
  import { EngineEvent, ParticipantEvent, RoomEvent } from '../room/events';
10
11
  import type RemoteTrack from '../room/track/RemoteTrack';
12
+ import RemoteVideoTrack from '../room/track/RemoteVideoTrack';
11
13
  import type { Track } from '../room/track/Track';
12
- import type { VideoCodec } from '../room/track/options';
14
+ import type { TrackPublishOptions, VideoCodec } from '../room/track/options';
13
15
  import { mimeTypeToVideoCodecString } from '../room/track/utils';
14
- import { Future, isChromiumBased, isLocalTrack, isSafariBased, isVideoTrack } from '../room/utils';
16
+ import {
17
+ Future,
18
+ isLocalTrack,
19
+ isSafariBased,
20
+ isScriptTransformSupportedForWorker,
21
+ isVideoTrack,
22
+ } from '../room/utils';
15
23
  import type { BaseKeyProvider } from './KeyProvider';
16
24
  import { E2EE_FLAG } from './constants';
17
25
  import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
@@ -34,7 +42,7 @@ import type {
34
42
  SifTrailerMessage,
35
43
  UpdateCodecMessage,
36
44
  } from './types';
37
- import { isE2EESupported, isScriptTransformSupported } from './utils';
45
+ import { isE2EESupported } from './utils';
38
46
 
39
47
  export interface BaseE2EEManager {
40
48
  setup(room: Room): void;
@@ -42,11 +50,11 @@ export interface BaseE2EEManager {
42
50
  isEnabled: boolean;
43
51
  isDataChannelEncryptionEnabled: boolean;
44
52
  setParticipantCryptorEnabled(enabled: boolean, participantIdentity: string): void;
45
- setSifTrailer(trailer: Uint8Array): void;
46
- encryptData(data: Uint8Array): Promise<EncryptDataResponseMessage['data']>;
53
+ setSifTrailer(trailer: NonSharedUint8Array): void;
54
+ encryptData(data: NonSharedUint8Array): Promise<EncryptDataResponseMessage['data']>;
47
55
  handleEncryptedData(
48
- payload: Uint8Array,
49
- iv: Uint8Array,
56
+ payload: NonSharedUint8Array,
57
+ iv: NonSharedUint8Array,
50
58
  participantIdentity: string,
51
59
  keyIndex: number,
52
60
  ): Promise<DecryptDataResponseMessage['data']>;
@@ -133,7 +141,7 @@ export class E2EEManager
133
141
  /**
134
142
  * @internal
135
143
  */
136
- setSifTrailer(trailer: Uint8Array) {
144
+ setSifTrailer(trailer: NonSharedUint8Array) {
137
145
  if (!trailer || trailer.length === 0) {
138
146
  log.warn("ignoring server sent trailer as it's empty");
139
147
  } else {
@@ -221,6 +229,9 @@ export class E2EEManager
221
229
  encryptFuture.resolve(data as EncryptDataResponseMessage['data']);
222
230
  }
223
231
  break;
232
+ case 'packetTrailerMetadata':
233
+ this.handlePacketTrailerMetadata(data.trackId, data.rtpTimestamp, data.ssrc, data.metadata);
234
+ break;
224
235
  default:
225
236
  break;
226
237
  }
@@ -231,6 +242,33 @@ export class E2EEManager
231
242
  this.emit(EncryptionEvent.EncryptionError, ev.error, undefined);
232
243
  };
233
244
 
245
+ private handlePacketTrailerMetadata(
246
+ trackId: string,
247
+ rtpTimestamp: number,
248
+ ssrc: number,
249
+ metadata: { userTimestamp: bigint; frameId: number },
250
+ ) {
251
+ if (!this.room) {
252
+ return;
253
+ }
254
+ for (const participant of [
255
+ this.room.localParticipant,
256
+ ...this.room.remoteParticipants.values(),
257
+ ]) {
258
+ for (const pub of participant.trackPublications.values()) {
259
+ if (
260
+ pub.track &&
261
+ pub.track.mediaStreamID === trackId &&
262
+ pub.track instanceof RemoteVideoTrack &&
263
+ pub.track.packetTrailerExtractor
264
+ ) {
265
+ pub.track.packetTrailerExtractor.storeMetadata(rtpTimestamp, ssrc, metadata);
266
+ return;
267
+ }
268
+ }
269
+ }
270
+ }
271
+
234
272
  public setupEngine(engine: RTCEngine) {
235
273
  engine.on(EngineEvent.RTPVideoMapUpdate, (rtpMap) => {
236
274
  this.postRTPMap(rtpMap);
@@ -299,6 +337,7 @@ export class E2EEManager
299
337
  trackId: publication.track!.mediaStreamID,
300
338
  codec: mimeTypeToVideoCodecString(publication.trackInfo!.codecs[0].mimeType),
301
339
  participantIdentity: this.room!.localParticipant.identity,
340
+ hasPacketTrailer: false,
302
341
  },
303
342
  };
304
343
 
@@ -314,7 +353,7 @@ export class E2EEManager
314
353
  );
315
354
  }
316
355
 
317
- async encryptData(data: Uint8Array): Promise<EncryptDataResponseMessage['data']> {
356
+ async encryptData(data: NonSharedUint8Array): Promise<EncryptDataResponseMessage['data']> {
318
357
  if (!this.worker) {
319
358
  throw Error('could not encrypt data, worker is missing');
320
359
  }
@@ -337,8 +376,8 @@ export class E2EEManager
337
376
  }
338
377
 
339
378
  handleEncryptedData(
340
- payload: Uint8Array,
341
- iv: Uint8Array,
379
+ payload: NonSharedUint8Array,
380
+ iv: NonSharedUint8Array,
342
381
  participantIdentity: string,
343
382
  keyIndex: number,
344
383
  ) {
@@ -428,7 +467,7 @@ export class E2EEManager
428
467
  this.worker.postMessage(msg);
429
468
  }
430
469
 
431
- private postSifTrailer(trailer: Uint8Array) {
470
+ private postSifTrailer(trailer: NonSharedUint8Array) {
432
471
  if (!this.worker) {
433
472
  throw Error('could not post SIF trailer, worker is missing');
434
473
  }
@@ -448,11 +487,16 @@ export class E2EEManager
448
487
  if (!trackInfo?.mimeType || trackInfo.mimeType === '') {
449
488
  throw new TypeError('MimeType missing from trackInfo, cannot set up E2EE cryptor');
450
489
  }
490
+ const hasPacketTrailer =
491
+ track.kind === 'video' &&
492
+ !!trackInfo.packetTrailerFeatures &&
493
+ trackInfo.packetTrailerFeatures.length > 0;
451
494
  this.handleReceiver(
452
495
  track.receiver,
453
496
  track.mediaStreamID,
454
497
  remoteId,
455
498
  track.kind === 'video' ? mimeTypeToVideoCodecString(trackInfo.mimeType) : undefined,
499
+ hasPacketTrailer,
456
500
  );
457
501
  }
458
502
 
@@ -461,7 +505,12 @@ export class E2EEManager
461
505
  if (!sender) log.warn('early return because sender is not ready');
462
506
  return;
463
507
  }
464
- this.handleSender(sender, track.mediaStreamID, undefined);
508
+ this.handleSender(
509
+ sender,
510
+ track.mediaStreamID,
511
+ undefined,
512
+ isVideoTrack(track) ? track.publishOptions?.packetTrailer : undefined,
513
+ );
465
514
  }
466
515
 
467
516
  /**
@@ -473,35 +522,33 @@ export class E2EEManager
473
522
  receiver: RTCRtpReceiver,
474
523
  trackId: string,
475
524
  participantIdentity: string,
476
- codec?: VideoCodec,
525
+ codec: VideoCodec | undefined,
526
+ hasPacketTrailer: boolean,
477
527
  ) {
478
528
  if (!this.worker) {
479
529
  return;
480
530
  }
481
531
 
482
- if (
483
- isScriptTransformSupported() &&
484
- // Chrome occasionally throws an `InvalidState` error when using script transforms directly after introducing this API in 141.
485
- // Disabling it for Chrome based browsers until the API has stabilized
486
- !isChromiumBased()
487
- ) {
532
+ if (isScriptTransformSupportedForWorker()) {
488
533
  const options: ScriptTransformOptions = {
489
534
  kind: 'decode',
490
535
  participantIdentity,
491
536
  trackId,
492
537
  codec,
538
+ hasPacketTrailer,
493
539
  };
494
540
  // @ts-ignore
495
541
  receiver.transform = new RTCRtpScriptTransform(this.worker, options);
496
542
  } else {
497
543
  if (E2EE_FLAG in receiver && codec) {
498
- // only update codec
544
+ // update track-specific state when the transceiver is reused
499
545
  const msg: UpdateCodecMessage = {
500
546
  kind: 'updateCodec',
501
547
  data: {
502
548
  trackId,
503
549
  codec,
504
- participantIdentity: participantIdentity,
550
+ participantIdentity,
551
+ hasPacketTrailer,
505
552
  },
506
553
  };
507
554
  this.worker.postMessage(msg);
@@ -532,6 +579,7 @@ export class E2EEManager
532
579
  codec,
533
580
  participantIdentity: participantIdentity,
534
581
  isReuse: E2EE_FLAG in receiver,
582
+ hasPacketTrailer,
535
583
  },
536
584
  };
537
585
  this.worker.postMessage(msg, [readable, writable]);
@@ -546,7 +594,12 @@ export class E2EEManager
546
594
  * a frame encoder.
547
595
  *
548
596
  */
549
- private handleSender(sender: RTCRtpSender, trackId: string, codec?: VideoCodec) {
597
+ private handleSender(
598
+ sender: RTCRtpSender,
599
+ trackId: string,
600
+ codec?: VideoCodec,
601
+ packetTrailer?: TrackPublishOptions['packetTrailer'],
602
+ ) {
550
603
  if (E2EE_FLAG in sender || !this.worker) {
551
604
  return;
552
605
  }
@@ -555,18 +608,15 @@ export class E2EEManager
555
608
  throw TypeError('local identity needs to be known in order to set up encrypted sender');
556
609
  }
557
610
 
558
- if (
559
- isScriptTransformSupported() &&
560
- // Chrome occasionally throws an `InvalidState` error when using script transforms directly after introducing this API in 141.
561
- // Disabling it for Chrome based browsers until the API has stabilized
562
- !isChromiumBased()
563
- ) {
611
+ if (isScriptTransformSupportedForWorker()) {
564
612
  log.info('initialize script transform');
565
- const options = {
613
+ const options: ScriptTransformOptions = {
566
614
  kind: 'encode',
567
615
  participantIdentity: this.room.localParticipant.identity,
568
616
  trackId,
569
617
  codec,
618
+ hasPacketTrailer: hasPacketTrailerPublishOptions(packetTrailer),
619
+ packetTrailer,
570
620
  };
571
621
  // @ts-ignore
572
622
  sender.transform = new RTCRtpScriptTransform(this.worker, options);
@@ -583,6 +633,8 @@ export class E2EEManager
583
633
  trackId,
584
634
  participantIdentity: this.room.localParticipant.identity,
585
635
  isReuse: false,
636
+ hasPacketTrailer: hasPacketTrailerPublishOptions(packetTrailer),
637
+ packetTrailer,
586
638
  },
587
639
  };
588
640
  this.worker.postMessage(msg, [senderStreams.readable, senderStreams.writable]);
package/src/e2ee/types.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { LogLevel } from '../logger';
2
+ import type { PacketTrailerFramePayload } from '../packetTrailer/packetTrailer';
3
+ import type { PacketTrailerPublishOptions } from '../packetTrailer/types';
2
4
  import type { VideoCodec } from '../room/track/options';
3
5
  import type { BaseE2EEManager } from './E2eeManager';
4
6
  import type { BaseKeyProvider } from './KeyProvider';
@@ -38,7 +40,7 @@ export interface RTPVideoMapMessage extends BaseMessage {
38
40
  export interface SifTrailerMessage extends BaseMessage {
39
41
  kind: 'setSifTrailer';
40
42
  data: {
41
- trailer: Uint8Array;
43
+ trailer: NonSharedUint8Array;
42
44
  };
43
45
  }
44
46
 
@@ -51,6 +53,16 @@ export interface EncodeMessage extends BaseMessage {
51
53
  trackId: string;
52
54
  codec?: VideoCodec;
53
55
  isReuse: boolean;
56
+ /**
57
+ * Whether the published track advertises packet trailer features.
58
+ * When false, the cryptor skips the per-frame trailer extraction path
59
+ * entirely on decode.
60
+ */
61
+ hasPacketTrailer: boolean;
62
+ /**
63
+ * Packet trailer metadata to append on published video frames.
64
+ */
65
+ packetTrailer?: PacketTrailerPublishOptions;
54
66
  };
55
67
  }
56
68
 
@@ -68,6 +80,7 @@ export interface UpdateCodecMessage extends BaseMessage {
68
80
  participantIdentity: string;
69
81
  trackId: string;
70
82
  codec: VideoCodec;
83
+ hasPacketTrailer: boolean;
71
84
  };
72
85
  }
73
86
 
@@ -116,8 +129,8 @@ export interface DecryptDataRequestMessage extends BaseMessage {
116
129
  kind: 'decryptDataRequest';
117
130
  data: {
118
131
  uuid: string;
119
- payload: Uint8Array;
120
- iv: Uint8Array;
132
+ payload: NonSharedUint8Array;
133
+ iv: NonSharedUint8Array;
121
134
  participantIdentity: string;
122
135
  keyIndex: number;
123
136
  };
@@ -127,7 +140,7 @@ export interface DecryptDataResponseMessage extends BaseMessage {
127
140
  kind: 'decryptDataResponse';
128
141
  data: {
129
142
  uuid: string;
130
- payload: Uint8Array;
143
+ payload: NonSharedUint8Array;
131
144
  };
132
145
  }
133
146
 
@@ -135,7 +148,7 @@ export interface EncryptDataRequestMessage extends BaseMessage {
135
148
  kind: 'encryptDataRequest';
136
149
  data: {
137
150
  uuid: string;
138
- payload: Uint8Array;
151
+ payload: NonSharedUint8Array;
139
152
  participantIdentity: string;
140
153
  };
141
154
  }
@@ -144,12 +157,17 @@ export interface EncryptDataResponseMessage extends BaseMessage {
144
157
  kind: 'encryptDataResponse';
145
158
  data: {
146
159
  uuid: string;
147
- payload: Uint8Array;
148
- iv: Uint8Array;
160
+ payload: NonSharedUint8Array;
161
+ iv: NonSharedUint8Array;
149
162
  keyIndex: number;
150
163
  };
151
164
  }
152
165
 
166
+ export interface PTMetadataFromE2EEMessage extends BaseMessage {
167
+ kind: 'packetTrailerMetadata';
168
+ data: PacketTrailerFramePayload;
169
+ }
170
+
153
171
  export type E2EEWorkerMessage =
154
172
  | InitMessage
155
173
  | SetKeyMessage
@@ -166,7 +184,8 @@ export type E2EEWorkerMessage =
166
184
  | DecryptDataRequestMessage
167
185
  | DecryptDataResponseMessage
168
186
  | EncryptDataRequestMessage
169
- | EncryptDataResponseMessage;
187
+ | EncryptDataResponseMessage
188
+ | PTMetadataFromE2EEMessage;
170
189
 
171
190
  export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
172
191
 
@@ -221,4 +240,14 @@ export type ScriptTransformOptions = {
221
240
  participantIdentity: string;
222
241
  trackId: string;
223
242
  codec?: VideoCodec;
243
+ /**
244
+ * Whether the published track advertises packet trailer features.
245
+ * When false, the cryptor skips the per-frame trailer extraction path
246
+ * entirely on decode.
247
+ */
248
+ hasPacketTrailer: boolean;
249
+ /**
250
+ * Packet trailer metadata to append on published video frames.
251
+ */
252
+ packetTrailer?: PacketTrailerPublishOptions;
224
253
  };
package/src/e2ee/utils.ts CHANGED
@@ -8,11 +8,12 @@ export function isE2EESupported() {
8
8
 
9
9
  export function isScriptTransformSupported() {
10
10
  // @ts-ignore
11
- return typeof window.RTCRtpScriptTransform !== 'undefined';
11
+ return typeof window !== 'undefined' && typeof window.RTCRtpScriptTransform !== 'undefined';
12
12
  }
13
13
 
14
14
  export function isInsertableStreamSupported() {
15
15
  return (
16
+ typeof window !== 'undefined' &&
16
17
  typeof window.RTCRtpSender !== 'undefined' &&
17
18
  // @ts-ignore
18
19
  typeof window.RTCRtpSender.prototype.createEncodedStreams !== 'undefined'
@@ -26,7 +27,7 @@ export function isVideoFrame(
26
27
  }
27
28
 
28
29
  export async function importKey(
29
- keyBytes: Uint8Array | ArrayBuffer,
30
+ keyBytes: NonSharedUint8Array | ArrayBuffer,
30
31
  algorithm: string | { name: string } = { name: ENCRYPTION_ALGORITHM },
31
32
  usage: 'derive' | 'encrypt' = 'encrypt',
32
33
  ) {
@@ -112,7 +113,7 @@ export async function deriveKeys(material: CryptoKey, options: KeyProviderOption
112
113
  return { material, encryptionKey };
113
114
  }
114
115
 
115
- export function createE2EEKey(): Uint8Array {
116
+ export function createE2EEKey(): NonSharedUint8Array {
116
117
  return window.crypto.getRandomValues(new Uint8Array(32));
117
118
  }
118
119
 
@@ -127,14 +128,14 @@ export async function ratchet(material: CryptoKey, salt: string): Promise<ArrayB
127
128
  return crypto.subtle.deriveBits(algorithmOptions, material, 256);
128
129
  }
129
130
 
130
- export function needsRbspUnescaping(frameData: Uint8Array) {
131
+ export function needsRbspUnescaping(frameData: NonSharedUint8Array) {
131
132
  for (var i = 0; i < frameData.length - 3; i++) {
132
133
  if (frameData[i] == 0 && frameData[i + 1] == 0 && frameData[i + 2] == 3) return true;
133
134
  }
134
135
  return false;
135
136
  }
136
137
 
137
- export function parseRbsp(stream: Uint8Array): Uint8Array {
138
+ export function parseRbsp(stream: NonSharedUint8Array): NonSharedUint8Array {
138
139
  const dataOut: number[] = [];
139
140
  var length = stream.length;
140
141
  for (var i = 0; i < stream.length; ) {
@@ -159,7 +160,7 @@ export function parseRbsp(stream: Uint8Array): Uint8Array {
159
160
  const kZerosInStartSequence = 2;
160
161
  const kEmulationByte = 3;
161
162
 
162
- export function writeRbsp(data_in: Uint8Array): Uint8Array {
163
+ export function writeRbsp(data_in: NonSharedUint8Array): NonSharedUint8Array {
163
164
  const dataOut: number[] = [];
164
165
  var numConsecutiveZeros = 0;
165
166
  for (var i = 0; i < data_in.length; ++i) {
@@ -21,11 +21,11 @@ export class DataCryptor {
21
21
  }
22
22
 
23
23
  static async encrypt(
24
- data: Uint8Array,
24
+ data: NonSharedUint8Array,
25
25
  keys: ParticipantKeyHandler,
26
26
  ): Promise<{
27
- payload: Uint8Array;
28
- iv: Uint8Array;
27
+ payload: NonSharedUint8Array;
28
+ iv: NonSharedUint8Array;
29
29
  keyIndex: number;
30
30
  }> {
31
31
  const iv = DataCryptor.makeIV(performance.now());
@@ -51,14 +51,14 @@ export class DataCryptor {
51
51
  }
52
52
 
53
53
  static async decrypt(
54
- data: Uint8Array,
55
- iv: Uint8Array,
54
+ data: NonSharedUint8Array,
55
+ iv: NonSharedUint8Array,
56
56
  keys: ParticipantKeyHandler,
57
57
  keyIndex: number = 0,
58
58
  initialMaterial?: KeySet,
59
59
  ratchetOpts: DecodeRatchetOptions = { ratchetCount: 0 },
60
60
  ): Promise<{
61
- payload: Uint8Array;
61
+ payload: NonSharedUint8Array;
62
62
  }> {
63
63
  const keySet = await keys.getKeySet(keyIndex);
64
64
  if (!keySet) {
@@ -1,4 +1,6 @@
1
1
  import { afterEach, describe, expect, it, vitest } from 'vitest';
2
+ import { appendPacketTrailer, extractPacketTrailer } from '../../packetTrailer/packetTrailer';
3
+ import type { PacketTrailerPublishOptions } from '../../packetTrailer/types';
2
4
  import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
3
5
  import { CryptorEvent } from '../events';
4
6
  import type { KeyProviderOptions } from '../types';
@@ -13,7 +15,7 @@ function mockEncryptedRTCEncodedVideoFrame(keyIndex: number): RTCEncodedVideoFra
13
15
  return mockRTCEncodedVideoFrame(data);
14
16
  }
15
17
 
16
- function mockRTCEncodedVideoFrame(data: Uint8Array): RTCEncodedVideoFrame {
18
+ function mockRTCEncodedVideoFrame(data: NonSharedUint8Array): RTCEncodedVideoFrame {
17
19
  return {
18
20
  data: data.buffer,
19
21
  timestamp: vitest.getMockedSystemTime()?.getTime() ?? 0,
@@ -24,7 +26,7 @@ function mockRTCEncodedVideoFrame(data: Uint8Array): RTCEncodedVideoFrame {
24
26
  };
25
27
  }
26
28
 
27
- function mockFrameTrailer(keyIndex: number): Uint8Array {
29
+ function mockFrameTrailer(keyIndex: number): NonSharedUint8Array {
28
30
  const frameTrailer = new Uint8Array(2);
29
31
 
30
32
  frameTrailer[0] = IV_LENGTH;
@@ -67,14 +69,21 @@ function prepareParticipantTestDecoder(
67
69
  function prepareParticipantTestEncoder(
68
70
  participantIdentity: string,
69
71
  partialKeyProviderOptions: Partial<KeyProviderOptions>,
72
+ packetTrailer?: PacketTrailerPublishOptions,
70
73
  ) {
71
- return prepareParticipantTest('encode', participantIdentity, partialKeyProviderOptions);
74
+ return prepareParticipantTest(
75
+ 'encode',
76
+ participantIdentity,
77
+ partialKeyProviderOptions,
78
+ packetTrailer,
79
+ );
72
80
  }
73
81
 
74
82
  function prepareParticipantTest(
75
83
  mode: 'encode' | 'decode',
76
84
  participantIdentity: string,
77
85
  partialKeyProviderOptions: Partial<KeyProviderOptions>,
86
+ packetTrailer?: PacketTrailerPublishOptions,
78
87
  ): {
79
88
  keys: ParticipantKeyHandler;
80
89
  cryptor: FrameCryptor;
@@ -95,7 +104,15 @@ function prepareParticipantTest(
95
104
 
96
105
  const input = new TestUnderlyingSource<RTCEncodedVideoFrame>();
97
106
  const output = new TestUnderlyingSink<RTCEncodedVideoFrame>();
98
- cryptor.setupTransform(mode, new ReadableStream(input), new WritableStream(output), 'testTrack');
107
+ cryptor.setupTransform(
108
+ mode,
109
+ new ReadableStream(input),
110
+ new WritableStream(output),
111
+ 'testTrack',
112
+ false,
113
+ undefined,
114
+ packetTrailer,
115
+ );
99
116
 
100
117
  return { keys, cryptor, input, output };
101
118
  }
@@ -215,6 +232,127 @@ describe('FrameCryptor', () => {
215
232
  vitest.useRealTimers();
216
233
  }
217
234
  });
235
+
236
+ it('appends packet trailer after encryption', async () => {
237
+ vitest.useFakeTimers();
238
+ const now = new Date('2025-04-10T12:00:00.123Z');
239
+ vitest.setSystemTime(now);
240
+ try {
241
+ const { keys, input, output } = prepareParticipantTestEncoder(
242
+ participantIdentity,
243
+ {},
244
+ { timestamp: true, frameId: true },
245
+ );
246
+
247
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
248
+
249
+ const frame = mockRTCEncodedVideoFrame(
250
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
251
+ );
252
+
253
+ input.write(frame);
254
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
255
+
256
+ const extracted = extractPacketTrailer(frame.data);
257
+ expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
258
+ BigInt(now.getTime()) * BigInt(1000),
259
+ );
260
+ expect(extracted.metadata?.frameId).toBe(1);
261
+
262
+ const frameTrailer = new Uint8Array(
263
+ extracted.data.buffer.slice(extracted.data.byteLength - 2),
264
+ );
265
+ expect(frameTrailer[0]).toEqual(IV_LENGTH);
266
+ expect(frameTrailer[1]).toEqual(1);
267
+ } finally {
268
+ vitest.useRealTimers();
269
+ }
270
+ });
271
+
272
+ it('does not append packet trailer after encryption when no trailer features are enabled', async () => {
273
+ vitest.useFakeTimers();
274
+ try {
275
+ const { keys, input, output } = prepareParticipantTestEncoder(
276
+ participantIdentity,
277
+ {},
278
+ { timestamp: false, frameId: false },
279
+ );
280
+
281
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
282
+
283
+ const frame = mockRTCEncodedVideoFrame(
284
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
285
+ );
286
+
287
+ input.write(frame);
288
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
289
+
290
+ const extracted = extractPacketTrailer(frame.data);
291
+ expect(extracted.metadata).toBeUndefined();
292
+ expect(extracted.data.byteLength).toBe(frame.data.byteLength);
293
+
294
+ const frameTrailer = new Uint8Array(frame.data.slice(frame.data.byteLength - 2));
295
+ expect(frameTrailer[0]).toEqual(IV_LENGTH);
296
+ expect(frameTrailer[1]).toEqual(1);
297
+ } finally {
298
+ vitest.useRealTimers();
299
+ }
300
+ });
301
+
302
+ it('appends only timestamp packet trailer metadata after encryption', async () => {
303
+ vitest.useFakeTimers();
304
+ const now = new Date('2025-04-10T12:00:00.123Z');
305
+ vitest.setSystemTime(now);
306
+ try {
307
+ const { keys, input, output } = prepareParticipantTestEncoder(
308
+ participantIdentity,
309
+ {},
310
+ { timestamp: true },
311
+ );
312
+
313
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
314
+
315
+ const frame = mockRTCEncodedVideoFrame(
316
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
317
+ );
318
+
319
+ input.write(frame);
320
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
321
+
322
+ const extracted = extractPacketTrailer(frame.data);
323
+ expect(frame.data.byteLength - extracted.data.byteLength).toBe(15);
324
+ expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
325
+ BigInt(now.getTime()) * BigInt(1000),
326
+ );
327
+ expect(extracted.metadata?.frameId).toBe(0);
328
+ } finally {
329
+ vitest.useRealTimers();
330
+ }
331
+ });
332
+
333
+ it('appends only frame id packet trailer metadata after encryption', async () => {
334
+ const { keys, input, output } = prepareParticipantTestEncoder(
335
+ participantIdentity,
336
+ {},
337
+ { frameId: true },
338
+ );
339
+
340
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
341
+
342
+ const frame = mockRTCEncodedVideoFrame(
343
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
344
+ );
345
+
346
+ input.write(frame);
347
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
348
+
349
+ const extracted = extractPacketTrailer(frame.data);
350
+ expect(frame.data.byteLength - extracted.data.byteLength).toBe(11);
351
+ expect(extracted.metadata).toEqual({
352
+ userTimestamp: BigInt(0),
353
+ frameId: 1,
354
+ });
355
+ });
218
356
  });
219
357
 
220
358
  describe('decode', () => {
@@ -391,6 +529,41 @@ describe('FrameCryptor', () => {
391
529
  }
392
530
  });
393
531
 
532
+ it('posts packet trailer metadata when the advertised trailer path is active', async () => {
533
+ vitest.useFakeTimers();
534
+ try {
535
+ const { cryptor, input, output } = prepareParticipantTestDecoder(participantIdentity, {});
536
+ const postMessage = vitest.fn();
537
+ vitest.stubGlobal('postMessage', postMessage);
538
+ encryptionEnabledMap.set(participantIdentity, false);
539
+ cryptor.setHasPacketTrailer(true);
540
+
541
+ const payload = Uint8Array.from([1, 2, 3, 4]);
542
+ const trailer = appendPacketTrailer(payload, 1_744_249_600_123_456n, 0);
543
+ const frame = mockRTCEncodedVideoFrame(trailer);
544
+
545
+ input.write(frame);
546
+ await vitest.advanceTimersToNextTimerAsync();
547
+
548
+ expect(new Uint8Array(output.chunks[0].data)).toEqual(payload);
549
+ expect(postMessage).toHaveBeenCalledWith({
550
+ kind: 'packetTrailerMetadata',
551
+ data: {
552
+ trackId: 'testTrack',
553
+ rtpTimestamp: frame.timestamp,
554
+ ssrc: 0,
555
+ metadata: {
556
+ userTimestamp: 1_744_249_600_123_456n,
557
+ frameId: 0,
558
+ },
559
+ },
560
+ });
561
+ } finally {
562
+ vitest.unstubAllGlobals();
563
+ vitest.useRealTimers();
564
+ }
565
+ });
566
+
394
567
  it('recovers from delayed use of rotated key', async () => {
395
568
  vitest.useFakeTimers();
396
569
  try {