livekit-client 1.15.10 → 2.0.0

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 (76) hide show
  1. package/README.md +21 -17
  2. package/dist/livekit-client.esm.mjs +1603 -1493
  3. package/dist/livekit-client.esm.mjs.map +1 -1
  4. package/dist/livekit-client.umd.js +1 -1
  5. package/dist/livekit-client.umd.js.map +1 -1
  6. package/dist/src/api/SignalClient.d.ts +1 -3
  7. package/dist/src/api/SignalClient.d.ts.map +1 -1
  8. package/dist/src/index.d.ts +3 -3
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/options.d.ts +3 -9
  11. package/dist/src/options.d.ts.map +1 -1
  12. package/dist/src/proto/livekit_models_pb.d.ts +47 -0
  13. package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
  14. package/dist/src/room/RTCEngine.d.ts +1 -0
  15. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  16. package/dist/src/room/Room.d.ts +14 -16
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/defaults.d.ts.map +1 -1
  19. package/dist/src/room/events.d.ts +0 -4
  20. package/dist/src/room/events.d.ts.map +1 -1
  21. package/dist/src/room/participant/LocalParticipant.d.ts +8 -25
  22. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  23. package/dist/src/room/participant/Participant.d.ts +6 -10
  24. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  25. package/dist/src/room/participant/RemoteParticipant.d.ts +9 -6
  26. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  27. package/dist/src/room/timers.d.ts +4 -5
  28. package/dist/src/room/timers.d.ts.map +1 -1
  29. package/dist/src/room/track/LocalVideoTrack.d.ts +3 -3
  30. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  31. package/dist/src/room/track/RemoteTrackPublication.d.ts +2 -2
  32. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  33. package/dist/src/room/track/Track.d.ts +5 -0
  34. package/dist/src/room/track/Track.d.ts.map +1 -1
  35. package/dist/src/room/track/options.d.ts +0 -5
  36. package/dist/src/room/track/options.d.ts.map +1 -1
  37. package/dist/src/room/types.d.ts +11 -3
  38. package/dist/src/room/types.d.ts.map +1 -1
  39. package/dist/src/version.d.ts +1 -1
  40. package/dist/ts4.2/src/api/SignalClient.d.ts +1 -3
  41. package/dist/ts4.2/src/index.d.ts +3 -3
  42. package/dist/ts4.2/src/options.d.ts +3 -9
  43. package/dist/ts4.2/src/proto/livekit_models_pb.d.ts +47 -0
  44. package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -0
  45. package/dist/ts4.2/src/room/Room.d.ts +14 -16
  46. package/dist/ts4.2/src/room/events.d.ts +0 -4
  47. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +8 -25
  48. package/dist/ts4.2/src/room/participant/Participant.d.ts +6 -10
  49. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +9 -6
  50. package/dist/ts4.2/src/room/timers.d.ts +4 -5
  51. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +3 -3
  52. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +2 -2
  53. package/dist/ts4.2/src/room/track/Track.d.ts +5 -0
  54. package/dist/ts4.2/src/room/track/options.d.ts +0 -5
  55. package/dist/ts4.2/src/room/types.d.ts +11 -3
  56. package/dist/ts4.2/src/version.d.ts +1 -1
  57. package/package.json +8 -7
  58. package/src/api/SignalClient.ts +10 -10
  59. package/src/e2ee/E2eeManager.ts +2 -2
  60. package/src/index.ts +2 -4
  61. package/src/options.ts +3 -10
  62. package/src/proto/livekit_models_pb.ts +66 -0
  63. package/src/room/RTCEngine.ts +6 -1
  64. package/src/room/Room.ts +169 -129
  65. package/src/room/defaults.ts +1 -5
  66. package/src/room/events.ts +0 -5
  67. package/src/room/participant/LocalParticipant.ts +36 -77
  68. package/src/room/participant/Participant.ts +23 -24
  69. package/src/room/participant/RemoteParticipant.ts +27 -24
  70. package/src/room/track/LocalVideoTrack.test.ts +1 -1
  71. package/src/room/track/LocalVideoTrack.ts +11 -7
  72. package/src/room/track/RemoteTrackPublication.ts +2 -7
  73. package/src/room/track/Track.ts +10 -1
  74. package/src/room/track/options.ts +0 -6
  75. package/src/room/types.ts +11 -4
  76. package/src/version.ts +1 -1
