livekit-client 1.6.5 → 1.6.7
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.esm.mjs +149 -130
- 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/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +10 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +1 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +2 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +2 -2
- package/dist/ts4.2/src/room/RTCEngine.d.ts +10 -3
- package/dist/ts4.2/src/room/events.d.ts +2 -1
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +1 -0
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/ts4.2/src/room/track/Track.d.ts +2 -1
- package/dist/ts4.2/src/room/track/options.d.ts +2 -2
- package/package.json +13 -13
- package/src/room/PCTransport.ts +2 -0
- package/src/room/RTCEngine.ts +46 -51
- package/src/room/Room.ts +13 -3
- package/src/room/events.ts +2 -1
- package/src/room/participant/LocalParticipant.ts +20 -8
- package/src/room/participant/RemoteParticipant.ts +2 -3
- package/src/room/track/LocalVideoTrack.ts +62 -46
- package/src/room/track/RemoteTrackPublication.ts +3 -2
- package/src/room/track/RemoteVideoTrack.ts +0 -3
- package/src/room/track/Track.ts +2 -1
- package/src/room/track/options.ts +2 -2
package/src/room/RTCEngine.ts
CHANGED
@@ -118,8 +118,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
118
118
|
|
119
119
|
private clientConfiguration?: ClientConfiguration;
|
120
120
|
|
121
|
-
private connectedServerAddr?: string;
|
122
|
-
|
123
121
|
private attemptingReconnect: boolean = false;
|
124
122
|
|
125
123
|
private reconnectPolicy: ReconnectPolicy;
|
@@ -136,6 +134,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
136
134
|
|
137
135
|
private closingLock: Mutex;
|
138
136
|
|
137
|
+
private shouldFailNext: boolean = false;
|
138
|
+
|
139
139
|
constructor(private options: InternalRoomOptions) {
|
140
140
|
super();
|
141
141
|
this.client = new SignalClient();
|
@@ -247,7 +247,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
247
247
|
});
|
248
248
|
}
|
249
249
|
|
250
|
-
|
250
|
+
/**
|
251
|
+
* Removes sender from PeerConnection, returning true if it was removed successfully
|
252
|
+
* and a negotiation is necessary
|
253
|
+
* @param sender
|
254
|
+
* @returns
|
255
|
+
*/
|
256
|
+
removeTrack(sender: RTCRtpSender): boolean {
|
251
257
|
if (sender.track && this.pendingTrackResolvers[sender.track.id]) {
|
252
258
|
const { reject } = this.pendingTrackResolvers[sender.track.id];
|
253
259
|
if (reject) {
|
@@ -257,9 +263,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
257
263
|
}
|
258
264
|
try {
|
259
265
|
this.publisher?.pc.removeTrack(sender);
|
266
|
+
return true;
|
260
267
|
} catch (e: unknown) {
|
261
268
|
log.warn('failed to remove track', { error: e, method: 'removeTrack' });
|
262
269
|
}
|
270
|
+
return false;
|
263
271
|
}
|
264
272
|
|
265
273
|
updateMuteStatus(trackSid: string, muted: boolean) {
|
@@ -270,8 +278,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
270
278
|
return this.reliableDCSub?.readyState;
|
271
279
|
}
|
272
280
|
|
273
|
-
|
274
|
-
|
281
|
+
async getConnectedServerAddress(): Promise<string | undefined> {
|
282
|
+
if (this.primaryPC === undefined) {
|
283
|
+
return undefined;
|
284
|
+
}
|
285
|
+
return getConnectedAddress(this.primaryPC);
|
275
286
|
}
|
276
287
|
|
277
288
|
private configure(joinResponse: JoinResponse) {
|
@@ -317,11 +328,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
317
328
|
primaryPC.onconnectionstatechange = async () => {
|
318
329
|
log.debug(`primary PC state changed ${primaryPC.connectionState}`);
|
319
330
|
if (primaryPC.connectionState === 'connected') {
|
320
|
-
try {
|
321
|
-
this.connectedServerAddr = await getConnectedAddress(primaryPC);
|
322
|
-
} catch (e) {
|
323
|
-
log.warn('could not get connected server address', { error: e });
|
324
|
-
}
|
325
331
|
const shouldEmit = this.pcState === PCState.New;
|
326
332
|
this.pcState = PCState.Connected;
|
327
333
|
if (shouldEmit) {
|
@@ -334,7 +340,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
334
340
|
|
335
341
|
this.handleDisconnect(
|
336
342
|
'primary peerconnection',
|
337
|
-
false,
|
338
343
|
subscriberPrimary
|
339
344
|
? ReconnectReason.REASON_SUBSCRIBER_FAILED
|
340
345
|
: ReconnectReason.REASON_PUBLISHER_FAILED,
|
@@ -348,7 +353,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
348
353
|
if (secondaryPC.connectionState === 'failed') {
|
349
354
|
this.handleDisconnect(
|
350
355
|
'secondary peerconnection',
|
351
|
-
false,
|
352
356
|
subscriberPrimary
|
353
357
|
? ReconnectReason.REASON_PUBLISHER_FAILED
|
354
358
|
: ReconnectReason.REASON_SUBSCRIBER_FAILED,
|
@@ -419,7 +423,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
419
423
|
};
|
420
424
|
|
421
425
|
this.client.onClose = () => {
|
422
|
-
this.handleDisconnect('signal',
|
426
|
+
this.handleDisconnect('signal', ReconnectReason.REASON_SIGNAL_DISCONNECTED);
|
423
427
|
};
|
424
428
|
|
425
429
|
this.client.onLeave = (leave?: LeaveRequest) => {
|
@@ -690,11 +694,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
690
694
|
// websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
|
691
695
|
// continues to work, we can reconnect to websocket to continue the session
|
692
696
|
// after a number of retries, we'll close and give up permanently
|
693
|
-
private handleDisconnect = (
|
694
|
-
connection: string,
|
695
|
-
signalEvents: boolean = false,
|
696
|
-
disconnectReason?: ReconnectReason,
|
697
|
-
) => {
|
697
|
+
private handleDisconnect = (connection: string, disconnectReason?: ReconnectReason) => {
|
698
698
|
if (this._isClosed) {
|
699
699
|
return;
|
700
700
|
}
|
@@ -731,12 +731,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
731
731
|
|
732
732
|
this.clearReconnectTimeout();
|
733
733
|
this.reconnectTimeout = CriticalTimers.setTimeout(
|
734
|
-
() => this.attemptReconnect(
|
734
|
+
() => this.attemptReconnect(disconnectReason),
|
735
735
|
delay,
|
736
736
|
);
|
737
737
|
};
|
738
738
|
|
739
|
-
private async attemptReconnect(
|
739
|
+
private async attemptReconnect(reason?: ReconnectReason) {
|
740
740
|
if (this._isClosed) {
|
741
741
|
return;
|
742
742
|
}
|
@@ -756,35 +756,26 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
756
756
|
try {
|
757
757
|
this.attemptingReconnect = true;
|
758
758
|
if (this.fullReconnectOnNext) {
|
759
|
-
await this.restartConnection(
|
759
|
+
await this.restartConnection();
|
760
760
|
} else {
|
761
|
-
await this.resumeConnection(
|
761
|
+
await this.resumeConnection(reason);
|
762
762
|
}
|
763
763
|
this.clearPendingReconnect();
|
764
764
|
this.fullReconnectOnNext = false;
|
765
765
|
} catch (e) {
|
766
766
|
this.reconnectAttempts += 1;
|
767
|
-
let reconnectRequired = false;
|
768
767
|
let recoverable = true;
|
769
|
-
let requireSignalEvents = false;
|
770
768
|
if (e instanceof UnexpectedConnectionState) {
|
771
769
|
log.debug('received unrecoverable error', { error: e });
|
772
770
|
// unrecoverable
|
773
771
|
recoverable = false;
|
774
772
|
} else if (!(e instanceof SignalReconnectError)) {
|
775
773
|
// cannot resume
|
776
|
-
reconnectRequired = true;
|
777
|
-
}
|
778
|
-
|
779
|
-
// when we flip from resume to reconnect
|
780
|
-
// we need to fire the right reconnecting events
|
781
|
-
if (reconnectRequired && !this.fullReconnectOnNext) {
|
782
774
|
this.fullReconnectOnNext = true;
|
783
|
-
requireSignalEvents = true;
|
784
775
|
}
|
785
776
|
|
786
777
|
if (recoverable) {
|
787
|
-
this.handleDisconnect('reconnect',
|
778
|
+
this.handleDisconnect('reconnect', ReconnectReason.REASON_UNKOWN);
|
788
779
|
} else {
|
789
780
|
log.info(
|
790
781
|
`could not recover connection after ${this.reconnectAttempts} attempts, ${
|
@@ -810,16 +801,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
810
801
|
return null;
|
811
802
|
}
|
812
803
|
|
813
|
-
private async restartConnection(
|
804
|
+
private async restartConnection() {
|
814
805
|
if (!this.url || !this.token) {
|
815
806
|
// permanent failure, don't attempt reconnection
|
816
807
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
817
808
|
}
|
818
809
|
|
819
810
|
log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
|
820
|
-
|
821
|
-
this.emit(EngineEvent.Restarting);
|
822
|
-
}
|
811
|
+
this.emit(EngineEvent.Restarting);
|
823
812
|
|
824
813
|
if (this.client.isConnected) {
|
825
814
|
await this.client.sendLeave();
|
@@ -842,6 +831,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
842
831
|
throw new SignalReconnectError();
|
843
832
|
}
|
844
833
|
|
834
|
+
if (this.shouldFailNext) {
|
835
|
+
this.shouldFailNext = false;
|
836
|
+
throw new Error('simulated failure');
|
837
|
+
}
|
838
|
+
|
845
839
|
await this.waitForPCConnected();
|
846
840
|
this.client.setReconnected();
|
847
841
|
|
@@ -849,10 +843,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
849
843
|
this.emit(EngineEvent.Restarted, joinResponse);
|
850
844
|
}
|
851
845
|
|
852
|
-
private async resumeConnection(
|
853
|
-
emitResuming: boolean = false,
|
854
|
-
reason?: ReconnectReason,
|
855
|
-
): Promise<void> {
|
846
|
+
private async resumeConnection(reason?: ReconnectReason): Promise<void> {
|
856
847
|
if (!this.url || !this.token) {
|
857
848
|
// permanent failure, don't attempt reconnection
|
858
849
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
@@ -863,9 +854,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
863
854
|
}
|
864
855
|
|
865
856
|
log.info(`resuming signal connection, attempt ${this.reconnectAttempts}`);
|
866
|
-
|
867
|
-
this.emit(EngineEvent.Resuming);
|
868
|
-
}
|
857
|
+
this.emit(EngineEvent.Resuming);
|
869
858
|
|
870
859
|
try {
|
871
860
|
const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
|
@@ -883,6 +872,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
883
872
|
}
|
884
873
|
this.emit(EngineEvent.SignalResumed);
|
885
874
|
|
875
|
+
if (this.shouldFailNext) {
|
876
|
+
this.shouldFailNext = false;
|
877
|
+
throw new Error('simulated failure');
|
878
|
+
}
|
879
|
+
|
886
880
|
this.subscriber.restartingIce = true;
|
887
881
|
|
888
882
|
// only restart publisher if it's needed
|
@@ -921,11 +915,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
921
915
|
this.primaryPC?.connectionState === 'connected'
|
922
916
|
) {
|
923
917
|
this.pcState = PCState.Connected;
|
924
|
-
try {
|
925
|
-
this.connectedServerAddr = await getConnectedAddress(this.primaryPC);
|
926
|
-
} catch (e) {
|
927
|
-
log.warn('could not get connected server address', { error: e });
|
928
|
-
}
|
929
918
|
}
|
930
919
|
if (this.pcState === PCState.Connected) {
|
931
920
|
return;
|
@@ -1022,7 +1011,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1022
1011
|
|
1023
1012
|
const negotiationTimeout = setTimeout(() => {
|
1024
1013
|
reject('negotiation timed out');
|
1025
|
-
this.handleDisconnect('negotiation',
|
1014
|
+
this.handleDisconnect('negotiation', ReconnectReason.REASON_SIGNAL_DISCONNECTED);
|
1026
1015
|
}, this.peerConnectionTimeout);
|
1027
1016
|
|
1028
1017
|
const cleanup = () => {
|
@@ -1043,7 +1032,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1043
1032
|
if (e instanceof NegotiationError) {
|
1044
1033
|
this.fullReconnectOnNext = true;
|
1045
1034
|
}
|
1046
|
-
this.handleDisconnect('negotiation',
|
1035
|
+
this.handleDisconnect('negotiation', ReconnectReason.REASON_UNKOWN);
|
1047
1036
|
});
|
1048
1037
|
});
|
1049
1038
|
}
|
@@ -1066,6 +1055,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1066
1055
|
}
|
1067
1056
|
}
|
1068
1057
|
|
1058
|
+
/* @internal */
|
1059
|
+
failNext() {
|
1060
|
+
// debugging method to fail the next reconnect/resume attempt
|
1061
|
+
this.shouldFailNext = true;
|
1062
|
+
}
|
1063
|
+
|
1069
1064
|
private clearReconnectTimeout() {
|
1070
1065
|
if (this.reconnectTimeout) {
|
1071
1066
|
CriticalTimers.clearTimeout(this.reconnectTimeout);
|
@@ -1081,7 +1076,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1081
1076
|
// in case the engine is currently reconnecting, attempt a reconnect immediately after the browser state has changed to 'onLine'
|
1082
1077
|
if (this.client.isReconnecting) {
|
1083
1078
|
this.clearReconnectTimeout();
|
1084
|
-
this.attemptReconnect(
|
1079
|
+
this.attemptReconnect(ReconnectReason.REASON_SIGNAL_DISCONNECTED);
|
1085
1080
|
}
|
1086
1081
|
};
|
1087
1082
|
|
package/src/room/Room.ts
CHANGED
@@ -56,8 +56,8 @@ import type { AdaptiveStreamSettings } from './track/types';
|
|
56
56
|
import { getNewAudioContext } from './track/utils';
|
57
57
|
import type { SimulationOptions } from './types';
|
58
58
|
import {
|
59
|
-
Future,
|
60
59
|
createDummyVideoStreamTrack,
|
60
|
+
Future,
|
61
61
|
getEmptyAudioStreamTrack,
|
62
62
|
isWeb,
|
63
63
|
Mutex,
|
@@ -516,6 +516,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
516
516
|
},
|
517
517
|
});
|
518
518
|
break;
|
519
|
+
case 'resume-reconnect':
|
520
|
+
this.engine.failNext();
|
521
|
+
await this.engine.client.close();
|
522
|
+
if (this.engine.client.onClose) {
|
523
|
+
this.engine.client.onClose('simulate resume-reconnect');
|
524
|
+
}
|
525
|
+
break;
|
519
526
|
case 'force-tcp':
|
520
527
|
case 'force-tls':
|
521
528
|
req = SimulateScenario.fromPartial({
|
@@ -786,6 +793,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
786
793
|
});
|
787
794
|
await track.restartTrack();
|
788
795
|
}
|
796
|
+
log.debug('publishing new track', {
|
797
|
+
track: pub.trackSid,
|
798
|
+
});
|
789
799
|
await this.localParticipant.publishTrack(track, pub.options);
|
790
800
|
}
|
791
801
|
}),
|
@@ -886,7 +896,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
886
896
|
participant.tracks.forEach((publication) => {
|
887
897
|
participant.unpublishTrack(publication.trackSid, true);
|
888
898
|
});
|
889
|
-
this.
|
899
|
+
this.emit(RoomEvent.ParticipantDisconnected, participant);
|
890
900
|
}
|
891
901
|
|
892
902
|
// updates are sent only when there's a change to speaker ordering
|
@@ -1114,7 +1124,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1114
1124
|
},
|
1115
1125
|
)
|
1116
1126
|
.on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
|
1117
|
-
this.
|
1127
|
+
this.emit(RoomEvent.TrackUnpublished, publication, participant);
|
1118
1128
|
})
|
1119
1129
|
.on(
|
1120
1130
|
ParticipantEvent.TrackUnsubscribed,
|
package/src/room/events.ts
CHANGED
@@ -252,7 +252,8 @@ export enum RoomEvent {
|
|
252
252
|
SignalConnected = 'signalConnected',
|
253
253
|
|
254
254
|
/**
|
255
|
-
* Recording of a room has started/stopped.
|
255
|
+
* Recording of a room has started/stopped. Room.isRecording will be updated too.
|
256
|
+
* args: (isRecording: boolean)
|
256
257
|
*/
|
257
258
|
RecordingStatusChanged = 'recordingStatusChanged',
|
258
259
|
}
|
@@ -439,7 +439,13 @@ export default class LocalParticipant extends Participant {
|
|
439
439
|
`Opus DTX will be disabled for stereo tracks by default. Enable them explicitly to make it work.`,
|
440
440
|
);
|
441
441
|
}
|
442
|
+
if (options.red === undefined) {
|
443
|
+
log.info(
|
444
|
+
`Opus RED will be disabled for stereo tracks by default. Enable them explicitly to make it work.`,
|
445
|
+
);
|
446
|
+
}
|
442
447
|
options.dtx ??= false;
|
448
|
+
options.red ??= false;
|
443
449
|
}
|
444
450
|
const opts: TrackPublishOptions = {
|
445
451
|
...this.roomOptions.publishDefaults,
|
@@ -721,17 +727,24 @@ export default class LocalParticipant extends Participant {
|
|
721
727
|
track.stop();
|
722
728
|
}
|
723
729
|
|
730
|
+
let negotiationNeeded = false;
|
731
|
+
const trackSender = track.sender;
|
732
|
+
track.sender = undefined;
|
724
733
|
if (
|
725
734
|
this.engine.publisher &&
|
726
735
|
this.engine.publisher.pc.connectionState !== 'closed' &&
|
727
|
-
|
736
|
+
trackSender
|
728
737
|
) {
|
729
738
|
try {
|
730
|
-
this.engine.removeTrack(
|
739
|
+
if (this.engine.removeTrack(trackSender)) {
|
740
|
+
negotiationNeeded = true;
|
741
|
+
}
|
731
742
|
if (track instanceof LocalVideoTrack) {
|
732
743
|
for (const [, trackInfo] of track.simulcastCodecs) {
|
733
744
|
if (trackInfo.sender) {
|
734
|
-
this.engine.removeTrack(trackInfo.sender)
|
745
|
+
if (this.engine.removeTrack(trackInfo.sender)) {
|
746
|
+
negotiationNeeded = true;
|
747
|
+
}
|
735
748
|
trackInfo.sender = undefined;
|
736
749
|
}
|
737
750
|
}
|
@@ -739,13 +752,9 @@ export default class LocalParticipant extends Participant {
|
|
739
752
|
}
|
740
753
|
} catch (e) {
|
741
754
|
log.warn('failed to unpublish track', { error: e, method: 'unpublishTrack' });
|
742
|
-
} finally {
|
743
|
-
await this.engine.negotiate();
|
744
755
|
}
|
745
756
|
}
|
746
757
|
|
747
|
-
track.sender = undefined;
|
748
|
-
|
749
758
|
// remove from our maps
|
750
759
|
this.tracks.delete(publication.trackSid);
|
751
760
|
switch (publication.kind) {
|
@@ -762,6 +771,9 @@ export default class LocalParticipant extends Participant {
|
|
762
771
|
this.emit(ParticipantEvent.LocalTrackUnpublished, publication);
|
763
772
|
publication.setTrack(undefined);
|
764
773
|
|
774
|
+
if (negotiationNeeded) {
|
775
|
+
await this.engine.negotiate();
|
776
|
+
}
|
765
777
|
return publication;
|
766
778
|
}
|
767
779
|
|
@@ -957,7 +969,7 @@ export default class LocalParticipant extends Participant {
|
|
957
969
|
}
|
958
970
|
}
|
959
971
|
} else if (update.subscribedQualities.length > 0) {
|
960
|
-
pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
|
972
|
+
await pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
|
961
973
|
}
|
962
974
|
};
|
963
975
|
|
@@ -236,8 +236,7 @@ export default class RemoteParticipant extends Participant {
|
|
236
236
|
}
|
237
237
|
publication = new RemoteTrackPublication(
|
238
238
|
kind,
|
239
|
-
ti
|
240
|
-
ti.name,
|
239
|
+
ti,
|
241
240
|
this.signalClient.connectOptions?.autoSubscribe,
|
242
241
|
);
|
243
242
|
publication.updateInfo(ti);
|
@@ -246,7 +245,7 @@ export default class RemoteParticipant extends Participant {
|
|
246
245
|
(publishedTrack) => publishedTrack.source === publication?.source,
|
247
246
|
);
|
248
247
|
if (existingTrackOfSource && publication.source !== Track.Source.Unknown) {
|
249
|
-
log.
|
248
|
+
log.debug(
|
250
249
|
`received a second track publication for ${this.identity} with the same source: ${publication.source}`,
|
251
250
|
{
|
252
251
|
oldTrack: existingTrackOfSource,
|
@@ -3,7 +3,7 @@ import log from '../../logger';
|
|
3
3
|
import { VideoLayer, VideoQuality } from '../../proto/livekit_models';
|
4
4
|
import type { SubscribedCodec, SubscribedQuality } from '../../proto/livekit_rtc';
|
5
5
|
import { computeBitrate, monitorFrequency, VideoSenderStats } from '../stats';
|
6
|
-
import { isFireFox, isMobile, isWeb } from '../utils';
|
6
|
+
import { isFireFox, isMobile, isWeb, Mutex } from '../utils';
|
7
7
|
import LocalTrack from './LocalTrack';
|
8
8
|
import type { VideoCaptureOptions, VideoCodec } from './options';
|
9
9
|
import { Track } from './Track';
|
@@ -39,6 +39,12 @@ export default class LocalVideoTrack extends LocalTrack {
|
|
39
39
|
|
40
40
|
private subscribedCodecs?: SubscribedCodec[];
|
41
41
|
|
42
|
+
// prevents concurrent manipulations to track sender
|
43
|
+
// if multiple get/setParameter are called concurrently, certain timing of events
|
44
|
+
// could lead to the browser throwing an exception in `setParameter`, due to
|
45
|
+
// a missing `getParameter` call.
|
46
|
+
private senderLock: Mutex;
|
47
|
+
|
42
48
|
/**
|
43
49
|
*
|
44
50
|
* @param mediaTrack
|
@@ -51,6 +57,7 @@ export default class LocalVideoTrack extends LocalTrack {
|
|
51
57
|
userProvidedTrack = true,
|
52
58
|
) {
|
53
59
|
super(mediaTrack, Track.Kind.Video, constraints, userProvidedTrack);
|
60
|
+
this.senderLock = new Mutex();
|
54
61
|
}
|
55
62
|
|
56
63
|
get isSimulcast(): boolean {
|
@@ -257,6 +264,7 @@ export default class LocalVideoTrack extends LocalTrack {
|
|
257
264
|
simulcastCodecInfo.sender,
|
258
265
|
simulcastCodecInfo.encodings!,
|
259
266
|
codec.qualities,
|
267
|
+
this.senderLock,
|
260
268
|
);
|
261
269
|
}
|
262
270
|
}
|
@@ -274,7 +282,7 @@ export default class LocalVideoTrack extends LocalTrack {
|
|
274
282
|
return;
|
275
283
|
}
|
276
284
|
|
277
|
-
await setPublishingLayersForSender(this.sender, this.encodings, qualities);
|
285
|
+
await setPublishingLayersForSender(this.sender, this.encodings, qualities, this.senderLock);
|
278
286
|
}
|
279
287
|
|
280
288
|
protected monitorSender = async () => {
|
@@ -317,58 +325,66 @@ async function setPublishingLayersForSender(
|
|
317
325
|
sender: RTCRtpSender,
|
318
326
|
senderEncodings: RTCRtpEncodingParameters[],
|
319
327
|
qualities: SubscribedQuality[],
|
328
|
+
senderLock: Mutex,
|
320
329
|
) {
|
330
|
+
const unlock = await senderLock.lock();
|
321
331
|
log.debug('setPublishingLayersForSender', { sender, qualities, senderEncodings });
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
if (encodings.length !== senderEncodings.length) {
|
329
|
-
log.warn('cannot set publishing layers, encodings mismatch');
|
330
|
-
return;
|
331
|
-
}
|
332
|
-
|
333
|
-
let hasChanged = false;
|
334
|
-
encodings.forEach((encoding, idx) => {
|
335
|
-
let rid = encoding.rid ?? '';
|
336
|
-
if (rid === '') {
|
337
|
-
rid = 'q';
|
332
|
+
try {
|
333
|
+
const params = sender.getParameters();
|
334
|
+
const { encodings } = params;
|
335
|
+
if (!encodings) {
|
336
|
+
return;
|
338
337
|
}
|
339
|
-
|
340
|
-
|
341
|
-
|
338
|
+
|
339
|
+
if (encodings.length !== senderEncodings.length) {
|
340
|
+
log.warn('cannot set publishing layers, encodings mismatch');
|
342
341
|
return;
|
343
342
|
}
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
if (
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
343
|
+
|
344
|
+
let hasChanged = false;
|
345
|
+
encodings.forEach((encoding, idx) => {
|
346
|
+
let rid = encoding.rid ?? '';
|
347
|
+
if (rid === '') {
|
348
|
+
rid = 'q';
|
349
|
+
}
|
350
|
+
const quality = videoQualityForRid(rid);
|
351
|
+
const subscribedQuality = qualities.find((q) => q.quality === quality);
|
352
|
+
if (!subscribedQuality) {
|
353
|
+
return;
|
354
|
+
}
|
355
|
+
if (encoding.active !== subscribedQuality.enabled) {
|
356
|
+
hasChanged = true;
|
357
|
+
encoding.active = subscribedQuality.enabled;
|
358
|
+
log.debug(
|
359
|
+
`setting layer ${subscribedQuality.quality} to ${
|
360
|
+
encoding.active ? 'enabled' : 'disabled'
|
361
|
+
}`,
|
362
|
+
);
|
363
|
+
|
364
|
+
// FireFox does not support setting encoding.active to false, so we
|
365
|
+
// have a workaround of lowering its bitrate and resolution to the min.
|
366
|
+
if (isFireFox()) {
|
367
|
+
if (subscribedQuality.enabled) {
|
368
|
+
encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
|
369
|
+
encoding.maxBitrate = senderEncodings[idx].maxBitrate;
|
370
|
+
/* @ts-ignore */
|
371
|
+
encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
|
372
|
+
} else {
|
373
|
+
encoding.scaleResolutionDownBy = 4;
|
374
|
+
encoding.maxBitrate = 10;
|
375
|
+
/* @ts-ignore */
|
376
|
+
encoding.maxFrameRate = 2;
|
377
|
+
}
|
364
378
|
}
|
365
379
|
}
|
366
|
-
}
|
367
|
-
});
|
380
|
+
});
|
368
381
|
|
369
|
-
|
370
|
-
|
371
|
-
|
382
|
+
if (hasChanged) {
|
383
|
+
params.encodings = encodings;
|
384
|
+
await sender.setParameters(params);
|
385
|
+
}
|
386
|
+
} finally {
|
387
|
+
unlock();
|
372
388
|
}
|
373
389
|
}
|
374
390
|
|
@@ -24,9 +24,10 @@ export default class RemoteTrackPublication extends TrackPublication {
|
|
24
24
|
|
25
25
|
protected fps?: number;
|
26
26
|
|
27
|
-
constructor(kind: Track.Kind,
|
28
|
-
super(kind,
|
27
|
+
constructor(kind: Track.Kind, ti: TrackInfo, autoSubscribe: boolean | undefined) {
|
28
|
+
super(kind, ti.sid, ti.name);
|
29
29
|
this.subscribed = autoSubscribe;
|
30
|
+
this.updateInfo(ti);
|
30
31
|
}
|
31
32
|
|
32
33
|
/**
|
@@ -31,9 +31,6 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
31
31
|
) {
|
32
32
|
super(mediaTrack, sid, Track.Kind.Video, receiver);
|
33
33
|
this.adaptiveStreamSettings = adaptiveStreamSettings;
|
34
|
-
if (this.isAdaptiveStream) {
|
35
|
-
this.streamState = Track.StreamState.Paused;
|
36
|
-
}
|
37
34
|
}
|
38
35
|
|
39
36
|
get isAdaptiveStream(): boolean {
|
package/src/room/track/Track.ts
CHANGED
@@ -32,7 +32,8 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
|
|
32
32
|
mediaStream?: MediaStream;
|
33
33
|
|
34
34
|
/**
|
35
|
-
* indicates current state of stream
|
35
|
+
* indicates current state of stream, it'll indicate `paused` if the track
|
36
|
+
* has been paused by congestion controller
|
36
37
|
*/
|
37
38
|
streamState: Track.StreamState = Track.StreamState.Active;
|
38
39
|
|
@@ -28,12 +28,12 @@ export interface TrackPublishDefaults {
|
|
28
28
|
audioBitrate?: number;
|
29
29
|
|
30
30
|
/**
|
31
|
-
* dtx (Discontinuous Transmission of audio),
|
31
|
+
* dtx (Discontinuous Transmission of audio), enabled by default for mono tracks.
|
32
32
|
*/
|
33
33
|
dtx?: boolean;
|
34
34
|
|
35
35
|
/**
|
36
|
-
* red (Redundant Audio Data),
|
36
|
+
* red (Redundant Audio Data), enabled by default for mono tracks.
|
37
37
|
*/
|
38
38
|
red?: boolean;
|
39
39
|
|