livekit-client 1.15.0 → 1.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/dist/livekit-client.esm.mjs +5485 -5234
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/room/PCTransport.d.ts +9 -4
  6. package/dist/src/room/PCTransport.d.ts.map +1 -1
  7. package/dist/src/room/PCTransportManager.d.ts +51 -0
  8. package/dist/src/room/PCTransportManager.d.ts.map +1 -0
  9. package/dist/src/room/RTCEngine.d.ts +8 -5
  10. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  11. package/dist/src/room/Room.d.ts +9 -0
  12. package/dist/src/room/Room.d.ts.map +1 -1
  13. package/dist/src/room/events.d.ts +10 -0
  14. package/dist/src/room/events.d.ts.map +1 -1
  15. package/dist/src/room/participant/LocalParticipant.d.ts +0 -5
  16. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  17. package/dist/src/room/track/Track.d.ts +6 -2
  18. package/dist/src/room/track/Track.d.ts.map +1 -1
  19. package/dist/src/room/track/utils.d.ts +3 -0
  20. package/dist/src/room/track/utils.d.ts.map +1 -1
  21. package/dist/ts4.2/src/room/PCTransport.d.ts +9 -4
  22. package/dist/ts4.2/src/room/PCTransportManager.d.ts +51 -0
  23. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -5
  24. package/dist/ts4.2/src/room/Room.d.ts +9 -0
  25. package/dist/ts4.2/src/room/events.d.ts +10 -0
  26. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +0 -5
  27. package/dist/ts4.2/src/room/track/Track.d.ts +6 -2
  28. package/dist/ts4.2/src/room/track/utils.d.ts +3 -0
  29. package/package.json +1 -1
  30. package/src/connectionHelper/checks/webrtc.ts +2 -2
  31. package/src/room/PCTransport.ts +62 -29
  32. package/src/room/PCTransportManager.ts +336 -0
  33. package/src/room/RTCEngine.ts +178 -246
  34. package/src/room/Room.ts +49 -46
  35. package/src/room/events.ts +11 -0
  36. package/src/room/participant/LocalParticipant.ts +9 -51
  37. package/src/room/track/Track.ts +30 -9
  38. package/src/room/track/utils.ts +19 -0
@@ -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);