livekit-client 1.2.3 → 1.2.6

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 (42) hide show
  1. package/dist/livekit-client.esm.mjs +1002 -778
  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/proto/google/protobuf/timestamp.d.ts +1 -1
  6. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  7. package/dist/src/proto/livekit_models.d.ts +34 -34
  8. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  9. package/dist/src/proto/livekit_rtc.d.ts +124 -124
  10. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  11. package/dist/src/room/DeviceManager.d.ts +1 -0
  12. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  13. package/dist/src/room/PCTransport.d.ts +4 -1
  14. package/dist/src/room/PCTransport.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +1 -0
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts +4 -1
  18. package/dist/src/room/Room.d.ts.map +1 -1
  19. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  20. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  21. package/dist/src/room/track/LocalAudioTrack.d.ts +0 -1
  22. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  23. package/dist/src/room/track/LocalVideoTrack.d.ts +1 -1
  24. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  26. package/dist/src/room/track/RemoteVideoTrack.d.ts +0 -2
  27. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  28. package/dist/src/room/track/create.d.ts.map +1 -1
  29. package/dist/src/room/utils.d.ts +1 -1
  30. package/dist/src/room/utils.d.ts.map +1 -1
  31. package/package.json +36 -34
  32. package/src/room/DeviceManager.ts +23 -1
  33. package/src/room/RTCEngine.ts +30 -6
  34. package/src/room/Room.ts +149 -152
  35. package/src/room/participant/LocalParticipant.ts +13 -3
  36. package/src/room/participant/RemoteParticipant.ts +14 -13
  37. package/src/room/track/LocalAudioTrack.ts +0 -2
  38. package/src/room/track/LocalVideoTrack.ts +3 -7
  39. package/src/room/track/RemoteTrackPublication.ts +1 -1
  40. package/src/room/track/RemoteVideoTrack.ts +0 -3
  41. package/src/room/track/create.ts +16 -1
  42. package/src/room/utils.ts +7 -5
package/src/room/Room.ts CHANGED
@@ -99,8 +99,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
99
99
  /** used for aborting pending connections to a LiveKit server */
100
100
  private abortController?: AbortController;
101
101
 
102
+ /** future holding client initiated connection attempt */
102
103
  private connectFuture?: Future<void>;
103
104
 
