livekit-client 1.6.5 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|