livekit-client 1.11.4 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. package/README.md +1 -3
  2. package/dist/livekit-client.e2ee.worker.js +2 -0
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -0
  4. package/dist/livekit-client.e2ee.worker.mjs +1525 -0
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -0
  6. package/dist/livekit-client.esm.mjs +4749 -4055
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/api/SignalClient.d.ts +4 -1
  11. package/dist/src/api/SignalClient.d.ts.map +1 -1
  12. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  13. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  14. package/dist/src/e2ee/E2eeManager.d.ts +45 -0
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -0
  16. package/dist/src/e2ee/KeyProvider.d.ts +42 -0
  17. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -0
  18. package/dist/src/e2ee/constants.d.ts +14 -0
  19. package/dist/src/e2ee/constants.d.ts.map +1 -0
  20. package/dist/src/e2ee/errors.d.ts +11 -0
  21. package/dist/src/e2ee/errors.d.ts.map +1 -0
  22. package/dist/src/e2ee/index.d.ts +4 -0
  23. package/dist/src/e2ee/index.d.ts.map +1 -0
  24. package/dist/src/e2ee/types.d.ts +129 -0
  25. package/dist/src/e2ee/types.d.ts.map +1 -0
  26. package/dist/src/e2ee/utils.d.ts +24 -0
  27. package/dist/src/e2ee/utils.d.ts.map +1 -0
  28. package/dist/src/e2ee/worker/FrameCryptor.d.ts +175 -0
  29. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -0
  30. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +46 -0
  31. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -0
  32. package/dist/src/e2ee/worker/e2ee.worker.d.ts +2 -0
  33. package/dist/src/e2ee/worker/e2ee.worker.d.ts.map +1 -0
  34. package/dist/src/index.d.ts +1 -0
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/logger.d.ts +4 -1
  37. package/dist/src/logger.d.ts.map +1 -1
  38. package/dist/src/options.d.ts +5 -0
  39. package/dist/src/options.d.ts.map +1 -1
  40. package/dist/src/proto/livekit_models.d.ts +2 -2
  41. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  42. package/dist/src/room/PCTransport.d.ts +3 -1
  43. package/dist/src/room/PCTransport.d.ts.map +1 -1
  44. package/dist/src/room/RTCEngine.d.ts +17 -3
  45. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  46. package/dist/src/room/Room.d.ts +10 -0
  47. package/dist/src/room/Room.d.ts.map +1 -1
  48. package/dist/src/room/events.d.ts +14 -2
  49. package/dist/src/room/events.d.ts.map +1 -1
  50. package/dist/src/room/participant/LocalParticipant.d.ts +7 -2
  51. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  52. package/dist/src/room/participant/Participant.d.ts +1 -0
  53. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  54. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  55. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  56. package/dist/src/room/track/TrackPublication.d.ts +3 -0
  57. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  58. package/dist/src/room/track/options.d.ts +2 -2
  59. package/dist/src/room/track/options.d.ts.map +1 -1
  60. package/dist/src/room/track/utils.d.ts +9 -0
  61. package/dist/src/room/track/utils.d.ts.map +1 -1
  62. package/dist/src/room/utils.d.ts +2 -0
  63. package/dist/src/room/utils.d.ts.map +1 -1
  64. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  65. package/dist/ts4.2/src/api/SignalClient.d.ts +4 -1
  66. package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +45 -0
  67. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +42 -0
  68. package/dist/ts4.2/src/e2ee/constants.d.ts +14 -0
  69. package/dist/ts4.2/src/e2ee/errors.d.ts +11 -0
  70. package/dist/ts4.2/src/e2ee/index.d.ts +4 -0
  71. package/dist/ts4.2/src/e2ee/types.d.ts +129 -0
  72. package/dist/ts4.2/src/e2ee/utils.d.ts +24 -0
  73. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +175 -0
  74. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +46 -0
  75. package/dist/ts4.2/src/e2ee/worker/e2ee.worker.d.ts +2 -0
  76. package/dist/ts4.2/src/index.d.ts +1 -0
  77. package/dist/ts4.2/src/logger.d.ts +4 -1
  78. package/dist/ts4.2/src/options.d.ts +5 -0
  79. package/dist/ts4.2/src/proto/livekit_models.d.ts +2 -2
  80. package/dist/ts4.2/src/room/PCTransport.d.ts +3 -1
  81. package/dist/ts4.2/src/room/RTCEngine.d.ts +17 -3
  82. package/dist/ts4.2/src/room/Room.d.ts +10 -0
  83. package/dist/ts4.2/src/room/events.d.ts +14 -2
  84. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -2
  85. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
  86. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +3 -0
  87. package/dist/ts4.2/src/room/track/options.d.ts +6 -6
  88. package/dist/ts4.2/src/room/track/utils.d.ts +9 -0
  89. package/dist/ts4.2/src/room/utils.d.ts +2 -0
  90. package/package.json +17 -7
  91. package/src/api/SignalClient.ts +28 -9
  92. package/src/connectionHelper/checks/turn.ts +1 -0
  93. package/src/connectionHelper/checks/websocket.ts +1 -0
  94. package/src/e2ee/E2eeManager.ts +374 -0
  95. package/src/e2ee/KeyProvider.ts +77 -0
  96. package/src/e2ee/constants.ts +40 -0
  97. package/src/e2ee/errors.ts +16 -0
  98. package/src/e2ee/index.ts +3 -0
  99. package/src/e2ee/types.ts +160 -0
  100. package/src/e2ee/utils.ts +127 -0
  101. package/src/e2ee/worker/FrameCryptor.test.ts +21 -0
  102. package/src/e2ee/worker/FrameCryptor.ts +614 -0
  103. package/src/e2ee/worker/ParticipantKeyHandler.ts +129 -0
  104. package/src/e2ee/worker/e2ee.worker.ts +217 -0
  105. package/src/e2ee/worker/tsconfig.json +6 -0
  106. package/src/index.ts +1 -0
  107. package/src/logger.ts +10 -2
  108. package/src/options.ts +6 -0
  109. package/src/proto/livekit_models.ts +12 -12
  110. package/src/room/PCTransport.ts +39 -9
  111. package/src/room/RTCEngine.ts +127 -34
  112. package/src/room/Room.ts +77 -22
  113. package/src/room/defaults.ts +1 -1
  114. package/src/room/events.ts +14 -0
  115. package/src/room/participant/LocalParticipant.ts +52 -8
  116. package/src/room/participant/Participant.ts +4 -0
  117. package/src/room/track/LocalTrack.ts +5 -4
  118. package/src/room/track/RemoteVideoTrack.ts +2 -2
  119. package/src/room/track/TrackPublication.ts +9 -1
  120. package/src/room/track/options.ts +3 -2
  121. package/src/room/track/utils.ts +27 -0
  122. package/src/room/utils.ts +5 -0
  123. package/src/room/worker.d.ts +4 -0
  124. package/src/test/MockMediaStreamTrack.ts +1 -0