105
+ /** future holding sdk initiated reconnection attempt */
106
+ private reconnectFuture?: Future<void>;
107
+
104
108
  /**
105
109
  * Creates a new Room, the primary construct for a LiveKit session.
106
110
  * @param options
@@ -156,12 +160,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
156
160
  .on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate)
157
161
  .on(EngineEvent.DataPacketReceived, this.handleDataPacket)
158
162
  .on(EngineEvent.Resuming, () => {
163
+ if (!this.reconnectFuture) {
164
+ this.reconnectFuture = new Future();
165
+ }
159
166
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
160
167
  this.emit(RoomEvent.Reconnecting);
161
168
  }
162
169
  })
163
170
  .on(EngineEvent.Resumed, () => {
164
171
  this.setAndEmitConnectionState(ConnectionState.Connected);
172
+ this.reconnectFuture?.resolve();
173
+ this.reconnectFuture = undefined;
165
174
  this.emit(RoomEvent.Reconnected);
166
175
  this.updateSubscriptions();
167
176
  })
@@ -193,145 +202,144 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
193
202
  return DeviceManager.getInstance().getDevices(kind, requestPermissions);
194
203
  }
195
204
 
196
- connect = async (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
205
+ connect = (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
197
206
  if (this.state === ConnectionState.Connected) {
198
207
  // when the state is reconnecting or connected, this function returns immediately
199
208
  log.warn(`already connected to room ${this.name}`);
200
- return;
209
+ return Promise.resolve();
201
210
  }
202
211
 
203
212
  if (this.connectFuture) {
204
213
  return this.connectFuture.promise;
214
+ } else if (this.reconnectFuture) {
215
+ this.connectFuture = this.reconnectFuture;
216
+ return this.connectFuture.promise;
205
217
  }
206
- this.setAndEmitConnectionState(ConnectionState.Connecting);
218
+ const connectPromise = new Promise<void>(async (resolve, reject) => {
219
+ this.setAndEmitConnectionState(ConnectionState.Connecting);
220
+ if (!this.abortController || this.abortController.signal.aborted) {
221
+ this.abortController = new AbortController();
222
+ }
207
223
 
208
- if (!this.abortController || this.abortController.signal.aborted) {
209
- this.abortController = new AbortController();
210
- }
224
+ // recreate engine if previously disconnected
225
+ this.createEngine();
211
226
 
212
- // recreate engine if previously disconnected
213
- this.createEngine();
227
+ this.acquireAudioContext();
214
228
 
215
- this.acquireAudioContext();
229
+ if (opts?.rtcConfig) {
230
+ this.engine.rtcConfig = opts.rtcConfig;
231
+ }
216
232
 
217
- if (opts?.rtcConfig) {
218
- this.engine.rtcConfig = opts.rtcConfig;
219
- }
233
+ this.connOptions = opts;
234
+
235
+ try {
236
+ const joinResponse = await this.engine.join(
237
+ url,
238
+ token,
239
+ {
240
+ autoSubscribe: opts?.autoSubscribe,
241
+ publishOnly: opts?.publishOnly,
242
+ adaptiveStream:
243
+ typeof this.options?.adaptiveStream === 'object'
244
+ ? true
245
+ : this.options?.adaptiveStream,
246
+ },
247
+ this.abortController.signal,
248
+ );
249
+ log.debug(
250
+ `connected to Livekit Server version: ${joinResponse.serverVersion}, region: ${joinResponse.serverRegion}`,
251
+ );
220
252
 
221
- this.connOptions = opts;
253
+ if (!joinResponse.serverVersion) {
254
+ throw new UnsupportedServer('unknown server version');
255
+ }
222
256
 
223
- try {
224
- const joinResponse = await this.engine.join(
225
- url,
226
- token,
227
- {
228
- autoSubscribe: opts?.autoSubscribe,
229
- publishOnly: opts?.publishOnly,
230
- adaptiveStream:
231
- typeof this.options?.adaptiveStream === 'object' ? true : this.options?.adaptiveStream,
232
- },
233
- this.abortController.signal,
234
- );
235
- log.debug(
236
- `connected to Livekit Server version: ${joinResponse.serverVersion}, region: ${joinResponse.serverRegion}`,
237
- );
257
+ if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
258
+ log.debug('disabling dynacast due to server version');
259
+ // dynacast has a bug in 0.15.1, so we cannot use it then
260
+ this.options.dynacast = false;
261
+ }
238
262
 
239
- if (!joinResponse.serverVersion) {
240
- throw new UnsupportedServer('unknown server version');
241
- }
263
+ const pi = joinResponse.participant!;
264
+
265
+ this.localParticipant.sid = pi.sid;
266
+ this.localParticipant.identity = pi.identity;
267
+
268
+ this.localParticipant.updateInfo(pi);
269
+ // forward metadata changed for the local participant
270
+ this.localParticipant
271
+ .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
272
+ .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
273
+ .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
274
+ .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
275
+ .on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
276
+ .on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
277
+ .on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
278
+ .on(
279
+ ParticipantEvent.ParticipantPermissionsChanged,
280
+ this.onLocalParticipantPermissionsChanged,
281
+ );
242
282
 
243
- if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
244
- log.debug('disabling dynacast due to server version');
245
- // dynacast has a bug in 0.15.1, so we cannot use it then
246
- this.options.dynacast = false;
247
- }
283
+ // populate remote participants, these should not trigger new events
284
+ joinResponse.otherParticipants.forEach((info) => {
285
+ if (
286
+ info.sid !== this.localParticipant.sid &&
287
+ info.identity !== this.localParticipant.identity
288
+ ) {
289
+ this.getOrCreateParticipant(info.sid, info);
290
+ } else {
291
+ log.warn('received info to create local participant as remote participant', {
292
+ info,
293
+ localParticipant: this.localParticipant,
294
+ });
295
+ }
296
+ });
248
297
 
249
- const pi = joinResponse.participant!;
250
-
251
- this.localParticipant.sid = pi.sid;
252
- this.localParticipant.identity = pi.identity;
253
-
254
- this.localParticipant.updateInfo(pi);
255
- // forward metadata changed for the local participant
256
- this.localParticipant
257
- .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
258
- .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
259
- .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
260
- .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
261
- .on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
262
- .on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
263
- .on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
264
- .on(
265
- ParticipantEvent.ParticipantPermissionsChanged,
266
- this.onLocalParticipantPermissionsChanged,
267
- );
298
+ this.name = joinResponse.room!.name;
299
+ this.sid = joinResponse.room!.sid;
300
+ this.metadata = joinResponse.room!.metadata;
301
+ this.emit(RoomEvent.SignalConnected);
302
+ } catch (err) {
303
+ this.recreateEngine();
304
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
305
+ reject(new ConnectionError('could not establish signal connection'));
306
+ return;
307
+ }
268
308
 
269
- // populate remote participants, these should not trigger new events
270
- joinResponse.otherParticipants.forEach((info) => {
271
- if (
272
- info.sid !== this.localParticipant.sid &&
273
- info.identity !== this.localParticipant.identity
274
- ) {
275
- this.getOrCreateParticipant(info.sid, info);
276
- } else {
277
- log.warn('received info to create local participant as remote participant', {
278
- info,
279
- localParticipant: this.localParticipant,
280
- });
309
+ // don't return until ICE connected
310
+ const connectTimeout = setTimeout(() => {
311
+ // timeout
312
+ this.recreateEngine();
313
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
314
+ reject(new ConnectionError('could not connect PeerConnection after timeout'));
315
+ }, maxICEConnectTimeout);
316
+ const abortHandler = () => {
317
+ log.warn('closing engine');
318
+ clearTimeout(connectTimeout);
319
+ this.recreateEngine();
320
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
321
+ reject(new ConnectionError('room connection has been cancelled'));
322
+ };
323
+ if (this.abortController?.signal.aborted) {
324
+ abortHandler();
325
+ }
326
+ this.abortController?.signal.addEventListener('abort', abortHandler);
327
+
328
+ this.engine.once(EngineEvent.Connected, () => {
329
+ clearTimeout(connectTimeout);
330
+ this.abortController?.signal.removeEventListener('abort', abortHandler);
331
+ // also hook unload event
332
+ if (isWeb()) {
333
+ window.addEventListener('beforeunload', this.onBeforeUnload);
334
+ navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
281
335
  }
336
+ this.setAndEmitConnectionState(ConnectionState.Connected);
337
+ resolve();
282
338
  });
339
+ }).finally(() => (this.connectFuture = undefined));
340
+ this.connectFuture = new Future(connectPromise);
283
341
 
284
- this.name = joinResponse.room!.name;
285
- this.sid = joinResponse.room!.sid;
286
- this.metadata = joinResponse.room!.metadata;
287
- this.emit(RoomEvent.SignalConnected);
288
- } catch (err) {
289
- this.recreateEngine();
290
- this.setAndEmitConnectionState(
291
- ConnectionState.Disconnected,
292
- new ConnectionError('could not establish signal connection'),
293
- );
294
- throw err;
295
- }
296
-
297
- // don't return until ICE connected
298
- const connectTimeout = setTimeout(() => {
299
- // timeout
300
- this.recreateEngine();
301
- this.setAndEmitConnectionState(
302
- ConnectionState.Disconnected,
303
- new ConnectionError('could not connect PeerConnection after timeout'),
304
- );
305
- }, maxICEConnectTimeout);
306
- const abortHandler = () => {
307
- log.warn('closing engine');
308
- clearTimeout(connectTimeout);
309
- this.recreateEngine();
310
- this.setAndEmitConnectionState(
311
- ConnectionState.Disconnected,
312
- new ConnectionError('room connection has been cancelled'),
313
- );
314
- };
315
- if (this.abortController?.signal.aborted) {
316
- abortHandler();
317
- }
318
- this.abortController?.signal.addEventListener('abort', abortHandler);
319
-
320
- this.engine.once(EngineEvent.Connected, () => {
321
- clearTimeout(connectTimeout);
322
- this.abortController?.signal.removeEventListener('abort', abortHandler);
323
- // also hook unload event
324
- if (isWeb()) {
325
- window.addEventListener('beforeunload', this.onBeforeUnload);
326
- navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
327
- }
328
- this.setAndEmitConnectionState(ConnectionState.Connected);
329
- });
330
-
331
- if (this.connectFuture) {
332
- /** @ts-ignore */
333
- return this.connectFuture.promise;
334
- }
342
+ return this.connectFuture.promise;
335
343
  };
