livekit-client 1.14.4 → 1.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  4. package/dist/livekit-client.esm.mjs +5488 -5230
  5. package/dist/livekit-client.esm.mjs.map +1 -1
  6. package/dist/livekit-client.umd.js +1 -1
  7. package/dist/livekit-client.umd.js.map +1 -1
  8. package/dist/src/api/SignalClient.d.ts.map +1 -1
  9. package/dist/src/room/PCTransport.d.ts +10 -4
  10. package/dist/src/room/PCTransport.d.ts.map +1 -1
  11. package/dist/src/room/PCTransportManager.d.ts +51 -0
  12. package/dist/src/room/PCTransportManager.d.ts.map +1 -0
  13. package/dist/src/room/RTCEngine.d.ts +8 -5
  14. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/src/room/Room.d.ts +9 -0
  16. package/dist/src/room/Room.d.ts.map +1 -1
  17. package/dist/src/room/events.d.ts +10 -0
  18. package/dist/src/room/events.d.ts.map +1 -1
  19. package/dist/src/room/participant/LocalParticipant.d.ts +0 -5
  20. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  21. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  22. package/dist/src/room/track/Track.d.ts +6 -2
  23. package/dist/src/room/track/Track.d.ts.map +1 -1
  24. package/dist/src/room/track/options.d.ts +2 -0
  25. package/dist/src/room/track/options.d.ts.map +1 -1
  26. package/dist/src/room/track/utils.d.ts +3 -0
  27. package/dist/src/room/track/utils.d.ts.map +1 -1
  28. package/dist/src/room/utils.d.ts.map +1 -1
  29. package/dist/src/test/mocks.d.ts +1 -1
  30. package/dist/src/test/mocks.d.ts.map +1 -1
  31. package/dist/ts4.2/src/room/PCTransport.d.ts +10 -4
  32. package/dist/ts4.2/src/room/PCTransportManager.d.ts +51 -0
  33. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -5
  34. package/dist/ts4.2/src/room/Room.d.ts +9 -0
  35. package/dist/ts4.2/src/room/events.d.ts +10 -0
  36. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +0 -5
  37. package/dist/ts4.2/src/room/track/Track.d.ts +6 -2
  38. package/dist/ts4.2/src/room/track/options.d.ts +2 -0
  39. package/dist/ts4.2/src/room/track/utils.d.ts +3 -0
  40. package/dist/ts4.2/src/test/mocks.d.ts +1 -1
  41. package/package.json +20 -19
  42. package/src/api/SignalClient.ts +7 -1
  43. package/src/connectionHelper/checks/webrtc.ts +2 -2
  44. package/src/room/PCTransport.ts +66 -29
  45. package/src/room/PCTransportManager.ts +336 -0
  46. package/src/room/RTCEngine.ts +178 -246
  47. package/src/room/Room.ts +49 -46
  48. package/src/room/defaults.ts +1 -1
  49. package/src/room/events.ts +11 -0
  50. package/src/room/participant/LocalParticipant.ts +9 -51
  51. package/src/room/track/LocalTrack.ts +2 -0
  52. package/src/room/track/Track.ts +30 -9
  53. package/src/room/track/options.ts +2 -0
  54. package/src/room/track/utils.ts +19 -0
  55. package/src/room/utils.ts +2 -1
  56. 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
+ }