livekit-client 2.18.0 → 2.18.1

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 (133) 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 +8 -7
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +7832 -5799
  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 +12 -4
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/e2ee/constants.d.ts.map +1 -1
  12. package/dist/src/e2ee/types.d.ts +6 -0
  13. package/dist/src/e2ee/types.d.ts.map +1 -1
  14. package/dist/src/e2ee/utils.d.ts +2 -1
  15. package/dist/src/e2ee/utils.d.ts.map +1 -1
  16. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  17. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  18. package/dist/src/index.d.ts +5 -4
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/room/PCTransport.d.ts +5 -0
  21. package/dist/src/room/PCTransport.d.ts.map +1 -1
  22. package/dist/src/room/PCTransportManager.d.ts +1 -1
  23. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  24. package/dist/src/room/RTCEngine.d.ts +24 -8
  25. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  26. package/dist/src/room/Room.d.ts +13 -3
  27. package/dist/src/room/Room.d.ts.map +1 -1
  28. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  29. package/dist/src/room/data-track/LocalDataTrack.d.ts +28 -5
  30. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  31. package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -5
  32. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  33. package/dist/src/room/data-track/depacketizer.d.ts +4 -4
  34. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  35. package/dist/src/room/data-track/frame.d.ts +14 -0
  36. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  37. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +19 -11
  38. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  39. package/dist/src/room/data-track/incoming/pipeline.d.ts +6 -5
  40. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  41. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +57 -23
  42. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  43. package/dist/src/room/data-track/outgoing/errors.d.ts +16 -6
  44. package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -1
  45. package/dist/src/room/data-track/outgoing/pipeline.d.ts +7 -6
  46. package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -1
  47. package/dist/src/room/data-track/outgoing/types.d.ts +14 -4
  48. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  49. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  50. package/dist/src/room/data-track/packetizer.d.ts +4 -4
  51. package/dist/src/room/data-track/packetizer.d.ts.map +1 -1
  52. package/dist/src/room/data-track/track-interfaces.d.ts +1 -1
  53. package/dist/src/room/data-track/track-interfaces.d.ts.map +1 -1
  54. package/dist/src/room/data-track/types.d.ts +6 -1
  55. package/dist/src/room/data-track/types.d.ts.map +1 -1
  56. package/dist/src/room/events.d.ts +24 -3
  57. package/dist/src/room/events.d.ts.map +1 -1
  58. package/dist/src/room/participant/LocalParticipant.d.ts +11 -1
  59. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  60. package/dist/src/room/participant/RemoteParticipant.d.ts +13 -0
  61. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  62. package/dist/src/room/utils.d.ts +1 -0
  63. package/dist/src/room/utils.d.ts.map +1 -1
  64. package/dist/src/utils/deferrable-map.d.ts +32 -0
  65. package/dist/src/utils/deferrable-map.d.ts.map +1 -0
  66. package/dist/ts4.2/api/SignalClient.d.ts +12 -4
  67. package/dist/ts4.2/e2ee/types.d.ts +6 -0
  68. package/dist/ts4.2/e2ee/utils.d.ts +2 -1
  69. package/dist/ts4.2/index.d.ts +5 -4
  70. package/dist/ts4.2/room/PCTransport.d.ts +5 -0
  71. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -1
  72. package/dist/ts4.2/room/RTCEngine.d.ts +24 -8
  73. package/dist/ts4.2/room/Room.d.ts +13 -3
  74. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +27 -4
  75. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +4 -4
  76. package/dist/ts4.2/room/data-track/depacketizer.d.ts +4 -4
  77. package/dist/ts4.2/room/data-track/frame.d.ts +14 -0
  78. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +21 -10
  79. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +6 -5
  80. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +57 -23
  81. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +16 -6
  82. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +7 -6
  83. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +14 -4
  84. package/dist/ts4.2/room/data-track/packetizer.d.ts +4 -4
  85. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +1 -1
  86. package/dist/ts4.2/room/data-track/types.d.ts +6 -1
  87. package/dist/ts4.2/room/events.d.ts +24 -3
  88. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +11 -1
  89. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +13 -0
  90. package/dist/ts4.2/room/utils.d.ts +1 -0
  91. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -0
  92. package/package.json +1 -1
  93. package/src/api/SignalClient.test.ts +9 -4
  94. package/src/api/SignalClient.ts +116 -9
  95. package/src/e2ee/constants.ts +1 -0
  96. package/src/e2ee/types.ts +6 -0
  97. package/src/e2ee/utils.ts +4 -3
  98. package/src/e2ee/worker/DataCryptor.ts +1 -4
  99. package/src/e2ee/worker/FrameCryptor.ts +1 -4
  100. package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -1
  101. package/src/index.ts +6 -4
  102. package/src/room/PCTransport.ts +41 -1
  103. package/src/room/PCTransportManager.ts +1 -1
  104. package/src/room/RTCEngine.ts +266 -111
  105. package/src/room/Room.ts +149 -12
  106. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -7
  107. package/src/room/data-track/LocalDataTrack.ts +83 -10
  108. package/src/room/data-track/RemoteDataTrack.ts +7 -9
  109. package/src/room/data-track/depacketizer.ts +21 -12
  110. package/src/room/data-track/frame.ts +28 -2
  111. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +58 -73
  112. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +132 -80
  113. package/src/room/data-track/incoming/pipeline.ts +29 -24
  114. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +225 -32
  115. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +150 -75
  116. package/src/room/data-track/outgoing/errors.ts +36 -7
  117. package/src/room/data-track/outgoing/pipeline.ts +23 -17
  118. package/src/room/data-track/outgoing/types.ts +12 -3
  119. package/src/room/data-track/packet/extensions.ts +17 -22
  120. package/src/room/data-track/packet/index.test.ts +22 -33
  121. package/src/room/data-track/packetizer.test.ts +2 -2
  122. package/src/room/data-track/packetizer.ts +4 -4
  123. package/src/room/data-track/track-interfaces.ts +1 -1
  124. package/src/room/data-track/types.ts +21 -1
  125. package/src/room/events.ts +26 -1
  126. package/src/room/participant/LocalParticipant.ts +57 -6
  127. package/src/room/participant/RemoteParticipant.ts +25 -0
  128. package/src/room/utils.ts +4 -0
  129. package/src/utils/deferrable-map.ts +109 -0
  130. package/dist/src/room/data-track/e2ee.d.ts +0 -12
  131. package/dist/src/room/data-track/e2ee.d.ts.map +0 -1
  132. package/dist/ts4.2/room/data-track/e2ee.d.ts +0 -12
  133. package/src/room/data-track/e2ee.ts +0 -15
