livekit-client 2.15.7 → 2.15.9

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 (196) 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 +253 -118
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2442 -323
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +31 -2
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/api/WebSocketStream.d.ts +29 -0
  12. package/dist/src/api/WebSocketStream.d.ts.map +1 -0
  13. package/dist/src/api/utils.d.ts +2 -0
  14. package/dist/src/api/utils.d.ts.map +1 -1
  15. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
  16. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  17. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  18. package/dist/src/e2ee/E2eeManager.d.ts +16 -2
  19. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  20. package/dist/src/e2ee/types.d.ts +35 -1
  21. package/dist/src/e2ee/types.d.ts.map +1 -1
  22. package/dist/src/e2ee/utils.d.ts +2 -0
  23. package/dist/src/e2ee/utils.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/DataCryptor.d.ts +15 -0
  25. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -0
  26. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +3 -2
  27. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  28. package/dist/src/e2ee/worker/sifPayload.d.ts +6 -6
  29. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  30. package/dist/src/index.d.ts +5 -3
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/logger.d.ts +1 -0
  33. package/dist/src/logger.d.ts.map +1 -1
  34. package/dist/src/options.d.ts +10 -2
  35. package/dist/src/options.d.ts.map +1 -1
  36. package/dist/src/room/PCTransport.d.ts +1 -0
  37. package/dist/src/room/PCTransport.d.ts.map +1 -1
  38. package/dist/src/room/PCTransportManager.d.ts +6 -4
  39. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  40. package/dist/src/room/RTCEngine.d.ts +6 -3
  41. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  42. package/dist/src/room/Room.d.ts +3 -2
  43. package/dist/src/room/Room.d.ts.map +1 -1
  44. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -2
  45. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  46. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  47. package/dist/src/room/defaults.d.ts.map +1 -1
  48. package/dist/src/room/errors.d.ts +2 -1
  49. package/dist/src/room/errors.d.ts.map +1 -1
  50. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  51. package/dist/src/room/participant/Participant.d.ts +2 -2
  52. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  53. package/dist/src/room/token-source/TokenSource.d.ts +70 -0
  54. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -0
  55. package/dist/src/room/token-source/types.d.ts +68 -0
  56. package/dist/src/room/token-source/types.d.ts.map +1 -0
  57. package/dist/src/room/token-source/utils.d.ts +5 -0
  58. package/dist/src/room/token-source/utils.d.ts.map +1 -0
  59. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  60. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  61. package/dist/src/room/track/options.d.ts +7 -3
  62. package/dist/src/room/track/options.d.ts.map +1 -1
  63. package/dist/src/room/track/utils.d.ts.map +1 -1
  64. package/dist/src/room/types.d.ts +1 -0
  65. package/dist/src/room/types.d.ts.map +1 -1
  66. package/dist/src/room/utils.d.ts +8 -1
  67. package/dist/src/room/utils.d.ts.map +1 -1
  68. package/dist/src/utils/camelToSnakeCase.d.ts +8 -0
  69. package/dist/src/utils/camelToSnakeCase.d.ts.map +1 -0
  70. package/dist/ts4.2/{src/api → api}/SignalClient.d.ts +31 -2
  71. package/dist/ts4.2/api/WebSocketStream.d.ts +29 -0
  72. package/dist/ts4.2/{src/api → api}/utils.d.ts +2 -0
  73. package/dist/ts4.2/{src/e2ee → e2ee}/E2eeManager.d.ts +16 -2
  74. package/dist/ts4.2/{src/e2ee → e2ee}/types.d.ts +35 -1
  75. package/dist/ts4.2/{src/e2ee → e2ee}/utils.d.ts +3 -0
  76. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +15 -0
  77. package/dist/ts4.2/{src/e2ee → e2ee}/worker/ParticipantKeyHandler.d.ts +3 -2
  78. package/dist/ts4.2/{src/e2ee → e2ee}/worker/sifPayload.d.ts +6 -6
  79. package/dist/ts4.2/{src/index.d.ts → index.d.ts} +5 -3
  80. package/dist/ts4.2/{src/logger.d.ts → logger.d.ts} +1 -0
  81. package/dist/ts4.2/{src/options.d.ts → options.d.ts} +10 -2
  82. package/dist/ts4.2/{src/room → room}/PCTransport.d.ts +1 -0
  83. package/dist/ts4.2/{src/room → room}/PCTransportManager.d.ts +6 -4
  84. package/dist/ts4.2/{src/room → room}/RTCEngine.d.ts +6 -3
  85. package/dist/ts4.2/{src/room → room}/Room.d.ts +3 -2
  86. package/dist/ts4.2/{src/room → room}/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -1
  87. package/dist/ts4.2/{src/room → room}/errors.d.ts +2 -1
  88. package/dist/ts4.2/{src/room → room}/participant/Participant.d.ts +2 -2
  89. package/dist/ts4.2/room/token-source/TokenSource.d.ts +71 -0
  90. package/dist/ts4.2/room/token-source/types.d.ts +68 -0
  91. package/dist/ts4.2/room/token-source/utils.d.ts +5 -0
  92. package/dist/ts4.2/{src/room → room}/track/LocalTrack.d.ts +1 -1
  93. package/dist/ts4.2/{src/room → room}/track/options.d.ts +10 -3
  94. package/dist/ts4.2/{src/room → room}/types.d.ts +1 -0
  95. package/dist/ts4.2/{src/room → room}/utils.d.ts +8 -1
  96. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -0
  97. package/package.json +11 -10
  98. package/src/api/SignalClient.test.ts +688 -0
  99. package/src/api/SignalClient.ts +308 -161
  100. package/src/api/WebSocketStream.test.ts +625 -0
  101. package/src/api/WebSocketStream.ts +118 -0
  102. package/src/api/utils.ts +10 -0
  103. package/src/connectionHelper/checks/publishVideo.ts +5 -0
  104. package/src/connectionHelper/checks/turn.ts +1 -0
  105. package/src/connectionHelper/checks/webrtc.ts +1 -1
  106. package/src/connectionHelper/checks/websocket.ts +1 -0
  107. package/src/e2ee/E2eeManager.ts +94 -2
  108. package/src/e2ee/types.ts +44 -1
  109. package/src/e2ee/utils.ts +16 -0
  110. package/src/e2ee/worker/DataCryptor.test.ts +271 -0
  111. package/src/e2ee/worker/DataCryptor.ts +147 -0
  112. package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -3
  113. package/src/e2ee/worker/e2ee.worker.ts +47 -0
  114. package/src/e2ee/worker/sifPayload.ts +10 -6
  115. package/src/index.ts +16 -1
  116. package/src/logger.ts +1 -0
  117. package/src/options.ts +15 -2
  118. package/src/room/PCTransport.ts +7 -3
  119. package/src/room/PCTransportManager.ts +39 -35
  120. package/src/room/RTCEngine.ts +109 -22
  121. package/src/room/Room.ts +43 -18
  122. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +64 -17
  123. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -0
  124. package/src/room/defaults.ts +1 -0
  125. package/src/room/errors.ts +3 -0
  126. package/src/room/participant/LocalParticipant.ts +8 -6
  127. package/src/room/participant/Participant.ts +6 -1
  128. package/src/room/token-source/TokenSource.ts +285 -0
  129. package/src/room/token-source/types.ts +84 -0
  130. package/src/room/token-source/utils.test.ts +63 -0
  131. package/src/room/token-source/utils.ts +40 -0
  132. package/src/room/track/LocalAudioTrack.ts +1 -1
  133. package/src/room/track/LocalTrack.ts +1 -1
  134. package/src/room/track/options.ts +12 -4
  135. package/src/room/track/utils.ts +10 -2
  136. package/src/room/types.ts +1 -0
  137. package/src/room/utils.ts +37 -4
  138. package/src/utils/camelToSnakeCase.ts +16 -0
  139. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/ConnectionCheck.d.ts +0 -0
  140. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/Checker.d.ts +0 -0
  141. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/cloudRegion.d.ts +0 -0
  142. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/connectionProtocol.d.ts +0 -0
  143. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishAudio.d.ts +0 -0
  144. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishVideo.d.ts +0 -0
  145. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/reconnect.d.ts +0 -0
  146. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/turn.d.ts +0 -0
  147. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/webrtc.d.ts +0 -0
  148. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/websocket.d.ts +0 -0
  149. /package/dist/ts4.2/{src/e2ee → e2ee}/KeyProvider.d.ts +0 -0
  150. /package/dist/ts4.2/{src/e2ee → e2ee}/constants.d.ts +0 -0
  151. /package/dist/ts4.2/{src/e2ee → e2ee}/errors.d.ts +0 -0
  152. /package/dist/ts4.2/{src/e2ee → e2ee}/events.d.ts +0 -0
  153. /package/dist/ts4.2/{src/e2ee → e2ee}/index.d.ts +0 -0
  154. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/FrameCryptor.d.ts +0 -0
  155. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/e2ee.worker.d.ts +0 -0
  156. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/naluUtils.d.ts +0 -0
  157. /package/dist/ts4.2/{src/room → room}/DefaultReconnectPolicy.d.ts +0 -0
  158. /package/dist/ts4.2/{src/room → room}/DeviceManager.d.ts +0 -0
  159. /package/dist/ts4.2/{src/room → room}/ReconnectPolicy.d.ts +0 -0
  160. /package/dist/ts4.2/{src/room → room}/RegionUrlProvider.d.ts +0 -0
  161. /package/dist/ts4.2/{src/room → room}/attribute-typings.d.ts +0 -0
  162. /package/dist/ts4.2/{src/room → room}/data-stream/incoming/StreamReader.d.ts +0 -0
  163. /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -0
  164. /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/StreamWriter.d.ts +0 -0
  165. /package/dist/ts4.2/{src/room → room}/defaults.d.ts +0 -0
  166. /package/dist/ts4.2/{src/room → room}/events.d.ts +0 -0
  167. /package/dist/ts4.2/{src/room → room}/participant/LocalParticipant.d.ts +0 -0
  168. /package/dist/ts4.2/{src/room → room}/participant/ParticipantTrackPermission.d.ts +0 -0
  169. /package/dist/ts4.2/{src/room → room}/participant/RemoteParticipant.d.ts +0 -0
  170. /package/dist/ts4.2/{src/room → room}/participant/publishUtils.d.ts +0 -0
  171. /package/dist/ts4.2/{src/room → room}/rpc.d.ts +0 -0
  172. /package/dist/ts4.2/{src/room → room}/stats.d.ts +0 -0
  173. /package/dist/ts4.2/{src/room → room}/timers.d.ts +0 -0
  174. /package/dist/ts4.2/{src/room → room}/track/LocalAudioTrack.d.ts +0 -0
  175. /package/dist/ts4.2/{src/room → room}/track/LocalTrackPublication.d.ts +0 -0
  176. /package/dist/ts4.2/{src/room → room}/track/LocalVideoTrack.d.ts +0 -0
  177. /package/dist/ts4.2/{src/room → room}/track/RemoteAudioTrack.d.ts +0 -0
  178. /package/dist/ts4.2/{src/room → room}/track/RemoteTrack.d.ts +0 -0
  179. /package/dist/ts4.2/{src/room → room}/track/RemoteTrackPublication.d.ts +0 -0
  180. /package/dist/ts4.2/{src/room → room}/track/RemoteVideoTrack.d.ts +0 -0
  181. /package/dist/ts4.2/{src/room → room}/track/Track.d.ts +0 -0
  182. /package/dist/ts4.2/{src/room → room}/track/TrackPublication.d.ts +0 -0
  183. /package/dist/ts4.2/{src/room → room}/track/create.d.ts +0 -0
  184. /package/dist/ts4.2/{src/room → room}/track/facingMode.d.ts +0 -0
  185. /package/dist/ts4.2/{src/room → room}/track/processor/types.d.ts +0 -0
  186. /package/dist/ts4.2/{src/room → room}/track/record.d.ts +0 -0
  187. /package/dist/ts4.2/{src/room → room}/track/types.d.ts +0 -0
  188. /package/dist/ts4.2/{src/room → room}/track/utils.d.ts +0 -0
  189. /package/dist/ts4.2/{src/test → test}/MockMediaStreamTrack.d.ts +0 -0
  190. /package/dist/ts4.2/{src/test → test}/mocks.d.ts +0 -0
  191. /package/dist/ts4.2/{src/utils → utils}/AsyncQueue.d.ts +0 -0
  192. /package/dist/ts4.2/{src/utils → utils}/browserParser.d.ts +0 -0
  193. /package/dist/ts4.2/{src/utils → utils}/cloneDeep.d.ts +0 -0
  194. /package/dist/ts4.2/{src/utils → utils}/dataPacketBuffer.d.ts +0 -0
  195. /package/dist/ts4.2/{src/utils → utils}/ttlmap.d.ts +0 -0
  196. /package/dist/ts4.2/{src/version.d.ts → version.d.ts} +0 -0