336
344
 
337
345
  /**
@@ -339,11 +347,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
339
347
  */
340
348
  disconnect = async (stopTracks = true) => {
341
349
  log.info('disconnect from room', { identity: this.localParticipant.identity });
342
- if (this.state === ConnectionState.Connecting) {
350
+ if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
343
351
  // try aborting pending connection attempt
344
352
  log.warn('abort connection attempt');
345
353
  this.abortController?.abort();
346
- return;
354
+ // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
355
+ this.connectFuture?.reject(new ConnectionError('Client initiated disconnect'));
356
+ this.connectFuture = undefined;
347
357
  }
348
358
  // send leave
349
359
  if (this.engine?.client.isConnected) {
@@ -353,7 +363,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
353
363
  if (this.engine) {
354
364
  this.engine.close();
355
365
  }
356
-
357
366
  this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
358
367
  /* @ts-ignore */
359
368
  this.engine = undefined;
@@ -532,8 +541,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
532
541
  // at that time, ICE connectivity has not been established so the track is not
533
542
  // technically subscribed.
534
543
  // We'll defer these events until when the room is connected or eventually disconnected.
535
- if (this.connectFuture) {
536
- this.connectFuture.promise.then(() => {
544
+ if (this.connectFuture || this.reconnectFuture) {
545
+ Promise.allSettled([this.connectFuture?.promise, this.reconnectFuture?.promise]).then(() => {
537
546
  this.onTrackAdded(mediaTrack, stream, receiver);
538
547
  });
539
548
  return;
@@ -571,6 +580,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
571
580
  }
572
581
 
573
582
  private handleRestarting = () => {
583
+ if (!this.reconnectFuture) {
584
+ this.reconnectFuture = new Future();
585
+ }
574
586
  // also unwind existing participants & existing subscriptions
575
587
  for (const p of this.participants.values()) {
576
588
  this.handleParticipantDisconnected(p.sid, p);
@@ -587,6 +599,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
587
599
  });
588
600
  this.setAndEmitConnectionState(ConnectionState.Connected);
589
601
  this.emit(RoomEvent.Reconnected);
602
+ this.reconnectFuture?.resolve();
603
+ this.reconnectFuture = undefined;
590
604
 
591
605
  // rehydrate participants
592
606
  if (joinResponse.participant) {
@@ -630,6 +644,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
630
644
  if (this.state === ConnectionState.Disconnected) {
631
645
  return;
632
646
  }
647
+ // reject potentially ongoing reconnection attempt
648
+ if (this.connectFuture === this.reconnectFuture) {
649
+ this.connectFuture?.reject(undefined);
650
+ this.connectFuture = undefined;
651
+ this.reconnectFuture = undefined;
652
+ }
653
+
633
654
  this.participants.forEach((p) => {
634
655
  p.tracks.forEach((pub) => {
635
656
  p.unpublishTrack(pub.trackSid);
@@ -704,10 +725,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
704
725
  // when it's disconnected, send updates
705
726
  if (info.state === ParticipantInfo_State.DISCONNECTED) {
706
727
  this.handleParticipantDisconnected(info.sid, remoteParticipant);
707
- } else if (isNewParticipant) {
708
- // fire connected event
709
- this.emitWhenConnected(RoomEvent.ParticipantConnected, remoteParticipant);
710
- } else {
728
+ } else if (!isNewParticipant) {
711
729
  // just update, no events
712
730
  remoteParticipant.updateInfo(info);
713
731
  }
@@ -907,6 +925,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
907
925
  this.participants.set(id, participant);
908
926
  if (info) {
909
927
  this.identityToSid.set(info.identity, info.sid);
928
+ // if we have valid info and the participant wasn't in the map before, we can assume the participant is new
929
+ // firing here to make sure that `ParticipantConnected` fires before the initial track events
930
+ this.emitWhenConnected(RoomEvent.ParticipantConnected, participant);
910
931
  }
911
932
 
912
933
  // also forward events
@@ -1030,35 +1051,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1030
1051
  }
1031
1052
  }
1032
1053
 
1033
- private setAndEmitConnectionState(state: ConnectionState, error?: Error): boolean {
1054
+ private setAndEmitConnectionState(state: ConnectionState): boolean {
1034
1055
  if (state === this.state) {
1035
1056
  // unchanged
1036
1057
  return false;
1037
1058
  }
1038
- switch (state) {
1039
- case ConnectionState.Connecting:
1040
- case ConnectionState.Reconnecting:
1041
- if (!this.connectFuture) {
1042
- // reuse existing connect future if possible
1043
- this.connectFuture = new Future<void>();
1044
- }
1045
- break;
1046
- case ConnectionState.Connected:
1047
- if (this.connectFuture) {
1048
- this.connectFuture.resolve();
1049
- this.connectFuture = undefined;
1050
- }
1051
- break;
1052
- case ConnectionState.Disconnected:
1053
- if (this.connectFuture) {
1054
- error ??= new Error('disconnected from Room');
1055
- this.connectFuture.reject(error);
1056
- this.connectFuture = undefined;
1057
- }
1058
- break;
1059
- default:
1060
- // nothing
1061
- }
1062
1059
  this.state = state;
1063
1060
  this.emit(RoomEvent.ConnectionStateChanged, this.state);
1064
1061
  return true;
@@ -722,10 +722,20 @@ export default class LocalParticipant extends Participant {
722
722
  track.sender
723
723
  ) {
724
724
  try {
725
- this.engine.publisher.pc.removeTrack(track.sender);
726
- this.engine.negotiate();
725
+ this.engine.removeTrack(track.sender);
726
+ if (track instanceof LocalVideoTrack) {
727
+ for (const [, trackInfo] of track.simulcastCodecs) {
728
+ if (trackInfo.sender) {
729
+ this.engine.removeTrack(trackInfo.sender);
730
+ trackInfo.sender = undefined;
731
+ }
732
+ }
733
+ track.simulcastCodecs.clear();
734
+ }
727
735
  } catch (e) {
728
- log.warn('failed to remove track', { error: e, method: 'unpublishTrack' });
736
+ log.warn('failed to unpublish track', { error: e, method: 'unpublishTrack' });
737
+ } finally {
738
+ this.engine.negotiate();
729
739
  }
730
740
  }
731
741
 
@@ -210,6 +210,20 @@ export default class RemoteParticipant extends Participant {
210
210
  publication = new RemoteTrackPublication(kind, ti.sid, ti.name);
211
211
  publication.updateInfo(ti);
212
212
  newTracks.set(ti.sid, publication);
213
+ const existingTrackOfSource = Array.from(this.tracks.values()).find(
214
+ (publishedTrack) => publishedTrack.source === publication?.source,
215
+ );
216
+ if (existingTrackOfSource) {
217
+ log.warn(
218
+ `received a second track publication for ${this.identity} with the same source: ${publication.source}`,
219
+ {
220
+ oldTrack: existingTrackOfSource,
221
+ newTrack: publication,
222
+ participant: this,
223
+ participantInfo: info,
224
+ },
225
+ );
226
+ }
213
227
  this.addTrackPublication(publication);
214
228
  } else {
215
229
  publication.updateInfo(ti);
@@ -220,19 +234,6 @@ export default class RemoteParticipant extends Participant {
220
234
  // always emit events for new publications, Room will not forward them unless it's ready
221
235
  newTracks.forEach((publication) => {
222
236
  this.emit(ParticipantEvent.TrackPublished, publication);
223
- const existingTrackOfSource = Array.from(this.tracks.values()).find(
224
- (publishedTrack) => publishedTrack.source === publication.source,
225
- );
226
- if (existingTrackOfSource) {
227
- log.warn(
228
- `received a second track publication for ${this.identity} with the same source: ${publication.source}`,
229
- {
230
- oldTrack: existingTrackOfSource,
231
- newTrack: publication,
232
- participant: this,
233
- },
234
- );
235
- }
236
237
  });
237
238
 
238
239
  // detect removed tracks
@@ -8,8 +8,6 @@ import { Track } from './Track';
8
8
  import { constraintsForOptions, detectSilence } from './utils';
9
9
 
10
10
  export default class LocalAudioTrack extends LocalTrack {
11
- sender?: RTCRtpSender;
12
-
13
11
  /** @internal */
14
12
  stopOnMute: boolean = false;
15
13
 
@@ -27,17 +27,15 @@ export class SimulcastTrackInfo {
27
27
  const refreshSubscribedCodecAfterNewCodec = 5000;
28
28
 
29
29
  export default class LocalVideoTrack extends LocalTrack {
30
- /* internal */
30
+ /* @internal */
31
31
  signalClient?: SignalClient;
32
32
 
33
33
  private prevStats?: Map<string, VideoSenderStats>;
34
34
 
35
35
  private encodings?: RTCRtpEncodingParameters[];
36
36
 
37
- private simulcastCodecs: Map<VideoCodec, SimulcastTrackInfo> = new Map<
38
- VideoCodec,
39
- SimulcastTrackInfo
40
- >();
37
+ /* @internal */
38
+ simulcastCodecs: Map<VideoCodec, SimulcastTrackInfo> = new Map<VideoCodec, SimulcastTrackInfo>();
41
39
 
42
40
  private subscribedCodecs?: SubscribedCodec[];
43
41
 
@@ -78,9 +76,7 @@ export default class LocalVideoTrack extends LocalTrack {
78
76
  this._mediaStreamTrack.getConstraints();
79
77
  this.simulcastCodecs.forEach((trackInfo) => {
80
78
  trackInfo.mediaStreamTrack.stop();
81
- trackInfo.sender = undefined;
82
79
  });
83
- this.simulcastCodecs.clear();
84
80
  super.stop();
85
81
  }
86
82
 
@@ -8,7 +8,7 @@ import { TrackPublication } from './TrackPublication';
8
8
  import { RemoteTrack } from './types';
9
9
 
10
10
  export default class RemoteTrackPublication extends TrackPublication {
11
- track?: RemoteTrack;
11
+ track?: RemoteTrack = undefined;
12
12
 
13
13
  /** @internal */
14
14
  protected allowed = true;
@@ -10,9 +10,6 @@ import { AdaptiveStreamSettings } from './types';
10
10
  const REACTION_DELAY = 100;
11
11
 
12
12
  export default class RemoteVideoTrack extends RemoteTrack {
13
- /** @internal */
14
- receiver?: RTCRtpReceiver;
15
-
16
13
  private prevStats?: VideoReceiverStats;
17
14
 
18
15
  private elementInfos: ElementInfo[] = [];
@@ -1,3 +1,4 @@
1
+ import DeviceManager from '../DeviceManager';
1
2
  import { TrackInvalidError } from '../errors';
2
3
  import { mediaTrackToLocalTrack } from '../participant/publishUtils';
3
4
  import { audioDefaults, videoDefaults } from './defaults';
@@ -30,7 +31,21 @@ export async function createLocalTracks(
30
31
 
31
32
  const opts = mergeDefaultOptions(options, audioDefaults, videoDefaults);
32
33
  const constraints = constraintsForOptions(opts);
33
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
34
+
35
+ // Keep a reference to the promise on DeviceManager and await it in getLocalDevices()
36
+ // works around iOS Safari Bug https://bugs.webkit.org/show_bug.cgi?id=179363
37
+ const mediaPromise = navigator.mediaDevices.getUserMedia(constraints);
38
+
39
+ if (options.audio) {
40
+ DeviceManager.userMediaPromiseMap.set('audioinput', mediaPromise);
41
+ mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('audioinput'));
42
+ }
43
+ if (options.video) {
44
+ DeviceManager.userMediaPromiseMap.set('videoinput', mediaPromise);
45
+ mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('videoinput'));
46
+ }
47
+
48
+ const stream = await mediaPromise;
34
49
  return stream.getTracks().map((mediaStreamTrack) => {
35
50
  const isAudio = mediaStreamTrack.kind === 'audio';
36
51
  let trackOptions = isAudio ? options!.audio : options!.video;
package/src/room/utils.ts CHANGED
@@ -122,10 +122,12 @@ export class Future<T> {
122
122
 
123
123
  reject!: (e: any) => void;
124
124
 
125
- constructor() {
126
- this.promise = new Promise<T>((resolve, reject) => {
127
- this.resolve = resolve;
128
- this.reject = reject;
129
- });
125
+ constructor(promise?: Promise<T>) {
126
+ this.promise =
127
+ promise ??
128
+ new Promise<T>((resolve, reject) => {
129
+ this.resolve = resolve;
130
+ this.reject = reject;
131
+ });
130
132
  }
131
133
  }