livekit-client 1.2.2 → 1.2.5

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 (57) hide show
  1. package/dist/livekit-client.esm.mjs +1990 -905
  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/index.d.ts +2 -1
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/options.d.ts +5 -0
  8. package/dist/src/options.d.ts.map +1 -1
  9. package/dist/src/proto/google/protobuf/timestamp.d.ts +1 -1
  10. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  11. package/dist/src/proto/livekit_models.d.ts +34 -34
  12. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  13. package/dist/src/proto/livekit_rtc.d.ts +124 -124
  14. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  15. package/dist/src/room/DefaultReconnectPolicy.d.ts +8 -0
  16. package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -0
  17. package/dist/src/room/DeviceManager.d.ts +1 -0
  18. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  19. package/dist/src/room/PCTransport.d.ts +5 -1
  20. package/dist/src/room/PCTransport.d.ts.map +1 -1
  21. package/dist/src/room/RTCEngine.d.ts +7 -1
  22. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  23. package/dist/src/room/ReconnectPolicy.d.ts +23 -0
  24. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -0
  25. package/dist/src/room/Room.d.ts +12 -1
  26. package/dist/src/room/Room.d.ts.map +1 -1
  27. package/dist/src/room/events.d.ts +2 -2
  28. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  29. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalAudioTrack.d.ts +0 -1
  31. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalVideoTrack.d.ts +1 -1
  33. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  35. package/dist/src/room/track/RemoteVideoTrack.d.ts +0 -2
  36. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  37. package/dist/src/room/track/create.d.ts.map +1 -1
  38. package/dist/src/room/utils.d.ts +1 -1
  39. package/dist/src/room/utils.d.ts.map +1 -1
  40. package/package.json +37 -33
  41. package/src/index.ts +2 -0
  42. package/src/options.ts +6 -0
  43. package/src/room/DefaultReconnectPolicy.ts +35 -0
  44. package/src/room/DeviceManager.ts +23 -1
  45. package/src/room/PCTransport.ts +91 -17
  46. package/src/room/RTCEngine.ts +105 -33
  47. package/src/room/ReconnectPolicy.ts +25 -0
  48. package/src/room/Room.ts +190 -167
  49. package/src/room/events.ts +2 -2
  50. package/src/room/participant/LocalParticipant.ts +38 -14
  51. package/src/room/participant/RemoteParticipant.ts +14 -0
  52. package/src/room/track/LocalAudioTrack.ts +0 -2
  53. package/src/room/track/LocalVideoTrack.ts +3 -8
  54. package/src/room/track/RemoteTrackPublication.ts +1 -1
  55. package/src/room/track/RemoteVideoTrack.ts +0 -3
  56. package/src/room/track/create.ts +16 -1
  57. 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
@@ -134,9 +138,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
134
138
  return;
135
139
  }
136
140
 
137
- this.engine = new RTCEngine();
141
+ this.engine = new RTCEngine(this.options);
138
142
 
139
- this.engine.client.signalLatency = this.options.expSignalLatency;
140
143
  this.engine.client.onParticipantUpdate = this.handleParticipantUpdates;
141
144
  this.engine.client.onRoomUpdate = this.handleRoomUpdate;
142
145
  this.engine.client.onSpeakersChanged = this.handleSpeakersChanged;
@@ -157,12 +160,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
157
160
  .on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate)
158
161
  .on(EngineEvent.DataPacketReceived, this.handleDataPacket)
159
162
  .on(EngineEvent.Resuming, () => {
163
+ if (!this.reconnectFuture) {
164
+ this.reconnectFuture = new Future();
165
+ }
160
166
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
161
167
  this.emit(RoomEvent.Reconnecting);
162
168
  }
163
169
  })
164
170
  .on(EngineEvent.Resumed, () => {
165
171
  this.setAndEmitConnectionState(ConnectionState.Connected);
172
+ this.reconnectFuture?.resolve();
173
+ this.reconnectFuture = undefined;
166
174
  this.emit(RoomEvent.Reconnected);
167
175
  this.updateSubscriptions();
168
176
  })
@@ -194,165 +202,145 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
194
202
  return DeviceManager.getInstance().getDevices(kind, requestPermissions);
195
203
  }
196
204
 