@@ -1,4 +1,5 @@
1
1
  import EventEmitter from 'eventemitter3';
2
+ import type { MediaAttributes } from 'sdp-transform';
2
3
  import { SignalClient } from '../api/SignalClient';
3
4
  import type { SignalOptions } from '../api/SignalClient';
4
5
  import log from '../logger';
@@ -9,17 +10,23 @@ import {
9
10
  DataPacket,
10
11
  DataPacket_Kind,
11
12
  DisconnectReason,
13
+ ParticipantInfo,
12
14
  ReconnectReason,
15
+ Room as RoomModel,
13
16
  SpeakerInfo,
14
17
  TrackInfo,
15
18
  UserPacket,
16
19
  } from '../proto/livekit_models';
17
20
  import {
18
21
  AddTrackRequest,
22
+ ConnectionQualityUpdate,
19
23
  JoinResponse,
20
24
  LeaveRequest,
21
25
  ReconnectResponse,
22
26
  SignalTarget,
27
+ StreamStateUpdate,
28
+ SubscriptionPermissionUpdate,
29
+ SubscriptionResponse,
23
30
  TrackPublishedResponse,
24
31
  } from '../proto/livekit_rtc';
25
32
  import PCTransport, { PCEvents } from './PCTransport';
@@ -43,6 +50,7 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
43
50
  import {
44
51
  Mutex,
45
52
  isCloud,
53
+ isVideoCodec,
46
54
  isWeb,
47
55
  sleep,
48
56
  supportsAddTrack,
@@ -161,6 +169,17 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
161
169
  [DataPacket_Kind.LOSSY, true],
162
170
  [DataPacket_Kind.RELIABLE, true],
163
171
  ]);
172
+
173
+ this.client.onParticipantUpdate = (updates) =>
174
+ this.emit(EngineEvent.ParticipantUpdate, updates);
175
+ this.client.onConnectionQuality = (update) =>
176
+ this.emit(EngineEvent.ConnectionQualityUpdate, update);
177
+ this.client.onRoomUpdate = (update) => this.emit(EngineEvent.RoomUpdate, update);
178
+ this.client.onSubscriptionError = (resp) => this.emit(EngineEvent.SubscriptionError, resp);
179
+ this.client.onSubscriptionPermissionUpdate = (update) =>
180
+ this.emit(EngineEvent.SubscriptionPermissionUpdate, update);
181
+ this.client.onSpeakersChanged = (update) => this.emit(EngineEvent.SpeakersChanged, update);
182
+ this.client.onStreamStateUpdate = (update) => this.emit(EngineEvent.StreamStateChanged, update);
164
183
  }
165
184
 