package/src/room/Room.ts CHANGED
@@ -85,9 +85,6 @@ export enum ConnectionState {
85
85
 
86
86
  const connectionReconcileFrequency = 2 * 1000;
87
87
 
88
- /** @deprecated RoomState has been renamed to [[ConnectionState]] */
89
- export const RoomState = ConnectionState;
90
-
91
88
  /**
92
89
  * In LiveKit, a room is the logical grouping for a list of participants.
93
90
  * Participants in a room can publish tracks, and subscribe to others' tracks.
@@ -99,8 +96,10 @@ export const RoomState = ConnectionState;
99
96
  class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>) {
100
97
  state: ConnectionState = ConnectionState.Disconnected;
101
98
 
102
- /** map of sid: [[RemoteParticipant]] */
103
- participants: Map<string, RemoteParticipant>;
99
+ /**
100
+ * map of identity: [[RemoteParticipant]]
101
+ */
102
+ remoteParticipants: Map<string, RemoteParticipant>;
104
103
 
105
104
  /**
106
105
  * list of participants that are actively speaking. when this changes
@@ -122,7 +121,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
122
121
 
123
122
  private roomInfo?: RoomModel;
124
123
 
125
- private identityToSid: Map<string, string>;
124
+ private sidToIdentity: Map<string, string>;
126
125
 
127
126
  /** connect options of room */
128
127
  private connOptions?: InternalRoomConnectOptions;
@@ -141,8 +140,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
141
140
 
142
141
  private e2eeManager: E2EEManager | undefined;
143
142
 
144
- private cachedParticipantSids: Array<string>;
145
-
146
143
  private connectionReconcileInterval?: ReturnType<typeof setInterval>;
147
144
 
148
145
  private regionUrlProvider?: RegionUrlProvider;
@@ -153,6 +150,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
153
150
 
154
151
  private log = log;
155
152
 
153
+ private bufferedEvents: Array<any> = [];
154
+
155
+ private isResuming: boolean = false;
156
+
156
157
  /**
157
158
  * Creates a new Room, the primary construct for a LiveKit session.
158
159
  * @param options
@@ -160,9 +161,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
160
161
  constructor(options?: RoomOptions) {
161
162
  super();
162
163
  this.setMaxListeners(100);
163
- this.participants = new Map();
164
- this.cachedParticipantSids = [];
165
- this.identityToSid = new Map();
164
+ this.remoteParticipants = new Map();
165
+ this.sidToIdentity = new Map();
166
166
  this.options = { ...roomOptionDefaults, ...options };
167
167
 
168
168
  this.log = getLogger(this.options.loggerName ?? LoggerNames.Room);
@@ -246,7 +246,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
246
246
  private get logContext() {
247
247
  return {
248
248
  room: this.name,
249
- roomSid: this.sid,
249
+ roomSid: this.roomInfo?.sid,
250
250
  identity: this.localParticipant.identity,
251
251
  };
252
252
  }
@@ -258,9 +258,30 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
258
258
  return this.roomInfo?.activeRecording ?? false;
259
259
  }
260
260
 
261
- /** server assigned unique room id */
262
- get sid(): string {
263
- return this.roomInfo?.sid ?? '';
261
+ /**
262
+ * server assigned unique room id.
263
+ * returns once a sid has been issued by the server.
264
+ */
265
+ async getSid(): Promise<string> {
266
+ if (this.state === ConnectionState.Disconnected) {
267
+ return '';
268
+ }
269
+ if (this.roomInfo && this.roomInfo.sid !== '') {
270
+ return this.roomInfo.sid;
271
+ }
272
+ return new Promise((resolve, reject) => {
273
+ const handleRoomUpdate = (roomInfo: RoomModel) => {
274
+ if (roomInfo.sid !== '') {
275
+ this.engine.off(EngineEvent.RoomUpdate, handleRoomUpdate);
276
+ resolve(roomInfo.sid);
277
+ }
278
+ };
279
+ this.engine.on(EngineEvent.RoomUpdate, handleRoomUpdate);
280
+ this.once(RoomEvent.Disconnected, () => {
281
+ this.engine.off(EngineEvent.RoomUpdate, handleRoomUpdate);
282
+ reject('Room disconnected before room server id was available');
283
+ });
284
+ });
264
285
  }
265
286
 
266
287
  /** user assigned name, derived from JWT token */
@@ -309,26 +330,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
309
330
  .on(EngineEvent.DataPacketReceived, this.handleDataPacket)
310
331
  .on(EngineEvent.Resuming, () => {
311
332
  this.clearConnectionReconcile();
312
- if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
313
- this.emit(RoomEvent.Reconnecting);
314
- }
315
- this.cachedParticipantSids = Array.from(this.participants.keys());
333
+ this.isResuming = true;
334
+ this.log.info('Resuming signal connection', this.logContext);
316
335
  })
317
336
  .on(EngineEvent.Resumed, () => {
318
- this.setAndEmitConnectionState(ConnectionState.Connected);
319
- this.emit(RoomEvent.Reconnected);
320
337
  this.registerConnectionReconcile();
338
+ this.isResuming = false;
339
+ this.log.info('Resumed signal connection', this.logContext);
321
340
  this.updateSubscriptions();
322
-
323
- // once reconnected, figure out if any participants connected during reconnect and emit events for it
324
- const diffParticipants = Array.from(this.participants.values()).filter(
325
- (p) => !this.cachedParticipantSids.includes(p.sid),
326
- );
327
- diffParticipants.forEach((p) => this.emit(RoomEvent.ParticipantConnected, p));
328
- this.cachedParticipantSids = [];
341
+ this.emitBufferedEvents();
329
342
  })
330
343
  .on(EngineEvent.SignalResumed, () => {
331
- if (this.state === ConnectionState.Reconnecting) {
344
+ this.bufferedEvents = [];
345
+ if (this.state === ConnectionState.Reconnecting || this.isResuming) {
332
346
  this.sendSyncState();
333
347
  }
334
348
  })
@@ -515,7 +529,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
515
529
  token,
516
530
  {
517
531
  autoSubscribe: connectOptions.autoSubscribe,
518
- publishOnly: connectOptions.publishOnly,
519
532
  adaptiveStream:
520
533
  typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
521
534
  maxRetries: connectOptions.maxRetries,
@@ -578,7 +591,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
578
591
  opts: RoomConnectOptions | undefined,
579
592
  abortController: AbortController,
580
593
  ) => {
581
- if (this.state === ConnectionState.Reconnecting) {
594
+ if (
595
+ this.state === ConnectionState.Reconnecting ||
596
+ this.isResuming ||
597
+ this.engine?.pendingReconnect
598
+ ) {
582
599
  this.log.info('Reconnection attempt replaced by new connection attempt', this.logContext);
583
600
  // make sure we close and recreate the existing engine in order to get rid of any potentially ongoing reconnection attempts
584
601
  this.recreateEngine();
@@ -680,7 +697,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
680
697
  });
681
698
  if (
682
699
  this.state === ConnectionState.Connecting ||
683
- this.state === ConnectionState.Reconnecting
700
+ this.state === ConnectionState.Reconnecting ||
701
+ this.isResuming
684
702
  ) {
685
703
  // try aborting pending connection attempt
686
704
  this.log.warn('abort connection attempt', this.logContext);
@@ -714,10 +732,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
714
732
  if (this.localParticipant.identity === identity) {
715
733
  return this.localParticipant;
716
734
  }
717
- const sid = this.identityToSid.get(identity);
718
- if (sid) {
719
- return this.participants.get(sid);
720
- }
735
+ return this.remoteParticipants.get(identity);
721
736
  }
722
737
 
723
738
  private clearConnectionFutures() {
@@ -881,17 +896,25 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
881
896
  }
882
897
  // set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
883
898
  dummyAudioEl.srcObject = document.hidden ? null : stream;
899
+ if (!document.hidden) {
900
+ this.log.debug(
901
+ 'page visible again, triggering startAudio to resume playback and update playback status',
902
+ this.logContext,
903
+ );
904
+ this.startAudio();
905
+ }
884
906
  });
885
907
  document.body.append(dummyAudioEl);
886
908
  this.once(RoomEvent.Disconnected, () => {
887
909
  dummyAudioEl?.remove();
910
+ dummyAudioEl = null;
888
911
  });
889
912
  }
