livekit-client 1.14.4 → 1.15.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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  4. package/dist/livekit-client.esm.mjs +5488 -5230
  5. package/dist/livekit-client.esm.mjs.map +1 -1
  6. package/dist/livekit-client.umd.js +1 -1
  7. package/dist/livekit-client.umd.js.map +1 -1
  8. package/dist/src/api/SignalClient.d.ts.map +1 -1
  9. package/dist/src/room/PCTransport.d.ts +10 -4
  10. package/dist/src/room/PCTransport.d.ts.map +1 -1
  11. package/dist/src/room/PCTransportManager.d.ts +51 -0
  12. package/dist/src/room/PCTransportManager.d.ts.map +1 -0
  13. package/dist/src/room/RTCEngine.d.ts +8 -5
  14. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/src/room/Room.d.ts +9 -0
  16. package/dist/src/room/Room.d.ts.map +1 -1
  17. package/dist/src/room/events.d.ts +10 -0
  18. package/dist/src/room/events.d.ts.map +1 -1
  19. package/dist/src/room/participant/LocalParticipant.d.ts +0 -5
  20. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  21. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  22. package/dist/src/room/track/Track.d.ts +6 -2
  23. package/dist/src/room/track/Track.d.ts.map +1 -1
  24. package/dist/src/room/track/options.d.ts +2 -0
  25. package/dist/src/room/track/options.d.ts.map +1 -1
  26. package/dist/src/room/track/utils.d.ts +3 -0
  27. package/dist/src/room/track/utils.d.ts.map +1 -1
  28. package/dist/src/room/utils.d.ts.map +1 -1
  29. package/dist/src/test/mocks.d.ts +1 -1
  30. package/dist/src/test/mocks.d.ts.map +1 -1
  31. package/dist/ts4.2/src/room/PCTransport.d.ts +10 -4
  32. package/dist/ts4.2/src/room/PCTransportManager.d.ts +51 -0
  33. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -5
  34. package/dist/ts4.2/src/room/Room.d.ts +9 -0
  35. package/dist/ts4.2/src/room/events.d.ts +10 -0
  36. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +0 -5
  37. package/dist/ts4.2/src/room/track/Track.d.ts +6 -2
  38. package/dist/ts4.2/src/room/track/options.d.ts +2 -0
  39. package/dist/ts4.2/src/room/track/utils.d.ts +3 -0
  40. package/dist/ts4.2/src/test/mocks.d.ts +1 -1
  41. package/package.json +20 -19
  42. package/src/api/SignalClient.ts +7 -1
  43. package/src/connectionHelper/checks/webrtc.ts +2 -2
  44. package/src/room/PCTransport.ts +66 -29
  45. package/src/room/PCTransportManager.ts +336 -0
  46. package/src/room/RTCEngine.ts +178 -246
  47. package/src/room/Room.ts +49 -46
  48. package/src/room/defaults.ts +1 -1
  49. package/src/room/events.ts +11 -0
  50. package/src/room/participant/LocalParticipant.ts +9 -51
  51. package/src/room/track/LocalTrack.ts +2 -0
  52. package/src/room/track/Track.ts +30 -9
  53. package/src/room/track/options.ts +2 -0
  54. package/src/room/track/utils.ts +19 -0
  55. package/src/room/utils.ts +2 -1
  56. package/src/test/mocks.ts +5 -1
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
2
2
  import type { MediaAttributes } from 'sdp-transform';
3
3
  import type TypedEventEmitter from 'typed-emitter';
4
4
  import type { SignalOptions } from '../api/SignalClient';
5
- import { SignalClient } from '../api/SignalClient';
5
+ import { SignalClient, toProtoSessionDescription } from '../api/SignalClient';
6
6
  import log from '../logger';
7
7
  import type { InternalRoomOptions } from '../options';
