livekit-client 1.15.10 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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;