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.
Files changed (38) hide show
  1. package/dist/livekit-client.esm.mjs +5488 -5235
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/room/PCTransport.d.ts +9 -4
  6. package/dist/src/room/PCTransport.d.ts.map +1 -1
  7. package/dist/src/room/PCTransportManager.d.ts +51 -0
  8. package/dist/src/room/PCTransportManager.d.ts.map +1 -0
  9. package/dist/src/room/RTCEngine.d.ts +8 -5
  10. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  11. package/dist/src/room/Room.d.ts +9 -0
  12. package/dist/src/room/Room.d.ts.map +1 -1
  13. package/dist/src/room/events.d.ts +10 -0
  14. package/dist/src/room/events.d.ts.map +1 -1
  15. package/dist/src/room/participant/LocalParticipant.d.ts +0 -5
  16. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  17. package/dist/src/room/track/Track.d.ts +6 -2
  18. package/dist/src/room/track/Track.d.ts.map +1 -1
  19. package/dist/src/room/track/utils.d.ts +3 -0
  20. package/dist/src/room/track/utils.d.ts.map +1 -1
  21. package/dist/ts4.2/src/room/PCTransport.d.ts +9 -4
  22. package/dist/ts4.2/src/room/PCTransportManager.d.ts +51 -0
  23. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -5
  24. package/dist/ts4.2/src/room/Room.d.ts +9 -0
  25. package/dist/ts4.2/src/room/events.d.ts +10 -0
  26. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +0 -5
  27. package/dist/ts4.2/src/room/track/Track.d.ts +6 -2
  28. package/dist/ts4.2/src/room/track/utils.d.ts +3 -0
  29. package/package.json +1 -1
  30. package/src/connectionHelper/checks/webrtc.ts +2 -2
  31. package/src/room/PCTransport.ts +62 -29
  32. package/src/room/PCTransportManager.ts +336 -0
  33. package/src/room/RTCEngine.ts +178 -246
  34. package/src/room/Room.ts +49 -46
  35. package/src/room/events.ts +11 -0
  36. package/src/room/participant/LocalParticipant.ts +15 -52
  37. package/src/room/track/Track.ts +30 -9
  38. 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: Error) => void;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -38,8 +38,8 @@ export class WebRTCCheck extends Checker {
38
38
  }
39
39
  };
40
40
 
41
- if (this.room.engine.subscriber) {
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}`,
@@ -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) return this._pc;
37
- throw new UnexpectedConnectionState('Expected peer connection to be available');
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._pc = isChromiumBased()
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
- this._pc.onicecandidate = (ev) => {
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
- this._pc.onicecandidateerror = (ev) => {
91
+ pc.onicecandidateerror = (ev) => {
75
92
  this.onIceCandidateError?.(ev);
76
93
  };
77
- this._pc.onconnectionstatechange = () => {
78
- this.onConnectionStateChange?.(this._pc?.connectionState ?? 'closed');
94
+
95
+ pc.oniceconnectionstatechange = () => {
96
+ this.onIceConnectionStateChange?.(pc.iceConnectionState);
79
97
  };
80
- this._pc.ondatachannel = (ev) => {
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
- this._pc.ontrack = (ev) => {
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.pc.remoteDescription;
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.pc.setRemoteDescription(currentSD);
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
- return this.pc.addTrack(track);
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
- return this.pc.setConfiguration(rtcConfig);
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.pc.removeTrack;
355
+ return !!this._pc?.removeTrack;
323
356
  }
324
357
 
325
358
  removeTrack(sender: RTCRtpSender) {
326
- return this.pc.removeTrack(sender);
359
+ return this._pc?.removeTrack(sender);
327
360
  }
328
361
 
329
362
  getConnectionState() {
330
- return this.pc.connectionState;
363
+ return this._pc?.connectionState ?? 'closed';
331
364
  }
332
365
 
333
366
  getICEConnectionState() {
334
- return this.pc.iceConnectionState;
367
+ return this._pc?.iceConnectionState ?? 'closed';
335
368
  }
336
369
 
337
370
  getSignallingState() {
338
- return this.pc.signalingState;
371
+ return this._pc?.signalingState ?? 'closed';
339
372
  }
340
373
 
341
374
  getTransceivers() {
342
- return this.pc.getTransceivers();
375
+ return this._pc?.getTransceivers() ?? [];
343
376
  }
344
377
 
345
378
  getSenders() {
346
- return this.pc.getSenders();
379
+ return this._pc?.getSenders() ?? [];
347
380
  }
348
381
 
349
382
  getLocalDescription() {
350
- return this.pc.localDescription;
383
+ return this._pc?.localDescription;
351
384
  }
352
385
 
353
386
  getRemoteDescription() {
354
- return this.pc.remoteDescription;
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
+ }