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
@@ -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
|
+
}
|