livekit-client 1.11.4 → 1.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. package/README.md +13 -1
  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 +1545 -0
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -0
  6. package/dist/livekit-client.esm.mjs +4786 -4065
  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 +174 -0
  29. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -0
  30. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -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/participant/RemoteParticipant.d.ts +6 -4
  55. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  56. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  58. package/dist/src/room/track/TrackPublication.d.ts +3 -0
  59. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  60. package/dist/src/room/track/create.d.ts.map +1 -1
  61. package/dist/src/room/track/options.d.ts +2 -2
  62. package/dist/src/room/track/options.d.ts.map +1 -1
  63. package/dist/src/room/track/utils.d.ts +9 -0
  64. package/dist/src/room/track/utils.d.ts.map +1 -1
  65. package/dist/src/room/utils.d.ts +2 -0
  66. package/dist/src/room/utils.d.ts.map +1 -1
  67. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  68. package/dist/src/utils/browserParser.d.ts +2 -0
  69. package/dist/src/utils/browserParser.d.ts.map +1 -1
  70. package/dist/ts4.2/src/api/SignalClient.d.ts +4 -1
  71. package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +45 -0
  72. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +42 -0
  73. package/dist/ts4.2/src/e2ee/constants.d.ts +14 -0
  74. package/dist/ts4.2/src/e2ee/errors.d.ts +11 -0
  75. package/dist/ts4.2/src/e2ee/index.d.ts +4 -0
  76. package/dist/ts4.2/src/e2ee/types.d.ts +129 -0
  77. package/dist/ts4.2/src/e2ee/utils.d.ts +24 -0
  78. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +174 -0
  79. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
  80. package/dist/ts4.2/src/e2ee/worker/e2ee.worker.d.ts +2 -0
  81. package/dist/ts4.2/src/index.d.ts +1 -0
  82. package/dist/ts4.2/src/logger.d.ts +4 -1
  83. package/dist/ts4.2/src/options.d.ts +5 -0
  84. package/dist/ts4.2/src/proto/livekit_models.d.ts +2 -2
  85. package/dist/ts4.2/src/room/PCTransport.d.ts +3 -1
  86. package/dist/ts4.2/src/room/RTCEngine.d.ts +17 -3
  87. package/dist/ts4.2/src/room/Room.d.ts +10 -0
  88. package/dist/ts4.2/src/room/events.d.ts +14 -2
  89. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -2
  90. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
  91. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +6 -4
  92. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +3 -0
  93. package/dist/ts4.2/src/room/track/options.d.ts +6 -6
  94. package/dist/ts4.2/src/room/track/utils.d.ts +9 -0
  95. package/dist/ts4.2/src/room/utils.d.ts +2 -0
  96. package/dist/ts4.2/src/utils/browserParser.d.ts +2 -0
  97. package/package.json +17 -7
  98. package/src/api/SignalClient.ts +28 -9
  99. package/src/connectionHelper/checks/turn.ts +1 -0
  100. package/src/connectionHelper/checks/websocket.ts +1 -0
  101. package/src/e2ee/E2eeManager.ts +374 -0
  102. package/src/e2ee/KeyProvider.ts +77 -0
  103. package/src/e2ee/constants.ts +40 -0
  104. package/src/e2ee/errors.ts +16 -0
  105. package/src/e2ee/index.ts +3 -0
  106. package/src/e2ee/types.ts +160 -0
  107. package/src/e2ee/utils.ts +127 -0
  108. package/src/e2ee/worker/FrameCryptor.test.ts +21 -0
  109. package/src/e2ee/worker/FrameCryptor.ts +612 -0
  110. package/src/e2ee/worker/ParticipantKeyHandler.ts +144 -0
  111. package/src/e2ee/worker/e2ee.worker.ts +223 -0
  112. package/src/e2ee/worker/tsconfig.json +6 -0
  113. package/src/index.ts +1 -0
  114. package/src/logger.ts +10 -2
  115. package/src/options.ts +6 -0
  116. package/src/proto/livekit_models.ts +12 -12
  117. package/src/room/PCTransport.ts +39 -9
  118. package/src/room/RTCEngine.ts +127 -34
  119. package/src/room/Room.ts +94 -29
  120. package/src/room/defaults.ts +1 -1
  121. package/src/room/events.ts +14 -0
  122. package/src/room/participant/LocalParticipant.ts +52 -8
  123. package/src/room/participant/Participant.ts +4 -0
  124. package/src/room/participant/RemoteParticipant.ts +19 -15
  125. package/src/room/track/LocalTrack.ts +5 -4
  126. package/src/room/track/RemoteVideoTrack.ts +2 -2
  127. package/src/room/track/TrackPublication.ts +9 -1
  128. package/src/room/track/create.ts +9 -0
  129. package/src/room/track/options.ts +3 -2
  130. package/src/room/track/utils.ts +27 -0
  131. package/src/room/utils.ts +5 -0
  132. package/src/room/worker.d.ts +4 -0
  133. package/src/test/MockMediaStreamTrack.ts +1 -0
  134. package/src/utils/browserParser.ts +5 -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,
@@ -31,6 +33,7 @@ import {
31
33
  SubscriptionPermissionUpdate,
32
34
  SubscriptionResponse,
33
35
  } from '../proto/livekit_rtc';
36
+ import { getBrowser } from '../utils/browserParser';
34
37
  import DeviceManager from './DeviceManager';
35
38
  import RTCEngine from './RTCEngine';
36
39
  import { RegionUrlProvider } from './RegionUrlProvider';
@@ -56,7 +59,7 @@ import RemoteTrackPublication from './track/RemoteTrackPublication';
56
59
  import { Track } from './track/Track';