166
185
  async join(
@@ -187,6 +206,8 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
187
206
  if (!this.subscriberPrimary) {
188
207
  this.negotiate();
189
208
  }
209
+ this.setupSignalClientCallbacks();
210
+
190
211
  this.clientConfiguration = joinResponse.clientConfiguration;
191
212
 
192
213
  return joinResponse;
@@ -217,30 +238,63 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
217
238
  this.removeAllListeners();
218
239
  this.deregisterOnLineListener();
219
240
  this.clearPendingReconnect();
220
- if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
221
- this.publisher.pc.getSenders().forEach((sender) => {
222
- try {
223
- // TODO: react-native-webrtc doesn't have removeTrack yet.
224
- if (this.publisher?.pc.removeTrack) {
225
- this.publisher?.pc.removeTrack(sender);
226
- }
227
- } catch (e) {
228
- log.warn('could not removeTrack', { error: e });
229
- }
230
- });
231
- this.publisher.close();
232
- this.publisher = undefined;
233
- }
234
- if (this.subscriber) {
235
- this.subscriber.close();
236
- this.subscriber = undefined;
237
- }
238
- await this.client.close();
241
+ await this.cleanupPeerConnections();
242
+ await this.cleanupClient();
239
243
  } finally {
240
244
  unlock();
241
245
  }
242
246
  }
243
247
 
248
+ async cleanupPeerConnections() {
249
+ if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
250
+ this.publisher.pc.getSenders().forEach((sender) => {
251
+ try {
252
+ // TODO: react-native-webrtc doesn't have removeTrack yet.
253
+ if (this.publisher?.pc.removeTrack) {
254
+ this.publisher?.pc.removeTrack(sender);
255
+ }
256
+ } catch (e) {
257
+ log.warn('could not removeTrack', { error: e });
258
+ }
259
+ });
260
+ }
261
+ if (this.publisher) {
262
+ this.publisher.close();
263
+ this.publisher = undefined;
264
+ }
265
+ if (this.subscriber) {
266
+ this.subscriber.close();
267
+ this.subscriber = undefined;
268
+ }
269
+
270
+ this.primaryPC = undefined;
271
+
272
+ const dcCleanup = (dc: RTCDataChannel | undefined) => {
273
+ if (!dc) return;
274
+ dc.close();
275
+ dc.onbufferedamountlow = null;
276
+ dc.onclose = null;
277
+ dc.onclosing = null;
278
+ dc.onerror = null;
279
+ dc.onmessage = null;
280
+ dc.onopen = null;
281
+ };
282
+ dcCleanup(this.lossyDC);
283
+ dcCleanup(this.lossyDCSub);
284
+ dcCleanup(this.reliableDC);
285
+ dcCleanup(this.reliableDCSub);
286
+
287
+ this.lossyDC = undefined;
288
+ this.lossyDCSub = undefined;
289
+ this.reliableDC = undefined;
290
+ this.reliableDCSub = undefined;
291
+ }
292
+
293
+ async cleanupClient() {
294
+ await this.client.close();
295
+ this.client.resetCallbacks();
296
+ }
297
+
244
298
  addTrack(req: AddTrackRequest): Promise<TrackInfo> {
245
299
  if (this.pendingTrackResolvers[req.cid]) {
246
300
  throw new TrackInvalidError('a track with the same ID has already been published');
@@ -313,6 +367,14 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
313
367
  this.participantSid = joinResponse.participant?.sid;
314
368
 
315
369
  const rtcConfig = this.makeRTCConfiguration(joinResponse);
370
+
371
+ if (this.signalOpts?.e2eeEnabled) {
372
+ log.debug('E2EE - setting up transports with insertable streams');
373
+ // this makes sure that no data is sent before the transforms are ready
374
+ // @ts-ignore
375
+ rtcConfig.encodedInsertableStreams = true;
376
+ }
377
+
316
378
  const googConstraints = { optional: [{ googDscp: true }] };
317
379
  this.publisher = new PCTransport(rtcConfig, googConstraints);
318
380
  this.subscriber = new PCTransport(rtcConfig);
@@ -384,7 +446,9 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
384
446
  };
385
447
 
386
448
  this.createDataChannels();
449
+ }
387
450
 
451
+ private setupSignalClientCallbacks() {
388
452
  // configure signaling client
389
453
  this.client.onAnswer = async (sd) => {
390
454
  if (!this.publisher) {
@@ -392,7 +456,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
392
456
  }
393
457
  log.debug('received server answer', {
394
458
  RTCSdpType: sd.type,
395
- signalingState: this.publisher.pc.signalingState,
459
+ signalingState: this.publisher.pc.signalingState.toString(),
396
460
  });
397
461
  await this.publisher.setRemoteDescription(sd);
398
462
  };
@@ -417,7 +481,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
417
481
  }
