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