57
60
  import type { TrackPublication } from './track/TrackPublication';
58
61
  import type { AdaptiveStreamSettings } from './track/types';
59
- import { getNewAudioContext } from './track/utils';
62
+ import { getNewAudioContext, sourceToKind } from './track/utils';
60
63
  import type { SimulationOptions, SimulationScenario } from './types';
61
64
  import {
62
65
  Future,
@@ -64,7 +67,6 @@ import {
64
67
  createDummyVideoStreamTrack,
65
68
  getEmptyAudioStreamTrack,
66
69
  isCloud,
67
- isSafari,
68
70
  isWeb,
69
71
  supportsSetSinkId,
70
72
  unpackStreamId,
@@ -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':
@@ -677,10 +718,10 @@ class Room extends EventEmitter<RoomEventCallbacks> {
677
718
  async startAudio() {
678
719
  await this.acquireAudioContext();
679
720
  const elements: Array<HTMLMediaElement> = [];
680
-
681
- if (isSafari()) {
721
+ const browser = getBrowser();
722
+ if (browser && browser.os === 'iOS') {
682
723
  /**
683
- * iOS Safari blocks audio element playback if
724
+ * iOS blocks audio element playback if
684
725
  * - user is not publishing audio themselves and
685
726
  * - no other audio source is playing
686
727
  *
@@ -691,6 +732,7 @@ class Room extends EventEmitter<RoomEventCallbacks> {
691
732
  let dummyAudioEl = document.getElementById(audioId) as HTMLAudioElement | null;
692
733
  if (!dummyAudioEl) {
693
734
  dummyAudioEl = document.createElement('audio');
735
+ dummyAudioEl.id = audioId;
694
736
  dummyAudioEl.autoplay = true;
695
737
  dummyAudioEl.hidden = true;
696
738
  const track = getEmptyAudioStreamTrack();
@@ -792,7 +834,7 @@ class Room extends EventEmitter<RoomEventCallbacks> {
792
834
  } else if (kind === 'audiooutput') {
793
835
  if (
794
836
  (!supportsSetSinkId() && !this.options.expWebAudioMix) ||
795
- (this.audioContext && !('setSinkId' in this.audioContext))
837
+ (this.options.expWebAudioMix && this.audioContext && !('setSinkId' in this.audioContext))
796
838
  ) {
797
839
  throw new Error('cannot switch audio output, setSinkId not supported');
798
840
  }
@@ -1262,13 +1304,22 @@ class Room extends EventEmitter<RoomEventCallbacks> {
1262
1304
  ) {
1263
1305
  // override audio context with custom audio context if supplied by user
1264
1306
  this.audioContext = this.options.expWebAudioMix.audioContext;
1265
- await this.audioContext.resume();
1266
- } else {
1307
+ } else if (!this.audioContext || this.audioContext.state === 'closed') {
1267
1308
  // by using an AudioContext, it reduces lag on audio elements
1268
1309
  // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
1269
1310
  this.audioContext = getNewAudioContext() ?? undefined;
1270
1311
  }
1271
1312
 
1313
+ if (this.audioContext && this.audioContext.state === 'suspended') {
1314
+ // for iOS a newly created AudioContext is always in `suspended` state.
1315
+ // we try our best to resume the context here, if that doesn't work, we just continue with regular processing
1316
+ try {
1317
+ await this.audioContext.resume();
1318
+ } catch (e: any) {
1319
+ log.warn(e);
1320
+ }
1321
+ }
1322
+
1272
1323
  if (this.options.expWebAudioMix) {
1273
1324
  this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
1274
1325
  }
@@ -1522,6 +1573,16 @@ class Room extends EventEmitter<RoomEventCallbacks> {
1522
1573
  this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
1523
1574
  }
1524
1575
  }
1576
+ const deviceId = await pub.track?.getDeviceId();
1577
+ const deviceKind = sourceToKind(pub.source);
1578
+ if (
1579
+ deviceKind &&
1580
+ deviceId &&
1581
+ deviceId !== this.localParticipant.activeDeviceMap.get(deviceKind)
1582
+ ) {
1583
+ this.localParticipant.activeDeviceMap.set(deviceKind, deviceId);
1584
+ this.emit(RoomEvent.ActiveDeviceChanged, deviceKind, deviceId);
1585
+ }
1525
1586
  };
1526
1587
 
1527
1588
  private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
@@ -1595,7 +1656,9 @@ class Room extends EventEmitter<RoomEventCallbacks> {
1595
1656
  }),
1596
1657
  new LocalVideoTrack(
1597
1658
  publishOptions.useRealTracks
1598
- ? (await navigator.mediaDevices.getUserMedia({ video: true })).getVideoTracks()[0]
1659
+ ? (
1660
+ await window.navigator.mediaDevices.getUserMedia({ video: true })
1661
+ ).getVideoTracks()[0]
1599
1662
  : createDummyVideoStreamTrack(
1600
1663
  160 * participantOptions.aspectRatios[0] ?? 1,
1601
1664
  160,
@@ -1758,6 +1821,8 @@ export type RoomEventCallbacks = {
1758
1821
  audioPlaybackChanged: (playing: boolean) => void;
1759
1822
  signalConnected: () => void;
1760
1823
  recordingStatusChanged: (recording: boolean) => void;
1824
+ participantEncryptionStatusChanged: (encrypted: boolean, participant?: Participant) => void;
1825
+ encryptionError: (error: Error) => void;
1761
1826
  dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1762
1827
  activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
1763
1828
  };
@@ -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 {