197
- connect = async (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
205
+ connect = (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
198
206
  if (this.state === ConnectionState.Connected) {
199
207
  // when the state is reconnecting or connected, this function returns immediately
200
208
  log.warn(`already connected to room ${this.name}`);
201
- return;
209
+ return Promise.resolve();
202
210
  }
203
211
 
204
212
  if (this.connectFuture) {
205
213
  return this.connectFuture.promise;
214
+ } else if (this.reconnectFuture) {
215
+ this.connectFuture = this.reconnectFuture;
216
+ return this.connectFuture.promise;
206
217
  }
207
- this.setAndEmitConnectionState(ConnectionState.Connecting);
208
-
209
- if (!this.abortController || this.abortController.signal.aborted) {
210
- this.abortController = new AbortController();
211
- }
212
-
213
- // recreate engine if previously disconnected
214
- this.createEngine();
215
-
216
- this.acquireAudioContext();
217
-
218
- if (opts?.rtcConfig) {
219
- this.engine.rtcConfig = opts.rtcConfig;
220
- }
221
-
222
- this.connOptions = opts;
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
+ }
223
223
 
224
- try {
225
- const joinResponse = await this.engine.join(
226
- url,
227
- token,
228
- {
229
- autoSubscribe: opts?.autoSubscribe,
230
- publishOnly: opts?.publishOnly,
231
- adaptiveStream:
232
- typeof this.options?.adaptiveStream === 'object' ? true : this.options?.adaptiveStream,
233
- },
234
- this.abortController.signal,
235
- );
236
- log.debug(
237
- `connected to Livekit Server version: ${joinResponse.serverVersion}, region: ${joinResponse.serverRegion}`,
238
- );
224
+ // recreate engine if previously disconnected
225
+ this.createEngine();
239
226
 
240
- if (!joinResponse.serverVersion) {
241
- throw new UnsupportedServer('unknown server version');
242
- }
227
+ this.acquireAudioContext();
243
228
 
244
- if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
245
- log.debug('disabling dynacast due to server version');
246
- // dynacast has a bug in 0.15.1, so we cannot use it then
247
- this.options.dynacast = false;
229
+ if (opts?.rtcConfig) {
230
+ this.engine.rtcConfig = opts.rtcConfig;
248
231
  }
249
232
 
250
- const pi = joinResponse.participant!;
251
-
252
- this.localParticipant.sid = pi.sid;
253
- this.localParticipant.identity = pi.identity;
254
-
255
- this.localParticipant.updateInfo(pi);
256
- // forward metadata changed for the local participant
257
- this.localParticipant
258
- .on(ParticipantEvent.ParticipantMetadataChanged, (metadata: string | undefined) => {
259
- this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
260
- })
261
- .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
262
- this.emit(RoomEvent.TrackMuted, pub, this.localParticipant);
263
- })
264
- .on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
265
- this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
266
- })
267
- .on(ParticipantEvent.LocalTrackPublished, (pub: LocalTrackPublication) => {
268
- this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
269
- })
270
- .on(ParticipantEvent.LocalTrackUnpublished, (pub: LocalTrackPublication) => {
271
- this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
272
- })
273
- .on(ParticipantEvent.ConnectionQualityChanged, (quality: ConnectionQuality) => {
274
- this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
275
- })
276
- .on(ParticipantEvent.MediaDevicesError, (e: Error) => {
277
- this.emit(RoomEvent.MediaDevicesError, e);
278
- })
279
- .on(
280
- ParticipantEvent.ParticipantPermissionsChanged,
281
- (prevPermissions: ParticipantPermission) => {
282
- this.emit(
283
- RoomEvent.ParticipantPermissionsChanged,
284
- prevPermissions,
285
- this.localParticipant,
286
- );
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,
287
246
  },
247
+ this.abortController.signal,
248
+ );
249
+ log.debug(
250
+ `connected to Livekit Server version: ${joinResponse.serverVersion}, region: ${joinResponse.serverRegion}`,
288
251
  );
289
252
 
290
- // populate remote participants, these should not trigger new events
291
- joinResponse.otherParticipants.forEach((info) => {
292
- if (
293
- info.sid !== this.localParticipant.sid &&
294
- info.identity !== this.localParticipant.identity
295
- ) {
296
- this.getOrCreateParticipant(info.sid, info);
297
- } else {
298
- log.warn('received info to create local participant as remote participant', {
299
- info,
300
- localParticipant: this.localParticipant,
301
- });
253
+ if (!joinResponse.serverVersion) {
254
+ throw new UnsupportedServer('unknown server version');
302
255
  }
303
- });
304
256
 
305
- this.name = joinResponse.room!.name;
306
- this.sid = joinResponse.room!.sid;
307
- this.metadata = joinResponse.room!.metadata;
308
- this.emit(RoomEvent.SignalConnected);
309
- } catch (err) {
310
- this.recreateEngine();
311
- this.setAndEmitConnectionState(
312
- ConnectionState.Disconnected,
313
- new ConnectionError('could not establish signal connection'),
314
- );
315
- throw err;
316
- }
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
+ }
317
262
 