890
913
  elements.push(dummyAudioEl);
891
914
  }
892
915
 
893
- this.participants.forEach((p) => {
894
- p.audioTracks.forEach((t) => {
916
+ this.remoteParticipants.forEach((p) => {
917
+ p.audioTrackPublications.forEach((t) => {
895
918
  if (t.track) {
896
919
  t.track.attachedElements.forEach((e) => {
897
920
  elements.push(e);
@@ -917,8 +940,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
917
940
 
918
941
  startVideo = async () => {
919
942
  const elements: HTMLMediaElement[] = [];
920
- for (const p of this.participants.values()) {
921
- p.videoTracks.forEach((tr) => {
943
+ for (const p of this.remoteParticipants.values()) {
944
+ p.videoTrackPublications.forEach((tr) => {
922
945
  tr.track?.attachedElements.forEach((el) => {
923
946
  if (!elements.includes(el)) {
924
947
  elements.push(el);
@@ -956,15 +979,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
956
979
  return !this.isVideoPlaybackBlocked;
957
980
  }
958
981
 
959
- /**
960
- * Returns the active audio output device used in this room.
961
- * @return the previously successfully set audio output device ID or an empty string if the default device is used.
962
- * @deprecated use `getActiveDevice('audiooutput')` instead
963
- */
964
- getActiveAudioOutputDevice(): string {
965
- return this.options.audioOutput?.deviceId ?? '';
966
- }
967
-
968
982
  getActiveDevice(kind: MediaDeviceKind): string | undefined {
969
983
  return this.localParticipant.activeDeviceMap.get(kind);
970
984
  }
@@ -987,7 +1001,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
987
1001
  const prevDeviceId = this.options.audioCaptureDefaults!.deviceId;
988
1002
  this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
989
1003
  deviceHasChanged = prevDeviceId !== deviceConstraint;
990
- const tracks = Array.from(this.localParticipant.audioTracks.values()).filter(
1004
+ const tracks = Array.from(this.localParticipant.audioTrackPublications.values()).filter(
991
1005
  (track) => track.source === Track.Source.Microphone,
992
1006
  );
993
1007
  try {
@@ -1002,7 +1016,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1002
1016
  const prevDeviceId = this.options.videoCaptureDefaults!.deviceId;
1003
1017
  this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
1004
1018
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1005
- const tracks = Array.from(this.localParticipant.videoTracks.values()).filter(
1019
+ const tracks = Array.from(this.localParticipant.videoTrackPublications.values()).filter(
1006
1020
  (track) => track.source === Track.Source.Camera,
1007
1021
  );
1008
1022
  try {
@@ -1015,8 +1029,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1015
1029
  }
1016
1030
  } else if (kind === 'audiooutput') {
1017
1031
  if (
1018
- (!supportsSetSinkId() && !this.options.expWebAudioMix) ||
1019
- (this.options.expWebAudioMix && this.audioContext && !('setSinkId' in this.audioContext))
1032
+ (!supportsSetSinkId() && !this.options.webAudioMix) ||
1033
+ (this.options.webAudioMix && this.audioContext && !('setSinkId' in this.audioContext))
1020
1034
  ) {
1021
1035
  throw new Error('cannot switch audio output, setSinkId not supported');
1022
1036
  }
@@ -1026,12 +1040,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1026
1040
  deviceHasChanged = prevDeviceId !== deviceConstraint;
1027
1041
 
1028
1042
  try {
1029
- if (this.options.expWebAudioMix) {
1043
+ if (this.options.webAudioMix) {
1030
1044
  // @ts-expect-error setSinkId is not yet in the typescript type of AudioContext
1031
1045
  this.audioContext?.setSinkId(deviceId);
1032
1046
  } else {
1033
1047
  await Promise.all(
1034
- Array.from(this.participants.values()).map((p) => p.setAudioOutput({ deviceId })),
1048
+ Array.from(this.remoteParticipants.values()).map((p) => p.setAudioOutput({ deviceId })),
1035
1049
  );
1036
1050
  }
1037
1051
  } catch (e) {
@@ -1068,10 +1082,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1068
1082
  this.engine?.close();
1069
1083
  /* @ts-ignore */
1070
1084
  this.engine = undefined;
1085
+ this.isResuming = false;
1071
1086
 
1072
1087
  // clear out existing remote participants, since they may have attached
1073
1088
  // the old engine
1074
- this.participants.clear();
1089
+ this.remoteParticipants.clear();
1090
+ this.sidToIdentity.clear();
1091
+ this.bufferedEvents = [];
1075
1092
  this.maybeCreateEngine();
1076
1093
  }
1077
1094
 
@@ -1105,23 +1122,25 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1105
1122
  return;
1106
1123
  }
1107
1124
  const parts = unpackStreamId(stream.id);
1108
- const participantId = parts[0];
1125
+ const participantSid = parts[0];
1109
1126
  let streamId = parts[1];
1110
1127
  let trackId = mediaTrack.id;
1111
1128
  // firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
1112
1129
  // and generates its own track id instead of infer from sdp track id.
1113
1130
  if (streamId && streamId.startsWith('TR')) trackId = streamId;
1114
1131
 
1115
- if (participantId === this.localParticipant.sid) {
1132
+ if (participantSid === this.localParticipant.sid) {
1116
1133
  this.log.warn('tried to create RemoteParticipant for local participant', this.logContext);
1117
1134
  return;
1118
1135
  }
1119
1136
 
1120
- const participant = this.participants.get(participantId) as RemoteParticipant | undefined;
1137
+ const participant = Array.from(this.remoteParticipants.values()).find(
1138
+ (p) => p.sid === participantSid,
1139
+ ) as RemoteParticipant | undefined;
1121
1140
 
1122
1141
  if (!participant) {
1123
1142
  this.log.error(
1124
- `Tried to add a track for a participant, that's not present. Sid: ${participantId}`,
1143
+ `Tried to add a track for a participant, that's not present. Sid: ${participantSid}`,
1125
1144
  this.logContext,
1126
1145
  );
1127
1146
  return;
@@ -1146,9 +1165,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1146
1165
 
1147
1166
  private handleRestarting = () => {
1148
1167
  this.clearConnectionReconcile();
1168
+ // in case we went from resuming to full-reconnect, make sure to reflect it on the isResuming flag
1169
+ this.isResuming = false;
1170
+
1149
1171
  // also unwind existing participants & existing subscriptions
1150
- for (const p of this.participants.values()) {
1151
- this.handleParticipantDisconnected(p.sid, p);
1172
+ for (const p of this.remoteParticipants.values()) {
1173
+ this.handleParticipantDisconnected(p.identity, p);
1152
1174
  }
1153
1175
 
1154
1176
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
@@ -1161,8 +1183,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1161
1183
  ...this.logContext,
1162
1184
  region: joinResponse.serverRegion,
1163
1185
  });
1186
+ this.bufferedEvents = [];
1164
1187
 
1165
- this.cachedParticipantSids = [];
1166
1188
  this.applyJoinResponse(joinResponse);
1167
1189
 
1168
1190
  try {
@@ -1188,15 +1210,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1188
1210
  this.setAndEmitConnectionState(ConnectionState.Connected);
1189
1211
  this.emit(RoomEvent.Reconnected);
1190
1212
  this.registerConnectionReconcile();
1191
-
1192
- // emit participant connected events after connection has been re-established
1193
- this.participants.forEach((participant) => {
1194
- this.emit(RoomEvent.ParticipantConnected, participant);
1195
- });
1213
+ this.emitBufferedEvents();
1196
1214
  };
1197
1215
 
1198
1216
  private handleDisconnect(shouldStopTracks = true, reason?: DisconnectReason) {
1199
1217
  this.clearConnectionReconcile();
1218
+ this.isResuming = false;
1219
+ this.bufferedEvents = [];
1200
1220
  if (this.state === ConnectionState.Disconnected) {
1201
1221
  return;
1202
1222
  }
@@ -1204,13 +1224,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1204
1224
  this.regionUrl = undefined;
1205
1225
 
1206
1226
  try {
1207
- this.participants.forEach((p) => {
1208
- p.tracks.forEach((pub) => {
1227
+ this.remoteParticipants.forEach((p) => {
1228
+ p.trackPublications.forEach((pub) => {
1209
1229
  p.unpublishTrack(pub.trackSid);
1210
1230
  });
1211
1231
  });
1212
1232
 
1213
- this.localParticipant.tracks.forEach((pub) => {
1233
+ this.localParticipant.trackPublications.forEach((pub) => {
1214
1234
  if (pub.track) {
1215
1235
  this.localParticipant.unpublishTrack(pub.track, shouldStopTracks);
1216
1236
  }
@@ -1235,13 +1255,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1235
1255
  this.onLocalParticipantPermissionsChanged,
1236
1256
  );
1237
1257
 
1238
- this.localParticipant.tracks.clear();
1239
- this.localParticipant.videoTracks.clear();
1240
- this.localParticipant.audioTracks.clear();
1258
+ this.localParticipant.trackPublications.clear();
1259
+ this.localParticipant.videoTrackPublications.clear();
1260
+ this.localParticipant.audioTrackPublications.clear();
1241
1261
 
1242
- this.participants.clear();
1262
+ this.remoteParticipants.clear();
1263
+ this.sidToIdentity.clear();
1243
1264
  this.activeSpeakers = [];
1244
- if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
1265
+ if (this.audioContext && typeof this.options.webAudioMix === 'boolean') {
1245
1266
  this.audioContext.close();
1246
1267
  this.audioContext = undefined;
1247
1268
  }
@@ -1265,39 +1286,32 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1265
1286
  return;
1266
1287
  }
1267
1288
 
1268
- // ensure identity <=> sid mapping
1269
- const sid = this.identityToSid.get(info.identity);
1270
- if (sid && sid !== info.sid) {
1271
- // sid had changed, need to remove previous participant
1272
- this.handleParticipantDisconnected(sid, this.participants.get(sid));
1289
+ // LiveKit server doesn't send identity info prior to version 1.5.2 in disconnect updates
1290
+ // so we try to map an empty identity to an already known sID manually
1291
+ if (info.identity === '') {
1292
+ info.identity = this.sidToIdentity.get(info.sid) ?? '';
1273
1293
  }
1274
1294
 
1275
- let remoteParticipant = this.participants.get(info.sid);
1276
- const isNewParticipant = !remoteParticipant;
1295
+ let remoteParticipant = this.remoteParticipants.get(info.identity);
1277
1296
 
1278
1297
  // when it's disconnected, send updates
1279
1298
  if (info.state === ParticipantInfo_State.DISCONNECTED) {
1280
- this.handleParticipantDisconnected(info.sid, remoteParticipant);
1299
+ this.handleParticipantDisconnected(info.identity, remoteParticipant);
1281
1300
  } else {
1282
1301
  // create participant if doesn't exist
1283
- remoteParticipant = this.getOrCreateParticipant(info.sid, info);
1284
- if (!isNewParticipant) {
1285
- // just update, no events
1286
- remoteParticipant.updateInfo(info);
1287
- }
1302
+ remoteParticipant = this.getOrCreateParticipant(info.identity, info);
1288
1303
  }
1289
1304
  });
1290
1305
  };
1291
1306
 
1292
- private handleParticipantDisconnected(sid: string, participant?: RemoteParticipant) {
1307
+ private handleParticipantDisconnected(identity: string, participant?: RemoteParticipant) {
1293
1308
  // remove and send event
1294
- this.participants.delete(sid);
1309
+ this.remoteParticipants.delete(identity);
1295
1310
  if (!participant) {
1296
1311
  return;
1297
1312
  }
1298
1313
 
1299
- this.identityToSid.delete(participant.identity);
1300
- participant.tracks.forEach((publication) => {
1314
+ participant.trackPublications.forEach((publication) => {
1301
1315
  participant.unpublishTrack(publication.trackSid, true);
1302
1316
  });
1303
1317
  this.emit(RoomEvent.ParticipantDisconnected, participant);
@@ -1314,7 +1328,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1314
1328
  this.localParticipant.setIsSpeaking(true);
1315
1329
  activeSpeakers.push(this.localParticipant);
1316
1330
  } else {
1317
- const p = this.participants.get(speaker.sid);
1331
+ const p = this.getRemoteParticipantBySid(speaker.sid);
1318
1332
  if (p) {
1319
1333
  p.audioLevel = speaker.level;
1320
1334
  p.setIsSpeaking(true);
@@ -1327,7 +1341,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1327
1341
  this.localParticipant.audioLevel = 0;
1328
1342
  this.localParticipant.setIsSpeaking(false);
1329
1343
  }
1330
- this.participants.forEach((p) => {
1344
+ this.remoteParticipants.forEach((p) => {
1331
1345
  if (!seenSids[p.sid]) {
1332
1346
  p.audioLevel = 0;
1333
1347
  p.setIsSpeaking(false);
@@ -1345,7 +1359,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1345
1359
  lastSpeakers.set(p.sid, p);
1346
1360
  });
1347
1361
  speakerUpdates.forEach((speaker) => {
1348
- let p: Participant | undefined = this.participants.get(speaker.sid);
1362
+ let p: Participant | undefined = this.getRemoteParticipantBySid(speaker.sid);
1349
1363
  if (speaker.sid === this.localParticipant.sid) {
1350
1364
  p = this.localParticipant;
1351
1365
  }
@@ -1369,11 +1383,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1369
1383
 
1370
1384
  private handleStreamStateUpdate = (streamStateUpdate: StreamStateUpdate) => {
1371
1385
  streamStateUpdate.streamStates.forEach((streamState) => {
1372
- const participant = this.participants.get(streamState.participantSid);
1386
+ const participant = this.getRemoteParticipantBySid(streamState.participantSid);
1373
1387
  if (!participant) {
1374
1388
  return;
1375
1389
  }
1376
- const pub = participant.getTrackPublication(streamState.trackSid);
1390
+ const pub = participant.getTrackPublicationBySid(streamState.trackSid);
1377
1391
  if (!pub || !pub.track) {
1378
1392
  return;
1379
1393
  }
@@ -1389,11 +1403,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1389
1403
  };
1390
1404
 
1391
1405
  private handleSubscriptionPermissionUpdate = (update: SubscriptionPermissionUpdate) => {
1392
- const participant = this.participants.get(update.participantSid);
1406
+ const participant = this.getRemoteParticipantBySid(update.participantSid);
1393
1407
  if (!participant) {
1394
1408
  return;
1395
1409
  }
1396
- const pub = participant.getTrackPublication(update.trackSid);
1410
+ const pub = participant.getTrackPublicationBySid(update.trackSid);
1397
1411
  if (!pub) {
1398
1412
  return;
1399
1413
  }
@@ -1402,13 +1416,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1402
1416
  };
1403
1417
 
1404
1418
  private handleSubscriptionError = (update: SubscriptionResponse) => {
1405
- const participant = Array.from(this.participants.values()).find((p) =>
1406
- p.tracks.has(update.trackSid),
1419
+ const participant = Array.from(this.remoteParticipants.values()).find((p) =>
1420
+ p.trackPublications.has(update.trackSid),
1407
1421
  );
1408
1422
  if (!participant) {
1409
1423
  return;
1410
1424
  }
1411
- const pub = participant.getTrackPublication(update.trackSid);
1425
+ const pub = participant.getTrackPublicationBySid(update.trackSid);
1412
1426
  if (!pub) {
1413
1427
  return;
1414
1428
  }
@@ -1418,7 +1432,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1418
1432
 
1419
1433
  private handleDataPacket = (userPacket: UserPacket, kind: DataPacket_Kind) => {
1420
1434
  // find the participant
1421
- const participant = this.participants.get(userPacket.participantSid);
1435
+ const participant = this.remoteParticipants.get(userPacket.participantIdentity);
1422
1436
 
1423
1437
  this.emit(RoomEvent.DataReceived, userPacket.payload, participant, kind, userPacket.topic);
1424
1438
 
@@ -1478,7 +1492,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1478
1492
  this.localParticipant.setConnectionQuality(info.quality);
1479
1493
  return;
1480
1494
  }
1481
- const participant = this.participants.get(info.participantSid);
1495
+ const participant = this.getRemoteParticipantBySid(info.participantSid);
1482
1496
  if (participant) {
1483
1497
  participant.setConnectionQuality(info.quality);
1484
1498
  }
@@ -1486,12 +1500,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1486
1500
  };
1487
1501
 
1488
1502
  private async acquireAudioContext() {
1489
- if (
1490
- typeof this.options.expWebAudioMix !== 'boolean' &&
1491
- this.options.expWebAudioMix.audioContext
1492
- ) {
1503
+ if (typeof this.options.webAudioMix !== 'boolean' && this.options.webAudioMix.audioContext) {
1493
1504
  // override audio context with custom audio context if supplied by user
1494
- this.audioContext = this.options.expWebAudioMix.audioContext;
1505
+ this.audioContext = this.options.webAudioMix.audioContext;
1495
1506
  } else if (!this.audioContext || this.audioContext.state === 'closed') {
1496
1507
  // by using an AudioContext, it reduces lag on audio elements
1497
1508
  // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
@@ -1508,8 +1519,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1508
1519
  }
1509
1520
  }
1510
1521
 
1511
- if (this.options.expWebAudioMix) {
1512
- this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
1522
+ if (this.options.webAudioMix) {
1523
+ this.remoteParticipants.forEach((participant) =>
1524
+ participant.setAudioContext(this.audioContext),
1525
+ );
1513
1526
  }
1514
1527
 
1515
1528
  this.localParticipant.setAudioContext(this.audioContext);
@@ -1521,17 +1534,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1521
1534
  }
1522
1535
  }
1523
1536
 
1524
- private createParticipant(id: string, info?: ParticipantInfo): RemoteParticipant {
1537
+ private createParticipant(identity: string, info?: ParticipantInfo): RemoteParticipant {
1525
1538
  let participant: RemoteParticipant;
1526
1539
  if (info) {
1527
1540
  participant = RemoteParticipant.fromParticipantInfo(this.engine.client, info);
1528
1541
  } else {
1529
- participant = new RemoteParticipant(this.engine.client, id, '', undefined, undefined, {
1542
+ participant = new RemoteParticipant(this.engine.client, '', identity, undefined, undefined, {
1530
1543
  loggerContextCb: () => this.logContext,
1531
1544
  loggerName: this.options.loggerName,
1532
1545
  });
1533
1546
  }
1534
- if (this.options.expWebAudioMix) {
1547
+ if (this.options.webAudioMix) {
1535
1548
  participant.setAudioContext(this.audioContext);
1536
1549
  }
1537
1550
  if (this.options.audioOutput?.deviceId) {
@@ -1542,14 +1555,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1542
1555
  return participant;
1543
1556
  }
1544
1557
 
1545
- private getOrCreateParticipant(id: string, info: ParticipantInfo): RemoteParticipant {
1546
- if (this.participants.has(id)) {
1547
- return this.participants.get(id) as RemoteParticipant;
1558
+ private getOrCreateParticipant(identity: string, info: ParticipantInfo): RemoteParticipant {
1559
+ if (this.remoteParticipants.has(identity)) {
1560
+ const existingParticipant = this.remoteParticipants.get(identity)!;
1561
+ if (info) {
1562
+ const wasUpdated = existingParticipant.updateInfo(info);
1563
+ if (wasUpdated) {
1564
+ this.sidToIdentity.set(info.sid, info.identity);
1565
+ }
1566
+ }
1567
+ return existingParticipant;
1548
1568
  }
1549
- const participant = this.createParticipant(id, info);
1550
- this.participants.set(id, participant);
1569
+ const participant = this.createParticipant(identity, info);
1570
+ this.remoteParticipants.set(identity, participant);
1551
1571
 
1552
- this.identityToSid.set(info.identity, info.sid);
1572
+ this.sidToIdentity.set(info.sid, info.identity);
1553
1573
  // if we have valid info and the participant wasn't in the map before, we can assume the participant is new
1554
1574
  // firing here to make sure that `ParticipantConnected` fires before the initial track events
1555
1575
  this.emitWhenConnected(RoomEvent.ParticipantConnected, participant);
@@ -1635,11 +1655,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1635
1655
  }
1636
1656
 
1637
1657
  private sendSyncState() {
1638
- const remoteTracks = Array.from(this.participants.values()).reduce((acc, participant) => {
1639
- acc.push(...(participant.getTracks() as RemoteTrackPublication[])); // FIXME would be nice to have this return RemoteTrackPublications directly instead of the type cast
1658
+ const remoteTracks = Array.from(this.remoteParticipants.values()).reduce((acc, participant) => {
1659
+ acc.push(...(participant.getTrackPublications() as RemoteTrackPublication[])); // FIXME would be nice to have this return RemoteTrackPublications directly instead of the type cast
1640
1660
  return acc;
1641
1661
  }, [] as RemoteTrackPublication[]);
1642
- const localTracks = this.localParticipant.getTracks() as LocalTrackPublication[]; // FIXME would be nice to have this return LocalTrackPublications directly instead of the type cast
1662
+ const localTracks = this.localParticipant.getTrackPublications() as LocalTrackPublication[]; // FIXME would be nice to have this return LocalTrackPublications directly instead of the type cast
1643
1663
  this.engine.sendSyncState(remoteTracks, localTracks);
1644
1664
  }
1645
1665
 
@@ -1648,8 +1668,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1648
1668
  * subscription settings.
1649
1669
  */
1650
1670
  private updateSubscriptions() {
1651
- for (const p of this.participants.values()) {
1652
- for (const pub of p.videoTracks.values()) {
1671
+ for (const p of this.remoteParticipants.values()) {
1672
+ for (const pub of p.videoTrackPublications.values()) {
1653
1673
  if (pub.isSubscribed && pub instanceof RemoteTrackPublication) {
1654
1674
  pub.emitTrackUpdate();
1655
1675
  }
@@ -1657,6 +1677,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1657
1677
  }
1658
1678
  }
1659
1679
 
1680
+ private getRemoteParticipantBySid(sid: string): RemoteParticipant | undefined {
1681
+ const identity = this.sidToIdentity.get(sid);
1682
+ if (identity) {
1683
+ return this.remoteParticipants.get(identity);
1684
+ }
1685
+ }
1686
+
1660
1687
  private registerConnectionReconcile() {
1661
1688
  this.clearConnectionReconcile();
1662
1689
  let consecutiveFailures = 0;
@@ -1707,11 +1734,26 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1707
1734
  return true;
1708
1735
  }
1709
1736
 
1737
+ private emitBufferedEvents() {
1738
+ this.bufferedEvents.forEach(([ev, args]) => {
1739
+ this.emit(ev, ...args);
1740
+ });
1741
+ this.bufferedEvents = [];
1742
+ }
1743
+
1710
1744
  private emitWhenConnected<E extends keyof RoomEventCallbacks>(
1711
1745
  event: E,
1712
1746
  ...args: Parameters<RoomEventCallbacks[E]>
1713
1747
  ): boolean {
1714
- if (this.state === ConnectionState.Connected) {
1748
+ if (
1749
+ this.state === ConnectionState.Reconnecting ||
1750
+ this.isResuming ||
1751
+ !this.engine ||
1752
+ this.engine.pendingReconnect
1753
+ ) {
1754
+ // in case the room is reconnecting, buffer the events by firing them later after emitting RoomEvent.Reconnected
1755
+ this.bufferedEvents.push([event, args]);
1756
+ } else if (this.state === ConnectionState.Connected) {
1715
1757
  return this.emit(event, ...args);
1716
1758
  }
1717
1759
  return false;
@@ -1943,8 +1985,6 @@ export type RoomEventCallbacks = {
1943
1985
  reconnecting: () => void;
1944
1986
  reconnected: () => void;
1945
1987
  disconnected: (reason?: DisconnectReason) => void;
1946
- /** @deprecated stateChanged has been renamed to connectionStateChanged */
1947
- stateChanged: (state: ConnectionState) => void;
1948
1988
  connectionStateChanged: (state: ConnectionState) => void;
1949
1989
  mediaDevicesChanged: () => void;
1950
1990
  participantConnected: (participant: RemoteParticipant) => void;