418
482
  log.debug('received server offer', {
419
483
  RTCSdpType: sd.type,
420
- signalingState: this.subscriber.pc.signalingState,
484
+ signalingState: this.subscriber.pc.signalingState.toString(),
421
485
  });
422
486
  await this.subscriber.setRemoteDescription(sd);
423
487
 
@@ -646,11 +710,13 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
646
710
  encodings?: RTCRtpEncodingParameters[],
647
711
  ) {
648
712
  if (supportsTransceiver()) {
649
- return this.createTransceiverRTCRtpSender(track, opts, encodings);
713
+ const sender = await this.createTransceiverRTCRtpSender(track, opts, encodings);
714
+ return sender;
650
715
  }
651
716
  if (supportsAddTrack()) {
652
- log.debug('using add-track fallback');
653
- return this.createRTCRtpSender(track.mediaStreamTrack);
717
+ log.warn('using add-track fallback');
718
+ const sender = await this.createRTCRtpSender(track.mediaStreamTrack);
719
+ return sender;
654
720
  }
655
721
  throw new UnexpectedConnectionState('Required webRTC APIs not supported on this device');
656
722
  }
@@ -662,7 +728,6 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
662
728
  encodings?: RTCRtpEncodingParameters[],
663
729
  ) {
664
730
  // store RTCRtpSender
665
- // @ts-ignore
666
731
  if (supportsTransceiver()) {
667
732
  return this.createSimulcastTransceiverSender(track, simulcastTrack, opts, encodings);
668
733
  }
@@ -683,7 +748,13 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
683
748
  throw new UnexpectedConnectionState('publisher is closed');
684
749
  }
685
750
 
686
- const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
751
+ const streams: MediaStream[] = [];
752
+
753
+ if (track.mediaStream) {
754
+ streams.push(track.mediaStream);
755
+ }
756
+
757
+ const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly', streams };
687
758
  if (encodings) {
688
759
  transceiverInit.sendEncodings = encodings;
689
760
  }
@@ -692,6 +763,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
692
763
  track.mediaStreamTrack,
693
764
  transceiverInit,
694
765
  );
766
+
695
767
  if (track.kind === Track.Kind.Video && opts.videoCodec) {
696
768
  this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
697
769
  track.codec = opts.videoCodec;
@@ -819,7 +891,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
819
891
  }
820
892
 