318
- // don't return until ICE connected
319
- const connectTimeout = setTimeout(() => {
320
- // timeout
321
- this.recreateEngine();
322
- this.setAndEmitConnectionState(
323
- ConnectionState.Disconnected,
324
- new ConnectionError('could not connect PeerConnection after timeout'),
325
- );
326
- }, maxICEConnectTimeout);
327
- const abortHandler = () => {
328
- log.warn('closing engine');
329
- clearTimeout(connectTimeout);
330
- this.recreateEngine();
331
- this.setAndEmitConnectionState(
332
- ConnectionState.Disconnected,
333
- new ConnectionError('room connection has been cancelled'),
334
- );
335
- };
336
- if (this.abortController?.signal.aborted) {
337
- abortHandler();
338
- }
339
- this.abortController?.signal.addEventListener('abort', abortHandler);
340
-
341
- this.engine.once(EngineEvent.Connected, () => {
342
- clearTimeout(connectTimeout);
343
- this.abortController?.signal.removeEventListener('abort', abortHandler);
344
- // also hook unload event
345
- if (isWeb()) {
346
- window.addEventListener('beforeunload', this.onBeforeUnload);
347
- navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
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
+ );
282
+
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
+ });
297
+
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
+ }
307
+
308
+ // don't return until ICE connected
309
+ const connectTimeout = setTimeout(() => {
310
+ // timeout
311
+ this.recreateEngine();
312
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
313
+ reject(new ConnectionError('could not connect PeerConnection after timeout'));
314
+ }, maxICEConnectTimeout);
315
+ const abortHandler = () => {
316
+ log.warn('closing engine');
317
+ clearTimeout(connectTimeout);
318
+ this.recreateEngine();
319
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
320
+ reject(new ConnectionError('room connection has been cancelled'));
321
+ };
322
+ if (this.abortController?.signal.aborted) {
323
+ abortHandler();
348
324
  }
349
- this.setAndEmitConnectionState(ConnectionState.Connected);
325
+ this.abortController?.signal.addEventListener('abort', abortHandler);
326
+
327
+ this.engine.once(EngineEvent.Connected, () => {
328
+ clearTimeout(connectTimeout);
329
+ this.abortController?.signal.removeEventListener('abort', abortHandler);
330
+ // also hook unload event
331
+ if (isWeb()) {
332
+ window.addEventListener('beforeunload', this.onBeforeUnload);
333
+ navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
334
+ }
335
+ this.setAndEmitConnectionState(ConnectionState.Connected);
336
+ resolve();
337
+ });
350
338
  });
339
+ this.connectFuture = new Future(connectPromise);
351
340
 
352
- if (this.connectFuture) {
353
- /** @ts-ignore */
354
- return this.connectFuture.promise;
355
- }
341
+ this.connectFuture.promise.finally(() => (this.connectFuture = undefined));
342
+
343
+ return this.connectFuture.promise;
356
344
  };
357
345
 
358
346
  /**
@@ -360,11 +348,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
360
348
  */
