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.
- package/README.md +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +5488 -5230
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +10 -4
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +51 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -0
- package/dist/src/room/RTCEngine.d.ts +8 -5
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +9 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +10 -0
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +0 -5
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +6 -2
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +2 -0
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +3 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/mocks.d.ts +1 -1
- package/dist/src/test/mocks.d.ts.map +1 -1
- package/dist/ts4.2/src/room/PCTransport.d.ts +10 -4
- package/dist/ts4.2/src/room/PCTransportManager.d.ts +51 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -5
- package/dist/ts4.2/src/room/Room.d.ts +9 -0
- package/dist/ts4.2/src/room/events.d.ts +10 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +0 -5
- package/dist/ts4.2/src/room/track/Track.d.ts +6 -2
- package/dist/ts4.2/src/room/track/options.d.ts +2 -0
- package/dist/ts4.2/src/room/track/utils.d.ts +3 -0
- package/dist/ts4.2/src/test/mocks.d.ts +1 -1
- package/package.json +20 -19
- package/src/api/SignalClient.ts +7 -1
- package/src/connectionHelper/checks/webrtc.ts +2 -2
- package/src/room/PCTransport.ts +66 -29
- package/src/room/PCTransportManager.ts +336 -0
- package/src/room/RTCEngine.ts +178 -246
- package/src/room/Room.ts +49 -46
- package/src/room/defaults.ts +1 -1
- package/src/room/events.ts +11 -0
- package/src/room/participant/LocalParticipant.ts +9 -51
- package/src/room/track/LocalTrack.ts +2 -0
- package/src/room/track/Track.ts +30 -9
- package/src/room/track/options.ts +2 -0
- package/src/room/track/utils.ts +19 -0
- package/src/room/utils.ts +2 -1
- package/src/test/mocks.ts +5 -1
package/src/room/RTCEngine.ts
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
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.
|
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
|
-
|
251
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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.
|
384
|
-
|
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.
|
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
|
-
|
397
|
-
|
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 ===
|
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 ===
|
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
|
-
'
|
421
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
409
|
+
if (!this.pcManager) {
|
464
410
|
return;
|
465
411
|
}
|
466
412
|
log.trace('got ICE candidate from peer', { candidate, target });
|
467
|
-
|
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.
|
418
|
+
if (!this.pcManager) {
|
477
419
|
return;
|
478
420
|
}
|
479
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
739
|
+
if (!this.pcManager) {
|
805
740
|
throw new UnexpectedConnectionState('publisher is closed');
|
806
741
|
}
|
807
|
-
return this.
|
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.
|
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.
|
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.
|
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.
|
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.
|
1042
|
-
|
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
|
-
|
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
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
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
|
1102
|
-
|
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
|
-
|
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
|
1173
|
-
this.publisher
|
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
|
-
|
1208
|
-
if (!this.primaryTransport) {
|
1095
|
+
if (!this.pcManager) {
|
1209
1096
|
return false;
|
1210
1097
|
}
|
1211
|
-
|
1212
|
-
|
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.
|
1243
|
-
reject(new NegotiationError('
|
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.
|
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
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
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
|
-
|
1290
|
-
|
1291
|
-
|
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);
|