livekit-client 1.11.4 → 1.12.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +1 -3
- package/dist/livekit-client.e2ee.worker.js +2 -0
- package/dist/livekit-client.e2ee.worker.js.map +1 -0
- package/dist/livekit-client.e2ee.worker.mjs +1525 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -0
- package/dist/livekit-client.esm.mjs +4749 -4055
- 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 +4 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +45 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -0
- package/dist/src/e2ee/KeyProvider.d.ts +42 -0
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -0
- package/dist/src/e2ee/constants.d.ts +14 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -0
- package/dist/src/e2ee/errors.d.ts +11 -0
- package/dist/src/e2ee/errors.d.ts.map +1 -0
- package/dist/src/e2ee/index.d.ts +4 -0
- package/dist/src/e2ee/index.d.ts.map +1 -0
- package/dist/src/e2ee/types.d.ts +129 -0
- package/dist/src/e2ee/types.d.ts.map +1 -0
- package/dist/src/e2ee/utils.d.ts +24 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +175 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +46 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -0
- package/dist/src/e2ee/worker/e2ee.worker.d.ts +2 -0
- package/dist/src/e2ee/worker/e2ee.worker.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +4 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +2 -2
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +3 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +17 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +14 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +7 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +3 -0
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +2 -2
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +9 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +2 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +4 -1
- package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +45 -0
- package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +42 -0
- package/dist/ts4.2/src/e2ee/constants.d.ts +14 -0
- package/dist/ts4.2/src/e2ee/errors.d.ts +11 -0
- package/dist/ts4.2/src/e2ee/index.d.ts +4 -0
- package/dist/ts4.2/src/e2ee/types.d.ts +129 -0
- package/dist/ts4.2/src/e2ee/utils.d.ts +24 -0
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +175 -0
- package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +46 -0
- package/dist/ts4.2/src/e2ee/worker/e2ee.worker.d.ts +2 -0
- package/dist/ts4.2/src/index.d.ts +1 -0
- package/dist/ts4.2/src/logger.d.ts +4 -1
- package/dist/ts4.2/src/options.d.ts +5 -0
- package/dist/ts4.2/src/proto/livekit_models.d.ts +2 -2
- package/dist/ts4.2/src/room/PCTransport.d.ts +3 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +17 -3
- package/dist/ts4.2/src/room/Room.d.ts +10 -0
- package/dist/ts4.2/src/room/events.d.ts +14 -2
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -2
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +3 -0
- package/dist/ts4.2/src/room/track/options.d.ts +6 -6
- package/dist/ts4.2/src/room/track/utils.d.ts +9 -0
- package/dist/ts4.2/src/room/utils.d.ts +2 -0
- package/package.json +17 -7
- package/src/api/SignalClient.ts +28 -9
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/websocket.ts +1 -0
- package/src/e2ee/E2eeManager.ts +374 -0
- package/src/e2ee/KeyProvider.ts +77 -0
- package/src/e2ee/constants.ts +40 -0
- package/src/e2ee/errors.ts +16 -0
- package/src/e2ee/index.ts +3 -0
- package/src/e2ee/types.ts +160 -0
- package/src/e2ee/utils.ts +127 -0
- package/src/e2ee/worker/FrameCryptor.test.ts +21 -0
- package/src/e2ee/worker/FrameCryptor.ts +614 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +129 -0
- package/src/e2ee/worker/e2ee.worker.ts +217 -0
- package/src/e2ee/worker/tsconfig.json +6 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +10 -2
- package/src/options.ts +6 -0
- package/src/proto/livekit_models.ts +12 -12
- package/src/room/PCTransport.ts +39 -9
- package/src/room/RTCEngine.ts +127 -34
- package/src/room/Room.ts +77 -22
- package/src/room/defaults.ts +1 -1
- package/src/room/events.ts +14 -0
- package/src/room/participant/LocalParticipant.ts +52 -8
- package/src/room/participant/Participant.ts +4 -0
- package/src/room/track/LocalTrack.ts +5 -4
- package/src/room/track/RemoteVideoTrack.ts +2 -2
- package/src/room/track/TrackPublication.ts +9 -1
- package/src/room/track/options.ts +3 -2
- package/src/room/track/utils.ts +27 -0
- package/src/room/utils.ts +5 -0
- package/src/room/worker.d.ts +4 -0
- package/src/test/MockMediaStreamTrack.ts +1 -0
package/src/room/RTCEngine.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import EventEmitter from 'eventemitter3';
|
2
|
+
import type { MediaAttributes } from 'sdp-transform';
|
2
3
|
import { SignalClient } from '../api/SignalClient';
|
3
4
|
import type { SignalOptions } from '../api/SignalClient';
|
4
5
|
import log from '../logger';
|
@@ -9,17 +10,23 @@ import {
|
|
9
10
|
DataPacket,
|
10
11
|
DataPacket_Kind,
|
11
12
|
DisconnectReason,
|
13
|
+
ParticipantInfo,
|
12
14
|
ReconnectReason,
|
15
|
+
Room as RoomModel,
|
13
16
|
SpeakerInfo,
|
14
17
|
TrackInfo,
|
15
18
|
UserPacket,
|
16
19
|
} from '../proto/livekit_models';
|
17
20
|
import {
|
18
21
|
AddTrackRequest,
|
22
|
+
ConnectionQualityUpdate,
|
19
23
|
JoinResponse,
|
20
24
|
LeaveRequest,
|
21
25
|
ReconnectResponse,
|
22
26
|
SignalTarget,
|
27
|
+
StreamStateUpdate,
|
28
|
+
SubscriptionPermissionUpdate,
|
29
|
+
SubscriptionResponse,
|
23
30
|
TrackPublishedResponse,
|
24
31
|
} from '../proto/livekit_rtc';
|
25
32
|
import PCTransport, { PCEvents } from './PCTransport';
|
@@ -43,6 +50,7 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
|
|
43
50
|
import {
|
44
51
|
Mutex,
|
45
52
|
isCloud,
|
53
|
+
isVideoCodec,
|
46
54
|
isWeb,
|
47
55
|
sleep,
|
48
56
|
supportsAddTrack,
|
@@ -161,6 +169,17 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
161
169
|
[DataPacket_Kind.LOSSY, true],
|
162
170
|
[DataPacket_Kind.RELIABLE, true],
|
163
171
|
]);
|
172
|
+
|
173
|
+
this.client.onParticipantUpdate = (updates) =>
|
174
|
+
this.emit(EngineEvent.ParticipantUpdate, updates);
|
175
|
+
this.client.onConnectionQuality = (update) =>
|
176
|
+
this.emit(EngineEvent.ConnectionQualityUpdate, update);
|
177
|
+
this.client.onRoomUpdate = (update) => this.emit(EngineEvent.RoomUpdate, update);
|
178
|
+
this.client.onSubscriptionError = (resp) => this.emit(EngineEvent.SubscriptionError, resp);
|
179
|
+
this.client.onSubscriptionPermissionUpdate = (update) =>
|
180
|
+
this.emit(EngineEvent.SubscriptionPermissionUpdate, update);
|
181
|
+
this.client.onSpeakersChanged = (update) => this.emit(EngineEvent.SpeakersChanged, update);
|
182
|
+
this.client.onStreamStateUpdate = (update) => this.emit(EngineEvent.StreamStateChanged, update);
|
164
183
|
}
|
165
184
|
|
166
185
|
async join(
|
@@ -187,6 +206,8 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
187
206
|
if (!this.subscriberPrimary) {
|
188
207
|
this.negotiate();
|
189
208
|
}
|
209
|
+
this.setupSignalClientCallbacks();
|
210
|
+
|
190
211
|
this.clientConfiguration = joinResponse.clientConfiguration;
|
191
212
|
|
192
213
|
return joinResponse;
|
@@ -217,30 +238,63 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
217
238
|
this.removeAllListeners();
|
218
239
|
this.deregisterOnLineListener();
|
219
240
|
this.clearPendingReconnect();
|
220
|
-
|
221
|
-
|
222
|
-
try {
|
223
|
-
// TODO: react-native-webrtc doesn't have removeTrack yet.
|
224
|
-
if (this.publisher?.pc.removeTrack) {
|
225
|
-
this.publisher?.pc.removeTrack(sender);
|
226
|
-
}
|
227
|
-
} catch (e) {
|
228
|
-
log.warn('could not removeTrack', { error: e });
|
229
|
-
}
|
230
|
-
});
|
231
|
-
this.publisher.close();
|
232
|
-
this.publisher = undefined;
|
233
|
-
}
|
234
|
-
if (this.subscriber) {
|
235
|
-
this.subscriber.close();
|
236
|
-
this.subscriber = undefined;
|
237
|
-
}
|
238
|
-
await this.client.close();
|
241
|
+
await this.cleanupPeerConnections();
|
242
|
+
await this.cleanupClient();
|
239
243
|
} finally {
|
240
244
|
unlock();
|
241
245
|
}
|
242
246
|
}
|
243
247
|
|
248
|
+
async cleanupPeerConnections() {
|
249
|
+
if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
|
250
|
+
this.publisher.pc.getSenders().forEach((sender) => {
|
251
|
+
try {
|
252
|
+
// TODO: react-native-webrtc doesn't have removeTrack yet.
|
253
|
+
if (this.publisher?.pc.removeTrack) {
|
254
|
+
this.publisher?.pc.removeTrack(sender);
|
255
|
+
}
|
256
|
+
} catch (e) {
|
257
|
+
log.warn('could not removeTrack', { error: e });
|
258
|
+
}
|
259
|
+
});
|
260
|
+
}
|
261
|
+
if (this.publisher) {
|
262
|
+
this.publisher.close();
|
263
|
+
this.publisher = undefined;
|
264
|
+
}
|
265
|
+
if (this.subscriber) {
|
266
|
+
this.subscriber.close();
|
267
|
+
this.subscriber = undefined;
|
268
|
+
}
|
269
|
+
|
270
|
+
this.primaryPC = undefined;
|
271
|
+
|
272
|
+
const dcCleanup = (dc: RTCDataChannel | undefined) => {
|
273
|
+
if (!dc) return;
|
274
|
+
dc.close();
|
275
|
+
dc.onbufferedamountlow = null;
|
276
|
+
dc.onclose = null;
|
277
|
+
dc.onclosing = null;
|
278
|
+
dc.onerror = null;
|
279
|
+
dc.onmessage = null;
|
280
|
+
dc.onopen = null;
|
281
|
+
};
|
282
|
+
dcCleanup(this.lossyDC);
|
283
|
+
dcCleanup(this.lossyDCSub);
|
284
|
+
dcCleanup(this.reliableDC);
|
285
|
+
dcCleanup(this.reliableDCSub);
|
286
|
+
|
287
|
+
this.lossyDC = undefined;
|
288
|
+
this.lossyDCSub = undefined;
|
289
|
+
this.reliableDC = undefined;
|
290
|
+
this.reliableDCSub = undefined;
|
291
|
+
}
|
292
|
+
|
293
|
+
async cleanupClient() {
|
294
|
+
await this.client.close();
|
295
|
+
this.client.resetCallbacks();
|
296
|
+
}
|
297
|
+
|
244
298
|
addTrack(req: AddTrackRequest): Promise<TrackInfo> {
|
245
299
|
if (this.pendingTrackResolvers[req.cid]) {
|
246
300
|
throw new TrackInvalidError('a track with the same ID has already been published');
|
@@ -313,6 +367,14 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
313
367
|
this.participantSid = joinResponse.participant?.sid;
|
314
368
|
|
315
369
|
const rtcConfig = this.makeRTCConfiguration(joinResponse);
|
370
|
+
|
371
|
+
if (this.signalOpts?.e2eeEnabled) {
|
372
|
+
log.debug('E2EE - setting up transports with insertable streams');
|
373
|
+
// this makes sure that no data is sent before the transforms are ready
|
374
|
+
// @ts-ignore
|
375
|
+
rtcConfig.encodedInsertableStreams = true;
|
376
|
+
}
|
377
|
+
|
316
378
|
const googConstraints = { optional: [{ googDscp: true }] };
|
317
379
|
this.publisher = new PCTransport(rtcConfig, googConstraints);
|
318
380
|
this.subscriber = new PCTransport(rtcConfig);
|
@@ -384,7 +446,9 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
384
446
|
};
|
385
447
|
|
386
448
|
this.createDataChannels();
|
449
|
+
}
|
387
450
|
|
451
|
+
private setupSignalClientCallbacks() {
|
388
452
|
// configure signaling client
|
389
453
|
this.client.onAnswer = async (sd) => {
|
390
454
|
if (!this.publisher) {
|
@@ -392,7 +456,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
392
456
|
}
|
393
457
|
log.debug('received server answer', {
|
394
458
|
RTCSdpType: sd.type,
|
395
|
-
signalingState: this.publisher.pc.signalingState,
|
459
|
+
signalingState: this.publisher.pc.signalingState.toString(),
|
396
460
|
});
|
397
461
|
await this.publisher.setRemoteDescription(sd);
|
398
462
|
};
|
@@ -417,7 +481,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
417
481
|
}
|
418
482
|
log.debug('received server offer', {
|
419
483
|
RTCSdpType: sd.type,
|
420
|
-
signalingState: this.subscriber.pc.signalingState,
|
484
|
+
signalingState: this.subscriber.pc.signalingState.toString(),
|
421
485
|
});
|
422
486
|
await this.subscriber.setRemoteDescription(sd);
|
423
487
|
|
@@ -646,11 +710,13 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
646
710
|
encodings?: RTCRtpEncodingParameters[],
|
647
711
|
) {
|
648
712
|
if (supportsTransceiver()) {
|
649
|
-
|
713
|
+
const sender = await this.createTransceiverRTCRtpSender(track, opts, encodings);
|
714
|
+
return sender;
|
650
715
|
}
|
651
716
|
if (supportsAddTrack()) {
|
652
|
-
log.
|
653
|
-
|
717
|
+
log.warn('using add-track fallback');
|
718
|
+
const sender = await this.createRTCRtpSender(track.mediaStreamTrack);
|
719
|
+
return sender;
|
654
720
|
}
|
655
721
|
throw new UnexpectedConnectionState('Required webRTC APIs not supported on this device');
|
656
722
|
}
|
@@ -662,7 +728,6 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
662
728
|
encodings?: RTCRtpEncodingParameters[],
|
663
729
|
) {
|
664
730
|
// store RTCRtpSender
|
665
|
-
// @ts-ignore
|
666
731
|
if (supportsTransceiver()) {
|
667
732
|
return this.createSimulcastTransceiverSender(track, simulcastTrack, opts, encodings);
|
668
733
|
}
|
@@ -683,7 +748,13 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
683
748
|
throw new UnexpectedConnectionState('publisher is closed');
|
684
749
|
}
|
685
750
|
|
686
|
-
const
|
751
|
+
const streams: MediaStream[] = [];
|
752
|
+
|
753
|
+
if (track.mediaStream) {
|
754
|
+
streams.push(track.mediaStream);
|
755
|
+
}
|
756
|
+
|
757
|
+
const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly', streams };
|
687
758
|
if (encodings) {
|
688
759
|
transceiverInit.sendEncodings = encodings;
|
689
760
|
}
|
@@ -692,6 +763,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
692
763
|
track.mediaStreamTrack,
|
693
764
|
transceiverInit,
|
694
765
|
);
|
766
|
+
|
695
767
|
if (track.kind === Track.Kind.Video && opts.videoCodec) {
|
696
768
|
this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
|
697
769
|
track.codec = opts.videoCodec;
|
@@ -819,7 +891,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
819
891
|
}
|
820
892
|
|
821
893
|
if (recoverable) {
|
822
|
-
this.handleDisconnect('reconnect', ReconnectReason.
|
894
|
+
this.handleDisconnect('reconnect', ReconnectReason.RR_UNKNOWN);
|
823
895
|
} else {
|
824
896
|
log.info(
|
825
897
|
`could not recover connection after ${this.reconnectAttempts} attempts, ${
|
@@ -858,12 +930,8 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
858
930
|
if (this.client.isConnected) {
|
859
931
|
await this.client.sendLeave();
|
860
932
|
}
|
861
|
-
await this.
|
862
|
-
this.
|
863
|
-
this.publisher?.close();
|
864
|
-
this.publisher = undefined;
|
865
|
-
this.subscriber?.close();
|
866
|
-
this.subscriber = undefined;
|
933
|
+
await this.cleanupPeerConnections();
|
934
|
+
await this.cleanupClient();
|
867
935
|
|
868
936
|
let joinResponse: JoinResponse;
|
869
937
|
try {
|
@@ -919,6 +987,7 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
919
987
|
this.emit(EngineEvent.Resuming);
|
920
988
|
|
921
989
|
try {
|
990
|
+
this.setupSignalClientCallbacks();
|
922
991
|
const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
|
923
992
|
if (res) {
|
924
993
|
const rtcConfig = this.makeRTCConfiguration(res);
|
@@ -1178,6 +1247,9 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
1178
1247
|
return;
|
1179
1248
|
};
|
1180
1249
|
|
1250
|
+
if (this.isClosed) {
|
1251
|
+
reject('cannot negotiate on closed engine');
|
1252
|
+
}
|
1181
1253
|
this.on(EngineEvent.Closing, handleClosed);
|
1182
1254
|
|
1183
1255
|
const negotiationTimeout = setTimeout(() => {
|
@@ -1197,13 +1269,24 @@ export default class RTCEngine extends EventEmitter<EngineEventCallbacks> {
|
|
1197
1269
|
});
|
1198
1270
|
});
|
1199
1271
|
|
1272
|
+
this.publisher.once(PCEvents.RTPVideoPayloadTypes, (rtpTypes: MediaAttributes['rtp']) => {
|
1273
|
+
const rtpMap = new Map<number, VideoCodec>();
|
1274
|
+
rtpTypes.forEach((rtp) => {
|
1275
|
+
const codec = rtp.codec.toLowerCase();
|
1276
|
+
if (isVideoCodec(codec)) {
|
1277
|
+
rtpMap.set(rtp.payload, codec);
|
1278
|
+
}
|
1279
|
+
});
|
1280
|
+
this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
|
1281
|
+
});
|
1282
|
+
|
1200
1283
|
this.publisher.negotiate((e) => {
|
1201
1284
|
cleanup();
|
1202
1285
|
reject(e);
|
1203
1286
|
if (e instanceof NegotiationError) {
|
1204
1287
|
this.fullReconnectOnNext = true;
|
1205
1288
|
}
|
1206
|
-
this.handleDisconnect('negotiation', ReconnectReason.
|
1289
|
+
this.handleDisconnect('negotiation', ReconnectReason.RR_UNKNOWN);
|
1207
1290
|
});
|
1208
1291
|
});
|
1209
1292
|
}
|
@@ -1318,5 +1401,15 @@ export type EngineEventCallbacks = {
|
|
1318
1401
|
activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
|
1319
1402
|
dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void;
|
1320
1403
|
transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
|
1404
|
+
/** @internal */
|
1405
|
+
trackSenderAdded: (track: Track, sender: RTCRtpSender) => void;
|
1406
|
+
rtpVideoMapUpdate: (rtpMap: Map<number, VideoCodec>) => void;
|
1321
1407
|
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
1408
|
+
participantUpdate: (infos: ParticipantInfo[]) => void;
|
1409
|
+
roomUpdate: (room: RoomModel) => void;
|
1410
|
+
connectionQualityUpdate: (update: ConnectionQualityUpdate) => void;
|
1411
|
+
speakersChanged: (speakerUpdates: SpeakerInfo[]) => void;
|
1412
|
+
streamStateChanged: (update: StreamStateUpdate) => void;
|
1413
|
+
subscriptionError: (resp: SubscriptionResponse) => void;
|
1414
|
+
subscriptionPermissionUpdate: (update: SubscriptionPermissionUpdate) => void;
|
1322
1415
|
};
|
package/src/room/Room.ts
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import EventEmitter from 'eventemitter3';
|
2
2
|
import 'webrtc-adapter';
|
3
3
|
import { toProtoSessionDescription } from '../api/SignalClient';
|
4
|
+
import { EncryptionEvent } from '../e2ee';
|
5
|
+
import { E2EEManager } from '../e2ee/E2eeManager';
|
4
6
|
import log from '../logger';
|
5
7
|
import type {
|
6
8
|
InternalRoomConnectOptions,
|
@@ -56,7 +58,7 @@ import RemoteTrackPublication from './track/RemoteTrackPublication';
|
|
56
58
|
import { Track } from './track/Track';
|
57
59
|
import type { TrackPublication } from './track/TrackPublication';
|
58
60
|
import type { AdaptiveStreamSettings } from './track/types';
|
59
|
-
import { getNewAudioContext } from './track/utils';
|
61
|
+
import { getNewAudioContext, sourceToKind } from './track/utils';
|
60
62
|
import type { SimulationOptions, SimulationScenario } from './types';
|
61
63
|
import {
|
62
64
|
Future,
|
@@ -112,6 +114,9 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
112
114
|
/** options of room */
|
113
115
|
options: InternalRoomOptions;
|
114
116
|
|
117
|
+
/** reflects the sender encryption status of the local participant */
|
118
|
+
isE2EEEnabled: boolean = false;
|
119
|
+
|
115
120
|
private roomInfo?: RoomModel;
|
116
121
|
|
117
122
|
private identityToSid: Map<string, string>;
|
@@ -131,6 +136,8 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
131
136
|
|
132
137
|
private disconnectLock: Mutex;
|
133
138
|
|
139
|
+
private e2eeManager: E2EEManager | undefined;
|
140
|
+
|
134
141
|
private cachedParticipantSids: Array<string>;
|
135
142
|
|
136
143
|
private connectionReconcileInterval?: ReturnType<typeof setInterval>;
|
@@ -180,6 +187,43 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
180
187
|
if (this.options.audioOutput?.deviceId) {
|
181
188
|
this.switchActiveDevice('audiooutput', unwrapConstraint(this.options.audioOutput.deviceId));
|
182
189
|
}
|
190
|
+
|
191
|
+
if (this.options.e2ee) {
|
192
|
+
this.setupE2EE();
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
/**
|
197
|
+
* @experimental
|
198
|
+
*/
|
199
|
+
async setE2EEEnabled(enabled: boolean) {
|
200
|
+
if (this.e2eeManager) {
|
201
|
+
await Promise.all([
|
202
|
+
this.localParticipant.setE2EEEnabled(enabled),
|
203
|
+
this.e2eeManager.setParticipantCryptorEnabled(enabled),
|
204
|
+
]);
|
205
|
+
} else {
|
206
|
+
throw Error('e2ee not configured, please set e2ee settings within the room options');
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
private setupE2EE() {
|
211
|
+
if (this.options.e2ee) {
|
212
|
+
this.e2eeManager = new E2EEManager(this.options.e2ee);
|
213
|
+
this.e2eeManager.on(
|
214
|
+
EncryptionEvent.ParticipantEncryptionStatusChanged,
|
215
|
+
(enabled, participant) => {
|
216
|
+
if (participant instanceof LocalParticipant) {
|
217
|
+
this.isE2EEEnabled = enabled;
|
218
|
+
}
|
219
|
+
this.emit(RoomEvent.ParticipantEncryptionStatusChanged, enabled, participant);
|
220
|
+
},
|
221
|
+
);
|
222
|
+
this.e2eeManager.on(EncryptionEvent.Error, (error) =>
|
223
|
+
this.emit(RoomEvent.EncryptionError, error),
|
224
|
+
);
|
225
|
+
this.e2eeManager?.setup(this);
|
226
|
+
}
|
183
227
|
}
|
184
228
|
|
185
229
|
/**
|
@@ -219,15 +263,14 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
219
263
|
|
220
264
|
this.engine = new RTCEngine(this.options);
|
221
265
|
|
222
|
-
this.engine.client.onParticipantUpdate = this.handleParticipantUpdates;
|
223
|
-
this.engine.client.onRoomUpdate = this.handleRoomUpdate;
|
224
|
-
this.engine.client.onSpeakersChanged = this.handleSpeakersChanged;
|
225
|
-
this.engine.client.onStreamStateUpdate = this.handleStreamStateUpdate;
|
226
|
-
this.engine.client.onSubscriptionPermissionUpdate = this.handleSubscriptionPermissionUpdate;
|
227
|
-
this.engine.client.onConnectionQuality = this.handleConnectionQualityUpdate;
|
228
|
-
this.engine.client.onSubscriptionError = this.handleSubscriptionError;
|
229
|
-
|
230
266
|
this.engine
|
267
|
+
.on(EngineEvent.ParticipantUpdate, this.handleParticipantUpdates)
|
268
|
+
.on(EngineEvent.RoomUpdate, this.handleRoomUpdate)
|
269
|
+
.on(EngineEvent.SpeakersChanged, this.handleSpeakersChanged)
|
270
|
+
.on(EngineEvent.StreamStateChanged, this.handleStreamStateUpdate)
|
271
|
+
.on(EngineEvent.ConnectionQualityUpdate, this.handleConnectionQualityUpdate)
|
272
|
+
.on(EngineEvent.SubscriptionError, this.handleSubscriptionError)
|
273
|
+
.on(EngineEvent.SubscriptionPermissionUpdate, this.handleSubscriptionPermissionUpdate)
|
231
274
|
.on(
|
232
275
|
EngineEvent.MediaTrackAdded,
|
233
276
|
(mediaTrack: MediaStreamTrack, stream: MediaStream, receiver?: RTCRtpReceiver) => {
|
@@ -273,6 +316,9 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
273
316
|
if (this.localParticipant) {
|
274
317
|
this.localParticipant.setupEngine(this.engine);
|
275
318
|
}
|
319
|
+
if (this.e2eeManager) {
|
320
|
+
this.e2eeManager.setupEngine(this.engine);
|
321
|
+
}
|
276
322
|
}
|
277
323
|
|
278
324
|
/**
|
@@ -392,6 +438,7 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
392
438
|
adaptiveStream:
|
393
439
|
typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
|
394
440
|
maxRetries: connectOptions.maxRetries,
|
441
|
+
e2eeEnabled: !!this.e2eeManager,
|
395
442
|
},
|
396
443
|
abortController.signal,
|
397
444
|
);
|
@@ -586,10 +633,8 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
586
633
|
let req: SimulateScenario | undefined;
|
587
634
|
switch (scenario) {
|
588
635
|
case 'signal-reconnect':
|
589
|
-
|
590
|
-
|
591
|
-
this.engine.client.onClose('simulate disconnect');
|
592
|
-
}
|
636
|
+
// @ts-expect-error function is private
|
637
|
+
await this.engine.client.handleOnClose('simulate disconnect');
|
593
638
|
break;
|
594
639
|
case 'speaker':
|
595
640
|
req = SimulateScenario.fromPartial({
|
@@ -625,17 +670,13 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
625
670
|
break;
|
626
671
|
case 'resume-reconnect':
|
627
672
|
this.engine.failNext();
|
628
|
-
|
629
|
-
|
630
|
-
this.engine.client.onClose('simulate resume-reconnect');
|
631
|
-
}
|
673
|
+
// @ts-expect-error function is private
|
674
|
+
await this.engine.client.handleOnClose('simulate resume-disconnect');
|
632
675
|
break;
|
633
676
|
case 'full-reconnect':
|
634
677
|
this.engine.fullReconnectOnNext = true;
|
635
|
-
|
636
|
-
|
637
|
-
this.engine.client.onClose('simulate full-reconnect');
|
638
|
-
}
|
678
|
+
// @ts-expect-error function is private
|
679
|
+
await this.engine.client.handleOnClose('simulate full-reconnect');
|
639
680
|
break;
|
640
681
|
case 'force-tcp':
|
641
682
|
case 'force-tls':
|
@@ -1522,6 +1563,16 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
1522
1563
|
this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
|
1523
1564
|
}
|
1524
1565
|
}
|
1566
|
+
const deviceId = await pub.track?.getDeviceId();
|
1567
|
+
const deviceKind = sourceToKind(pub.source);
|
1568
|
+
if (
|
1569
|
+
deviceKind &&
|
1570
|
+
deviceId &&
|
1571
|
+
deviceId !== this.localParticipant.activeDeviceMap.get(deviceKind)
|
1572
|
+
) {
|
1573
|
+
this.localParticipant.activeDeviceMap.set(deviceKind, deviceId);
|
1574
|
+
this.emit(RoomEvent.ActiveDeviceChanged, deviceKind, deviceId);
|
1575
|
+
}
|
1525
1576
|
};
|
1526
1577
|
|
1527
1578
|
private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
|
@@ -1595,7 +1646,9 @@ class Room extends EventEmitter<RoomEventCallbacks> {
|
|
1595
1646
|
}),
|
1596
1647
|
new LocalVideoTrack(
|
1597
1648
|
publishOptions.useRealTracks
|
1598
|
-
? (
|
1649
|
+
? (
|
1650
|
+
await window.navigator.mediaDevices.getUserMedia({ video: true })
|
1651
|
+
).getVideoTracks()[0]
|
1599
1652
|
: createDummyVideoStreamTrack(
|
1600
1653
|
160 * participantOptions.aspectRatios[0] ?? 1,
|
1601
1654
|
160,
|
@@ -1758,6 +1811,8 @@ export type RoomEventCallbacks = {
|
|
1758
1811
|
audioPlaybackChanged: (playing: boolean) => void;
|
1759
1812
|
signalConnected: () => void;
|
1760
1813
|
recordingStatusChanged: (recording: boolean) => void;
|
1814
|
+
participantEncryptionStatusChanged: (encrypted: boolean, participant?: Participant) => void;
|
1815
|
+
encryptionError: (error: Error) => void;
|
1761
1816
|
dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
|
1762
1817
|
activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
|
1763
1818
|
};
|
package/src/room/defaults.ts
CHANGED
@@ -20,7 +20,7 @@ export const publishDefaults: TrackPublishDefaults = {
|
|
20
20
|
screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
|
21
21
|
stopMicTrackOnMute: false,
|
22
22
|
videoCodec: 'vp8',
|
23
|
-
backupCodec:
|
23
|
+
backupCodec: false,
|
24
24
|
} as const;
|
25
25
|
|
26
26
|
export const audioDefaults: AudioCaptureOptions = {
|
package/src/room/events.ts
CHANGED
@@ -273,6 +273,9 @@ export enum RoomEvent {
|
|
273
273
|
*/
|
274
274
|
RecordingStatusChanged = 'recordingStatusChanged',
|
275
275
|
|
276
|
+
ParticipantEncryptionStatusChanged = 'participantEncryptionStatusChanged',
|
277
|
+
|
278
|
+
EncryptionError = 'encryptionError',
|
276
279
|
/**
|
277
280
|
* Emits whenever the current buffer status of a data channel changes
|
278
281
|
* args: (isLow: boolean, kind: [[DataPacket_Kind]])
|
@@ -443,6 +446,9 @@ export enum ParticipantEvent {
|
|
443
446
|
* args: (prevPermissions: [[ParticipantPermission]])
|
444
447
|
*/
|
445
448
|
ParticipantPermissionsChanged = 'participantPermissionsChanged',
|
449
|
+
|
450
|
+
/** @internal */
|
451
|
+
PCTrackAdded = 'pcTrackAdded',
|
446
452
|
}
|
447
453
|
|
448
454
|
/** @internal */
|
@@ -460,7 +466,15 @@ export enum EngineEvent {
|
|
460
466
|
MediaTrackAdded = 'mediaTrackAdded',
|
461
467
|
ActiveSpeakersUpdate = 'activeSpeakersUpdate',
|
462
468
|
DataPacketReceived = 'dataPacketReceived',
|
469
|
+
RTPVideoMapUpdate = 'rtpVideoMapUpdate',
|
463
470
|
DCBufferStatusChanged = 'dcBufferStatusChanged',
|
471
|
+
ParticipantUpdate = 'participantUpdate',
|
472
|
+
RoomUpdate = 'roomUpdate',
|
473
|
+
SpeakersChanged = 'speakersChanged',
|
474
|
+
StreamStateChanged = 'streamStateChanged',
|
475
|
+
ConnectionQualityUpdate = 'connectionQualityUpdate',
|
476
|
+
SubscriptionError = 'subscriptionError',
|
477
|
+
SubscriptionPermissionUpdate = 'subscriptionPermissionUpdate',
|
464
478
|
}
|
465
479
|
|
466
480
|
export enum TrackEvent {
|
@@ -1,6 +1,12 @@
|
|
1
1
|
import log from '../../logger';
|
2
2
|
import type { InternalRoomOptions } from '../../options';
|
3
|
-
import {
|
3
|
+
import {
|
4
|
+
DataPacket,
|
5
|
+
DataPacket_Kind,
|
6
|
+
Encryption_Type,
|
7
|
+
ParticipantInfo,
|
8
|
+
ParticipantPermission,
|
9
|
+
} from '../../proto/livekit_models';
|
4
10
|
import {
|
5
11
|
AddTrackRequest,
|
6
12
|
DataChannelInfo,
|
@@ -68,6 +74,8 @@ export default class LocalParticipant extends Participant {
|
|
68
74
|
// keep a pointer to room options
|
69
75
|
private roomOptions: InternalRoomOptions;
|
70
76
|
|
77
|
+
private encryptionType: Encryption_Type = Encryption_Type.NONE;
|
78
|
+
|
71
79
|
private reconnectFuture?: Future<void>;
|
72
80
|
|
73
81
|
/** @internal */
|
@@ -214,6 +222,22 @@ export default class LocalParticipant extends Participant {
|
|
214
222
|
return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options, publishOptions);
|
215
223
|
}
|
216
224
|
|
225
|
+
/** @internal */
|
226
|
+
setPermissions(permissions: ParticipantPermission): boolean {
|
227
|
+
const prevPermissions = this.permissions;
|
228
|
+
const changed = super.setPermissions(permissions);
|
229
|
+
if (changed && prevPermissions) {
|
230
|
+
this.emit(ParticipantEvent.ParticipantPermissionsChanged, prevPermissions);
|
231
|
+
}
|
232
|
+
return changed;
|
233
|
+
}
|
234
|
+
|
235
|
+
/** @internal */
|
236
|
+
async setE2EEEnabled(enabled: boolean) {
|
237
|
+
this.encryptionType = enabled ? Encryption_Type.GCM : Encryption_Type.NONE;
|
238
|
+
await this.republishAllTracks(undefined, false);
|
239
|
+
}
|
240
|
+
|
217
241
|
/**
|
218
242
|
* Enable or disable publishing for a track by source. This serves as a simple
|
219
243
|
* way to manage the common tracks (camera, mic, or screen share).
|
@@ -552,6 +576,12 @@ export default class LocalParticipant extends Participant {
|
|
552
576
|
...options,
|
553
577
|
};
|
554
578
|
|
579
|
+
// disable simulcast if e2ee is set on safari
|
580
|
+
if (isSafari() && this.roomOptions.e2ee) {
|
581
|
+
log.info(`End-to-end encryption is set up, simulcast publishing will be disabled on Safari`);
|
582
|
+
opts.simulcast = false;
|
583
|
+
}
|
584
|
+
|
555
585
|
if (opts.source) {
|
556
586
|
track.source = opts.source;
|
557
587
|
}
|
@@ -624,8 +654,9 @@ export default class LocalParticipant extends Participant {
|
|
624
654
|
muted: track.isMuted,
|
625
655
|
source: Track.sourceToProto(track.source),
|
626
656
|
disableDtx: !(opts.dtx ?? true),
|
657
|
+
encryption: this.encryptionType,
|
627
658
|
stereo: isStereo,
|
628
|
-
disableRed: !(opts.red ?? true),
|
659
|
+
// disableRed: !(opts.red ?? true),
|
629
660
|
});
|
630
661
|
|
631
662
|
// compute encodings and layers for video
|
@@ -760,11 +791,11 @@ export default class LocalParticipant extends Participant {
|
|
760
791
|
|
761
792
|
if (encodings) {
|
762
793
|
if (isFireFox() && track.kind === Track.Kind.Audio) {
|
763
|
-
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
794
|
+
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
764
795
|
livekit-server uses maxaveragebitrate=510000in the answer sdp to permit client to
|
765
|
-
publish high quality audio track. But firefox always uses this value as the actual
|
796
|
+
publish high quality audio track. But firefox always uses this value as the actual
|
766
797
|
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
|
767
|
-
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
798
|
+
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
768
799
|
fix the issue.
|
769
800
|
*/
|
770
801
|
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
@@ -790,7 +821,7 @@ export default class LocalParticipant extends Participant {
|
|
790
821
|
}
|
791
822
|
}
|
792
823
|
|
793
|
-
this.engine.negotiate();
|
824
|
+
await this.engine.negotiate();
|
794
825
|
|
795
826
|
if (track instanceof LocalVideoTrack) {
|
796
827
|
track.startMonitor(this.engine.client);
|
@@ -876,7 +907,7 @@ export default class LocalParticipant extends Participant {
|
|
876
907
|
}
|
877
908
|
await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);
|
878
909
|
|
879
|
-
this.engine.negotiate();
|
910
|
+
await this.engine.negotiate();
|
880
911
|
log.debug(`published ${videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
|
881
912
|
}
|
882
913
|
|
@@ -980,7 +1011,7 @@ export default class LocalParticipant extends Participant {
|
|
980
1011
|
) as LocalTrackPublication[];
|
981
1012
|
}
|
982
1013
|
|
983
|
-
async republishAllTracks(options?: TrackPublishOptions) {
|
1014
|
+
async republishAllTracks(options?: TrackPublishOptions, restartTracks: boolean = true) {
|
984
1015
|
const localPubs: LocalTrackPublication[] = [];
|
985
1016
|
this.tracks.forEach((pub) => {
|
986
1017
|
if (pub.track) {
|
@@ -995,6 +1026,19 @@ export default class LocalParticipant extends Participant {
|
|
995
1026
|
localPubs.map(async (pub) => {
|
996
1027
|
const track = pub.track!;
|
997
1028
|
await this.unpublishTrack(track, false);
|
1029
|
+
if (
|
1030
|
+
restartTracks &&
|
1031
|
+
!track.isMuted &&
|
1032
|
+
(track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
|
1033
|
+
!track.isUserProvided
|
1034
|
+
) {
|
1035
|
+
// generally we need to restart the track before publishing, often a full reconnect
|
1036
|
+
// is necessary because computer had gone to sleep.
|
1037
|
+
log.debug('restarting existing track', {
|
1038
|
+
track: pub.trackSid,
|
1039
|
+
});
|
1040
|
+
await track.restartTrack();
|
1041
|
+
}
|
998
1042
|
await this.publishTrack(track, pub.options);
|
999
1043
|
}),
|
1000
1044
|
);
|
@@ -68,6 +68,10 @@ export default class Participant extends EventEmitter<ParticipantEventCallbacks>
|
|
68
68
|
|
69
69
|
private _connectionQuality: ConnectionQuality = ConnectionQuality.Unknown;
|
70
70
|
|
71
|
+
get isEncrypted() {
|
72
|
+
return this.tracks.size > 0 && Array.from(this.tracks.values()).every((tr) => tr.isEncrypted);
|
73
|
+
}
|
74
|
+
|
71
75
|
/** @internal */
|
72
76
|
constructor(sid: string, identity: string, name?: string, metadata?: string) {
|
73
77
|
super();
|