361
349
  disconnect = async (stopTracks = true) => {
362
350
  log.info('disconnect from room', { identity: this.localParticipant.identity });
363
- if (this.state === ConnectionState.Connecting) {
351
+ if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
364
352
  // try aborting pending connection attempt
365
353
  log.warn('abort connection attempt');
366
354
  this.abortController?.abort();
367
- return;
355
+ // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
356
+ this.connectFuture?.reject(new ConnectionError('Client initiated disconnect'));
357
+ this.connectFuture = undefined;
368
358
  }
369
359
  // send leave
370
360
  if (this.engine?.client.isConnected) {
@@ -374,7 +364,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
374
364
  if (this.engine) {
375
365
  this.engine.close();
376
366
  }
377
-
378
367
  this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
379
368
  /* @ts-ignore */
380
369
  this.engine = undefined;
@@ -592,6 +581,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
592
581
  }
593
582
 
594
583
  private handleRestarting = () => {
584
+ if (!this.reconnectFuture) {
585
+ this.reconnectFuture = new Future();
586
+ }
595
587
  // also unwind existing participants & existing subscriptions
596
588
  for (const p of this.participants.values()) {
597
589
  this.handleParticipantDisconnected(p.sid, p);
@@ -608,6 +600,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
608
600
  });
609
601
  this.setAndEmitConnectionState(ConnectionState.Connected);
610
602
  this.emit(RoomEvent.Reconnected);
603
+ this.reconnectFuture?.resolve();
604
+ this.reconnectFuture = undefined;
611
605
 
612
606
  // rehydrate participants
613
607
  if (joinResponse.participant) {
@@ -651,12 +645,32 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
651
645
  if (this.state === ConnectionState.Disconnected) {
652
646
  return;
653
647
  }
648
+ // reject potentially ongoing reconnection attempt
649
+ if (this.connectFuture === this.reconnectFuture) {
650
+ this.connectFuture?.reject(undefined);
651
+ this.connectFuture = undefined;
652
+ this.reconnectFuture = undefined;
653
+ }
654
+
654
655
  this.participants.forEach((p) => {
655
656
  p.tracks.forEach((pub) => {
656
657
  p.unpublishTrack(pub.trackSid);
657
658
  });
658
659
  });
659
660
 
661
+ this.localParticipant
662
+ .off(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
663
+ .off(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
664
+ .off(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
665
+ .off(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
666
+ .off(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
667
+ .off(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
668
+ .off(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
669
+ .off(
670
+ ParticipantEvent.ParticipantPermissionsChanged,
671
+ this.onLocalParticipantPermissionsChanged,
672
+ );
673
+
660
674
  this.localParticipant.tracks.forEach((pub) => {
661
675
  if (pub.track) {
662
676
  this.localParticipant.unpublishTrack(pub.track, shouldStopTracks);
@@ -666,6 +680,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
666
680
  pub.track?.stop();
667
681
  }
668
682
  });
683
+
669
684
  this.localParticipant.tracks.clear();
670
685
  this.localParticipant.videoTracks.clear();
671
686
  this.localParticipant.audioTracks.clear();
@@ -1037,35 +1052,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1037
1052
  }
1038
1053
  }
1039
1054
 
1040
- private setAndEmitConnectionState(state: ConnectionState, error?: Error): boolean {
1055
+ private setAndEmitConnectionState(state: ConnectionState): boolean {
1041
1056
  if (state === this.state) {
1042
1057
  // unchanged
1043
1058
  return false;
1044
1059
  }
1045
- switch (state) {
1046
- case ConnectionState.Connecting:
1047
- case ConnectionState.Reconnecting:
1048
- if (!this.connectFuture) {
1049
- // reuse existing connect future if possible
1050
- this.connectFuture = new Future<void>();
1051
- }
1052
- break;
1053
- case ConnectionState.Connected:
1054
- if (this.connectFuture) {
1055
- this.connectFuture.resolve();
1056
- this.connectFuture = undefined;
1057
- }
1058
- break;
1059
- case ConnectionState.Disconnected:
1060
- if (this.connectFuture) {
1061
- error ??= new Error('disconnected from Room');
1062
- this.connectFuture.reject(error);
1063
- this.connectFuture = undefined;
1064
- }
1065
- break;
1066
- default:
1067
- // nothing
1068
- }
1069
1060
  this.state = state;
1070
1061
  this.emit(RoomEvent.ConnectionStateChanged, this.state);
1071
1062
  return true;
@@ -1081,6 +1072,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1081
1072
  return false;
1082
1073
  }
1083
1074
 
1075
+ private onLocalParticipantMetadataChanged = (metadata: string | undefined) => {
1076
+ this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
1077
+ };
1078
+
1079
+ private onLocalTrackMuted = (pub: TrackPublication) => {
1080
+ this.emit(RoomEvent.TrackMuted, pub, this.localParticipant);
1081
+ };
1082
+
1083
+ private onLocalTrackUnmuted = (pub: TrackPublication) => {
1084
+ this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
1085
+ };
1086
+
1087
+ private onLocalTrackPublished = (pub: LocalTrackPublication) => {
1088
+ this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
1089
+ };
1090
+
1091
+ private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
1092
+ this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
1093
+ };
1094
+
1095
+ private onLocalConnectionQualityChanged = (quality: ConnectionQuality) => {
1096
+ this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
1097
+ };
1098
+
1099
+ private onMediaDevicesError = (e: Error) => {
1100
+ this.emit(RoomEvent.MediaDevicesError, e);
1101
+ };
1102
+
1103
+ private onLocalParticipantPermissionsChanged = (prevPermissions: ParticipantPermission) => {
1104
+ this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
1105
+ };
1106
+
1084
1107
  // /** @internal */
1085
1108
  emit<E extends keyof RoomEventCallbacks>(
1086
1109
  event: E,
@@ -218,8 +218,8 @@ export enum RoomEvent {
218
218
  * When we have encountered an error while attempting to create a track.
219
219
  * The errors take place in getUserMedia().
220
220
  * Use MediaDeviceFailure.getFailure(error) to get the reason of failure.
221
- * [[getAudioCreateError]] and [[getVideoCreateError]] will indicate if it had
222
- * an error while creating the audio or video track respectively.
221
+ * [[LocalParticipant.lastCameraError]] and [[LocalParticipant.lastMicrophoneError]]
222
+ * will indicate if it had an error while creating the audio or video track respectively.
223
223
  *
224
224
  * args: (error: Error)
225
225
  */
@@ -434,6 +434,23 @@ export default class LocalParticipant extends Participant {
434
434
  if (opts.source) {
435
435
  track.source = opts.source;
436
436
  }
437
+ const existingTrackOfSource = Array.from(this.tracks.values()).find(
438
+ (publishedTrack) => track instanceof LocalTrack && publishedTrack.source === track.source,
439
+ );
440
+ if (existingTrackOfSource) {
441
+ try {
442
+ // throw an Error in order to capture the stack trace
443
+ throw Error(`publishing a second track with the same source: ${track.source}`);
444
+ } catch (e: unknown) {
445
+ if (e instanceof Error) {
446
+ log.warn(e.message, {
447
+ oldTrack: existingTrackOfSource,
448
+ newTrack: track,
449
+ trace: e.stack,
450
+ });
451
+ }
452
+ }
453
+ }
437
454
  if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) {
438
455
  track.stopOnMute = true;
439
456
  }
@@ -686,8 +703,6 @@ export default class LocalParticipant extends Participant {
686
703
  }
687
704
 
688
705
  track = publication.track;
689
-
690
- track.sender = undefined;
691
706
  track.off(TrackEvent.Muted, this.onTrackMuted);
692
707
  track.off(TrackEvent.Unmuted, this.onTrackUnmuted);
693
708
  track.off(TrackEvent.Ended, this.handleTrackEnded);
@@ -701,22 +716,31 @@ export default class LocalParticipant extends Participant {
701
716
  track.stop();
702
717
  }
703
718
 
704
- const { mediaStreamTrack } = track;
705
-
706
- if (this.engine.publisher && this.engine.publisher.pc.connectionState !== 'closed') {
707
- const senders = this.engine.publisher.pc.getSenders();
708
- senders.forEach((sender) => {
709
- if (sender.track === mediaStreamTrack) {
710
- try {
711
- this.engine.publisher?.pc.removeTrack(sender);
712
- this.engine.negotiate();
713
- } catch (e) {
714
- log.warn('failed to remove track', { error: e, method: 'unpublishTrack' });
719
+ if (
720
+ this.engine.publisher &&
721
+ this.engine.publisher.pc.connectionState !== 'closed' &&
722
+ track.sender
723
+ ) {
724
+ try {
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
+ }
715
732
  }
733
+ track.simulcastCodecs.clear();
716
734
  }
717
- });
735
+ } catch (e) {
736
+ log.warn('failed to unpublish track', { error: e, method: 'unpublishTrack' });
737
+ } finally {
738
+ this.engine.negotiate();
739
+ }
718
740
  }
719
741
 
742
+ track.sender = undefined;
743
+
720
744
  // remove from our maps
721
745
  this.tracks.delete(publication.trackSid);
722
746
  switch (publication.kind) {
@@ -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);
@@ -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
 
@@ -75,13 +73,10 @@ export default class LocalVideoTrack extends LocalTrack {
75
73
  }
76
74
 
77
75
  stop() {
78
- this.sender = undefined;
79
76
  this._mediaStreamTrack.getConstraints();
80
77
  this.simulcastCodecs.forEach((trackInfo) => {
81
78
  trackInfo.mediaStreamTrack.stop();
82
- trackInfo.sender = undefined;
83
79
  });
84
- this.simulcastCodecs.clear();
85
80
  super.stop();
86
81
  }
87
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[] = [];