821
893
  if (recoverable) {
822
- this.handleDisconnect('reconnect', ReconnectReason.RR_UNKOWN);
894
+ this.handleDisconnect('reconnect', ReconnectReason.RR_UNKNOWN);
823
895
  } else {
824
896
  log.info(
825
897
  `could not recover connection after ${this.reconnectAttempts} attempts, ${
@@ -858,12 +930,8 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
858
930
  if (this.client.isConnected) {
859
931
  await this.client.sendLeave();
860
932
  }
861
- await this.client.close();
862
- this.primaryPC = undefined;
863
- this.publisher?.close();
864
- this.publisher = undefined;
865
- this.subscriber?.close();
866
- this.subscriber = undefined;
933
+ await this.cleanupPeerConnections();
934
+ await this.cleanupClient();
867
935
 
868
936
  let joinResponse: JoinResponse;
869
937
  try {
@@ -919,6 +987,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
919
987
  this.emit(EngineEvent.Resuming);
920
988
 
921
989
  try {
990
+ this.setupSignalClientCallbacks();
922
991
  const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
923
992
  if (res) {
924
993
  const rtcConfig = this.makeRTCConfiguration(res);
@@ -1178,6 +1247,9 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
1178
1247
  return;
1179
1248
  };
1180
1249
 
1250
+ if (this.isClosed) {
1251
+ reject('cannot negotiate on closed engine');
1252
+ }
1181
1253
  this.on(EngineEvent.Closing, handleClosed);
1182
1254
 
1183
1255
  const negotiationTimeout = setTimeout(() => {
@@ -1197,13 +1269,24 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
1197
1269
  });
1198
1270
  });
1199
1271
 
1272
+ this.publisher.once(PCEvents.RTPVideoPayloadTypes, (rtpTypes: MediaAttributes['rtp']) => {
1273
+ const rtpMap = new Map<number, VideoCodec>();
1274
+ rtpTypes.forEach((rtp) => {
1275
+ const codec = rtp.codec.toLowerCase();
1276
+ if (isVideoCodec(codec)) {
1277
+ rtpMap.set(rtp.payload, codec);
1278
+ }
1279
+ });
1280
+ this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
1281
+ });
1282
+
1200
1283
  this.publisher.negotiate((e) => {
1201
1284
  cleanup();
1202
1285
  reject(e);
1203
1286
  if (e instanceof NegotiationError) {
1204
1287
  this.fullReconnectOnNext = true;
1205
1288
  }
1206
- this.handleDisconnect('negotiation', ReconnectReason.RR_UNKOWN);
1289
+ this.handleDisconnect('negotiation', ReconnectReason.RR_UNKNOWN);
1207
1290
  });
1208
1291
  });
1209
1292
  }
@@ -1318,5 +1401,15 @@ export type EngineEventCallbacks = {
1318
1401
  activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
1319
1402
  dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void;
1320
1403
  transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
1404
+ /** @internal */
1405
+ trackSenderAdded: (track: Track, sender: RTCRtpSender) => void;
1406
+ rtpVideoMapUpdate: (rtpMap: Map<number, VideoCodec>) => void;
1321
1407
  dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1408
+ participantUpdate: (infos: ParticipantInfo[]) => void;
1409
+ roomUpdate: (room: RoomModel) => void;
1410
+ connectionQualityUpdate: (update: ConnectionQualityUpdate) => void;
1411
+ speakersChanged: (speakerUpdates: SpeakerInfo[]) => void;
1412
+ streamStateChanged: (update: StreamStateUpdate) => void;
1413
+ subscriptionError: (resp: SubscriptionResponse) => void;
1414
+ subscriptionPermissionUpdate: (update: SubscriptionPermissionUpdate) => void;
1322
1415
  };
package/src/room/Room.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import EventEmitter from 'eventemitter3';
2
2
  import 'webrtc-adapter';
3
3
  import { toProtoSessionDescription } from '../api/SignalClient';
4
+ import { EncryptionEvent } from '../e2ee';
5
+ import { E2EEManager } from '../e2ee/E2eeManager';
4
6
  import log from '../logger';
5
7
  import type {
6
8
  InternalRoomConnectOptions,
@@ -56,7 +58,7 @@ import RemoteTrackPublication from './track/RemoteTrackPublication';
56
58
  import { Track } from './track/Track';
57
59
  import type { TrackPublication } from './track/TrackPublication';
58
60
  import type { AdaptiveStreamSettings } from './track/types';
59
- import { getNewAudioContext } from './track/utils';
61
+ import { getNewAudioContext, sourceToKind } from './track/utils';
60
62
  import type { SimulationOptions, SimulationScenario } from './types';
61
63
  import {
62
64
  Future,
@@ -112,6 +114,9 @@ class Room extends EventEmitter<RoomEventCallbacks> {
112
114
  /** options of room */
113
115
  options: InternalRoomOptions;
114
116
 
117
+ /** reflects the sender encryption status of the local participant */
118
+ isE2EEEnabled: boolean = false;
119
+
115
120
  private roomInfo?: RoomModel;
116
121
 
117
122
  private identityToSid: Map<string, string>;
@@ -131,6 +136,8 @@ class Room extends EventEmitter<RoomEventCallbacks> {
131
136
 
132
137
  private disconnectLock: Mutex;
133
138
 
139
+ private e2eeManager: E2EEManager | undefined;
140
+
134
141
  private cachedParticipantSids: Array<string>;
135
142
 
136
143
  private connectionReconcileInterval?: ReturnType<typeof setInterval>;
@@ -180,6 +187,43 @@ class Room extends EventEmitter<RoomEventCallbacks> {
180
187
  if (this.options.audioOutput?.deviceId) {
181
188
  this.switchActiveDevice('audiooutput', unwrapConstraint(this.options.audioOutput.deviceId));
182
189
  }
190
+
191
+ if (this.options.e2ee) {
192
+ this.setupE2EE();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * @experimental
198
+ */
199
+ async setE2EEEnabled(enabled: boolean) {
200
+ if (this.e2eeManager) {
201
+ await Promise.all([
202
+ this.localParticipant.setE2EEEnabled(enabled),
203
+ this.e2eeManager.setParticipantCryptorEnabled(enabled),
204
+ ]);
205
+ } else {
206
+ throw Error('e2ee not configured, please set e2ee settings within the room options');
207
+ }
208
+ }
209
+
210
+ private setupE2EE() {
211
+ if (this.options.e2ee) {
212
+ this.e2eeManager = new E2EEManager(this.options.e2ee);
213
+ this.e2eeManager.on(
214
+ EncryptionEvent.ParticipantEncryptionStatusChanged,
215
+ (enabled, participant) => {
216
+ if (participant instanceof LocalParticipant) {
217
+ this.isE2EEEnabled = enabled;
218
+ }
219
+ this.emit(RoomEvent.ParticipantEncryptionStatusChanged, enabled, participant);
220
+ },
221
+ );
222
+ this.e2eeManager.on(EncryptionEvent.Error, (error) =>
223
+ this.emit(RoomEvent.EncryptionError, error),
224
+ );
225
+ this.e2eeManager?.setup(this);
226
+ }
183
227
  }
184
228
 
185
229
  /**
@@ -219,15 +263,14 @@ class Room extends EventEmitter<RoomEventCallbacks> {
219
263
 
220
264
  this.engine = new RTCEngine(this.options);
221
265
 
222
- this.engine.client.onParticipantUpdate = this.handleParticipantUpdates;
223
- this.engine.client.onRoomUpdate = this.handleRoomUpdate;
224
- this.engine.client.onSpeakersChanged = this.handleSpeakersChanged;
225
- this.engine.client.onStreamStateUpdate = this.handleStreamStateUpdate;
226
- this.engine.client.onSubscriptionPermissionUpdate = this.handleSubscriptionPermissionUpdate;
227
- this.engine.client.onConnectionQuality = this.handleConnectionQualityUpdate;
228
- this.engine.client.onSubscriptionError = this.handleSubscriptionError;
229
-
230
266
  this.engine
267
+ .on(EngineEvent.ParticipantUpdate, this.handleParticipantUpdates)
268
+ .on(EngineEvent.RoomUpdate, this.handleRoomUpdate)
269
+ .on(EngineEvent.SpeakersChanged, this.handleSpeakersChanged)
270
+ .on(EngineEvent.StreamStateChanged, this.handleStreamStateUpdate)
271
+ .on(EngineEvent.ConnectionQualityUpdate, this.handleConnectionQualityUpdate)
272
+ .on(EngineEvent.SubscriptionError, this.handleSubscriptionError)
273
+ .on(EngineEvent.SubscriptionPermissionUpdate, this.handleSubscriptionPermissionUpdate)
231
274
  .on(
232
275
  EngineEvent.MediaTrackAdded,
233
276
  (mediaTrack: MediaStreamTrack, stream: MediaStream, receiver?: RTCRtpReceiver) => {
@@ -273,6 +316,9 @@ class Room extends EventEmitter<RoomEventCallbacks> {
273
316
  if (this.localParticipant) {
274
317
  this.localParticipant.setupEngine(this.engine);
275
318
  }
319
+ if (this.e2eeManager) {
320
+ this.e2eeManager.setupEngine(this.engine);
321
+ }
276
322
  }
277
323
 
278
324
  /**
@@ -392,6 +438,7 @@ class Room extends EventEmitter<RoomEventCallbacks> {
392
438
  adaptiveStream:
393
439
  typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
394
440
  maxRetries: connectOptions.maxRetries,
441
+ e2eeEnabled: !!this.e2eeManager,
395
442
  },
396
443
  abortController.signal,
397
444
  );
@@ -586,10 +633,8 @@ class Room extends EventEmitter<RoomEventCallbacks> {
586
633
  let req: SimulateScenario | undefined;
587
634
  switch (scenario) {
588
635
  case 'signal-reconnect':
589
- await this.engine.client.close();
590
- if (this.engine.client.onClose) {
591
- this.engine.client.onClose('simulate disconnect');
592
- }
636
+ // @ts-expect-error function is private
637
+ await this.engine.client.handleOnClose('simulate disconnect');
593
638
  break;
594
639
  case 'speaker':
595
640
  req = SimulateScenario.fromPartial({
@@ -625,17 +670,13 @@ class Room extends EventEmitter<RoomEventCallbacks> {
625
670
  break;
626
671
  case 'resume-reconnect':
627
672
  this.engine.failNext();
628
- await this.engine.client.close();
629
- if (this.engine.client.onClose) {
630
- this.engine.client.onClose('simulate resume-reconnect');
631
- }
673
+ // @ts-expect-error function is private
674
+ await this.engine.client.handleOnClose('simulate resume-disconnect');
632
675
  break;
633
676
  case 'full-reconnect':
634
677
  this.engine.fullReconnectOnNext = true;
635
- await this.engine.client.close();
636
- if (this.engine.client.onClose) {
637
- this.engine.client.onClose('simulate full-reconnect');
638
- }
678
+ // @ts-expect-error function is private
679
+ await this.engine.client.handleOnClose('simulate full-reconnect');
639
680
  break;
640
681
  case 'force-tcp':
641
682
  case 'force-tls':
@@ -1522,6 +1563,16 @@ class Room extends EventEmitter<RoomEventCallbacks> {
1522
1563
  this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
1523
1564
  }
1524
1565
  }
1566
+ const deviceId = await pub.track?.getDeviceId();
1567
+ const deviceKind = sourceToKind(pub.source);
1568
+ if (
1569
+ deviceKind &&
1570
+ deviceId &&
1571
+ deviceId !== this.localParticipant.activeDeviceMap.get(deviceKind)
1572
+ ) {
1573
+ this.localParticipant.activeDeviceMap.set(deviceKind, deviceId);
1574
+ this.emit(RoomEvent.ActiveDeviceChanged, deviceKind, deviceId);
1575
+ }
1525
1576
  };
1526
1577
 
1527
1578
  private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
@@ -1595,7 +1646,9 @@ class Room extends EventEmitter<RoomEventCallbacks> {
1595
1646
  }),
1596
1647
  new LocalVideoTrack(
1597
1648
  publishOptions.useRealTracks
1598
- ? (await navigator.mediaDevices.getUserMedia({ video: true })).getVideoTracks()[0]
1649
+ ? (
1650
+ await window.navigator.mediaDevices.getUserMedia({ video: true })
1651
+ ).getVideoTracks()[0]
1599
1652
  : createDummyVideoStreamTrack(
1600
1653
  160 * participantOptions.aspectRatios[0] ?? 1,
1601
1654
  160,
@@ -1758,6 +1811,8 @@ export type RoomEventCallbacks = {
1758
1811
  audioPlaybackChanged: (playing: boolean) => void;
1759
1812
  signalConnected: () => void;
1760
1813
  recordingStatusChanged: (recording: boolean) => void;
1814
+ participantEncryptionStatusChanged: (encrypted: boolean, participant?: Participant) => void;
1815
+ encryptionError: (error: Error) => void;
1761
1816
  dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1762
1817
  activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
1763
1818
  };
@@ -20,7 +20,7 @@ export const publishDefaults: TrackPublishDefaults = {
20
20
  screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
21
21
  stopMicTrackOnMute: false,
22
22
  videoCodec: 'vp8',
23
- backupCodec: { codec: 'vp8', encoding: VideoPresets.h540.encoding },
23
+ backupCodec: false,
24
24
  } as const;
25
25
 
26
26
  export const audioDefaults: AudioCaptureOptions = {
@@ -273,6 +273,9 @@ export enum RoomEvent {
273
273
  */
274
274
  RecordingStatusChanged = 'recordingStatusChanged',
275
275
 
276
+ ParticipantEncryptionStatusChanged = 'participantEncryptionStatusChanged',
277
+
278
+ EncryptionError = 'encryptionError',
276
279
  /**
277
280
  * Emits whenever the current buffer status of a data channel changes
278
281
  * args: (isLow: boolean, kind: [[DataPacket_Kind]])
@@ -443,6 +446,9 @@ export enum ParticipantEvent {
443
446
  * args: (prevPermissions: [[ParticipantPermission]])
444
447
  */
445
448
  ParticipantPermissionsChanged = 'participantPermissionsChanged',
449
+
450
+ /** @internal */
451
+ PCTrackAdded = 'pcTrackAdded',
446
452
  }
447
453
 
448
454
  /** @internal */
@@ -460,7 +466,15 @@ export enum EngineEvent {
460
466
  MediaTrackAdded = 'mediaTrackAdded',
461
467
  ActiveSpeakersUpdate = 'activeSpeakersUpdate',
462
468
  DataPacketReceived = 'dataPacketReceived',
469
+ RTPVideoMapUpdate = 'rtpVideoMapUpdate',
463
470
  DCBufferStatusChanged = 'dcBufferStatusChanged',
471
+ ParticipantUpdate = 'participantUpdate',
472
+ RoomUpdate = 'roomUpdate',
473
+ SpeakersChanged = 'speakersChanged',
474
+ StreamStateChanged = 'streamStateChanged',
475
+ ConnectionQualityUpdate = 'connectionQualityUpdate',
476
+ SubscriptionError = 'subscriptionError',
477
+ SubscriptionPermissionUpdate = 'subscriptionPermissionUpdate',
464
478
  }
465
479
 
466
480
  export enum TrackEvent {
@@ -1,6 +1,12 @@
1
1
  import log from '../../logger';
2
2
  import type { InternalRoomOptions } from '../../options';
3
- import { DataPacket, DataPacket_Kind, ParticipantInfo } from '../../proto/livekit_models';
3
+ import {
4
+ DataPacket,
5
+ DataPacket_Kind,
6
+ Encryption_Type,
7
+ ParticipantInfo,
8
+ ParticipantPermission,
9
+ } from '../../proto/livekit_models';
4
10
  import {
5
11
  AddTrackRequest,
6
12
  DataChannelInfo,
@@ -68,6 +74,8 @@ export default class LocalParticipant extends Participant {
68
74
  // keep a pointer to room options
69
75
  private roomOptions: InternalRoomOptions;
70
76
 
77
+ private encryptionType: Encryption_Type = Encryption_Type.NONE;
78
+
71
79
  private reconnectFuture?: Future<void>;
72
80
 
73
81
  /** @internal */
@@ -214,6 +222,22 @@ export default class LocalParticipant extends Participant {
214
222
  return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options, publishOptions);
215
223
  }
216
224
 
225
+ /** @internal */
226
+ setPermissions(permissions: ParticipantPermission): boolean {
227
+ const prevPermissions = this.permissions;
228
+ const changed = super.setPermissions(permissions);
229
+ if (changed && prevPermissions) {
230
+ this.emit(ParticipantEvent.ParticipantPermissionsChanged, prevPermissions);
231
+ }
232
+ return changed;
233
+ }
234
+
235
+ /** @internal */
236
+ async setE2EEEnabled(enabled: boolean) {
237
+ this.encryptionType = enabled ? Encryption_Type.GCM : Encryption_Type.NONE;
238
+ await this.republishAllTracks(undefined, false);
239
+ }
240
+
217
241
  /**
218
242
  * Enable or disable publishing for a track by source. This serves as a simple
219
243
  * way to manage the common tracks (camera, mic, or screen share).
@@ -552,6 +576,12 @@ export default class LocalParticipant extends Participant {
552
576
  ...options,
553
577
  };
554
578
 
579
+ // disable simulcast if e2ee is set on safari
580
+ if (isSafari() && this.roomOptions.e2ee) {
581
+ log.info(`End-to-end encryption is set up, simulcast publishing will be disabled on Safari`);
582
+ opts.simulcast = false;
583
+ }
584
+
555
585
  if (opts.source) {
556
586
  track.source = opts.source;
557
587
  }
@@ -624,8 +654,9 @@ export default class LocalParticipant extends Participant {
624
654
  muted: track.isMuted,
625
655
  source: Track.sourceToProto(track.source),
626
656
  disableDtx: !(opts.dtx ?? true),
657
+ encryption: this.encryptionType,
627
658
  stereo: isStereo,
628
- disableRed: !(opts.red ?? true),
659
+ // disableRed: !(opts.red ?? true),
629
660
  });
630
661
 
631
662
  // compute encodings and layers for video
@@ -760,11 +791,11 @@ export default class LocalParticipant extends Participant {
760
791
 
761
792
  if (encodings) {
762
793
  if (isFireFox() && track.kind === Track.Kind.Audio) {
763
- /* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
794
+ /* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
764
795
  livekit-server uses maxaveragebitrate=510000in the answer sdp to permit client to
765
- publish high quality audio track. But firefox always uses this value as the actual
796
+ publish high quality audio track. But firefox always uses this value as the actual
766
797
  bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
767
- So the client need to modify maxaverragebitrates in answer sdp to user provided value to
798
+ So the client need to modify maxaverragebitrates in answer sdp to user provided value to
768
799
  fix the issue.
769
800
  */
770
801
  let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
@@ -790,7 +821,7 @@ export default class LocalParticipant extends Participant {
790
821
  }
791
822
  }
792
823
 
793
- this.engine.negotiate();
824
+ await this.engine.negotiate();
794
825
 
795
826
  if (track instanceof LocalVideoTrack) {
796
827
  track.startMonitor(this.engine.client);
@@ -876,7 +907,7 @@ export default class LocalParticipant extends Participant {
876
907
  }
877
908
  await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);
878
909
 
879
- this.engine.negotiate();
910
+ await this.engine.negotiate();
880
911
  log.debug(`published ${videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
881
912
  }
882
913
 
@@ -980,7 +1011,7 @@ export default class LocalParticipant extends Participant {
980
1011
  ) as LocalTrackPublication[];
981
1012
  }
982
1013
 
983
- async republishAllTracks(options?: TrackPublishOptions) {
1014
+ async republishAllTracks(options?: TrackPublishOptions, restartTracks: boolean = true) {
984
1015
  const localPubs: LocalTrackPublication[] = [];
985
1016
  this.tracks.forEach((pub) => {
986
1017
  if (pub.track) {
@@ -995,6 +1026,19 @@ export default class LocalParticipant extends Participant {
995
1026
  localPubs.map(async (pub) => {
996
1027
  const track = pub.track!;
997
1028
  await this.unpublishTrack(track, false);
1029
+ if (
1030
+ restartTracks &&
1031
+ !track.isMuted &&
1032
+ (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
1033
+ !track.isUserProvided
1034
+ ) {
1035
+ // generally we need to restart the track before publishing, often a full reconnect
1036
+ // is necessary because computer had gone to sleep.
1037
+ log.debug('restarting existing track', {
1038
+ track: pub.trackSid,
1039
+ });
1040
+ await track.restartTrack();
1041
+ }
998
1042
  await this.publishTrack(track, pub.options);
999
1043
  }),
1000
1044
  );
@@ -68,6 +68,10 @@ export default class Participant extends EventEmitter<ParticipantEventCallbacks>
68
68
 
69
69
  private _connectionQuality: ConnectionQuality = ConnectionQuality.Unknown;
70
70
 
71
+ get isEncrypted() {
72
+ return this.tracks.size > 0 && Array.from(this.tracks.values()).every((tr) => tr.isEncrypted);
73
+ }
74
+
71
75
  /** @internal */
72
76
  constructor(sid: string, identity: string, name?: string, metadata?: string) {
73
77
  super();