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.
- package/README.md +21 -17
- package/dist/livekit-client.esm.mjs +1603 -1493
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +1 -3
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +3 -9
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/livekit_models_pb.d.ts +47 -0
- package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +14 -16
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +0 -4
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +8 -25
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +6 -10
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +9 -6
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/timers.d.ts +4 -5
- package/dist/src/room/timers.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +3 -3
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +2 -2
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +5 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +0 -5
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +11 -3
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +1 -3
- package/dist/ts4.2/src/index.d.ts +3 -3
- package/dist/ts4.2/src/options.d.ts +3 -9
- package/dist/ts4.2/src/proto/livekit_models_pb.d.ts +47 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -0
- package/dist/ts4.2/src/room/Room.d.ts +14 -16
- package/dist/ts4.2/src/room/events.d.ts +0 -4
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +8 -25
- package/dist/ts4.2/src/room/participant/Participant.d.ts +6 -10
- package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +9 -6
- package/dist/ts4.2/src/room/timers.d.ts +4 -5
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +3 -3
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +2 -2
- package/dist/ts4.2/src/room/track/Track.d.ts +5 -0
- package/dist/ts4.2/src/room/track/options.d.ts +0 -5
- package/dist/ts4.2/src/room/types.d.ts +11 -3
- package/dist/ts4.2/src/version.d.ts +1 -1
- package/package.json +8 -7
- package/src/api/SignalClient.ts +10 -10
- package/src/e2ee/E2eeManager.ts +2 -2
- package/src/index.ts +2 -4
- package/src/options.ts +3 -10
- package/src/proto/livekit_models_pb.ts +66 -0
- package/src/room/RTCEngine.ts +6 -1
- package/src/room/Room.ts +169 -129
- package/src/room/defaults.ts +1 -5
- package/src/room/events.ts +0 -5
- package/src/room/participant/LocalParticipant.ts +36 -77
- package/src/room/participant/Participant.ts +23 -24
- package/src/room/participant/RemoteParticipant.ts +27 -24
- package/src/room/track/LocalVideoTrack.test.ts +1 -1
- package/src/room/track/LocalVideoTrack.ts +11 -7
- package/src/room/track/RemoteTrackPublication.ts +2 -7
- package/src/room/track/Track.ts +10 -1
- package/src/room/track/options.ts +0 -6
- package/src/room/types.ts +11 -4
- 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
|
-
/**
|
103
|
-
|
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
|
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.
|
164
|
-
this.
|
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
|
-
/**
|
262
|
-
|
263
|
-
|
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
|
-
|
313
|
-
|
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
|
-
|
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 (
|
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
|
-
|
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.
|
894
|
-
p.
|
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.
|
921
|
-
p.
|
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.
|
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.
|
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.
|
1019
|
-
(this.options.
|
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.
|
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.
|
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.
|
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
|
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 (
|
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.
|
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: ${
|
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.
|
1151
|
-
this.handleParticipantDisconnected(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.
|
1208
|
-
p.
|
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.
|
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.
|
1239
|
-
this.localParticipant.
|
1240
|
-
this.localParticipant.
|
1258
|
+
this.localParticipant.trackPublications.clear();
|
1259
|
+
this.localParticipant.videoTrackPublications.clear();
|
1260
|
+
this.localParticipant.audioTrackPublications.clear();
|
1241
1261
|
|
1242
|
-
this.
|
1262
|
+
this.remoteParticipants.clear();
|
1263
|
+
this.sidToIdentity.clear();
|
1243
1264
|
this.activeSpeakers = [];
|
1244
|
-
if (this.audioContext && typeof this.options.
|
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
|
-
//
|
1269
|
-
|
1270
|
-
if (
|
1271
|
-
|
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.
|
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.
|
1299
|
+
this.handleParticipantDisconnected(info.identity, remoteParticipant);
|
1281
1300
|
} else {
|
1282
1301
|
// create participant if doesn't exist
|
1283
|
-
remoteParticipant = this.getOrCreateParticipant(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(
|
1307
|
+
private handleParticipantDisconnected(identity: string, participant?: RemoteParticipant) {
|
1293
1308
|
// remove and send event
|
1294
|
-
this.
|
1309
|
+
this.remoteParticipants.delete(identity);
|
1295
1310
|
if (!participant) {
|
1296
1311
|
return;
|
1297
1312
|
}
|
1298
1313
|
|
1299
|
-
|
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.
|
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.
|
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.
|
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.
|
1386
|
+
const participant = this.getRemoteParticipantBySid(streamState.participantSid);
|
1373
1387
|
if (!participant) {
|
1374
1388
|
return;
|
1375
1389
|
}
|
1376
|
-
const pub = participant.
|
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.
|
1406
|
+
const participant = this.getRemoteParticipantBySid(update.participantSid);
|
1393
1407
|
if (!participant) {
|
1394
1408
|
return;
|
1395
1409
|
}
|
1396
|
-
const pub = participant.
|
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.
|
1406
|
-
p.
|
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.
|
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.
|
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.
|
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.
|
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.
|
1512
|
-
this.
|
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(
|
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,
|
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.
|
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(
|
1546
|
-
if (this.
|
1547
|
-
|
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(
|
1550
|
-
this.
|
1569
|
+
const participant = this.createParticipant(identity, info);
|
1570
|
+
this.remoteParticipants.set(identity, participant);
|
1551
1571
|
|
1552
|
-
this.
|
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.
|
1639
|
-
acc.push(...(participant.
|
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.
|
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.
|
1652
|
-
for (const pub of p.
|
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 (
|
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;
|