@@ -0,0 +1,118 @@
1
+ // https://github.com/CarterLi/websocketstream-polyfill
2
+ import { sleep } from '../room/utils';
3
+
4
+ export interface WebSocketConnection<T extends ArrayBuffer | string = ArrayBuffer | string> {
5
+ readable: ReadableStream<T>;
6
+ writable: WritableStream<T>;
7
+ protocol: string;
8
+ extensions: string;
9
+ }
10
+
11
+ export interface WebSocketCloseInfo {
12
+ closeCode?: number;
13
+ reason?: string;
14
+ }
15
+
16
+ export interface WebSocketStreamOptions {
17
+ protocols?: string[];
18
+ signal?: AbortSignal;
19
+ }
20
+
21
+ /**
22
+ * [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
23
+ *
24
+ * @see https://web.dev/websocketstream/
25
+ */
26
+ export class WebSocketStream<T extends ArrayBuffer | string = ArrayBuffer | string> {
27
+ readonly url: string;
28
+
29
+ readonly opened: Promise<WebSocketConnection<T>>;
30
+
31
+ readonly closed: Promise<WebSocketCloseInfo>;
32
+
33
+ readonly close: (closeInfo?: WebSocketCloseInfo) => void;
34
+
35
+ get readyState(): number {
36
+ return this.ws.readyState;
37
+ }
38
+
39
+ private ws: WebSocket;
40
+
41
+ constructor(url: string, options: WebSocketStreamOptions = {}) {
42
+ if (options.signal?.aborted) {
43
+ throw new DOMException('This operation was aborted', 'AbortError');
44
+ }
45
+
46
+ this.url = url;
47
+
48
+ const ws = new WebSocket(url, options.protocols ?? []);
49
+ ws.binaryType = 'arraybuffer';
50
+ this.ws = ws;
51
+
52
+ const closeWithInfo = ({ closeCode: code, reason }: WebSocketCloseInfo = {}) =>
53
+ ws.close(code, reason);
54
+
55
+ this.opened = new Promise((resolve, reject) => {
56
+ ws.onopen = () => {
57
+ resolve({
58
+ readable: new ReadableStream<T>({
59
+ start(controller) {
60
+ ws.onmessage = ({ data }) => controller.enqueue(data);
61
+ ws.onerror = (e) => controller.error(e);
62
+ },
63
+ cancel: closeWithInfo,
64
+ }),
65
+ writable: new WritableStream<T>({
66
+ write(chunk) {
67
+ ws.send(chunk);
68
+ },
69
+ abort() {
70
+ ws.close();
71
+ },
72
+ close: closeWithInfo,
73
+ }),
74
+ protocol: ws.protocol,
75
+ extensions: ws.extensions,
76
+ });
77
+ ws.removeEventListener('error', reject);
78
+ };
79
+ ws.addEventListener('error', reject);
80
+ });
81
+
82
+ this.closed = new Promise<WebSocketCloseInfo>((resolve, reject) => {
83
+ const rejectHandler = async () => {
84
+ const closePromise = new Promise<CloseEvent>((res) => {
85
+ if (ws.readyState === WebSocket.CLOSED) return;
86
+ else {
87
+ ws.addEventListener(
88
+ 'close',
89
+ (closeEv: CloseEvent) => {
90
+ res(closeEv);
91
+ },
92
+ { once: true },
93
+ );
94
+ }
95
+ });
96
+ const reason = await Promise.race([sleep(250), closePromise]);
97
+ if (!reason) {
98
+ reject(new Error('Encountered unspecified websocket error without a timely close event'));
99
+ } else {
100
+ // if we can infer the close reason from the close event then resolve the promise, we don't need to throw
101
+ resolve(reason);
102
+ }
103
+ };
104
+ ws.onclose = ({ code, reason }) => {
105
+ resolve({ closeCode: code, reason });
106
+ ws.removeEventListener('error', rejectHandler);
107
+ };
108
+
109
+ ws.addEventListener('error', rejectHandler);
110
+ });
111
+
112
+ if (options.signal) {
113
+ options.signal.onabort = () => ws.close();
114
+ }
115
+
116
+ this.close = closeWithInfo;
117
+ }
118
+ }
package/src/api/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SignalResponse } from '@livekit/protocol';
1
2
  import { toHttpUrl, toWebsocketUrl } from '../room/utils';
