livekit-client 1.15.0 → 1.15.2
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/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
@@ -215,6 +215,12 @@ export declare enum RoomEvent {
|
|
215
215
|
* `Room.canPlaybackAudio` will indicate if audio playback is permitted.
|
216
216
|
*/
|
217
217
|
AudioPlaybackStatusChanged = "audioPlaybackChanged",
|
218
|
+
/**
|
219
|
+
* LiveKit will attempt to autoplay all video tracks when you attach them to
|
220
|
+
* a video element. However, if that fails, we'll notify you via VideoPlaybackStatusChanged.
|
221
|
+
* Calling `room.startVideo()` in a user gesture event handler will resume the video playback.
|
222
|
+
*/
|
223
|
+
VideoPlaybackStatusChanged = "videoPlaybackChanged",
|
218
224
|
/**
|
219
225
|
* When we have encountered an error while attempting to create a track.
|
220
226
|
* The errors take place in getUserMedia().
|
@@ -449,6 +455,10 @@ export declare enum TrackEvent {
|
|
449
455
|
/** @internal */
|
450
456
|
VideoDimensionsChanged = "videoDimensionsChanged",
|
451
457
|
/** @internal */
|
458
|
+
VideoPlaybackStarted = "videoPlaybackStarted",
|
459
|
+
/** @internal */
|
460
|
+
VideoPlaybackFailed = "videoPlaybackFailed",
|
461
|
+
/** @internal */
|
452
462
|
ElementAttached = "elementAttached",
|
453
463
|
/** @internal */
|
454
464
|
ElementDetached = "elementDetached",
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import type { InternalRoomOptions } from '../../options';
|
2
2
|
import { DataPacket_Kind, ParticipantInfo, ParticipantPermission } from '../../proto/livekit_models_pb';
|
3
|
-
import { DataChannelInfo, TrackPublishedResponse } from '../../proto/livekit_rtc_pb';
|
4
3
|
import type RTCEngine from '../RTCEngine';
|
5
4
|
import LocalTrack from '../track/LocalTrack';
|
6
5
|
import LocalTrackPublication from '../track/LocalTrackPublication';
|
@@ -174,9 +173,5 @@ export default class LocalParticipant extends Participant {
|
|
174
173
|
private handleLocalTrackUnpublished;
|
175
174
|
private handleTrackEnded;
|
176
175
|
private getPublicationForTrack;
|
177
|
-
/** @internal */
|
178
|
-
publishedTracksInfo(): TrackPublishedResponse[];
|
179
|
-
/** @internal */
|
180
|
-
dataChannelsInfo(): DataChannelInfo[];
|
181
176
|
}
|
182
177
|
//# sourceMappingURL=LocalParticipant.d.ts.map
|
@@ -64,8 +64,10 @@ export declare abstract class Track extends Track_base {
|
|
64
64
|
protected handleAppVisibilityChanged(): Promise<void>;
|
65
65
|
protected addAppVisibilityListener(): void;
|
66
66
|
protected removeAppVisibilityListener(): void;
|
67
|
+
private handleElementSuspended;
|
68
|
+
private handleElementPlay;
|
69
|
+
private debouncedPlaybackStateChange;
|
67
70
|
}
|
68
|
-
/** @internal */
|
69
71
|
export declare function attachToElement(track: MediaStreamTrack, element: HTMLMediaElement): void;
|
70
72
|
/** @internal */
|
71
73
|
export declare function detachTrack(track: MediaStreamTrack, element: HTMLMediaElement): void;
|
@@ -112,10 +114,12 @@ export type TrackEventCallbacks = {
|
|
112
114
|
updateSettings: () => void;
|
113
115
|
updateSubscription: () => void;
|
114
116
|
audioPlaybackStarted: () => void;
|
115
|
-
audioPlaybackFailed: (error
|
117
|
+
audioPlaybackFailed: (error?: Error) => void;
|
116
118
|
audioSilenceDetected: () => void;
|
117
119
|
visibilityChanged: (visible: boolean, track?: any) => void;
|
118
120
|
videoDimensionsChanged: (dimensions: Track.Dimensions, track?: any) => void;
|
121
|
+
videoPlaybackStarted: () => void;
|
122
|
+
videoPlaybackFailed: (error?: Error) => void;
|
119
123
|
elementAttached: (element: HTMLMediaElement) => void;
|
120
124
|
elementDetached: (element: HTMLMediaElement) => void;
|
121
125
|
upstreamPaused: (track: any) => void;
|
@@ -1,4 +1,6 @@
|
|
1
|
+
import { TrackPublishedResponse } from '../../proto/livekit_rtc_pb';
|
1
2
|
import { Track } from './Track';
|
3
|
+
import type { TrackPublication } from './TrackPublication';
|
2
4
|
import type { AudioCaptureOptions, CreateLocalTracksOptions, ScreenShareCaptureOptions, VideoCaptureOptions } from './options';
|
3
5
|
import type { AudioTrack } from './types';
|
4
6
|
export declare function mergeDefaultOptions(options?: CreateLocalTracksOptions, audioDefaults?: AudioCaptureOptions, videoDefaults?: VideoCaptureOptions): CreateLocalTracksOptions;
|
@@ -25,4 +27,5 @@ export declare function sourceToKind(source: Track.Source): MediaDeviceKind | un
|
|
25
27
|
*/
|
26
28
|
export declare function screenCaptureToDisplayMediaStreamOptions(options: ScreenShareCaptureOptions): DisplayMediaStreamOptions;
|
27
29
|
export declare function mimeTypeToVideoCodecString(mimeType: string): "vp8" | "h264" | "vp9" | "av1";
|
30
|
+
export declare function getTrackPublicationInfo<T extends TrackPublication>(tracks: T[]): TrackPublishedResponse[];
|
28
31
|
//# sourceMappingURL=utils.d.ts.map
|
package/package.json
CHANGED
@@ -38,8 +38,8 @@ export class WebRTCCheck extends Checker {
|
|
38
38
|
}
|
39
39
|
};
|
40
40
|
|
41
|
-
if (this.room.engine.
|
42
|
-
this.room.engine.subscriber.onIceCandidateError = (ev) => {
|
41
|
+
if (this.room.engine.pcManager) {
|
42
|
+
this.room.engine.pcManager.subscriber.onIceCandidateError = (ev) => {
|
43
43
|
if (ev instanceof RTCPeerConnectionIceErrorEvent) {
|
44
44
|
this.appendWarning(
|
45
45
|
`error with ICE candidate: ${ev.errorCode} ${ev.errorText} ${ev.url}`,
|
package/src/room/PCTransport.ts
CHANGED
@@ -33,10 +33,16 @@ export default class PCTransport extends EventEmitter {
|
|
33
33
|
private _pc: RTCPeerConnection | null;
|
34
34
|
|
35
35
|
private get pc() {
|
36
|
-
if (this._pc)
|
37
|
-
|
36
|
+
if (!this._pc) {
|
37
|
+
this._pc = this.createPC();
|
38
|
+
}
|
39
|
+
return this._pc;
|
38
40
|
}
|
39
41
|
|
42
|
+
private config?: RTCConfiguration;
|
43
|
+
|
44
|
+
private mediaConstraints: Record<string, unknown>;
|
45
|
+
|
40
46
|
pendingCandidates: RTCIceCandidateInit[] = [];
|
41
47
|
|
42
48
|
restartingIce: boolean = false;
|
@@ -57,32 +63,53 @@ export default class PCTransport extends EventEmitter {
|
|
57
63
|
|
58
64
|
onConnectionStateChange?: (state: RTCPeerConnectionState) => void;
|
59
65
|
|
66
|
+
onIceConnectionStateChange?: (state: RTCIceConnectionState) => void;
|
67
|
+
|
68
|
+
onSignalingStatechange?: (state: RTCSignalingState) => void;
|
69
|
+
|
60
70
|
onDataChannel?: (ev: RTCDataChannelEvent) => void;
|
61
71
|
|
62
72
|
onTrack?: (ev: RTCTrackEvent) => void;
|
63
73
|
|
64
74
|
constructor(config?: RTCConfiguration, mediaConstraints: Record<string, unknown> = {}) {
|
65
75
|
super();
|
66
|
-
this.
|
76
|
+
this.config = config;
|
77
|
+
this.mediaConstraints = mediaConstraints;
|
78
|
+
this._pc = this.createPC();
|
79
|
+
}
|
80
|
+
|
81
|
+
private createPC() {
|
82
|
+
const pc = isChromiumBased()
|
67
83
|
? // @ts-expect-error chrome allows additional media constraints to be passed into the RTCPeerConnection constructor
|
68
|
-
new RTCPeerConnection(config, mediaConstraints)
|
69
|
-
: new RTCPeerConnection(config);
|
70
|
-
|
84
|
+
new RTCPeerConnection(this.config, this.mediaConstraints)
|
85
|
+
: new RTCPeerConnection(this.config);
|
86
|
+
|
87
|
+
pc.onicecandidate = (ev) => {
|
71
88
|
if (!ev.candidate) return;
|
72
89
|
this.onIceCandidate?.(ev.candidate);
|
73
90
|
};
|
74
|
-
|
91
|
+
pc.onicecandidateerror = (ev) => {
|
75
92
|
this.onIceCandidateError?.(ev);
|
76
93
|
};
|
77
|
-
|
78
|
-
|
94
|
+
|
95
|
+
pc.oniceconnectionstatechange = () => {
|
96
|
+
this.onIceConnectionStateChange?.(pc.iceConnectionState);
|
79
97
|
};
|
80
|
-
|
98
|
+
|
99
|
+
pc.onsignalingstatechange = () => {
|
100
|
+
this.onSignalingStatechange?.(pc.signalingState);
|
101
|
+
};
|
102
|
+
|
103
|
+
pc.onconnectionstatechange = () => {
|
104
|
+
this.onConnectionStateChange?.(pc.connectionState);
|
105
|
+
};
|
106
|
+
pc.ondatachannel = (ev) => {
|
81
107
|
this.onDataChannel?.(ev);
|
82
108
|
};
|
83
|
-
|
109
|
+
pc.ontrack = (ev) => {
|
84
110
|
this.onTrack?.(ev);
|
85
111
|
};
|
112
|
+
return pc;
|
86
113
|
}
|
87
114
|
|
88
115
|
get isICEConnected(): boolean {
|
@@ -168,7 +195,7 @@ export default class PCTransport extends EventEmitter {
|
|
168
195
|
|
169
196
|
if (this.renegotiate) {
|
170
197
|
this.renegotiate = false;
|
171
|
-
this.createAndSendOffer();
|
198
|
+
await this.createAndSendOffer();
|
172
199
|
} else if (sd.type === 'answer') {
|
173
200
|
this.emit(PCEvents.NegotiationComplete);
|
174
201
|
if (sd.sdp) {
|
@@ -183,10 +210,10 @@ export default class PCTransport extends EventEmitter {
|
|
183
210
|
}
|
184
211
|
|
185
212
|
// debounced negotiate interface
|
186
|
-
negotiate = debounce((onError?: (e: Error) => void) => {
|
213
|
+
negotiate = debounce(async (onError?: (e: Error) => void) => {
|
187
214
|
this.emit(PCEvents.NegotiationStarted);
|
188
215
|
try {
|
189
|
-
this.createAndSendOffer();
|
216
|
+
await this.createAndSendOffer();
|
190
217
|
} catch (e) {
|
191
218
|
if (onError) {
|
192
219
|
onError(e as Error);
|
@@ -209,11 +236,11 @@ export default class PCTransport extends EventEmitter {
|
|
209
236
|
if (this._pc && this._pc.signalingState === 'have-local-offer') {
|
210
237
|
// we're waiting for the peer to accept our offer, so we'll just wait
|
211
238
|
// the only exception to this is when ICE restart is needed
|
212
|
-
const currentSD = this.
|
239
|
+
const currentSD = this._pc.remoteDescription;
|
213
240
|
if (options?.iceRestart && currentSD) {
|
214
241
|
// TODO: handle when ICE restart is needed but we don't have a remote description
|
215
242
|
// the best thing to do is to recreate the peerconnection
|
216
|
-
await this.
|
243
|
+
await this._pc.setRemoteDescription(currentSD);
|
217
244
|
} else {
|
218
245
|
this.renegotiate = true;
|
219
246
|
return;
|
@@ -307,7 +334,10 @@ export default class PCTransport extends EventEmitter {
|
|
307
334
|
}
|
308
335
|
|
309
336
|
addTrack(track: MediaStreamTrack) {
|
310
|
-
|
337
|
+
if (!this._pc) {
|
338
|
+
throw new UnexpectedConnectionState('PC closed, cannot add track');
|
339
|
+
}
|
340
|
+
return this._pc.addTrack(track);
|
311
341
|
}
|
312
342
|
|
313
343
|
setTrackCodecBitrate(info: TrackBitrateInfo) {
|
@@ -315,43 +345,46 @@ export default class PCTransport extends EventEmitter {
|
|
315
345
|
}
|
316
346
|
|
317
347
|
setConfiguration(rtcConfig: RTCConfiguration) {
|
318
|
-
|
348
|
+
if (!this._pc) {
|
349
|
+
throw new UnexpectedConnectionState('PC closed, cannot configure');
|
350
|
+
}
|
351
|
+
return this._pc?.setConfiguration(rtcConfig);
|
319
352
|
}
|
320
353
|
|
321
354
|
canRemoveTrack(): boolean {
|
322
|
-
return !!this.
|
355
|
+
return !!this._pc?.removeTrack;
|
323
356
|
}
|
324
357
|
|
325
358
|
removeTrack(sender: RTCRtpSender) {
|
326
|
-
return this.
|
359
|
+
return this._pc?.removeTrack(sender);
|
327
360
|
}
|
328
361
|
|
329
362
|
getConnectionState() {
|
330
|
-
return this.
|
363
|
+
return this._pc?.connectionState ?? 'closed';
|
331
364
|
}
|
332
365
|
|
333
366
|
getICEConnectionState() {
|
334
|
-
return this.
|
367
|
+
return this._pc?.iceConnectionState ?? 'closed';
|
335
368
|
}
|
336
369
|
|
337
370
|
getSignallingState() {
|
338
|
-
return this.
|
371
|
+
return this._pc?.signalingState ?? 'closed';
|
339
372
|
}
|
340
373
|
|
341
374
|
getTransceivers() {
|
342
|
-
return this.
|
375
|
+
return this._pc?.getTransceivers() ?? [];
|
343
376
|
}
|
344
377
|
|
345
378
|
getSenders() {
|
346
|
-
return this.
|
379
|
+
return this._pc?.getSenders() ?? [];
|
347
380
|
}
|
348
381
|
|
349
382
|
getLocalDescription() {
|
350
|
-
return this.
|
383
|
+
return this._pc?.localDescription;
|
351
384
|
}
|
352
385
|
|
353
386
|
getRemoteDescription() {
|
354
|
-
return this.pc
|
387
|
+
return this.pc?.remoteDescription;
|
355
388
|
}
|
356
389
|
|
357
390
|
getStats() {
|
@@ -395,7 +428,7 @@ export default class PCTransport extends EventEmitter {
|
|
395
428
|
return candidates.get(selectedID);
|
396
429
|
}
|
397
430
|
|
398
|
-
close() {
|
431
|
+
close = () => {
|
399
432
|
if (!this._pc) {
|
400
433
|
return;
|
401
434
|
}
|
@@ -412,7 +445,7 @@ export default class PCTransport extends EventEmitter {
|
|
412
445
|
this._pc.onconnectionstatechange = null;
|
413
446
|
this._pc.oniceconnectionstatechange = null;
|
414
447
|
this._pc = null;
|
415
|
-
}
|
448
|
+
};
|
416
449
|
|
417
450
|
private async setMungedSDP(sd: RTCSessionDescriptionInit, munged?: string, remote?: boolean) {
|
418
451
|
if (munged) {
|
@@ -0,0 +1,336 @@
|
|
1
|
+
import log from '../logger';
|
2
|
+
import { SignalTarget } from '../proto/livekit_rtc_pb';
|
3
|
+
import PCTransport, { PCEvents } from './PCTransport';
|
4
|
+
import { roomConnectOptionDefaults } from './defaults';
|
5
|
+
import { ConnectionError, ConnectionErrorReason } from './errors';
|
6
|
+
import CriticalTimers from './timers';
|
7
|
+
import { Mutex, sleep } from './utils';
|
8
|
+
|
9
|
+
export enum PCTransportState {
|
10
|
+
NEW,
|
11
|
+
CONNECTING,
|
12
|
+
CONNECTED,
|
13
|
+
FAILED,
|
14
|
+
CLOSING,
|
15
|
+
CLOSED,
|
16
|
+
}
|
17
|
+
|
18
|
+
export class PCTransportManager {
|
19
|
+
public publisher: PCTransport;
|
20
|
+
|
21
|
+
public subscriber: PCTransport;
|
22
|
+
|
23
|
+
public peerConnectionTimeout: number = roomConnectOptionDefaults.peerConnectionTimeout;
|
24
|
+
|
25
|
+
public get needsPublisher() {
|
26
|
+
return this.isPublisherConnectionRequired;
|
27
|
+
}
|
28
|
+
|
29
|
+
public get needsSubscriber() {
|
30
|
+
return this.isSubscriberConnectionRequired;
|
31
|
+
}
|
32
|
+
|
33
|
+
public get currentState() {
|
34
|
+
return this.state;
|
35
|
+
}
|
36
|
+
|
37
|
+
public onStateChange?: (
|
38
|
+
state: PCTransportState,
|
39
|
+
pubState: RTCPeerConnectionState,
|
40
|
+
subState: RTCPeerConnectionState,
|
41
|
+
) => void;
|
42
|
+
|
43
|
+
public onIceCandidate?: (ev: RTCIceCandidate, target: SignalTarget) => void;
|
44
|
+
|
45
|
+
public onDataChannel?: (ev: RTCDataChannelEvent) => void;
|
46
|
+
|
47
|
+
public onTrack?: (ev: RTCTrackEvent) => void;
|
48
|
+
|
49
|
+
public onPublisherOffer?: (offer: RTCSessionDescriptionInit) => void;
|
50
|
+
|
51
|
+
private isPublisherConnectionRequired: boolean;
|
52
|
+
|
53
|
+
private isSubscriberConnectionRequired: boolean;
|
54
|
+
|
55
|
+
private state: PCTransportState;
|
56
|
+
|
57
|
+
private connectionLock: Mutex;
|
58
|
+
|
59
|
+
constructor(rtcConfig: RTCConfiguration, subscriberPrimary: boolean) {
|
60
|
+
this.isPublisherConnectionRequired = !subscriberPrimary;
|
61
|
+
this.isSubscriberConnectionRequired = subscriberPrimary;
|
62
|
+
const googConstraints = { optional: [{ googDscp: true }] };
|
63
|
+
this.publisher = new PCTransport(rtcConfig, googConstraints);
|
64
|
+
this.subscriber = new PCTransport(rtcConfig);
|
65
|
+
|
66
|
+
this.publisher.onConnectionStateChange = this.updateState;
|
67
|
+
this.subscriber.onConnectionStateChange = this.updateState;
|
68
|
+
this.publisher.onIceConnectionStateChange = this.updateState;
|
69
|
+
this.subscriber.onIceConnectionStateChange = this.updateState;
|
70
|
+
this.publisher.onSignalingStatechange = this.updateState;
|
71
|
+
this.subscriber.onSignalingStatechange = this.updateState;
|
72
|
+
this.publisher.onIceCandidate = (candidate) => {
|
73
|
+
this.onIceCandidate?.(candidate, SignalTarget.PUBLISHER);
|
74
|
+
};
|
75
|
+
this.subscriber.onIceCandidate = (candidate) => {
|
76
|
+
this.onIceCandidate?.(candidate, SignalTarget.SUBSCRIBER);
|
77
|
+
};
|
78
|
+
// in subscriber primary mode, server side opens sub data channels.
|
79
|
+
this.subscriber.onDataChannel = (ev) => {
|
80
|
+
this.onDataChannel?.(ev);
|
81
|
+
};
|
82
|
+
this.subscriber.onTrack = (ev) => {
|
83
|
+
this.onTrack?.(ev);
|
84
|
+
};
|
85
|
+
this.publisher.onOffer = (offer) => {
|
86
|
+
this.onPublisherOffer?.(offer);
|
87
|
+
};
|
88
|
+
|
89
|
+
this.state = PCTransportState.NEW;
|
90
|
+
|
91
|
+
this.connectionLock = new Mutex();
|
92
|
+
}
|
93
|
+
|
94
|
+
requirePublisher(require = true) {
|
95
|
+
this.isPublisherConnectionRequired = require;
|
96
|
+
this.updateState();
|
97
|
+
}
|
98
|
+
|
99
|
+
requireSubscriber(require = true) {
|
100
|
+
this.isSubscriberConnectionRequired = require;
|
101
|
+
this.updateState();
|
102
|
+
}
|
103
|
+
|
104
|
+
createAndSendPublisherOffer(options?: RTCOfferOptions) {
|
105
|
+
return this.publisher.createAndSendOffer(options);
|
106
|
+
}
|
107
|
+
|
108
|
+
setPublisherAnswer(sd: RTCSessionDescriptionInit) {
|
109
|
+
return this.publisher.setRemoteDescription(sd);
|
110
|
+
}
|
111
|
+
|
112
|
+
removeTrack(sender: RTCRtpSender) {
|
113
|
+
return this.publisher.removeTrack(sender);
|
114
|
+
}
|
115
|
+
|
116
|
+
async close() {
|
117
|
+
if (this.publisher && this.publisher.getSignallingState() !== 'closed') {
|
118
|
+
const publisher = this.publisher;
|
119
|
+
for (const sender of publisher.getSenders()) {
|
120
|
+
try {
|
121
|
+
// TODO: react-native-webrtc doesn't have removeTrack yet.
|
122
|
+
if (publisher.canRemoveTrack()) {
|
123
|
+
publisher.removeTrack(sender);
|
124
|
+
}
|
125
|
+
} catch (e) {
|
126
|
+
log.warn('could not removeTrack', { error: e });
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
await Promise.all([this.publisher.close(), this.subscriber.close()]);
|
131
|
+
this.updateState();
|
132
|
+
}
|
133
|
+
|
134
|
+
async triggerIceRestart() {
|
135
|
+
this.subscriber.restartingIce = true;
|
136
|
+
// only restart publisher if it's needed
|
137
|
+
if (this.needsPublisher) {
|
138
|
+
await this.createAndSendPublisherOffer({ iceRestart: true });
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
async addIceCandidate(candidate: RTCIceCandidateInit, target: SignalTarget) {
|
143
|
+
if (target === SignalTarget.PUBLISHER) {
|
144
|
+
await this.publisher.addIceCandidate(candidate);
|
145
|
+
} else {
|
146
|
+
await this.subscriber.addIceCandidate(candidate);
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit) {
|
151
|
+
log.debug('received server offer', {
|
152
|
+
RTCSdpType: sd.type,
|
153
|
+
signalingState: this.subscriber.getSignallingState().toString(),
|
154
|
+
});
|
155
|
+
await this.subscriber.setRemoteDescription(sd);
|
156
|
+
|
157
|
+
// answer the offer
|
158
|
+
const answer = await this.subscriber.createAndSetAnswer();
|
159
|
+
return answer;
|
160
|
+
}
|
161
|
+
|
162
|
+
updateConfiguration(config: RTCConfiguration, iceRestart?: boolean) {
|
163
|
+
this.publisher.setConfiguration(config);
|
164
|
+
this.subscriber.setConfiguration(config);
|
165
|
+
if (iceRestart) {
|
166
|
+
this.triggerIceRestart();
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
async ensurePCTransportConnection(abortController?: AbortController, timeout?: number) {
|
171
|
+
const unlock = await this.connectionLock.lock();
|
172
|
+
try {
|
173
|
+
if (
|
174
|
+
this.isPublisherConnectionRequired &&
|
175
|
+
this.publisher.getConnectionState() !== 'connected' &&
|
176
|
+
this.publisher.getConnectionState() !== 'connecting'
|
177
|
+
) {
|
178
|
+
log.debug('negotiation required, start negotiating');
|
179
|
+
this.publisher.negotiate();
|
180
|
+
}
|
181
|
+
await Promise.all(
|
182
|
+
this.requiredTransports?.map((transport) =>
|
183
|
+
this.ensureTransportConnected(transport, abortController, timeout),
|
184
|
+
),
|
185
|
+
);
|
186
|
+
} finally {
|
187
|
+
unlock();
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
async negotiate(abortController: AbortController) {
|
192
|
+
return new Promise<void>(async (resolve, reject) => {
|
193
|
+
const negotiationTimeout = setTimeout(() => {
|
194
|
+
reject('negotiation timed out');
|
195
|
+
}, this.peerConnectionTimeout);
|
196
|
+
|
197
|
+
const abortHandler = () => {
|
198
|
+
clearTimeout(negotiationTimeout);
|
199
|
+
reject('negotiation aborted');
|
200
|
+
};
|
201
|
+
|
202
|
+
abortController.signal.addEventListener('abort', abortHandler);
|
203
|
+
this.publisher.once(PCEvents.NegotiationStarted, () => {
|
204
|
+
if (abortController.signal.aborted) {
|
205
|
+
return;
|
206
|
+
}
|
207
|
+
this.publisher.once(PCEvents.NegotiationComplete, () => {
|
208
|
+
clearTimeout(negotiationTimeout);
|
209
|
+
resolve();
|
210
|
+
});
|
211
|
+
});
|
212
|
+
|
213
|
+
await this.publisher.negotiate((e) => {
|
214
|
+
clearTimeout(negotiationTimeout);
|
215
|
+
reject(e);
|
216
|
+
});
|
217
|
+
});
|
218
|
+
}
|
219
|
+
|
220
|
+
addPublisherTransceiver(track: MediaStreamTrack, transceiverInit: RTCRtpTransceiverInit) {
|
221
|
+
return this.publisher.addTransceiver(track, transceiverInit);
|
222
|
+
}
|
223
|
+
|
224
|
+
addPublisherTrack(track: MediaStreamTrack) {
|
225
|
+
return this.publisher.addTrack(track);
|
226
|
+
}
|
227
|
+
|
228
|
+
createPublisherDataChannel(label: string, dataChannelDict: RTCDataChannelInit) {
|
229
|
+
return this.publisher.createDataChannel(label, dataChannelDict);
|
230
|
+
}
|
231
|
+
|
232
|
+
/**
|
233
|
+
* Returns the first required transport's address if no explicit target is specified
|
234
|
+
*/
|
235
|
+
getConnectedAddress(target?: SignalTarget) {
|
236
|
+
if (target === SignalTarget.PUBLISHER) {
|
237
|
+
return this.publisher.getConnectedAddress();
|
238
|
+
} else if (target === SignalTarget.SUBSCRIBER) {
|
239
|
+
return this.publisher.getConnectedAddress();
|
240
|
+
}
|
241
|
+
return this.requiredTransports[0].getConnectedAddress();
|
242
|
+
}
|
243
|
+
|
244
|
+
private get requiredTransports() {
|
245
|
+
const transports: PCTransport[] = [];
|
246
|
+
if (this.isPublisherConnectionRequired) {
|
247
|
+
transports.push(this.publisher);
|
248
|
+
}
|
249
|
+
if (this.isSubscriberConnectionRequired) {
|
250
|
+
transports.push(this.subscriber);
|
251
|
+
}
|
252
|
+
return transports;
|
253
|
+
}
|
254
|
+
|
255
|
+
private updateState = () => {
|
256
|
+
const previousState = this.state;
|
257
|
+
|
258
|
+
const connectionStates = this.requiredTransports.map((tr) => tr.getConnectionState());
|
259
|
+
if (connectionStates.every((st) => st === 'connected')) {
|
260
|
+
this.state = PCTransportState.CONNECTED;
|
261
|
+
} else if (connectionStates.some((st) => st === 'failed')) {
|
262
|
+
this.state = PCTransportState.FAILED;
|
263
|
+
} else if (connectionStates.some((st) => st === 'connecting')) {
|
264
|
+
this.state = PCTransportState.CONNECTING;
|
265
|
+
} else if (connectionStates.every((st) => st === 'closed')) {
|
266
|
+
this.state = PCTransportState.CLOSED;
|
267
|
+
} else if (connectionStates.some((st) => st === 'closed')) {
|
268
|
+
this.state = PCTransportState.CLOSING;
|
269
|
+
} else if (connectionStates.every((st) => st === 'new')) {
|
270
|
+
this.state = PCTransportState.NEW;
|
271
|
+
}
|
272
|
+
|
273
|
+
if (previousState !== this.state) {
|
274
|
+
log.debug(
|
275
|
+
`pc state change: from ${PCTransportState[previousState]} to ${
|
276
|
+
PCTransportState[this.state]
|
277
|
+
}`,
|
278
|
+
);
|
279
|
+
this.onStateChange?.(
|
280
|
+
this.state,
|
281
|
+
this.publisher.getConnectionState(),
|
282
|
+
this.subscriber.getConnectionState(),
|
283
|
+
);
|
284
|
+
}
|
285
|
+
};
|
286
|
+
|
287
|
+
private async ensureTransportConnected(
|
288
|
+
pcTransport: PCTransport,
|
289
|
+
abortController?: AbortController,
|
290
|
+
timeout: number = this.peerConnectionTimeout,
|
291
|
+
) {
|
292
|
+
const connectionState = pcTransport.getConnectionState();
|
293
|
+
if (connectionState === 'connected') {
|
294
|
+
return;
|
295
|
+
}
|
296
|
+
|
297
|
+
return new Promise<void>(async (resolve, reject) => {
|
298
|
+
const abortHandler = () => {
|
299
|
+
log.warn('abort transport connection');
|
300
|
+
CriticalTimers.clearTimeout(connectTimeout);
|
301
|
+
|
302
|
+
reject(
|
303
|
+
new ConnectionError(
|
304
|
+
'room connection has been cancelled',
|
305
|
+
ConnectionErrorReason.Cancelled,
|
306
|
+
),
|
307
|
+
);
|
308
|
+
};
|
309
|
+
if (abortController?.signal.aborted) {
|
310
|
+
abortHandler();
|
311
|
+
}
|
312
|
+
abortController?.signal.addEventListener('abort', abortHandler);
|
313
|
+
|
314
|
+
const connectTimeout = CriticalTimers.setTimeout(() => {
|
315
|
+
abortController?.signal.removeEventListener('abort', abortHandler);
|
316
|
+
reject(new ConnectionError('could not establish pc connection'));
|
317
|
+
}, timeout);
|
318
|
+
|
319
|
+
while (this.state !== PCTransportState.CONNECTED) {
|
320
|
+
await sleep(50); // FIXME we shouldn't rely on `sleep` in the connection paths, as it invokes `setTimeout` which can be drastically throttled by browser implementations
|
321
|
+
if (abortController?.signal.aborted) {
|
322
|
+
reject(
|
323
|
+
new ConnectionError(
|
324
|
+
'room connection has been cancelled',
|
325
|
+
ConnectionErrorReason.Cancelled,
|
326
|
+
),
|
327
|
+
);
|
328
|
+
return;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
CriticalTimers.clearTimeout(connectTimeout);
|
332
|
+
abortController?.signal.removeEventListener('abort', abortHandler);
|
333
|
+
resolve();
|
334
|
+
});
|
335
|
+
}
|
336
|
+
}
|