livekit-client 2.18.9 → 2.19.0

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 (217) 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 +3553 -2813
  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 -4
  44. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  45. package/dist/src/room/Room.d.ts +7 -3
  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 +8 -14
  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/participant/RemoteParticipant.d.ts +5 -1
  73. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  74. package/dist/src/room/rpc/client/RpcClientManager.d.ts +39 -0
  75. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -0
  76. package/dist/src/room/rpc/client/events.d.ts +8 -0
  77. package/dist/src/room/rpc/client/events.d.ts.map +1 -0
  78. package/dist/src/room/rpc/index.d.ts +6 -0
  79. package/dist/src/room/rpc/index.d.ts.map +1 -0
  80. package/dist/src/room/rpc/server/RpcServerManager.d.ts +44 -0
  81. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -0
  82. package/dist/src/room/rpc/server/events.d.ts +8 -0
  83. package/dist/src/room/rpc/server/events.d.ts.map +1 -0
  84. package/dist/src/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  85. package/dist/src/room/rpc/utils.d.ts.map +1 -0
  86. package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
  87. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
  88. package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
  89. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  90. package/dist/src/room/track/Track.d.ts +1 -1
  91. package/dist/src/room/track/Track.d.ts.map +1 -1
  92. package/dist/src/room/track/create.d.ts.map +1 -1
  93. package/dist/src/room/track/options.d.ts +10 -0
  94. package/dist/src/room/track/options.d.ts.map +1 -1
  95. package/dist/src/room/track/utils.d.ts.map +1 -1
  96. package/dist/src/room/utils.d.ts +4 -3
  97. package/dist/src/room/utils.d.ts.map +1 -1
  98. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  99. package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
  100. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  101. package/dist/src/version.d.ts +9 -1
  102. package/dist/src/version.d.ts.map +1 -1
  103. package/dist/ts4.2/api/SignalClient.d.ts +2 -1
  104. package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
  105. package/dist/ts4.2/e2ee/types.d.ts +35 -8
  106. package/dist/ts4.2/e2ee/utils.d.ts +5 -5
  107. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
  108. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
  109. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
  110. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
  111. package/dist/ts4.2/index.d.ts +5 -1
  112. package/dist/ts4.2/options.d.ts +7 -0
  113. package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
  114. package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
  115. package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
  116. package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
  117. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  118. package/dist/ts4.2/room/RTCEngine.d.ts +2 -4
  119. package/dist/ts4.2/room/Room.d.ts +7 -3
  120. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
  121. package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
  122. package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
  123. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  124. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
  125. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +2 -2
  126. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
  127. package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
  128. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
  129. package/dist/ts4.2/room/data-track/types.d.ts +7 -0
  130. package/dist/ts4.2/room/events.d.ts +2 -2
  131. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +8 -14
  132. package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
  133. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +5 -1
  134. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -0
  135. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -0
  136. package/dist/ts4.2/room/rpc/index.d.ts +7 -0
  137. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -0
  138. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -0
  139. package/dist/ts4.2/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  140. package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
  141. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
  142. package/dist/ts4.2/room/track/Track.d.ts +1 -1
  143. package/dist/ts4.2/room/track/options.d.ts +10 -0
  144. package/dist/ts4.2/room/utils.d.ts +4 -3
  145. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
  146. package/dist/ts4.2/version.d.ts +9 -1
  147. package/package.json +24 -16
  148. package/src/api/SignalClient.test.ts +102 -10
  149. package/src/api/SignalClient.ts +4 -2
  150. package/src/api/WebSocketStream.test.ts +0 -1
  151. package/src/e2ee/E2eeManager.ts +82 -30
  152. package/src/e2ee/types.ts +37 -8
  153. package/src/e2ee/utils.ts +7 -6
  154. package/src/e2ee/worker/DataCryptor.ts +6 -6
  155. package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
  156. package/src/e2ee/worker/FrameCryptor.ts +94 -14
  157. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
  158. package/src/e2ee/worker/e2ee.worker.ts +13 -5
  159. package/src/e2ee/worker/naluUtils.ts +4 -4
  160. package/src/e2ee/worker/sifPayload.ts +10 -8
  161. package/src/index.ts +7 -0
  162. package/src/options.ts +8 -0
  163. package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
  164. package/src/packetTrailer/PacketTrailerManager.ts +250 -0
  165. package/src/packetTrailer/packetTrailer.test.ts +174 -0
  166. package/src/packetTrailer/packetTrailer.ts +276 -0
  167. package/src/packetTrailer/types.ts +75 -0
  168. package/src/packetTrailer/utils.test.ts +105 -0
  169. package/src/packetTrailer/utils.ts +50 -0
  170. package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
  171. package/src/packetTrailer/worker/tsconfig.json +14 -0
  172. package/src/room/BackOffStrategy.test.ts +1 -1
  173. package/src/room/RTCEngine.test.ts +219 -0
  174. package/src/room/RTCEngine.ts +86 -46
  175. package/src/room/Room.test.ts +62 -1
  176. package/src/room/Room.ts +111 -86
  177. package/src/room/data-track/RemoteDataTrack.ts +8 -1
  178. package/src/room/data-track/depacketizer.test.ts +433 -1
  179. package/src/room/data-track/depacketizer.ts +79 -61
  180. package/src/room/data-track/frame.ts +2 -2
  181. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
  182. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
  183. package/src/room/data-track/incoming/pipeline.ts +13 -2
  184. package/src/room/data-track/outgoing/types.ts +3 -2
  185. package/src/room/data-track/packet/extensions.ts +2 -2
  186. package/src/room/data-track/packet/index.ts +6 -6
  187. package/src/room/data-track/packet/serializable.ts +1 -1
  188. package/src/room/data-track/types.ts +8 -0
  189. package/src/room/events.ts +2 -2
  190. package/src/room/participant/LocalParticipant.test.ts +81 -0
  191. package/src/room/participant/LocalParticipant.ts +64 -187
  192. package/src/room/participant/Participant.ts +1 -1
  193. package/src/room/participant/RemoteParticipant.ts +9 -0
  194. package/src/room/participant/publishUtils.ts +1 -1
  195. package/src/room/rpc/client/RpcClientManager.test.ts +430 -0
  196. package/src/room/rpc/client/RpcClientManager.ts +269 -0
  197. package/src/room/rpc/client/events.ts +9 -0
  198. package/src/room/rpc/index.ts +14 -0
  199. package/src/room/rpc/server/RpcServerManager.test.ts +471 -0
  200. package/src/room/rpc/server/RpcServerManager.ts +293 -0
  201. package/src/room/rpc/server/events.ts +9 -0
  202. package/src/room/{rpc.ts → rpc/utils.ts} +49 -8
  203. package/src/room/track/PacketTrailerExtractor.ts +43 -0
  204. package/src/room/track/RemoteVideoTrack.ts +23 -2
  205. package/src/room/track/Track.ts +1 -1
  206. package/src/room/track/create.ts +0 -4
  207. package/src/room/track/options.ts +11 -0
  208. package/src/room/track/record.ts +1 -1
  209. package/src/room/track/utils.ts +4 -1
  210. package/src/room/utils.test.ts +14 -1
  211. package/src/room/utils.ts +19 -4
  212. package/src/test/MockMediaStreamTrack.ts +0 -1
  213. package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
  214. package/src/utils/dataPacketBuffer.ts +1 -1
  215. package/src/version.ts +11 -1
  216. package/dist/src/room/rpc.d.ts.map +0 -1
  217. package/src/room/rpc.test.ts +0 -301
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 {
@@ -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(