@@ -0,0 +1,32 @@
1
+ import type { Throws } from '@livekit/throws-transformer/throws';
2
+ /** An error which is thrown if a {@link DeferrableMap#getDeferred} call is aborted midway
3
+ * through. */
4
+ export declare class DeferrableMapAbortError extends DOMException {
5
+ reason: unknown;
6
+ constructor(message: string, reason?: unknown);
7
+ }
8
+ /**
9
+ * A Map-like container keyed by unique strings that supports the ability to wait
10
+ * for future keys to show up in the map.
11
+ *
12
+ * @example
13
+ * // An already existing key:
14
+ * const value = map.get("key");
15
+ * // Wait for a key which will be added soon:
16
+ * const value = await map.getDeferred("key");
17
+ */
18
+ export declare class DeferrableMap<K, V> extends Map<K, V> {
19
+ private pending;
20
+ set(key: K, value: V): this;
21
+ get [Symbol.toStringTag](): string;
22
+ /**
23
+ * Returns the value for `key` immediately if it exists, otherwise returns a
24
+ * promise that resolves once `set(key, value)` is called.
25
+ *
26
+ * If an `AbortSignal` is provided and it is aborted before the key appears,
27
+ * the returned promise rejects with an {@link DeferrableMapAbortError}.
28
+ */
29
+ getDeferred(key: K): Promise<V>;
30
+ getDeferred(key: K, signal: AbortSignal): Promise<Throws<V, DeferrableMapAbortError>>;
31
+ }
32
+ //# sourceMappingURL=deferrable-map.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.18.0",
3
+ "version": "2.18.1",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -300,8 +300,10 @@ describe('SignalClient.connect', () => {
300
300
 
301
301
  describe('Failure Case - WebSocket Connection Errors', () => {
302
302
  it('should reject with NotAllowed error for 4xx HTTP status', async () => {
303
+ const openedPromise = Promise.reject(new Error('Connection failed'));
304
+ openedPromise.catch(() => {}); // prevent unhandled rejection before join attaches its handler
303
305
  mockWebSocketStream({
304
- opened: Promise.reject(new Error('Connection failed')),
306
+ opened: openedPromise,
305
307
  readyState: 3,
306
308
  });
307
309
 
@@ -321,8 +323,10 @@ describe('SignalClient.connect', () => {
321
323
  });
322
324
 
323
325
  it('should reject with ServerUnreachable when fetch fails', async () => {
326
+ const openedPromise = Promise.reject(new Error('Connection failed'));
327
+ openedPromise.catch(() => {}); // prevent unhandled rejection before join attaches its handler
324
328
  mockWebSocketStream({
325
- opened: Promise.reject(new Error('Connection failed')),
329
+ opened: openedPromise,
326
330
  readyState: 3,
327
331
  });
328
332
 
@@ -338,9 +342,10 @@ describe('SignalClient.connect', () => {
338
342
 
339
343
  it('should handle ConnectionError from WebSocket rejection', async () => {
340
344
  const customError = ConnectionError.internal('Custom error', { status: 500 });
341
-
345
+ const openedPromise = Promise.reject(customError);
346
+ openedPromise.catch(() => {}); // prevent unhandled rejection before join attaches its handler
342
347
  mockWebSocketStream({
343
- opened: Promise.reject(customError),
348
+ opened: openedPromise,
344
349
  readyState: 3,
345
350
  });
346
351
 
@@ -5,7 +5,9 @@ import {
5
5
  ClientInfo,
6
6
  ConnectionQualityUpdate,
7
7
  ConnectionSettings,
8
+ DataTrackSubscriberHandles,
8
9
  DisconnectReason,
10
+ Encryption_Type,
9
11
  JoinRequest,
10
12
  JoinResponse,
11
13
  LeaveRequest,
@@ -14,6 +16,8 @@ import {
14
16
  MuteTrackRequest,
15
17
  ParticipantInfo,
16
18
  Ping,
19
+ PublishDataTrackRequest,
20
+ PublishDataTrackResponse,
17
21
  ReconnectReason,
18
22
  ReconnectResponse,
19
23
  RequestResponse,
@@ -35,6 +39,10 @@ import {
35
39
  TrackPublishedResponse,
36
40
  TrackUnpublishedResponse,
37
41
  TrickleRequest,
42
+ UnpublishDataTrackRequest,
43
+ UnpublishDataTrackResponse,
44
+ UpdateDataSubscription,
45
+ UpdateDataSubscription_Update,
38
46
  UpdateLocalAudioTrack,
39
47
  UpdateParticipantMetadata,
40
48
  UpdateSubscription,
@@ -42,13 +50,16 @@ import {
42
50
  UpdateVideoLayers,
43
51
  VideoLayer,
44
52
  WrappedJoinRequest,
53
+ WrappedJoinRequest_Compression,
45
54
  protoInt64,
46
55
  } from '@livekit/protocol';
47
56
  import log, { LoggerNames, getLogger } from '../logger';
57
+ import type { DataTrackHandle } from '../room/data-track/handle';
58
+ import { type DataTrackSid } from '../room/data-track/types';
48
59
  import { ConnectionError } from '../room/errors';
49
60
  import CriticalTimers from '../room/timers';
50
61
  import type { LoggerOptions } from '../room/types';
51
- import { getClientInfo, isReactNative, sleep } from '../room/utils';
62
+ import { getClientInfo, isCompressionStreamSupported, isReactNative, sleep } from '../room/utils';
52
63
  import { AsyncQueue } from '../utils/AsyncQueue';
53
64
  import { type WebSocketConnection, WebSocketStream } from './WebSocketStream';
54
65
  import {
@@ -174,6 +185,14 @@ export class SignalClient {
174
185
 
175
186
  onMediaSectionsRequirement?: (requirement: MediaSectionsRequirement) => void;
176
187
 
188
+ onPublishDataTrackResponse?: (event: PublishDataTrackResponse) => void;
189
+
190
+ onUnPublishDataTrackResponse?: (event: UnpublishDataTrackResponse) => void;
191
+
192
+ onDataTrackSubscriberHandles?: (event: DataTrackSubscriberHandles) => void;
193
+
194
+ onJoined?: (event: JoinResponse) => void;
195
+
177
196
  connectOptions?: ConnectOpts;
178
197
 
179
198
  ws?: WebSocketStream;
@@ -248,12 +267,13 @@ export class SignalClient {
248
267
  opts: SignalOptions,
249
268
  abortSignal?: AbortSignal,
250
269
  useV0Path: boolean = false,
270
+ publisherOffer?: SessionDescription,
251
271
  ): Promise<JoinResponse> {
252
272
  // during a full reconnect, we'd want to start the sequence even if currently
253
273
  // connected
254
274
  this.state = SignalConnectionState.CONNECTING;
255
275
  this.options = opts;
256
- const res = await this.connect(url, token, opts, abortSignal, useV0Path);
276
+ const res = await this.connect(url, token, opts, abortSignal, useV0Path, publisherOffer);
257
277
  return res as JoinResponse;
258
278
  }
259
279
 
@@ -296,6 +316,7 @@ export class SignalClient {
296
316
  abortSignal?: AbortSignal,
297
317
  /** setting this to true results in dual peer connection mode being used */
298
318
  useV0Path: boolean = false,
319
+ publisherOffer?: SessionDescription,
299
320
  ): Promise<JoinResponse | ReconnectResponse | undefined> {
300
321
  const unlock = await this.connectionLock.lock();
301
322
 
@@ -305,7 +326,7 @@ export class SignalClient {
305
326
  const clientInfo = getClientInfo();
306
327
  const params = useV0Path
307
328
  ? createConnectionParams(token, clientInfo, opts)
308
- : createJoinRequestConnectionParams(token, clientInfo, opts);
329
+ : await createJoinRequestConnectionParams(token, clientInfo, opts, publisherOffer);
309
330
  const rtcUrl = createRtcUrl(url, params, useV0Path).toString();
310
331
  const validateUrl = createValidateUrl(rtcUrl).toString();
311
332
 
@@ -437,8 +458,9 @@ export class SignalClient {
437
458
  return;
438
459
  }
439
460
 
440
- // Handle join response - set up ping configuration
461
+ // Handle join response
441
462
  if (firstSignalResponse.message?.case === 'join') {
463
+ // Set up ping configuration
442
464
  this.pingTimeoutDuration = firstSignalResponse.message.value.pingTimeout;
443
465
  this.pingIntervalDuration = firstSignalResponse.message.value.pingInterval;
444
466
 
@@ -449,6 +471,10 @@ export class SignalClient {
449
471
  interval: this.pingIntervalDuration,
450
472
  });
451
473
  }
474
+
475
+ if (this.onJoined) {
476
+ this.onJoined(firstSignalResponse.message.value);
477
+ }
452
478
  }
453
479
 
454
480
  // Handle successful connection
@@ -685,7 +711,40 @@ export class SignalClient {
685
711
  });
686
712
  }
687
713
 
688
- async sendRequest(message: SignalMessage, fromQueue: boolean = false) {
714
+ sendPublishDataTrackRequest(handle: DataTrackHandle, name: string, usesE2ee: boolean) {
715
+ return this.sendRequest({
716
+ case: 'publishDataTrackRequest',
717
+ value: new PublishDataTrackRequest({
718
+ pubHandle: handle,
719
+ name: name,
720
+ encryption: usesE2ee ? Encryption_Type.GCM : Encryption_Type.NONE,
721
+ }),
722
+ });
723
+ }
724
+
725
+ sendUnPublishDataTrackRequest(handle: DataTrackHandle) {
726
+ return this.sendRequest({
727
+ case: 'unpublishDataTrackRequest',
728
+ value: new UnpublishDataTrackRequest({ pubHandle: handle }),
729
+ });
730
+ }
731
+
732
+ sendUpdateDataSubscription(sid: DataTrackSid, subscribe: boolean) {
733
+ return this.sendRequest({
734
+ case: 'updateDataSubscription',
735
+ value: new UpdateDataSubscription({
736
+ // FIXME: consider refactoring to allow caller to pass an array of events through
737
+ updates: [
738
+ new UpdateDataSubscription_Update({
739
+ trackSid: sid,
740
+ subscribe,
741
+ }),
742
+ ],
743
+ }),
744
+ });
745
+ }
746
+
747
+ private async sendRequest(message: SignalMessage, fromQueue: boolean = false) {
689
748
  // capture all requests while reconnecting and put them in a queue
690
749
  // unless the request originates from the queue, then don't enqueue again
691
750
  const canQueue = !fromQueue && !canPassThroughQueue(message);
@@ -827,6 +886,18 @@ export class SignalClient {
827
886
  if (this.onMediaSectionsRequirement) {
828
887
  this.onMediaSectionsRequirement(msg.value);
829
888
  }
889
+ } else if (msg.case === 'publishDataTrackResponse') {
890
+ if (this.onPublishDataTrackResponse) {
891
+ this.onPublishDataTrackResponse(msg.value);
892
+ }
893
+ } else if (msg.case === 'unpublishDataTrackResponse') {
894
+ if (this.onUnPublishDataTrackResponse) {
895
+ this.onUnPublishDataTrackResponse(msg.value);
896
+ }
897
+ } else if (msg.case === 'dataTrackSubscriberHandles') {
898
+ if (this.onDataTrackSubscriberHandles) {
899
+ this.onDataTrackSubscriberHandles(msg.value);
900
+ }
830
901
  } else {
831
902
  this.log.debug('unsupported message', { ...this.logContext, msgCase: msg.case });
832
903
  }
@@ -1125,11 +1196,12 @@ function createConnectionParams(
1125
1196
  return params;
1126
1197
  }
1127
1198
 
1128
- function createJoinRequestConnectionParams(
1199
+ async function createJoinRequestConnectionParams(
1129
1200
  token: string,
1130
1201
  info: ClientInfo,
1131
1202
  opts: ConnectOpts,
1132
- ): URLSearchParams {
1203
+ publisherOffer?: SessionDescription,
1204
+ ): Promise<URLSearchParams> {
1133
1205
  const params = new URLSearchParams();
1134
1206
  params.set('access_token', token);
1135
1207
 
@@ -1141,14 +1213,49 @@ function createJoinRequestConnectionParams(
1141
1213
  }),
1142
1214
  reconnect: !!opts.reconnect,
1143
1215
  participantSid: opts.sid ? opts.sid : undefined,
1216
+ publisherOffer: publisherOffer,
1144
1217
  });
1145
1218
  if (opts.reconnectReason) {
1146
1219
  joinRequest.reconnectReason = opts.reconnectReason;
1147
1220
  }
1221
+ const joinRequestBytes = joinRequest.toBinary();
1222
+ let requestBytes: Uint8Array;
1223
+ let compression: WrappedJoinRequest_Compression;
1224
+ if (isCompressionStreamSupported()) {
1225
+ const stream = new CompressionStream('gzip');
1226
+ const writer = stream.writable.getWriter();
1227
+ writer.write(new Uint8Array(joinRequestBytes));
1228
+ writer.close();
1229
+ const chunks: Uint8Array[] = [];
1230
+ const reader = stream.readable.getReader();
1231
+ while (true) {
1232
+ const { done, value } = await reader.read();
1233
+ if (done) break;
1234
+ chunks.push(value);
1235
+ }
1236
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
1237
+ const result = new Uint8Array(totalLength);
1238
+ let offset = 0;
1239
+ for (const chunk of chunks) {
1240
+ result.set(chunk, offset);
1241
+ offset += chunk.length;
1242
+ }
1243
+ requestBytes = result;
1244
+ compression = WrappedJoinRequest_Compression.GZIP;
1245
+ } else {
1246
+ requestBytes = joinRequestBytes;
1247
+ compression = WrappedJoinRequest_Compression.NONE;
1248
+ }
1148
1249
  const wrappedJoinRequest = new WrappedJoinRequest({
1149
- joinRequest: joinRequest.toBinary(),
1250
+ joinRequest: requestBytes,
1251
+ compression,
1150
1252
  });
1151
- params.set('join_request', btoa(new TextDecoder('utf-8').decode(wrappedJoinRequest.toBinary())));
1253
+ const wrappedBytes = wrappedJoinRequest.toBinary();
1254
+ const bytesToBase64 = (bytes: Uint8Array) => {
1255
+ const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');
1256
+ return btoa(binString);
1257
+ };
1258
+ params.set('join_request', bytesToBase64(wrappedBytes).replace(/\+/g, '-').replace(/\//g, '_'));
1152
1259
 
1153
1260
  return params;
1154
1261
  }
@@ -37,6 +37,7 @@ export const KEY_PROVIDER_DEFAULTS: KeyProviderOptions = {
37
37
  ratchetWindowSize: 8,
38
38
  failureTolerance: DECRYPTION_FAILURE_TOLERANCE,
39
39
  keyringSize: 16,
40
+ keySize: 128,
40
41
  } as const;
41
42
 
42
43
  export const MAX_SIF_COUNT = 100;
package/src/e2ee/types.ts CHANGED
@@ -183,6 +183,12 @@ export type KeyProviderOptions = {
183
183
  ratchetWindowSize: number;
184
184
  failureTolerance: number;
185
185
  keyringSize: number;
186
+ /**
187
+ * Size of the encryption key in bits.
188
+ * Defaults to 128. Note that 128 is currently the only value
189
+ * supported by non-web SDKs.
190
+ */
191
+ keySize: 128 | 256;
186
192
  };
187
193
 
188
194
  export type KeyInfo = {
package/src/e2ee/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type DataPacket, EncryptedPacketPayload } from '@livekit/protocol';
2
2
  import { ENCRYPTION_ALGORITHM } from './constants';
3
+ import type { KeyProviderOptions } from './types';
3
4
 
4
5
  export function isE2EESupported() {
5
6
  return isInsertableStreamSupported() || isScriptTransformSupported();
@@ -92,8 +93,8 @@ function getAlgoOptions(algorithmName: string, salt: string) {
92
93
  * Derives a set of keys from the master key.
93
94
  * See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
94
95
  */
95
- export async function deriveKeys(material: CryptoKey, salt: string) {
96
- const algorithmOptions = getAlgoOptions(material.algorithm.name, salt);
96
+ export async function deriveKeys(material: CryptoKey, options: KeyProviderOptions) {
97
+ const algorithmOptions = getAlgoOptions(material.algorithm.name, options.ratchetSalt);
97
98
 
98
99
  // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
99
100
  // https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
@@ -102,7 +103,7 @@ export async function deriveKeys(material: CryptoKey, salt: string) {
102
103
  material,
103
104
  {
104
105
  name: ENCRYPTION_ALGORITHM,
105
- length: 128,
106
+ length: options.keySize,
106
107
  },
107
108
  false,
108
109
  ['encrypt', 'decrypt'],
@@ -93,10 +93,7 @@ export class DataCryptor {
93
93
  // if not, it might be that a different frame has already ratcheted and we try with that one first
94
94
  ratchetResult = await keys.ratchetKey(keyIndex, false);
95
95
 
96
- ratchetedKeySet = await deriveKeys(
97
- ratchetResult.cryptoKey,
98
- keys.keyProviderOptions.ratchetSalt,
99
- );
96
+ ratchetedKeySet = await deriveKeys(ratchetResult.cryptoKey, keys.keyProviderOptions);
100
97
  }
101
98
 
102
99
  const decryptedData = await DataCryptor.decrypt(
@@ -603,10 +603,7 @@ export class FrameCryptor extends BaseFrameCryptor {
603
603
  // if not, it might be that a different frame has already ratcheted and we try with that one first
604
604
  ratchetResult = await this.keys.ratchetKey(keyIndex, false);
605
605
 
606
- ratchetedKeySet = await deriveKeys(
607
- ratchetResult.cryptoKey,
608
- this.keyProviderOptions.ratchetSalt,
609
- );
606
+ ratchetedKeySet = await deriveKeys(ratchetResult.cryptoKey, this.keyProviderOptions);
610
607
  }
611
608
 
612
609
  const frame = await this.decryptFrame(encodedFrame, keyIndex, initialMaterial || keySet, {
@@ -173,7 +173,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
173
173
  ratchetedResult: RatchetResult | null = null,
174
174
  updateCurrentKeyIndex = true,
175
175
  ) {
176
- const keySet = await deriveKeys(material, this.keyProviderOptions.ratchetSalt);
176
+ const keySet = await deriveKeys(material, this.keyProviderOptions);
177
177
  const newIndex = keyIndex >= 0 ? keyIndex % this.cryptoKeyRing.length : this.currentKeyIndex;
178
178
  workerLogger.debug(`setting new key with index ${keyIndex}`, {
179
179
  usage: material.usages,
package/src/index.ts CHANGED
@@ -11,10 +11,8 @@ import DefaultReconnectPolicy from './room/DefaultReconnectPolicy';
11
11
  import type { ReconnectContext, ReconnectPolicy } from './room/ReconnectPolicy';
12
12
  import Room, { ConnectionState, type RoomEventCallbacks } from './room/Room';
13
13
  import * as attributes from './room/attribute-typings';
14
- // FIXME: remove this import in a follow up data track pull request.
15
- import './room/data-track/incoming/IncomingDataTrackManager';
16
- // FIXME: remove this import in a follow up data track pull request.
17
- import './room/data-track/outgoing/OutgoingDataTrackManager';
14
+ import LocalDataTrack from './room/data-track/LocalDataTrack';
15
+ import RemoteDataTrack, { type DataTrackSubscribeOptions } from './room/data-track/RemoteDataTrack';
18
16
  import LocalParticipant from './room/participant/LocalParticipant';
19
17
  import Participant, {
20
18
  ConnectionQuality,
@@ -142,6 +140,8 @@ export {
142
140
  isVideoTrack,
143
141
  isLocalParticipant,
144
142
  isRemoteParticipant,
143
+ LocalDataTrack,
144
+ RemoteDataTrack,
145
145
  };
146
146
  export type {
147
147
  AudioAnalyserOptions,
@@ -158,6 +158,7 @@ export type {
158
158
  RoomEventCallbacks,
159
159
  ParticipantEventCallbacks,
160
160
  PublicationEventCallbacks,
161
+ DataTrackSubscribeOptions,
161
162
  };
162
163
  export { DataTrackPacket, type DataTrackPacketHeader } from './room/data-track/packet';
163
164
  export {
@@ -165,5 +166,6 @@ export {
165
166
  type DataTrackUserTimestampExtension,
166
167
  type DataTrackE2eeExtension,
167
168
  } from './room/data-track/packet/extensions';
169
+ export { type DataChannelKind } from './room/RTCEngine';
168
170
 
169
171
  export { LocalTrackRecorder } from './room/track/record';
@@ -55,6 +55,8 @@ export default class PCTransport extends EventEmitter {
55
55
 
56
56
  private offerLock: Mutex;
57
57
 
58
+ private pendingInitialOffer?: RTCSessionDescriptionInit;
59
+
58
60
  pendingCandidates: RTCIceCandidateInit[] = [];
59
61
 
60
62
  restartingIce: boolean = false;
@@ -163,6 +165,16 @@ export default class PCTransport extends EventEmitter {
163
165
  this.remoteStereoMids = stereoMids;
164
166
  this.remoteNackMids = nackMids;
165
167
  } else if (sd.type === 'answer') {
168
+ if (this.pendingInitialOffer) {
169
+ const initialOffer = this.pendingInitialOffer;
170
+ this.pendingInitialOffer = undefined;
171
+ const sdpParsed = parse(initialOffer.sdp ?? '');
172
+ sdpParsed.media.forEach((media) => {
173
+ ensureIPAddrMatchVersion(media);
174
+ });
175
+ this.log.debug('setting pending initial offer before processing answer', this.logContext);
176
+ await this.setMungedSDP(initialOffer, write(sdpParsed));
177
+ }
166
178
  const sdpParsed = parse(sd.sdp ?? '');
167
179
  sdpParsed.media.forEach((media) => {
168
180
  const mid = getMidString(media.mid!);
@@ -255,6 +267,31 @@ export default class PCTransport extends EventEmitter {
255
267
  }
256
268
  }, debounceInterval);
257
269
 
270
+ async createInitialOffer() {
271
+ const unlock = await this.offerLock.lock();
272
+ try {
273
+ if (this.pc.signalingState !== 'stable') {
274
+ this.log.warn(
275
+ 'signaling state is not stable, cannot create initial offer',
276
+ this.logContext,
277
+ );
278
+ return;
279
+ }
280
+ const offerId = this.latestOfferId + 1;
281
+ this.latestOfferId = offerId;
282
+ const offer = await this.pc.createOffer();
283
+ this.pendingInitialOffer = { sdp: offer.sdp, type: offer.type };
284
+ const sdpParsed = parse(offer.sdp ?? '');
285
+ sdpParsed.media.forEach((media) => {
286
+ ensureIPAddrMatchVersion(media);
287
+ });
288
+ offer.sdp = write(sdpParsed);
289
+ return { offer, offerId };
290
+ } finally {
291
+ unlock();
292
+ }
293
+ }
294
+
258
295
  async createAndSendOffer(options?: RTCOfferOptions) {
259
296
  const unlock = await this.offerLock.lock();
260
297
 
@@ -268,7 +305,10 @@ export default class PCTransport extends EventEmitter {
268
305
  this.restartingIce = true;
269
306
  }
270
307
 
271
- if (this._pc && this._pc.signalingState === 'have-local-offer') {
308
+ if (
309
+ this._pc &&
310
+ (this._pc.signalingState === 'have-local-offer' || this.pendingInitialOffer)
311
+ ) {
272
312
  // we're waiting for the peer to accept our offer, so we'll just wait
273
313
  // the only exception to this is when ICE restart is needed
274
314
  const currentSD = this._pc.remoteDescription;
@@ -72,7 +72,7 @@ export class PCTransportManager {
72
72
  return this._mode;
73
73
  }
74
74
 
75
- constructor(rtcConfig: RTCConfiguration, mode: PCMode, loggerOptions: LoggerOptions) {
75
+ constructor(mode: PCMode, loggerOptions: LoggerOptions, rtcConfig?: RTCConfiguration) {
76
76
  this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager);
77
77
  this.loggerOptions = loggerOptions;
78
78