8
8
  import {
@@ -19,18 +19,22 @@ import {
19
19
  UserPacket,
20
20
  } from '../proto/livekit_models_pb';
21
21
  import {
22
- AddTrackRequest,
23
- ConnectionQualityUpdate,
24
- JoinResponse,
25
- LeaveRequest,
26
- ReconnectResponse,
22
+ type AddTrackRequest,
23
+ type ConnectionQualityUpdate,
24
+ DataChannelInfo,
25
+ type JoinResponse,
26
+ type LeaveRequest,
27
+ type ReconnectResponse,
27
28
  SignalTarget,
28
- StreamStateUpdate,
29
- SubscriptionPermissionUpdate,
30
- SubscriptionResponse,
31
- TrackPublishedResponse,
29
+ type StreamStateUpdate,
30
+ type SubscriptionPermissionUpdate,
31
+ type SubscriptionResponse,
32
+ SyncState,
33
+ type TrackPublishedResponse,
34
+ UpdateSubscription,
32
35
  } from '../proto/livekit_rtc_pb';
33
36
  import PCTransport, { PCEvents } from './PCTransport';
37
+ import { PCTransportManager, PCTransportState } from './PCTransportManager';
34
38
  import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
35
39
  import type { RegionUrlProvider } from './RegionUrlProvider';
36
40
  import { roomConnectOptionDefaults } from './defaults';
@@ -44,10 +48,13 @@ import {
44
48
  import { EngineEvent } from './events';
45
49
  import CriticalTimers from './timers';
46
50
  import type LocalTrack from './track/LocalTrack';
51
+ import type LocalTrackPublication from './track/LocalTrackPublication';
47
52
  import type LocalVideoTrack from './track/LocalVideoTrack';
48
53
  import type { SimulcastTrackInfo } from './track/LocalVideoTrack';
54
+ import type RemoteTrackPublication from './track/RemoteTrackPublication';
49
55
  import { Track } from './track/Track';
50
56
  import type { TrackPublishOptions, VideoCodec } from './track/options';
57
+ import { getTrackPublicationInfo } from './track/utils';
51
58
  import {
52
59
  Mutex,
53
60
  isVideoCodec,
@@ -73,10 +80,6 @@ enum PCState {
73
80
 
74
81
  /** @internal */
75
82
  export default class RTCEngine extends (EventEmitter as new () => TypedEventEmitter<EngineEventCallbacks>) {
76
- publisher?: PCTransport;
77
-
78
- subscriber?: PCTransport;
79
-
80
83
  client: SignalClient;
81
84
 
82
85
  rtcConfig: RTCConfiguration = {};
@@ -85,6 +88,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
85
88
 
86
89
  fullReconnectOnNext: boolean = false;
87
90
 
91
+ pcManager?: PCTransportManager;
92
+
88
93
  /**
89
94
  * @internal
90
95
  */
@@ -108,8 +113,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
108
113
 
109
114
  private subscriberPrimary: boolean = false;
110
115
 
111
- private primaryTransport?: PCTransport;
112
-
113
116
  private pcState: PCState = PCState.New;
114
117
 
115
118
  private _isClosed: boolean = true;
@@ -118,10 +121,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
118
121
  [key: string]: { resolve: (info: TrackInfo) => void; reject: () => void };
119
122
  } = {};
120
123
 
121
- // true if publisher connection has already been established.
122
- // this is helpful to know if we need to restart ICE on the publisher connection
123
- private hasPublished: boolean = false;
124
-
125
124
  // keep join info around for reconnect, this could be a region url
126
125
  private url?: string;
127
126
 
@@ -201,8 +200,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
201
200
  this.latestJoinResponse = joinResponse;
202
201
 
203
202
  this.subscriberPrimary = joinResponse.subscriberPrimary;
204
- if (!this.publisher) {
205
- this.configure(joinResponse);
203
+ if (!this.pcManager) {
204
+ await this.configure(joinResponse);
206
205
  }
207
206
 
208
207
  // create offer
@@ -247,28 +246,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
247
246
  }
248
247
 
249
248
  async cleanupPeerConnections() {
250
- if (this.publisher && this.publisher.getSignallingState() !== 'closed') {
251
- this.publisher.getSenders().forEach((sender) => {
252
- try {
253
- // TODO: react-native-webrtc doesn't have removeTrack yet.
254
- if (this.publisher?.canRemoveTrack()) {
255
- this.publisher?.removeTrack(sender);
256
- }
257
- } catch (e) {
258
- log.warn('could not removeTrack', { error: e });
259
- }
260
- });
261
- }
262
- if (this.publisher) {
263
- this.publisher.close();
264
- this.publisher = undefined;
265
- }
266
- if (this.subscriber) {
267
- this.subscriber.close();
268
- this.subscriber = undefined;
269
- }
270
- this.hasPublished = false;
271
- this.primaryTransport = undefined;
249
+ await this.pcManager?.close();
250
+ this.pcManager = undefined;
272
251
 
273
252
  const dcCleanup = (dc: RTCDataChannel | undefined) => {
274
253
  if (!dc) return;
@@ -336,7 +315,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
336
315
  delete this.pendingTrackResolvers[sender.track.id];
337
316
  }
338
317
  try {
339
- this.publisher?.removeTrack(sender);
318
+ this.pcManager!.removeTrack(sender);
340
319
  return true;
341
320
  } catch (e: unknown) {
342
321
  log.warn('failed to remove track', { error: e, method: 'removeTrack' });
@@ -353,10 +332,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
353
332
  }
354
333
 
355
334
  async getConnectedServerAddress(): Promise<string | undefined> {
356
- if (this.primaryTransport === undefined) {
357
- return undefined;
358
- }
359
- return this.primaryTransport.getConnectedAddress();
335
+ return this.pcManager?.getConnectedAddress();
360
336
  }
361
337
 
362
338
  /* @internal */
@@ -364,9 +340,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
364
340
  this.regionUrlProvider = provider;
365
341
  }
366
342
 
367
- private configure(joinResponse: JoinResponse) {
343
+ private async configure(joinResponse: JoinResponse) {
368
344
  // already configured
369
- if (this.publisher || this.subscriber) {
345
+ if (this.pcManager && this.pcManager.currentState !== PCTransportState.NEW) {
370
346
  return;
371
347
  }
372
348
 
@@ -374,71 +350,42 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
374
350
 
375
351
  const rtcConfig = this.makeRTCConfiguration(joinResponse);
376
352
 
377
- const googConstraints = { optional: [{ googDscp: true }] };
378
- this.publisher = new PCTransport(rtcConfig, googConstraints);
379
- this.subscriber = new PCTransport(rtcConfig);
353
+ this.pcManager = new PCTransportManager(rtcConfig, joinResponse.subscriberPrimary);
380
354
 
381
- this.emit(EngineEvent.TransportsCreated, this.publisher, this.subscriber);
355
+ this.emit(EngineEvent.TransportsCreated, this.pcManager.publisher, this.pcManager.subscriber);
382
356
 
383
- this.publisher.onIceCandidate = (candidate) => {
384
- log.trace('adding ICE candidate for peer', candidate);
385
- this.client.sendIceCandidate(candidate, SignalTarget.PUBLISHER);
357
+ this.pcManager.onIceCandidate = (candidate, target) => {
358
+ this.client.sendIceCandidate(candidate, target);
386
359
  };
387
360
 
388
- this.subscriber.onIceCandidate = (candidate) => {
389
- this.client.sendIceCandidate(candidate, SignalTarget.SUBSCRIBER);
390
- };
391
-
392
- this.publisher.onOffer = (offer) => {
361
+ this.pcManager.onPublisherOffer = (offer) => {
393
362
  this.client.sendOffer(offer);
394
363
  };
395
364
 
396
- let primaryTransport = this.publisher;
397
- let secondaryTransport = this.subscriber;
398
- let subscriberPrimary = joinResponse.subscriberPrimary;
399
- if (subscriberPrimary) {
400
- primaryTransport = this.subscriber;
401
- secondaryTransport = this.publisher;
402
- // in subscriber primary mode, server side opens sub data channels.
403
- this.subscriber.onDataChannel = this.handleDataChannel;
404
- }
405
- this.primaryTransport = primaryTransport;
406
- primaryTransport.onConnectionStateChange = async (connectionState) => {
365
+ this.pcManager.onDataChannel = this.handleDataChannel;
366
+ this.pcManager.onStateChange = async (connectionState, publisherState, subscriberState) => {
407
367
  log.debug(`primary PC state changed ${connectionState}`);
408
- if (connectionState === 'connected') {
368
+ if (connectionState === PCTransportState.CONNECTED) {
409
369
  const shouldEmit = this.pcState === PCState.New;
410
370
  this.pcState = PCState.Connected;
411
371
  if (shouldEmit) {
412
372
  this.emit(EngineEvent.Connected, joinResponse);
413
373
  }
414
- } else if (connectionState === 'failed') {
374
+ } else if (connectionState === PCTransportState.FAILED) {
415
375
  // on Safari, PeerConnection will switch to 'disconnected' during renegotiation
416
376
  if (this.pcState === PCState.Connected) {
417
377
  this.pcState = PCState.Disconnected;
418
378
 
419
379
  this.handleDisconnect(
420
- 'primary peerconnection',
421
- subscriberPrimary
380
+ 'peerconnection failed',
381
+ subscriberState === 'failed'
422
382
  ? ReconnectReason.RR_SUBSCRIBER_FAILED
423
383
  : ReconnectReason.RR_PUBLISHER_FAILED,
424
384
  );
425
385
  }
426
386
  }
427
387
  };
428
- secondaryTransport.onConnectionStateChange = async (connectionState) => {
429
- log.debug(`secondary PC state changed ${connectionState}`);
430
- // also reconnect if secondary peerconnection fails
431
- if (connectionState === 'failed') {
432
- this.handleDisconnect(
433
- 'secondary peerconnection',
434
- subscriberPrimary
435
- ? ReconnectReason.RR_PUBLISHER_FAILED
436
- : ReconnectReason.RR_SUBSCRIBER_FAILED,
437
- );
438
- }
439
- };
440
-
441
- this.subscriber.onTrack = (ev: RTCTrackEvent) => {
388
+ this.pcManager.onTrack = (ev: RTCTrackEvent) => {
442
389
  this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
443
390
  };
444
391
 
@@ -448,42 +395,30 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
448
395
  private setupSignalClientCallbacks() {
449
396
  // configure signaling client
450
397
  this.client.onAnswer = async (sd) => {
451
- if (!this.publisher) {
398
+ if (!this.pcManager) {
452
399
  return;
453
400
  }
454
401
  log.debug('received server answer', {
455
402
  RTCSdpType: sd.type,
456
- signalingState: this.publisher.getSignallingState().toString(),
457
403
  });
458
- await this.publisher.setRemoteDescription(sd);
404
+ await this.pcManager.setPublisherAnswer(sd);
459
405
  };
460
406
 
461
407
  // add candidate on trickle
462
408
  this.client.onTrickle = (candidate, target) => {
463
- if (!this.publisher || !this.subscriber) {
409
+ if (!this.pcManager) {
464
410
  return;
465
411
  }
466
412
  log.trace('got ICE candidate from peer', { candidate, target });
467
- if (target === SignalTarget.PUBLISHER) {
468
- this.publisher.addIceCandidate(candidate);
469
- } else {
470
- this.subscriber.addIceCandidate(candidate);
471
- }
413
+ this.pcManager.addIceCandidate(candidate, target);
472
414
  };
473
415
 
474
416
  // when server creates an offer for the client
475
417
  this.client.onOffer = async (sd) => {
476
- if (!this.subscriber) {
418
+ if (!this.pcManager) {
477
419
  return;
478
420
  }
479
- log.debug('received server offer', {
480
- RTCSdpType: sd.type,
481
- signalingState: this.subscriber.getSignallingState().toString(),
482
- });
483
- await this.subscriber.setRemoteDescription(sd);
484
-
485
- // answer the offer
486
- const answer = await this.subscriber.createAndSetAnswer();
421
+ const answer = await this.pcManager.createSubscriberAnswerFromOffer(sd);
487
422
  this.client.sendAnswer(answer);
488
423
  };
489
424
 
@@ -509,7 +444,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
509
444
  this.client.onLeave = (leave?: LeaveRequest) => {
510
445
  if (leave?.canReconnect) {
511
446
  this.fullReconnectOnNext = true;
512
- this.primaryTransport = undefined;
513
447
  // reconnect immediately instead of waiting for next attempt
514
448
  this.handleDisconnect(leaveReconnect);
515
449
  } else {
@@ -522,6 +456,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
522
456
 
523
457
  private makeRTCConfiguration(serverResponse: JoinResponse | ReconnectResponse): RTCConfiguration {
524
458
  const rtcConfig = { ...this.rtcConfig };
459
+
525
460
  if (this.signalOpts?.e2eeEnabled) {
526
461
  log.debug('E2EE - setting up transports with insertable streams');
527
462
  // this makes sure that no data is sent before the transforms are ready
@@ -561,7 +496,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
561
496
  }
562
497
 
563
498
  private createDataChannels() {
564
- if (!this.publisher) {
499
+ if (!this.pcManager) {
565
500
  return;
566
501
  }
567
502
 
@@ -576,12 +511,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
576
511
  }
577
512
 
578
513
  // create data channels
579
- this.lossyDC = this.publisher.createDataChannel(lossyDataChannel, {
514
+ this.lossyDC = this.pcManager.createPublisherDataChannel(lossyDataChannel, {
580
515
  // will drop older packets that arrive
581
516
  ordered: true,
582
517
  maxRetransmits: 0,
583
518
  });
584
- this.reliableDC = this.publisher.createDataChannel(reliableDataChannel, {
519
+ this.reliableDC = this.pcManager.createPublisherDataChannel(reliableDataChannel, {
585
520
  ordered: true,
586
521
  });
587
522
 
@@ -747,7 +682,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
747
682
  opts: TrackPublishOptions,
748
683
  encodings?: RTCRtpEncodingParameters[],
749
684
  ) {
750
- if (!this.publisher) {
685
+ if (!this.pcManager) {
751
686
  throw new UnexpectedConnectionState('publisher is closed');
752
687
  }
753
688
 
@@ -762,7 +697,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
762
697
  transceiverInit.sendEncodings = encodings;
763
698
  }
764
699
  // addTransceiver for react-native is async. web is synchronous, but await won't effect it.
765
- const transceiver = await this.publisher.addTransceiver(
700
+ const transceiver = await this.pcManager.addPublisherTransceiver(
766
701
  track.mediaStreamTrack,
767
702
  transceiverInit,
768
703
  );
@@ -780,7 +715,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
780
715
  opts: TrackPublishOptions,
781
716
  encodings?: RTCRtpEncodingParameters[],
782
717
  ) {
783
- if (!this.publisher) {
718
+ if (!this.pcManager) {
784
719
  throw new UnexpectedConnectionState('publisher is closed');
785
720
  }
786
721
  const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
@@ -788,7 +723,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
788
723
  transceiverInit.sendEncodings = encodings;
789
724
  }
790
725
  // addTransceiver for react-native is async. web is synchronous, but await won't effect it.
791
- const transceiver = await this.publisher.addTransceiver(
726
+ const transceiver = await this.pcManager.addPublisherTransceiver(
792
727
  simulcastTrack.mediaStreamTrack,
793
728
  transceiverInit,
794
729
  );
@@ -801,10 +736,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
801
736
  }
802
737
 
803
738
  private async createRTCRtpSender(track: MediaStreamTrack) {
804
- if (!this.publisher) {
739
+ if (!this.pcManager) {
805
740
  throw new UnexpectedConnectionState('publisher is closed');
806
741
  }
807
- return this.publisher.addTrack(track);
742
+ return this.pcManager.addPublisherTrack(track);
808
743
  }
809
744
 
810
745
  // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
@@ -869,7 +804,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
869
804
  this.clientConfiguration?.resumeConnection === ClientConfigSetting.DISABLED ||
870
805
  // signaling state could change to closed due to hardware sleep
871
806
  // those connections cannot be resumed
872
- (this.primaryTransport?.getSignallingState() ?? 'closed') === 'closed'
807
+ (this.pcManager?.currentState ?? PCTransportState.NEW) === PCTransportState.NEW
873
808
  ) {
874
809
  this.fullReconnectOnNext = true;
875
810
  }
@@ -984,7 +919,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
984
919
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
985
920
  }
986
921
  // trigger publisher reconnect
987
- if (!this.publisher || !this.subscriber) {
922
+ if (!this.pcManager) {
988
923
  throw new UnexpectedConnectionState('publisher and subscriber connections unset');
989
924
  }
990
925
 
@@ -996,8 +931,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
996
931
  const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
997
932
  if (res) {
998
933
  const rtcConfig = this.makeRTCConfiguration(res);
999
- this.publisher.setConfiguration(rtcConfig);
1000
- this.subscriber.setConfiguration(rtcConfig);
934
+ this.pcManager.updateConfiguration(rtcConfig);
1001
935
  }
1002
936
  } catch (e) {
1003
937
  let message = '';
@@ -1017,12 +951,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1017
951
  throw new Error('simulated failure');
1018
952
  }
1019
953
 
1020
- this.subscriber.restartingIce = true;
1021
-
1022
- // only restart publisher if it's needed
1023
- if (this.hasPublished) {
1024
- await this.publisher.createAndSendOffer({ iceRestart: true });
1025
- }
954
+ await this.pcManager.triggerIceRestart();
1026
955
 
1027
956
  await this.waitForPCReconnected();
1028
957
  this.client.setReconnected();
@@ -1038,72 +967,28 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1038
967
  }
1039
968
 
1040
969
  async waitForPCInitialConnection(timeout?: number, abortController?: AbortController) {
1041
- if (this.pcState === PCState.Connected) {
1042
- return;
1043
- }
1044
- if (this.pcState !== PCState.New) {
1045
- throw new UnexpectedConnectionState(
1046
- 'Expected peer connection to be new on initial connection',
1047
- );
970
+ if (!this.pcManager) {
971
+ throw new UnexpectedConnectionState('PC manager is closed');
1048
972
  }
1049
- return new Promise<void>((resolve, reject) => {
1050
- const abortHandler = () => {
1051
- log.warn('closing engine');
1052
- CriticalTimers.clearTimeout(connectTimeout);
1053
-
1054
- reject(
1055
- new ConnectionError(
1056
- 'room connection has been cancelled',
1057
- ConnectionErrorReason.Cancelled,
1058
- ),
1059
- );
1060
- };
1061
- if (abortController?.signal.aborted) {
1062
- abortHandler();
1063
- }
1064
- abortController?.signal.addEventListener('abort', abortHandler);
1065
- const onConnected = () => {
1066
- CriticalTimers.clearTimeout(connectTimeout);
1067
- abortController?.signal.removeEventListener('abort', abortHandler);
1068
- resolve();
1069
- };
1070
- const connectTimeout = CriticalTimers.setTimeout(() => {
1071
- this.off(EngineEvent.Connected, onConnected);
1072
- reject(new ConnectionError('could not establish pc connection'));
1073
- }, timeout ?? this.peerConnectionTimeout);
1074
- this.once(EngineEvent.Connected, onConnected);
1075
- });
973
+ await this.pcManager.ensurePCTransportConnection(abortController, timeout);
1076
974
  }
1077
975
 
1078
976
  private async waitForPCReconnected() {
1079
- const startTime = Date.now();
1080
- let now = startTime;
1081
977
  this.pcState = PCState.Reconnecting;
1082
978
 
1083
979
  log.debug('waiting for peer connection to reconnect');
1084
- while (now - startTime < this.peerConnectionTimeout) {
1085
- if (this.primaryTransport === undefined) {
1086
- // we can abort early, connection is hosed
1087
- break;
1088
- } else if (
1089
- // on Safari, we don't get a connectionstatechanged event during ICE restart
1090
- // this means we'd have to check its status manually and update address
1091
- // manually
1092
- now - startTime > minReconnectWait &&
1093
- this.primaryTransport?.getConnectionState() === 'connected' &&
1094
- (!this.hasPublished || this.publisher?.getConnectionState() === 'connected')
1095
- ) {
1096
- this.pcState = PCState.Connected;
1097
- }
1098
- if (this.pcState === PCState.Connected) {
1099
- return;
980
+ try {
981
+ await sleep(minReconnectWait); // FIXME setTimeout again not ideal for a connection critical path
982
+ if (!this.pcManager) {
983
+ throw new UnexpectedConnectionState('PC manager is closed');
1100
984
  }
1101
- await sleep(100);
1102
- now = Date.now();
985
+ await this.pcManager.ensurePCTransportConnection(undefined, this.peerConnectionTimeout);
986
+ this.pcState = PCState.Connected;
987
+ } catch (e: any) {
988
+ // TODO do we need a `failed` state here for the PC?
989
+ this.pcState = PCState.Disconnected;
990
+ throw new ConnectionError(`could not establish PC connection, ${e.message}`);
1103
991
  }
1104
-
1105
- // have not reconnected, throw
1106
- throw new ConnectionError('could not establish PC connection');
1107
992
  }
1108
993
 
1109
994
  waitForRestarted = () => {
@@ -1161,7 +1046,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1161
1046
  kind: DataPacket_Kind,
1162
1047
  subscriber: boolean = this.subscriberPrimary,
1163
1048
  ) {
1164
- const transport = subscriber ? this.subscriber : this.publisher;
1049
+ if (!this.pcManager) {
1050
+ throw new UnexpectedConnectionState('PC manager is closed');
1051
+ }
1052
+ const transport = subscriber ? this.pcManager.subscriber : this.pcManager.publisher;
1165
1053
  const transportName = subscriber ? 'Subscriber' : 'Publisher';
1166
1054
  if (!transport) {
1167
1055
  throw new ConnectionError(`${transportName} connection not set`);
@@ -1169,8 +1057,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1169
1057
 
1170
1058
  if (
1171
1059
  !subscriber &&
1172
- !this.publisher?.isICEConnected &&
1173
- this.publisher?.getICEConnectionState() !== 'checking'
1060
+ !this.pcManager.publisher.isICEConnected &&
1061
+ this.pcManager.publisher.getICEConnectionState() !== 'checking'
1174
1062
  ) {
1175
1063
  // start negotiation
1176
1064
  this.negotiate();
@@ -1204,30 +1092,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1204
1092
 
1205
1093
  /* @internal */
1206
1094
  verifyTransport(): boolean {
1207
- // primary connection
1208
- if (!this.primaryTransport) {
1095
+ if (!this.pcManager) {
1209
1096
  return false;
1210
1097
  }
1211
- if (
1212
- this.primaryTransport.getConnectionState() === 'closed' ||
1213
- this.primaryTransport.getConnectionState() === 'failed'
1214
- ) {
1098
+ // primary connection
1099
+ if (this.pcManager.currentState !== PCTransportState.CONNECTED) {
1215
1100
  return false;
1216
1101
  }
1217
1102
 
1218
- // also verify publisher connection if it's needed or different
1219
- if (this.hasPublished && this.subscriberPrimary) {
1220
- if (!this.publisher) {
1221
- return false;
1222
- }
1223
- if (
1224
- this.publisher.getConnectionState() === 'closed' ||
1225
- this.publisher.getConnectionState() === 'failed'
1226
- ) {
1227
- return false;
1228
- }
1229
- }
1230
-
1231
1103
  // ensure signal is connected
1232
1104
  if (!this.client.ws || this.client.ws.readyState === WebSocket.CLOSED) {
1233
1105
  return false;
@@ -1236,19 +1108,21 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1236
1108
  }
1237
1109
 
1238
1110
  /** @internal */
1239
- negotiate(): Promise<void> {
1111
+ async negotiate(): Promise<void> {
1240
1112
  // observe signal state
1241
- return new Promise<void>((resolve, reject) => {
1242
- if (!this.publisher) {
1243
- reject(new NegotiationError('publisher is not defined'));
1113
+ return new Promise<void>(async (resolve, reject) => {
1114
+ if (!this.pcManager) {
1115
+ reject(new NegotiationError('PC manager is closed'));
1244
1116
  return;
1245
1117
  }
1246
1118
 
1247
- this.hasPublished = true;
1119
+ this.pcManager.requirePublisher();
1120
+
1121
+ const abortController = new AbortController();
1248
1122
 
1249
1123
  const handleClosed = () => {
1124
+ abortController.abort();
1250
1125
  log.debug('engine disconnected while negotiation was ongoing');
1251
- cleanup();
1252
1126
  resolve();
1253
1127
  return;
1254
1128
  };
@@ -1258,42 +1132,32 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1258
1132
  }
1259
1133
  this.on(EngineEvent.Closing, handleClosed);
1260
1134
 
1261
- const negotiationTimeout = setTimeout(() => {
1262
- reject('negotiation timed out');
1263
- this.handleDisconnect('negotiation', ReconnectReason.RR_SIGNAL_DISCONNECTED);
1264
- }, this.peerConnectionTimeout);
1265
-
1266
- const cleanup = () => {
1267
- clearTimeout(negotiationTimeout);
1268
- this.off(EngineEvent.Closing, handleClosed);
1269
- };
1270
-
1271
- this.publisher.once(PCEvents.NegotiationStarted, () => {
1272
- this.publisher?.once(PCEvents.NegotiationComplete, () => {
1273
- cleanup();
1274
- resolve();
1275
- });
1276
- });
1277
-
1278
- this.publisher.once(PCEvents.RTPVideoPayloadTypes, (rtpTypes: MediaAttributes['rtp']) => {
1279
- const rtpMap = new Map<number, VideoCodec>();
1280
- rtpTypes.forEach((rtp) => {
1281
- const codec = rtp.codec.toLowerCase();
1282
- if (isVideoCodec(codec)) {
1283
- rtpMap.set(rtp.payload, codec);
1284
- }
1285
- });
1286
- this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
1287
- });
1135
+ this.pcManager.publisher.once(
1136
+ PCEvents.RTPVideoPayloadTypes,
1137
+ (rtpTypes: MediaAttributes['rtp']) => {
1138
+ const rtpMap = new Map<number, VideoCodec>();
1139
+ rtpTypes.forEach((rtp) => {
1140
+ const codec = rtp.codec.toLowerCase();
1141
+ if (isVideoCodec(codec)) {
1142
+ rtpMap.set(rtp.payload, codec);
1143
+ }
1144
+ });
1145
+ this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
1146
+ },
1147
+ );
1288
1148
 
1289
- this.publisher.negotiate((e) => {
1290
- cleanup();
1291
- reject(e);
1149
+ try {
1150
+ await this.pcManager.negotiate(abortController);
1151
+ resolve();
1152
+ } catch (e: any) {
1292
1153
  if (e instanceof NegotiationError) {
1293
1154
  this.fullReconnectOnNext = true;
1294
1155
  }
1295
1156
  this.handleDisconnect('negotiation', ReconnectReason.RR_UNKNOWN);
1296
- });
1157
+ reject(e);
1158
+ } finally {
1159
+ this.off(EngineEvent.Closing, handleClosed);
1160
+ }
1297
1161
  });
1298
1162
  }
1299
1163
 
@@ -1315,12 +1179,80 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1315
1179
  }
1316
1180
  }
1317
1181
 
1182
+ /** @internal */
1183
+ sendSyncState(remoteTracks: RemoteTrackPublication[], localTracks: LocalTrackPublication[]) {
1184
+ if (!this.pcManager) {
1185
+ log.warn('sync state cannot be sent without peer connection setup');
1186
+ return;
1187
+ }
1188
+ const previousAnswer = this.pcManager.subscriber.getLocalDescription();
1189
+ const previousOffer = this.pcManager.subscriber.getRemoteDescription();
1190
+
1191
+ /* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
1192
+ in this case, we send unsub tracks, so server add all tracks to this
1193
+ subscribe pc and unsub special tracks from it.
1194
+ 2. autosubscribe off, we send subscribed tracks.
1195
+ */
1196
+ const autoSubscribe = this.signalOpts?.autoSubscribe ?? true;
1197
+ const trackSids = new Array<string>();
1198
+
1199
+ remoteTracks.forEach((track) => {
1200
+ if (track.isDesired !== autoSubscribe) {
1201
+ trackSids.push(track.trackSid);
1202
+ }
1203
+ });
1204
+
1205
+ this.client.sendSyncState(
1206
+ new SyncState({
1207
+ answer: previousAnswer
1208
+ ? toProtoSessionDescription({
1209
+ sdp: previousAnswer.sdp,
1210
+ type: previousAnswer.type,
1211
+ })
1212
+ : undefined,
1213
+ offer: previousOffer
1214
+ ? toProtoSessionDescription({
1215
+ sdp: previousOffer.sdp,
1216
+ type: previousOffer.type,
1217
+ })
1218
+ : undefined,
1219
+ subscription: new UpdateSubscription({
1220
+ trackSids,
1221
+ subscribe: !autoSubscribe,
1222
+ participantTracks: [],
1223
+ }),
1224
+ publishTracks: getTrackPublicationInfo(localTracks),
1225
+ dataChannels: this.dataChannelsInfo(),
1226
+ }),
1227
+ );
1228
+ }
1229
+
1318
1230
  /* @internal */
1319
1231
  failNext() {
1320
1232
  // debugging method to fail the next reconnect/resume attempt
1321
1233
  this.shouldFailNext = true;
1322
1234
  }
1323
1235
 
1236
+ private dataChannelsInfo(): DataChannelInfo[] {
1237
+ const infos: DataChannelInfo[] = [];
1238
+ const getInfo = (dc: RTCDataChannel | undefined, target: SignalTarget) => {
1239
+ if (dc?.id !== undefined && dc.id !== null) {
1240
+ infos.push(
1241
+ new DataChannelInfo({
1242
+ label: dc.label,
1243
+ id: dc.id,
1244
+ target,
1245
+ }),
1246
+ );
1247
+ }
1248
+ };
1249
+ getInfo(this.dataChannelForKind(DataPacket_Kind.LOSSY), SignalTarget.PUBLISHER);
1250
+ getInfo(this.dataChannelForKind(DataPacket_Kind.RELIABLE), SignalTarget.PUBLISHER);
1251
+ getInfo(this.dataChannelForKind(DataPacket_Kind.LOSSY, true), SignalTarget.SUBSCRIBER);
1252
+ getInfo(this.dataChannelForKind(DataPacket_Kind.RELIABLE, true), SignalTarget.SUBSCRIBER);
1253
+ return infos;
1254
+ }
1255
+
1324
1256
  private clearReconnectTimeout() {
1325
1257
  if (this.reconnectTimeout) {
1326
1258
  CriticalTimers.clearTimeout(this.reconnectTimeout);