livekit-client 1.15.0 → 1.15.2
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.esm.mjs +5488 -5235
- 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/room/PCTransport.d.ts +9 -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/Track.d.ts +6 -2
- package/dist/src/room/track/Track.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/ts4.2/src/room/PCTransport.d.ts +9 -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/utils.d.ts +3 -0
- package/package.json +1 -1
- package/src/connectionHelper/checks/webrtc.ts +2 -2
- package/src/room/PCTransport.ts +62 -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/events.ts +11 -0
- package/src/room/participant/LocalParticipant.ts +15 -52
- package/src/room/track/Track.ts +30 -9
- package/src/room/track/utils.ts +19 -0
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);
|