livekit-client 1.14.0 → 1.14.2
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +25 -44
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +418 -206
- 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/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +0 -1
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/proto/livekit_models_pb.d.ts +87 -11
- package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc_pb.d.ts +0 -4
- package/dist/src/proto/livekit_rtc_pb.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +20 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts +1 -0
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/timers.d.ts +1 -1
- package/dist/src/room/timers.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +3 -3
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +2 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +6 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +2 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/utils/cloneDeep.d.ts +2 -0
- package/dist/src/utils/cloneDeep.d.ts.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/src/e2ee/utils.d.ts +0 -1
- package/dist/ts4.2/src/proto/livekit_models_pb.d.ts +87 -11
- package/dist/ts4.2/src/proto/livekit_rtc_pb.d.ts +0 -4
- package/dist/ts4.2/src/room/PCTransport.d.ts +20 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -1
- package/dist/ts4.2/src/room/defaults.d.ts +1 -0
- package/dist/ts4.2/src/room/events.d.ts +1 -1
- package/dist/ts4.2/src/room/timers.d.ts +1 -1
- package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -3
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +2 -1
- package/dist/ts4.2/src/room/track/options.d.ts +6 -1
- package/dist/ts4.2/src/room/track/utils.d.ts +1 -0
- package/dist/ts4.2/src/utils/cloneDeep.d.ts +2 -0
- package/dist/ts4.2/src/version.d.ts +1 -1
- package/package.json +14 -14
- package/src/connectionHelper/checks/webrtc.ts +1 -1
- package/src/e2ee/E2eeManager.ts +2 -1
- package/src/e2ee/utils.ts +0 -10
- package/src/e2ee/worker/FrameCryptor.ts +13 -14
- package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -5
- package/src/e2ee/worker/e2ee.worker.ts +3 -1
- package/src/proto/livekit_models_pb.ts +140 -15
- package/src/proto/livekit_rtc_pb.ts +1 -7
- package/src/room/PCTransport.ts +116 -1
- package/src/room/RTCEngine.ts +49 -85
- package/src/room/Room.ts +15 -11
- package/src/room/defaults.ts +4 -2
- package/src/room/events.ts +1 -1
- package/src/room/participant/LocalParticipant.ts +45 -56
- package/src/room/track/LocalAudioTrack.ts +1 -1
- package/src/room/track/LocalTrack.ts +8 -5
- package/src/room/track/LocalVideoTrack.ts +2 -1
- package/src/room/track/RemoteTrack.ts +8 -6
- package/src/room/track/options.ts +7 -7
- package/src/room/track/utils.ts +17 -8
- package/src/room/utils.ts +3 -0
- package/src/utils/cloneDeep.test.ts +54 -0
- package/src/utils/cloneDeep.ts +11 -0
- package/src/version.ts +1 -1
package/src/room/RTCEngine.ts
CHANGED
@@ -108,7 +108,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
108
108
|
|
109
109
|
private subscriberPrimary: boolean = false;
|
110
110
|
|
111
|
-
private
|
111
|
+
private primaryTransport?: PCTransport;
|
112
112
|
|
113
113
|
private pcState: PCState = PCState.New;
|
114
114
|
|
@@ -247,12 +247,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
247
247
|
}
|
248
248
|
|
249
249
|
async cleanupPeerConnections() {
|
250
|
-
if (this.publisher && this.publisher.
|
251
|
-
this.publisher.
|
250
|
+
if (this.publisher && this.publisher.getSignallingState() !== 'closed') {
|
251
|
+
this.publisher.getSenders().forEach((sender) => {
|
252
252
|
try {
|
253
253
|
// TODO: react-native-webrtc doesn't have removeTrack yet.
|
254
|
-
if (this.publisher?.
|
255
|
-
this.publisher?.
|
254
|
+
if (this.publisher?.canRemoveTrack()) {
|
255
|
+
this.publisher?.removeTrack(sender);
|
256
256
|
}
|
257
257
|
} catch (e) {
|
258
258
|
log.warn('could not removeTrack', { error: e });
|
@@ -268,7 +268,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
268
268
|
this.subscriber = undefined;
|
269
269
|
}
|
270
270
|
this.hasPublished = false;
|
271
|
-
this.
|
271
|
+
this.primaryTransport = undefined;
|
272
272
|
|
273
273
|
const dcCleanup = (dc: RTCDataChannel | undefined) => {
|
274
274
|
if (!dc) return;
|
@@ -336,7 +336,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
336
336
|
delete this.pendingTrackResolvers[sender.track.id];
|
337
337
|
}
|
338
338
|
try {
|
339
|
-
this.publisher?.
|
339
|
+
this.publisher?.removeTrack(sender);
|
340
340
|
return true;
|
341
341
|
} catch (e: unknown) {
|
342
342
|
log.warn('failed to remove track', { error: e, method: 'removeTrack' });
|
@@ -353,10 +353,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
353
353
|
}
|
354
354
|
|
355
355
|
async getConnectedServerAddress(): Promise<string | undefined> {
|
356
|
-
if (this.
|
356
|
+
if (this.primaryTransport === undefined) {
|
357
357
|
return undefined;
|
358
358
|
}
|
359
|
-
return getConnectedAddress(
|
359
|
+
return this.primaryTransport.getConnectedAddress();
|
360
360
|
}
|
361
361
|
|
362
362
|
/* @internal */
|
@@ -387,40 +387,38 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
387
387
|
|
388
388
|
this.emit(EngineEvent.TransportsCreated, this.publisher, this.subscriber);
|
389
389
|
|
390
|
-
this.publisher.
|
391
|
-
|
392
|
-
|
393
|
-
this.client.sendIceCandidate(ev.candidate, SignalTarget.PUBLISHER);
|
390
|
+
this.publisher.onIceCandidate = (candidate) => {
|
391
|
+
log.trace('adding ICE candidate for peer', candidate);
|
392
|
+
this.client.sendIceCandidate(candidate, SignalTarget.PUBLISHER);
|
394
393
|
};
|
395
394
|
|
396
|
-
this.subscriber.
|
397
|
-
|
398
|
-
this.client.sendIceCandidate(ev.candidate, SignalTarget.SUBSCRIBER);
|
395
|
+
this.subscriber.onIceCandidate = (candidate) => {
|
396
|
+
this.client.sendIceCandidate(candidate, SignalTarget.SUBSCRIBER);
|
399
397
|
};
|
400
398
|
|
401
399
|
this.publisher.onOffer = (offer) => {
|
402
400
|
this.client.sendOffer(offer);
|
403
401
|
};
|
404
402
|
|
405
|
-
let
|
406
|
-
let
|
403
|
+
let primaryTransport = this.publisher;
|
404
|
+
let secondaryTransport = this.subscriber;
|
407
405
|
let subscriberPrimary = joinResponse.subscriberPrimary;
|
408
406
|
if (subscriberPrimary) {
|
409
|
-
|
410
|
-
|
407
|
+
primaryTransport = this.subscriber;
|
408
|
+
secondaryTransport = this.publisher;
|
411
409
|
// in subscriber primary mode, server side opens sub data channels.
|
412
|
-
this.subscriber.
|
410
|
+
this.subscriber.onDataChannel = this.handleDataChannel;
|
413
411
|
}
|
414
|
-
this.
|
415
|
-
|
416
|
-
log.debug(`primary PC state changed ${
|
417
|
-
if (
|
412
|
+
this.primaryTransport = primaryTransport;
|
413
|
+
primaryTransport.onConnectionStateChange = async (connectionState) => {
|
414
|
+
log.debug(`primary PC state changed ${connectionState}`);
|
415
|
+
if (connectionState === 'connected') {
|
418
416
|
const shouldEmit = this.pcState === PCState.New;
|
419
417
|
this.pcState = PCState.Connected;
|
420
418
|
if (shouldEmit) {
|
421
419
|
this.emit(EngineEvent.Connected, joinResponse);
|
422
420
|
}
|
423
|
-
} else if (
|
421
|
+
} else if (connectionState === 'failed') {
|
424
422
|
// on Safari, PeerConnection will switch to 'disconnected' during renegotiation
|
425
423
|
if (this.pcState === PCState.Connected) {
|
426
424
|
this.pcState = PCState.Disconnected;
|
@@ -434,10 +432,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
434
432
|
}
|
435
433
|
}
|
436
434
|
};
|
437
|
-
|
438
|
-
log.debug(`secondary PC state changed ${
|
435
|
+
secondaryTransport.onConnectionStateChange = async (connectionState) => {
|
436
|
+
log.debug(`secondary PC state changed ${connectionState}`);
|
439
437
|
// also reconnect if secondary peerconnection fails
|
440
|
-
if (
|
438
|
+
if (connectionState === 'failed') {
|
441
439
|
this.handleDisconnect(
|
442
440
|
'secondary peerconnection',
|
443
441
|
subscriberPrimary
|
@@ -447,7 +445,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
447
445
|
}
|
448
446
|
};
|
449
447
|
|
450
|
-
this.subscriber.
|
448
|
+
this.subscriber.onTrack = (ev: RTCTrackEvent) => {
|
451
449
|
this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
|
452
450
|
};
|
453
451
|
|
@@ -462,7 +460,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
462
460
|
}
|
463
461
|
log.debug('received server answer', {
|
464
462
|
RTCSdpType: sd.type,
|
465
|
-
signalingState: this.publisher.
|
463
|
+
signalingState: this.publisher.getSignallingState().toString(),
|
466
464
|
});
|
467
465
|
await this.publisher.setRemoteDescription(sd);
|
468
466
|
};
|
@@ -487,7 +485,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
487
485
|
}
|
488
486
|
log.debug('received server offer', {
|
489
487
|
RTCSdpType: sd.type,
|
490
|
-
signalingState: this.subscriber.
|
488
|
+
signalingState: this.subscriber.getSignallingState().toString(),
|
491
489
|
});
|
492
490
|
await this.subscriber.setRemoteDescription(sd);
|
493
491
|
|
@@ -518,7 +516,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
518
516
|
this.client.onLeave = (leave?: LeaveRequest) => {
|
519
517
|
if (leave?.canReconnect) {
|
520
518
|
this.fullReconnectOnNext = true;
|
521
|
-
this.
|
519
|
+
this.primaryTransport = undefined;
|
522
520
|
// reconnect immediately instead of waiting for next attempt
|
523
521
|
this.handleDisconnect(leaveReconnect);
|
524
522
|
} else {
|
@@ -579,12 +577,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
579
577
|
}
|
580
578
|
|
581
579
|
// create data channels
|
582
|
-
this.lossyDC = this.publisher.
|
580
|
+
this.lossyDC = this.publisher.createDataChannel(lossyDataChannel, {
|
583
581
|
// will drop older packets that arrive
|
584
582
|
ordered: true,
|
585
583
|
maxRetransmits: 0,
|
586
584
|
});
|
587
|
-
this.reliableDC = this.publisher.
|
585
|
+
this.reliableDC = this.publisher.createDataChannel(reliableDataChannel, {
|
588
586
|
ordered: true,
|
589
587
|
});
|
590
588
|
|
@@ -765,7 +763,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
765
763
|
transceiverInit.sendEncodings = encodings;
|
766
764
|
}
|
767
765
|
// addTransceiver for react-native is async. web is synchronous, but await won't effect it.
|
768
|
-
const transceiver = await this.publisher.
|
766
|
+
const transceiver = await this.publisher.addTransceiver(
|
769
767
|
track.mediaStreamTrack,
|
770
768
|
transceiverInit,
|
771
769
|
);
|
@@ -791,7 +789,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
791
789
|
transceiverInit.sendEncodings = encodings;
|
792
790
|
}
|
793
791
|
// addTransceiver for react-native is async. web is synchronous, but await won't effect it.
|
794
|
-
const transceiver = await this.publisher.
|
792
|
+
const transceiver = await this.publisher.addTransceiver(
|
795
793
|
simulcastTrack.mediaStreamTrack,
|
796
794
|
transceiverInit,
|
797
795
|
);
|
@@ -807,7 +805,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
807
805
|
if (!this.publisher) {
|
808
806
|
throw new UnexpectedConnectionState('publisher is closed');
|
809
807
|
}
|
810
|
-
return this.publisher.
|
808
|
+
return this.publisher.addTrack(track);
|
811
809
|
}
|
812
810
|
|
813
811
|
// websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
|
@@ -872,7 +870,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
872
870
|
this.clientConfiguration?.resumeConnection === ClientConfigSetting.DISABLED ||
|
873
871
|
// signaling state could change to closed due to hardware sleep
|
874
872
|
// those connections cannot be resumed
|
875
|
-
(this.
|
873
|
+
(this.primaryTransport?.getSignallingState() ?? 'closed') === 'closed'
|
876
874
|
) {
|
877
875
|
this.fullReconnectOnNext = true;
|
878
876
|
}
|
@@ -999,8 +997,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
999
997
|
const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
|
1000
998
|
if (res) {
|
1001
999
|
const rtcConfig = this.makeRTCConfiguration(res);
|
1002
|
-
this.publisher.
|
1003
|
-
this.subscriber.
|
1000
|
+
this.publisher.setConfiguration(rtcConfig);
|
1001
|
+
this.subscriber.setConfiguration(rtcConfig);
|
1004
1002
|
}
|
1005
1003
|
} catch (e) {
|
1006
1004
|
let message = '';
|
@@ -1084,7 +1082,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1084
1082
|
|
1085
1083
|
log.debug('waiting for peer connection to reconnect');
|
1086
1084
|
while (now - startTime < this.peerConnectionTimeout) {
|
1087
|
-
if (this.
|
1085
|
+
if (this.primaryTransport === undefined) {
|
1088
1086
|
// we can abort early, connection is hosed
|
1089
1087
|
break;
|
1090
1088
|
} else if (
|
@@ -1092,8 +1090,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1092
1090
|
// this means we'd have to check its status manually and update address
|
1093
1091
|
// manually
|
1094
1092
|
now - startTime > minReconnectWait &&
|
1095
|
-
this.
|
1096
|
-
(!this.hasPublished || this.publisher?.
|
1093
|
+
this.primaryTransport?.getConnectionState() === 'connected' &&
|
1094
|
+
(!this.hasPublished || this.publisher?.getConnectionState() === 'connected')
|
1097
1095
|
) {
|
1098
1096
|
this.pcState = PCState.Connected;
|
1099
1097
|
}
|
@@ -1172,7 +1170,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1172
1170
|
if (
|
1173
1171
|
!subscriber &&
|
1174
1172
|
!this.publisher?.isICEConnected &&
|
1175
|
-
this.publisher?.
|
1173
|
+
this.publisher?.getICEConnectionState() !== 'checking'
|
1176
1174
|
) {
|
1177
1175
|
// start negotiation
|
1178
1176
|
this.negotiate();
|
@@ -1196,7 +1194,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1196
1194
|
}
|
1197
1195
|
|
1198
1196
|
throw new ConnectionError(
|
1199
|
-
`could not establish ${transportName} connection, state: ${transport.
|
1197
|
+
`could not establish ${transportName} connection, state: ${transport.getICEConnectionState()}`,
|
1200
1198
|
);
|
1201
1199
|
}
|
1202
1200
|
|
@@ -1207,12 +1205,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1207
1205
|
/* @internal */
|
1208
1206
|
verifyTransport(): boolean {
|
1209
1207
|
// primary connection
|
1210
|
-
if (!this.
|
1208
|
+
if (!this.primaryTransport) {
|
1211
1209
|
return false;
|
1212
1210
|
}
|
1213
1211
|
if (
|
1214
|
-
this.
|
1215
|
-
this.
|
1212
|
+
this.primaryTransport.getConnectionState() === 'closed' ||
|
1213
|
+
this.primaryTransport.getConnectionState() === 'failed'
|
1216
1214
|
) {
|
1217
1215
|
return false;
|
1218
1216
|
}
|
@@ -1223,8 +1221,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1223
1221
|
return false;
|
1224
1222
|
}
|
1225
1223
|
if (
|
1226
|
-
this.publisher.
|
1227
|
-
this.publisher.
|
1224
|
+
this.publisher.getConnectionState() === 'closed' ||
|
1225
|
+
this.publisher.getConnectionState() === 'failed'
|
1228
1226
|
) {
|
1229
1227
|
return false;
|
1230
1228
|
}
|
@@ -1355,40 +1353,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1355
1353
|
}
|
1356
1354
|
}
|
1357
1355
|
|
1358
|
-
async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | undefined> {
|
1359
|
-
let selectedCandidatePairId = '';
|
1360
|
-
const candidatePairs = new Map<string, RTCIceCandidatePairStats>();
|
1361
|
-
// id -> candidate ip
|
1362
|
-
const candidates = new Map<string, string>();
|
1363
|
-
const stats: RTCStatsReport = await pc.getStats();
|
1364
|
-
stats.forEach((v) => {
|
1365
|
-
switch (v.type) {
|
1366
|
-
case 'transport':
|
1367
|
-
selectedCandidatePairId = v.selectedCandidatePairId;
|
1368
|
-
break;
|
1369
|
-
case 'candidate-pair':
|
1370
|
-
if (selectedCandidatePairId === '' && v.selected) {
|
1371
|
-
selectedCandidatePairId = v.id;
|
1372
|
-
}
|
1373
|
-
candidatePairs.set(v.id, v);
|
1374
|
-
break;
|
1375
|
-
case 'remote-candidate':
|
1376
|
-
candidates.set(v.id, `${v.address}:${v.port}`);
|
1377
|
-
break;
|
1378
|
-
default:
|
1379
|
-
}
|
1380
|
-
});
|
1381
|
-
|
1382
|
-
if (selectedCandidatePairId === '') {
|
1383
|
-
return undefined;
|
1384
|
-
}
|
1385
|
-
const selectedID = candidatePairs.get(selectedCandidatePairId)?.remoteCandidateId;
|
1386
|
-
if (selectedID === undefined) {
|
1387
|
-
return undefined;
|
1388
|
-
}
|
1389
|
-
return candidates.get(selectedID);
|
1390
|
-
}
|
1391
|
-
|
1392
1356
|
class SignalReconnectError extends Error {}
|
1393
1357
|
|
1394
1358
|
export type EngineEventCallbacks = {
|
package/src/room/Room.ts
CHANGED
@@ -452,6 +452,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
452
452
|
error instanceof ConnectionError &&
|
453
453
|
(error.status === 401 || error.reason === ConnectionErrorReason.Cancelled)
|
454
454
|
) {
|
455
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
455
456
|
reject(error);
|
456
457
|
return;
|
457
458
|
}
|
@@ -462,9 +463,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
462
463
|
);
|
463
464
|
await connectFn(resolve, reject, nextUrl);
|
464
465
|
} else {
|
466
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
465
467
|
reject(e);
|
466
468
|
}
|
467
469
|
} else {
|
470
|
+
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
468
471
|
reject(e);
|
469
472
|
}
|
470
473
|
}
|
@@ -593,8 +596,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
593
596
|
this.setupLocalParticipantEvents();
|
594
597
|
this.emit(RoomEvent.SignalConnected);
|
595
598
|
} catch (err) {
|
599
|
+
await this.engine.close();
|
596
600
|
this.recreateEngine();
|
597
|
-
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
598
601
|
const resultingError = new ConnectionError(`could not establish signal connection`);
|
599
602
|
if (err instanceof Error) {
|
600
603
|
resultingError.message = `${resultingError.message}: ${err.message}`;
|
@@ -608,8 +611,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
608
611
|
}
|
609
612
|
|
610
613
|
if (abortController.signal.aborted) {
|
614
|
+
await this.engine.close();
|
611
615
|
this.recreateEngine();
|
612
|
-
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
613
616
|
throw new ConnectionError(`Connection attempt aborted`);
|
614
617
|
}
|
615
618
|
|
@@ -619,8 +622,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
619
622
|
abortController,
|
620
623
|
);
|
621
624
|
} catch (e) {
|
625
|
+
await this.engine.close();
|
622
626
|
this.recreateEngine();
|
623
|
-
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
624
627
|
throw e;
|
625
628
|
}
|
626
629
|
|
@@ -1018,8 +1021,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1018
1021
|
}
|
1019
1022
|
const parts = unpackStreamId(stream.id);
|
1020
1023
|
const participantId = parts[0];
|
1021
|
-
let
|
1022
|
-
|
1024
|
+
let streamId = parts[1];
|
1025
|
+
let trackId = mediaTrack.id;
|
1026
|
+
// firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
|
1027
|
+
// and generates its own track id instead of infer from sdp track id.
|
1028
|
+
if (streamId && streamId.startsWith('TR')) trackId = streamId;
|
1023
1029
|
|
1024
1030
|
if (participantId === this.localParticipant.sid) {
|
1025
1031
|
log.warn('tried to create RemoteParticipant for local participant');
|
@@ -1543,14 +1549,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1543
1549
|
}
|
1544
1550
|
|
1545
1551
|
private sendSyncState() {
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1549
|
-
) {
|
1552
|
+
const previousAnswer = this.engine.subscriber?.getLocalDescription();
|
1553
|
+
const previousOffer = this.engine.subscriber?.getRemoteDescription();
|
1554
|
+
|
1555
|
+
if (!previousAnswer) {
|
1550
1556
|
return;
|
1551
1557
|
}
|
1552
|
-
const previousAnswer = this.engine.subscriber.pc.localDescription;
|
1553
|
-
const previousOffer = this.engine.subscriber.pc.remoteDescription;
|
1554
1558
|
|
1555
1559
|
/* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
|
1556
1560
|
in this case, we send unsub tracks, so server add all tracks to this
|
package/src/room/defaults.ts
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
import type { InternalRoomConnectOptions, InternalRoomOptions } from '../options';
|
2
2
|
import DefaultReconnectPolicy from './DefaultReconnectPolicy';
|
3
|
-
import { AudioPresets, ScreenSharePresets, VideoPresets } from './track/options';
|
4
3
|
import type {
|
5
4
|
AudioCaptureOptions,
|
6
5
|
TrackPublishDefaults,
|
7
6
|
VideoCaptureOptions,
|
8
7
|
} from './track/options';
|
8
|
+
import { AudioPresets, ScreenSharePresets, VideoPresets } from './track/options';
|
9
|
+
|
10
|
+
export const defaultVideoCodec = 'vp8';
|
9
11
|
|
10
12
|
export const publishDefaults: TrackPublishDefaults = {
|
11
13
|
/**
|
@@ -19,7 +21,7 @@ export const publishDefaults: TrackPublishDefaults = {
|
|
19
21
|
simulcast: true,
|
20
22
|
screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
|
21
23
|
stopMicTrackOnMute: false,
|
22
|
-
videoCodec:
|
24
|
+
videoCodec: defaultVideoCodec,
|
23
25
|
backupCodec: false,
|
24
26
|
} as const;
|
25
27
|
|
package/src/room/events.ts
CHANGED
@@ -223,7 +223,7 @@ export enum RoomEvent {
|
|
223
223
|
* be emitted.
|
224
224
|
*
|
225
225
|
* args: (pub: [[RemoteTrackPublication]],
|
226
|
-
* status: [[TrackPublication.
|
226
|
+
* status: [[TrackPublication.PermissionStatus]],
|
227
227
|
* participant: [[RemoteParticipant]])
|
228
228
|
*/
|
229
229
|
TrackSubscriptionPermissionChanged = 'trackSubscriptionPermissionChanged',
|
@@ -18,6 +18,7 @@ import {
|
|
18
18
|
TrackUnpublishedResponse,
|
19
19
|
} from '../../proto/livekit_rtc_pb';
|
20
20
|
import type RTCEngine from '../RTCEngine';
|
21
|
+
import { defaultVideoCodec } from '../defaults';
|
21
22
|
import { DeviceUnsupportedError, TrackInvalidError, UnexpectedConnectionState } from '../errors';
|
22
23
|
import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
|
23
24
|
import LocalAudioTrack from '../track/LocalAudioTrack';
|
@@ -33,10 +34,11 @@ import type {
|
|
33
34
|
TrackPublishOptions,
|
34
35
|
VideoCaptureOptions,
|
35
36
|
} from '../track/options';
|
36
|
-
import { VideoPresets, isBackupCodec
|
37
|
+
import { VideoPresets, isBackupCodec } from '../track/options';
|
37
38
|
import {
|
38
39
|
constraintsForOptions,
|
39
40
|
mergeDefaultOptions,
|
41
|
+
mimeTypeToVideoCodecString,
|
40
42
|
screenCaptureToDisplayMediaStreamOptions,
|
41
43
|
} from '../track/utils';
|
42
44
|
import type { DataPublishOptions } from '../types';
|
@@ -629,6 +631,10 @@ export default class LocalParticipant extends Participant {
|
|
629
631
|
if (opts.videoCodec === 'vp9' && !supportsVP9()) {
|
630
632
|
opts.videoCodec = undefined;
|
631
633
|
}
|
634
|
+
if (opts.videoCodec === undefined) {
|
635
|
+
opts.videoCodec = defaultVideoCodec;
|
636
|
+
}
|
637
|
+
const videoCodec = opts.videoCodec;
|
632
638
|
|
633
639
|
// handle track actions
|
634
640
|
track.on(TrackEvent.Muted, this.onTrackMuted);
|
@@ -649,11 +655,11 @@ export default class LocalParticipant extends Participant {
|
|
649
655
|
encryption: this.encryptionType,
|
650
656
|
stereo: isStereo,
|
651
657
|
disableRed: this.isE2EEEnabled || !(opts.red ?? true),
|
658
|
+
stream: opts?.stream,
|
652
659
|
});
|
653
660
|
|
654
661
|
// compute encodings and layers for video
|
655
662
|
let encodings: RTCRtpEncodingParameters[] | undefined;
|
656
|
-
let simEncodings: RTCRtpEncodingParameters[] | undefined;
|
657
663
|
if (track.kind === Track.Kind.Video) {
|
658
664
|
let dims: Track.Dimensions = {
|
659
665
|
width: 0,
|
@@ -678,53 +684,40 @@ export default class LocalParticipant extends Participant {
|
|
678
684
|
req.height = dims.height;
|
679
685
|
// for svc codecs, disable simulcast and use vp8 for backup codec
|
680
686
|
if (track instanceof LocalVideoTrack) {
|
681
|
-
if (isSVCCodec(
|
687
|
+
if (isSVCCodec(videoCodec)) {
|
682
688
|
// vp9 svc with screenshare has problem to encode, always use L1T3 here
|
683
|
-
if (track.source === Track.Source.ScreenShare &&
|
689
|
+
if (track.source === Track.Source.ScreenShare && videoCodec === 'vp9') {
|
684
690
|
opts.scalabilityMode = 'L1T3';
|
685
691
|
}
|
686
692
|
// set scalabilityMode to 'L3T3_KEY' by default
|
687
693
|
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3_KEY';
|
688
694
|
}
|
689
695
|
|
696
|
+
req.simulcastCodecs = [
|
697
|
+
new SimulcastCodec({
|
698
|
+
codec: videoCodec,
|
699
|
+
cid: track.mediaStreamTrack.id,
|
700
|
+
}),
|
701
|
+
];
|
702
|
+
|
690
703
|
// set up backup
|
691
|
-
if (opts.
|
704
|
+
if (opts.backupCodec && videoCodec !== opts.backupCodec.codec) {
|
692
705
|
if (!this.roomOptions.dynacast) {
|
693
706
|
this.roomOptions.dynacast = true;
|
694
707
|
}
|
695
|
-
|
696
|
-
simOpts.simulcast = true;
|
697
|
-
simEncodings = computeTrackBackupEncodings(track, opts.backupCodec.codec, simOpts);
|
698
|
-
|
699
|
-
req.simulcastCodecs = [
|
700
|
-
new SimulcastCodec({
|
701
|
-
codec: opts.videoCodec,
|
702
|
-
cid: track.mediaStreamTrack.id,
|
703
|
-
enableSimulcastLayers: true,
|
704
|
-
}),
|
708
|
+
req.simulcastCodecs.push(
|
705
709
|
new SimulcastCodec({
|
706
710
|
codec: opts.backupCodec.codec,
|
707
711
|
cid: '',
|
708
|
-
enableSimulcastLayers: true,
|
709
712
|
}),
|
710
|
-
|
711
|
-
} else if (opts.videoCodec) {
|
712
|
-
// pass codec info to sfu so it can prefer codec for the client which don't support
|
713
|
-
// setCodecPreferences
|
714
|
-
req.simulcastCodecs = [
|
715
|
-
new SimulcastCodec({
|
716
|
-
codec: opts.videoCodec,
|
717
|
-
cid: track.mediaStreamTrack.id,
|
718
|
-
enableSimulcastLayers: opts.simulcast ?? false,
|
719
|
-
}),
|
720
|
-
];
|
713
|
+
);
|
721
714
|
}
|
722
715
|
}
|
723
716
|
|
724
717
|
encodings = computeVideoEncodings(
|
725
718
|
track.source === Track.Source.ScreenShare,
|
726
|
-
|
727
|
-
|
719
|
+
req.width,
|
720
|
+
req.height,
|
728
721
|
opts,
|
729
722
|
);
|
730
723
|
req.layers = videoLayersFromEncodings(
|
@@ -748,30 +741,28 @@ export default class LocalParticipant extends Participant {
|
|
748
741
|
}
|
749
742
|
|
750
743
|
const ti = await this.engine.addTrack(req);
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
backupCodecSupported = true;
|
744
|
+
// server might not support the codec the client has requested, in that case, fallback
|
745
|
+
// to a supported codec
|
746
|
+
let primaryCodecMime: string | undefined;
|
747
|
+
ti.codecs.forEach((codec) => {
|
748
|
+
if (primaryCodecMime === undefined) {
|
749
|
+
primaryCodecMime = codec.mimeType;
|
758
750
|
}
|
759
751
|
});
|
760
|
-
|
761
|
-
|
762
|
-
if (
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
752
|
+
if (primaryCodecMime && track.kind === Track.Kind.Video) {
|
753
|
+
const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
|
754
|
+
if (updatedCodec !== videoCodec) {
|
755
|
+
log.debug('falling back to server selected codec', { codec: updatedCodec });
|
756
|
+
/* @ts-ignore */
|
757
|
+
opts.videoCodec = updatedCodec;
|
758
|
+
|
759
|
+
// recompute encodings since bitrates/etc could have changed
|
760
|
+
encodings = computeVideoEncodings(
|
761
|
+
track.source === Track.Source.ScreenShare,
|
762
|
+
req.width,
|
763
|
+
req.height,
|
764
|
+
opts,
|
771
765
|
);
|
772
|
-
opts.videoCodec = backupCodec.codec;
|
773
|
-
opts.videoEncoding = backupCodec.encoding;
|
774
|
-
encodings = simEncodings;
|
775
766
|
}
|
776
767
|
}
|
777
768
|
|
@@ -785,20 +776,19 @@ export default class LocalParticipant extends Participant {
|
|
785
776
|
}
|
786
777
|
log.debug(`publishing ${track.kind} with encodings`, { encodings, trackInfo: ti });
|
787
778
|
|
788
|
-
// store RTPSender
|
789
779
|
track.sender = await this.engine.createSender(track, opts, encodings);
|
790
780
|
|
791
781
|
if (encodings) {
|
792
782
|
if (isFireFox() && track.kind === Track.Kind.Audio) {
|
793
783
|
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
794
|
-
livekit-server uses maxaveragebitrate=
|
784
|
+
livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
|
795
785
|
publish high quality audio track. But firefox always uses this value as the actual
|
796
786
|
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
|
797
787
|
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
798
788
|
fix the issue.
|
799
789
|
*/
|
800
790
|
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
801
|
-
for (const transceiver of this.engine.publisher.
|
791
|
+
for (const transceiver of this.engine.publisher.getTransceivers()) {
|
802
792
|
if (transceiver.sender === track.sender) {
|
803
793
|
trackTransceiver = transceiver;
|
804
794
|
break;
|
@@ -888,7 +878,6 @@ export default class LocalParticipant extends Participant {
|
|
888
878
|
{
|
889
879
|
codec: opts.videoCodec,
|
890
880
|
cid: simulcastTrack.mediaStreamTrack.id,
|
891
|
-
enableSimulcastLayers: opts.simulcast,
|
892
881
|
},
|
893
882
|
],
|
894
883
|
});
|
@@ -946,11 +935,11 @@ export default class LocalParticipant extends Participant {
|
|
946
935
|
track.sender = undefined;
|
947
936
|
if (
|
948
937
|
this.engine.publisher &&
|
949
|
-
this.engine.publisher.
|
938
|
+
this.engine.publisher.getConnectionState() !== 'closed' &&
|
950
939
|
trackSender
|
951
940
|
) {
|
952
941
|
try {
|
953
|
-
for (const transceiver of this.engine.publisher.
|
942
|
+
for (const transceiver of this.engine.publisher.getTransceivers()) {
|
954
943
|
// if sender is not currently sending (after replaceTrack(null))
|
955
944
|
// removeTrack would have no effect.
|
956
945
|
// to ensure we end up successfully removing the track, manually set
|
@@ -138,7 +138,7 @@ export default class LocalAudioTrack extends LocalTrack {
|
|
138
138
|
this.prevStats = stats;
|
139
139
|
};
|
140
140
|
|
141
|
-
async setProcessor(processor: TrackProcessor<
|
141
|
+
async setProcessor(processor: TrackProcessor<this['kind']>) {
|
142
142
|
const unlock = await this.processorLock.lock();
|
143
143
|
try {
|
144
144
|
if (!this.audioContext) {
|