2
3
 
3
4
  export function createRtcUrl(url: string, searchParams: URLSearchParams) {
@@ -21,3 +22,12 @@ function appendUrlPath(urlObj: URL, path: string) {
21
22
  urlObj.pathname = `${ensureTrailingSlash(urlObj.pathname)}${path}`;
22
23
  return urlObj.toString();
23
24
  }
25
+
26
+ export function parseSignalResponse(value: ArrayBuffer | string) {
27
+ if (typeof value === 'string') {
28
+ return SignalResponse.fromJson(JSON.parse(value), { ignoreUnknownFields: true });
29
+ } else if (value instanceof ArrayBuffer) {
30
+ return SignalResponse.fromBinary(new Uint8Array(value));
31
+ }
32
+ throw new Error(`could not decode websocket message: ${typeof value}`);
33
+ }
@@ -46,6 +46,11 @@ export class PublishVideoCheck extends Checker {
46
46
  const video = document.createElement('video');
47
47
  video.srcObject = stream;
48
48
  video.muted = true;
49
+ video.autoplay = true;
50
+ video.playsInline = true;
51
+ // For iOS Safari
52
+ video.setAttribute('playsinline', 'true');
53
+ document.body.appendChild(video);
49
54
 
50
55
  await new Promise<void>((resolve) => {
51
56
  video.onplay = () => {
@@ -13,6 +13,7 @@ export class TURNCheck extends Checker {
13
13
  maxRetries: 0,
14
14
  e2eeEnabled: false,
15
15
  websocketTimeout: 15_000,
16
+ singlePeerConnection: false,
16
17
  });
17
18
 
18
19
  let hasTLS = false;
@@ -38,7 +38,7 @@ export class WebRTCCheck extends Checker {
38
38
  }
39
39
  };
40
40
 
41
- if (this.room.engine.pcManager) {
41
+ if (this.room.engine.pcManager?.subscriber) {
42
42
  this.room.engine.pcManager.subscriber.onIceCandidateError = (ev) => {
43
43
  if (ev instanceof RTCPeerConnectionIceErrorEvent) {
44
44
  this.appendWarning(
@@ -18,6 +18,7 @@ export class WebSocketCheck extends Checker {
18
18
  maxRetries: 0,
19
19
  e2eeEnabled: false,
20
20
  websocketTimeout: 15_000,
21
+ singlePeerConnection: false,
21
22
  });
22
23
  this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
23
24
  if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
@@ -11,15 +11,19 @@ import type RemoteTrack from '../room/track/RemoteTrack';
11
11
  import type { Track } from '../room/track/Track';
12
12
  import type { VideoCodec } from '../room/track/options';
13
13
  import { mimeTypeToVideoCodecString } from '../room/track/utils';
14
- import { isLocalTrack, isSafariBased, isVideoTrack } from '../room/utils';
14
+ import { Future, isLocalTrack, isSafariBased, isVideoTrack } from '../room/utils';
15
15
  import type { BaseKeyProvider } from './KeyProvider';
16
16
  import { E2EE_FLAG } from './constants';
17
17
  import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
18
18
  import type {
19
+ DecryptDataRequestMessage,
20
+ DecryptDataResponseMessage,
19
21
  E2EEManagerOptions,
20
22
  E2EEWorkerMessage,
21
23
  EnableMessage,
22
24
  EncodeMessage,
25
+ EncryptDataRequestMessage,
26
+ EncryptDataResponseMessage,
23
27
  InitMessage,
24
28
  KeyInfo,
25
29
  RTPVideoMapMessage,
@@ -35,8 +39,17 @@ import { isE2EESupported, isScriptTransformSupported } from './utils';
35
39
  export interface BaseE2EEManager {
36
40
  setup(room: Room): void;
37
41
  setupEngine(engine: RTCEngine): void;
42
+ isEnabled: boolean;
43
+ isDataChannelEncryptionEnabled: boolean;
38
44
  setParticipantCryptorEnabled(enabled: boolean, participantIdentity: string): void;
39
45
  setSifTrailer(trailer: Uint8Array): void;
46
+ encryptData(data: Uint8Array): Promise<EncryptDataResponseMessage['data']>;
47
+ handleEncryptedData(
48
+ payload: Uint8Array,
49
+ iv: Uint8Array,
50
+ participantIdentity: string,
51
+ keyIndex: number,
52
+ ): Promise<DecryptDataResponseMessage['data']>;
40
53
  on<E extends keyof E2EEManagerCallbacks>(event: E, listener: E2EEManagerCallbacks[E]): this;
41
54
  }
42
55
 
@@ -55,11 +68,26 @@ export class E2EEManager
55
68
 
56
69
  private keyProvider: BaseKeyProvider;
57
70
 
58
- constructor(options: E2EEManagerOptions) {
71
+ private decryptDataRequests: Map<string, Future<DecryptDataResponseMessage['data']>> = new Map();
72
+
73
+ private encryptDataRequests: Map<string, Future<EncryptDataResponseMessage['data']>> = new Map();
74
+
75
+ private dataChannelEncryptionEnabled: boolean;
76
+
77
+ constructor(options: E2EEManagerOptions, dcEncryptionEnabled: boolean) {
59
78
  super();
60
79
  this.keyProvider = options.keyProvider;
61
80
  this.worker = options.worker;
62
81
  this.encryptionEnabled = false;
82
+ this.dataChannelEncryptionEnabled = dcEncryptionEnabled;
83
+ }
84
+
85
+ get isEnabled(): boolean {
86
+ return this.encryptionEnabled;
87
+ }
88
+
89
+ get isDataChannelEncryptionEnabled(): boolean {
90
+ return this.isEnabled && this.dataChannelEncryptionEnabled;
63
91
  }
64
92
 
65
93
  /**
@@ -160,6 +188,19 @@ export class E2EEManager
160
188
  data.keyIndex,
161
189
  );
162
190
  break;
191
+
192
+ case 'decryptDataResponse':
193
+ const decryptFuture = this.decryptDataRequests.get(data.uuid);
194
+ if (decryptFuture?.resolve) {
195
+ decryptFuture.resolve(data);
196
+ }
197
+ break;
198
+ case 'encryptDataResponse':
199
+ const encryptFuture = this.encryptDataRequests.get(data.uuid);
200
+ if (encryptFuture?.resolve) {
201
+ encryptFuture.resolve(data as EncryptDataResponseMessage['data']);
202
+ }
203
+ break;
163
204
  default:
164
205
  break;
165
206
  }
@@ -250,6 +291,57 @@ export class E2EEManager
250
291
  );
251
292
  }
252
293
 
294
+ async encryptData(data: Uint8Array): Promise<EncryptDataResponseMessage['data']> {
295
+ if (!this.worker) {
296
+ throw Error('could not encrypt data, worker is missing');
297
+ }
298
+ const uuid = crypto.randomUUID();
299
+ const msg: EncryptDataRequestMessage = {
300
+ kind: 'encryptDataRequest',
301
+ data: {
302
+ uuid,
303
+ payload: data,
304
+ participantIdentity: this.room!.localParticipant.identity,
305
+ },
306
+ };
307
+ const future = new Future<EncryptDataResponseMessage['data']>();
308
+ future.onFinally = () => {
309
+ this.encryptDataRequests.delete(uuid);
310
+ };
311
+ this.encryptDataRequests.set(uuid, future);
312
+ this.worker.postMessage(msg);
313
+ return future!.promise!;
314
+ }
315
+
316
+ handleEncryptedData(
317
+ payload: Uint8Array,
318
+ iv: Uint8Array,
319
+ participantIdentity: string,
320
+ keyIndex: number,
321
+ ) {
322
+ if (!this.worker) {
323
+ throw Error('could not handle encrypted data, worker is missing');
324
+ }
325
+ const uuid = crypto.randomUUID();
326
+ const msg: DecryptDataRequestMessage = {
327
+ kind: 'decryptDataRequest',
328
+ data: {
329
+ uuid,
330
+ payload,
331
+ iv,
332
+ participantIdentity,
333
+ keyIndex,
334
+ },
335
+ };
336
+ const future = new Future<DecryptDataResponseMessage['data']>();
337
+ future.onFinally = () => {
338
+ this.decryptDataRequests.delete(uuid);
339
+ };
340
+ this.decryptDataRequests.set(uuid, future);
341
+ this.worker.postMessage(msg);
342
+ return future.promise;
343
+ }
344
+
253
345
  private postRatchetRequest(participantIdentity?: string, keyIndex?: number) {
254
346
  if (!this.worker) {
255
347
  throw Error('could not ratchet key, worker is missing');
package/src/e2ee/types.ts CHANGED
@@ -109,6 +109,44 @@ export interface InitAck extends BaseMessage {
109
109
  };
110
110
  }
111
111
 
112
+ export interface DecryptDataRequestMessage extends BaseMessage {
113
+ kind: 'decryptDataRequest';
114
+ data: {
115
+ uuid: string;
116
+ payload: Uint8Array;
117
+ iv: Uint8Array;
118
+ participantIdentity: string;
119
+ keyIndex: number;
120
+ };
121
+ }
122
+
123
+ export interface DecryptDataResponseMessage extends BaseMessage {
124
+ kind: 'decryptDataResponse';
125
+ data: {
126
+ uuid: string;
127
+ payload: Uint8Array;
128
+ };
129
+ }
130
+
131
+ export interface EncryptDataRequestMessage extends BaseMessage {
132
+ kind: 'encryptDataRequest';
133
+ data: {
134
+ uuid: string;
135
+ payload: Uint8Array;
136
+ participantIdentity: string;
137
+ };
138
+ }
139
+
140
+ export interface EncryptDataResponseMessage extends BaseMessage {
141
+ kind: 'encryptDataResponse';
142
+ data: {
143
+ uuid: string;
144
+ payload: Uint8Array;
145
+ iv: Uint8Array;
146
+ keyIndex: number;
147
+ };
148
+ }
149
+
112
150
  export type E2EEWorkerMessage =
113
151
  | InitMessage
114
152
  | SetKeyMessage
@@ -121,7 +159,11 @@ export type E2EEWorkerMessage =
121
159
  | RatchetRequestMessage
122
160
  | RatchetMessage
123
161
  | SifTrailerMessage
124
- | InitAck;
162
+ | InitAck
163
+ | DecryptDataRequestMessage
164
+ | DecryptDataResponseMessage
165
+ | EncryptDataRequestMessage
166
+ | EncryptDataResponseMessage;
125
167
 
126
168
  export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
127
169
 
@@ -150,6 +192,7 @@ export type E2EEManagerOptions = {
150
192
  keyProvider: BaseKeyProvider;
151
193
  worker: Worker;
152
194
  };
195
+
153
196
  export type E2EEOptions =
154
197
  | E2EEManagerOptions
155
198
  | {
package/src/e2ee/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type DataPacket, EncryptedPacketPayload } from '@livekit/protocol';
1
2
  import { ENCRYPTION_ALGORITHM } from './constants';
2
3
 
3
4
  export function isE2EESupported() {
@@ -176,3 +177,18 @@ export function writeRbsp(data_in: Uint8Array): Uint8Array {
176
177
  }
177
178
  return new Uint8Array(dataOut);
178
179
  }
180
+
181
+ export function asEncryptablePacket(packet: DataPacket): EncryptedPacketPayload | undefined {
182
+ if (
183
+ packet.value?.case !== 'sipDtmf' &&
184
+ packet.value?.case !== 'metrics' &&
185
+ packet.value?.case !== 'speaker' &&
186
+ packet.value?.case !== 'transcription' &&
187
+ packet.value?.case !== 'encryptedPacket'
188
+ ) {
189
+ return new EncryptedPacketPayload({
190
+ value: packet.value,
191
+ });
192
+ }
193
+ return undefined;
194
+ }
@@ -0,0 +1,271 @@
1
+ import { describe, expect, it, vitest } from 'vitest';
2
+ import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
3
+ import type { KeyProviderOptions } from '../types';
4
+ import { createKeyMaterialFromString } from '../utils';
5
+ import { DataCryptor } from './DataCryptor';
6
+ import { ParticipantKeyHandler } from './ParticipantKeyHandler';
7
+
8
+ function prepareParticipantTestKeys(
9
+ participantIdentity: string,
10
+ partialKeyProviderOptions: Partial<KeyProviderOptions>,
11
+ ): ParticipantKeyHandler {
12
+ const keyProviderOptions = { ...KEY_PROVIDER_DEFAULTS, ...partialKeyProviderOptions };
13
+ return new ParticipantKeyHandler(participantIdentity, keyProviderOptions);
14
+ }
15
+
16
+ describe('DataCryptor', () => {
17
+ const participantIdentity = 'testParticipant';
18
+
19
+ describe('encrypt', () => {
20
+ it('throws error when no key set', async () => {
21
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
22
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
23
+
24
+ await expect(DataCryptor.encrypt(data, keys)).rejects.toThrow('No key set found');
25
+ });
26
+
27
+ it('encrypts data successfully with key', async () => {
28
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
29
+ await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
30
+
31
+ const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
32
+ const result = await DataCryptor.encrypt(plainData, keys);
33
+
34
+ expect(result.payload).toBeInstanceOf(Uint8Array);
35
+ expect(result.iv).toBeInstanceOf(Uint8Array);
36
+ expect(result.iv.length).toBe(IV_LENGTH);
37
+ expect(result.keyIndex).toBe(1);
38
+ expect(result.payload).not.toEqual(plainData);
39
+ expect(result.payload.length).toBeGreaterThan(0);
40
+ });
41
+
42
+ it('generates different IV for each encryption', async () => {
43
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
44
+ await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
45
+
46
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
47
+
48
+ const result1 = await DataCryptor.encrypt(data, keys);
49
+ const result2 = await DataCryptor.encrypt(data, keys);
50
+
51
+ expect(result1.iv).not.toEqual(result2.iv);
52
+ expect(result1.payload).not.toEqual(result2.payload);
53
+ });
54
+
55
+ it('uses correct key index from key handler', async () => {
56
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
57
+ await keys.setKey(await createKeyMaterialFromString('test-key'), 5);
58
+
59
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
60
+ const result = await DataCryptor.encrypt(data, keys);
61
+
62
+ expect(result.keyIndex).toBe(5);
63
+ });
64
+ });
65
+
66
+ describe('decrypt', () => {
67
+ it('throws error when no key set for index', async () => {
68
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
69
+ const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
70
+ const iv = new Uint8Array(IV_LENGTH);
71
+
72
+ await expect(DataCryptor.decrypt(data, iv, keys, 1)).rejects.toThrow('No key set found');
73
+ });
74
+
75
+ it('decrypts data successfully with correct key', async () => {
76
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
77
+ await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
78
+
79
+ const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
80
+
81
+ // First encrypt the data
82
+ const encrypted = await DataCryptor.encrypt(plainData, keys);
83
+
84
+ // Then decrypt it
85
+ const decrypted = await DataCryptor.decrypt(
86
+ encrypted.payload,
87
+ encrypted.iv,
88
+ keys,
89
+ encrypted.keyIndex,
90
+ );
91
+
92
+ expect(decrypted.payload).toEqual(plainData);
93
+ });
94
+
95
+ it('fails to decrypt with incorrect key', async () => {
96
+ const keys1 = prepareParticipantTestKeys('participant1', {});
97
+ const keys2 = prepareParticipantTestKeys('participant2', {});
98
+
99
+ await keys1.setKey(await createKeyMaterialFromString('correct-key'), 1);
100
+ await keys2.setKey(await createKeyMaterialFromString('wrong-key'), 1);
101
+
102
+ const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
103
+
104
+ // Encrypt with first key
105
+ const encrypted = await DataCryptor.encrypt(plainData, keys1);
106
+
107
+ // Try to decrypt with second (wrong) key
108
+ await expect(
109
+ DataCryptor.decrypt(encrypted.payload, encrypted.iv, keys2, encrypted.keyIndex),
110
+ ).rejects.toThrow();
111
+ });
112
+
113
+ it('handles ratcheting when enabled', async () => {
114
+ const senderKeys = prepareParticipantTestKeys('sender', {
115
+ ratchetWindowSize: 2,
116
+ });
117
+ const receiverKeys = prepareParticipantTestKeys('receiver', {
118
+ ratchetWindowSize: 2,
119
+ });
120
+
121
+ // Both start with the same initial key
122
+ const initialMaterial = await createKeyMaterialFromString('test-key');
123
+ await senderKeys.setKey(initialMaterial, 1);
124
+ await receiverKeys.setKey(initialMaterial, 1);
125
+
126
+ const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
127
+
128
+ // Sender ratchets their key forward
129
+ await senderKeys.ratchetKey(1, false);
130
+
131
+ // Sender encrypts data with the ratcheted key
132
+ const encrypted = await DataCryptor.encrypt(plainData, senderKeys);
133
+
134
+ // Receiver should be able to decrypt by automatically ratcheting their key
135
+ const decrypted = await DataCryptor.decrypt(
136
+ encrypted.payload,
137
+ encrypted.iv,
138
+ receiverKeys,
139
+ encrypted.keyIndex,
140
+ );
141
+
142
+ expect(decrypted.payload).toEqual(plainData);
143
+ });
144
+
145
+ it('respects ratchet window size limit', async () => {
146
+ // Create a scenario where we have valid encrypted data that requires ratcheting but it's disabled
147
+ const senderKeys = prepareParticipantTestKeys('sender', {
148
+ ratchetWindowSize: 10, // Large window for sender
149
+ });
150
+ const receiverKeys = prepareParticipantTestKeys('receiver', {
151
+ ratchetWindowSize: 1, // No ratcheting allowed for receiver
152
+ });
153
+
154
+ // Both start with the same initial key
155
+ const initialMaterial = await createKeyMaterialFromString('test-key');
156
+ await senderKeys.setKey(initialMaterial, 1);
157
+ await receiverKeys.setKey(initialMaterial, 1);
158
+
159
+ const plainData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
160
+
161
+ // Sender ratchets their key forward once
162
+ await senderKeys.ratchetKey(1);
163
+ await senderKeys.ratchetKey(1);
164
+
165
+ // Sender encrypts data with the ratcheted key
166
+ const encrypted = await DataCryptor.encrypt(plainData, senderKeys);
167
+
168
+ // Receiver should fail to decrypt with invalid key because ratcheting is limited (window size 1)
169
+ await expect(
170
+ DataCryptor.decrypt(encrypted.payload, encrypted.iv, receiverKeys, encrypted.keyIndex),
171
+ ).rejects.toThrow('valid key missing for participant');
172
+ });
173
+
174
+ it('throws CryptorError when ratcheting disabled and decryption fails', async () => {
175
+ const keys = prepareParticipantTestKeys(participantIdentity, {
176
+ ratchetWindowSize: 0,
177
+ });
178
+
179
+ await keys.setKey(await createKeyMaterialFromString('test-key'), 1);
180
+
181
+ const invalidData = new Uint8Array([99, 98, 97, 96, 95, 94, 93, 92]);
182
+ const iv = new Uint8Array(IV_LENGTH);
183
+ crypto.getRandomValues(iv);
184
+
185
+ await expect(DataCryptor.decrypt(invalidData, iv, keys, 1)).rejects.toThrow(
186
+ 'Decryption failed',
187
+ );
188
+ });
189
+ });
190
+
191
+ describe('round-trip encryption/decryption', () => {
192
+ it('encrypts and decrypts data correctly', async () => {
193
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
194
+ await keys.setKey(await createKeyMaterialFromString('round-trip-key'), 2);
195
+
196
+ const originalData = new Uint8Array([
197
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
198
+ 26, 27, 28, 29, 30, 31, 32,
199
+ ]);
200
+
201
+ const encrypted = await DataCryptor.encrypt(originalData, keys);
202
+ const decrypted = await DataCryptor.decrypt(
203
+ encrypted.payload,
204
+ encrypted.iv,
205
+ keys,
206
+ encrypted.keyIndex,
207
+ );
208
+
209
+ expect(decrypted.payload).toEqual(originalData);
210
+ });
211
+
212
+ it('handles empty data', async () => {
213
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
214
+ await keys.setKey(await createKeyMaterialFromString('empty-data-key'), 1);
215
+
216
+ const emptyData = new Uint8Array(0);
217
+
218
+ const encrypted = await DataCryptor.encrypt(emptyData, keys);
219
+ const decrypted = await DataCryptor.decrypt(
220
+ encrypted.payload,
221
+ encrypted.iv,
222
+ keys,
223
+ encrypted.keyIndex,
224
+ );
225
+
226
+ expect(decrypted.payload).toEqual(emptyData);
227
+ });
228
+
229
+ it('handles large data', async () => {
230
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
231
+ await keys.setKey(await createKeyMaterialFromString('large-data-key'), 1);
232
+
233
+ const largeData = new Uint8Array(1024);
234
+ for (let i = 0; i < largeData.length; i++) {
235
+ largeData[i] = i % 256;
236
+ }
237
+
238
+ const encrypted = await DataCryptor.encrypt(largeData, keys);
239
+ const decrypted = await DataCryptor.decrypt(
240
+ encrypted.payload,
241
+ encrypted.iv,
242
+ keys,
243
+ encrypted.keyIndex,
244
+ );
245
+
246
+ expect(decrypted.payload).toEqual(largeData);
247
+ });
248
+ });
249
+
250
+ describe('IV generation', () => {
251
+ it('generates unique IVs with performance.now() timestamp', async () => {
252
+ const keys = prepareParticipantTestKeys(participantIdentity, {});
253
+ await keys.setKey(await createKeyMaterialFromString('iv-test-key'), 1);
254
+
255
+ const data = new Uint8Array([1, 2, 3, 4]);
256
+
257
+ vitest.useFakeTimers();
258
+ const time1 = 1000;
259
+ vitest.setSystemTime(time1);
260
+ const result1 = await DataCryptor.encrypt(data, keys);
261
+
262
+ vitest.setSystemTime(2000);
263
+ const result2 = await DataCryptor.encrypt(data, keys);
264
+
265
+ vitest.useRealTimers();
266
+
267
+ // IVs should be different due to different timestamps and sendCount
268
+ expect(result1.iv).not.toEqual(result2.iv);
269
+ });
270
+ });
271
+ });