livekit-client 2.5.0 → 2.5.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +4 -0
- package/dist/livekit-client.esm.mjs +268 -71
- 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/Room.d.ts +5 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +10 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +4 -1
- 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/timers.d.ts +4 -4
- package/dist/src/room/timers.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +1 -1
- package/dist/src/room/types.d.ts +2 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/room/Room.d.ts +5 -0
- package/dist/ts4.2/src/room/events.d.ts +10 -2
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +4 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/src/room/timers.d.ts +4 -4
- package/dist/ts4.2/src/room/track/options.d.ts +1 -1
- package/dist/ts4.2/src/room/types.d.ts +2 -0
- package/dist/ts4.2/src/room/utils.d.ts +1 -1
- package/package.json +2 -2
- package/src/room/PCTransport.ts +3 -1
- package/src/room/RTCEngine.ts +1 -1
- package/src/room/Room.ts +28 -1
- package/src/room/events.ts +10 -0
- package/src/room/participant/LocalParticipant.ts +112 -76
- package/src/room/participant/Participant.ts +1 -0
- package/src/room/timers.ts +15 -6
- package/src/room/track/LocalVideoTrack.test.ts +60 -0
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/options.ts +1 -1
- package/src/room/types.ts +2 -0
- package/src/room/utils.ts +10 -0
@@ -1,5 +1,6 @@
|
|
1
1
|
import {
|
2
2
|
AddTrackRequest,
|
3
|
+
Codec,
|
3
4
|
DataPacket,
|
4
5
|
DataPacket_Kind,
|
5
6
|
Encryption_Type,
|
@@ -9,6 +10,7 @@ import {
|
|
9
10
|
RequestResponse_Reason,
|
10
11
|
SimulcastCodec,
|
11
12
|
SubscribedQualityUpdate,
|
13
|
+
TrackInfo,
|
12
14
|
TrackUnpublishedResponse,
|
13
15
|
UserPacket,
|
14
16
|
} from '@livekit/protocol';
|
@@ -110,6 +112,8 @@ export default class LocalParticipant extends Participant {
|
|
110
112
|
}
|
111
113
|
>;
|
112
114
|
|
115
|
+
private enabledPublishVideoCodecs: Codec[] = [];
|
116
|
+
|
113
117
|
/** @internal */
|
114
118
|
constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
|
115
119
|
super(sid, identity, undefined, undefined, {
|
@@ -775,6 +779,17 @@ export default class LocalParticipant extends Participant {
|
|
775
779
|
if (opts.videoCodec === undefined) {
|
776
780
|
opts.videoCodec = defaultVideoCodec;
|
777
781
|
}
|
782
|
+
if (this.enabledPublishVideoCodecs.length > 0) {
|
783
|
+
// fallback to a supported codec if it is not supported
|
784
|
+
if (
|
785
|
+
!this.enabledPublishVideoCodecs.some(
|
786
|
+
(c) => opts.videoCodec === mimeTypeToVideoCodecString(c.mime),
|
787
|
+
)
|
788
|
+
) {
|
789
|
+
opts.videoCodec = mimeTypeToVideoCodecString(this.enabledPublishVideoCodecs[0].mime);
|
790
|
+
}
|
791
|
+
}
|
792
|
+
|
778
793
|
const videoCodec = opts.videoCodec;
|
779
794
|
|
780
795
|
// handle track actions
|
@@ -908,33 +923,87 @@ export default class LocalParticipant extends Participant {
|
|
908
923
|
throw new UnexpectedConnectionState('cannot publish track when not connected');
|
909
924
|
}
|
910
925
|
|
911
|
-
const
|
912
|
-
|
913
|
-
|
914
|
-
let primaryCodecMime: string | undefined;
|
915
|
-
ti.codecs.forEach((codec) => {
|
916
|
-
if (primaryCodecMime === undefined) {
|
917
|
-
primaryCodecMime = codec.mimeType;
|
926
|
+
const negotiate = async () => {
|
927
|
+
if (!this.engine.pcManager) {
|
928
|
+
throw new UnexpectedConnectionState('pcManager is not ready');
|
918
929
|
}
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
if (
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
930
|
+
|
931
|
+
track.sender = await this.engine.createSender(track, opts, encodings);
|
932
|
+
|
933
|
+
if (track instanceof LocalVideoTrack) {
|
934
|
+
opts.degradationPreference ??= getDefaultDegradationPreference(track);
|
935
|
+
track.setDegradationPreference(opts.degradationPreference);
|
936
|
+
}
|
937
|
+
|
938
|
+
if (encodings) {
|
939
|
+
if (isFireFox() && track.kind === Track.Kind.Audio) {
|
940
|
+
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
941
|
+
livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
|
942
|
+
publish high quality audio track. But firefox always uses this value as the actual
|
943
|
+
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
|
944
|
+
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
945
|
+
fix the issue.
|
946
|
+
*/
|
947
|
+
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
948
|
+
for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
|
949
|
+
if (transceiver.sender === track.sender) {
|
950
|
+
trackTransceiver = transceiver;
|
951
|
+
break;
|
952
|
+
}
|
953
|
+
}
|
954
|
+
if (trackTransceiver) {
|
955
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
956
|
+
transceiver: trackTransceiver,
|
957
|
+
codec: 'opus',
|
958
|
+
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
|
959
|
+
});
|
960
|
+
}
|
961
|
+
} else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
|
962
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
963
|
+
cid: req.cid,
|
964
|
+
codec: track.codec,
|
965
|
+
maxbr: encodings[0].maxBitrate / 1000,
|
966
|
+
});
|
967
|
+
}
|
968
|
+
}
|
969
|
+
|
970
|
+
await this.engine.negotiate();
|
971
|
+
};
|
972
|
+
|
973
|
+
let ti: TrackInfo;
|
974
|
+
if (this.enabledPublishVideoCodecs.length > 0) {
|
975
|
+
const rets = await Promise.all([this.engine.addTrack(req), negotiate()]);
|
976
|
+
ti = rets[0];
|
977
|
+
} else {
|
978
|
+
ti = await this.engine.addTrack(req);
|
979
|
+
// server might not support the codec the client has requested, in that case, fallback
|
980
|
+
// to a supported codec
|
981
|
+
let primaryCodecMime: string | undefined;
|
982
|
+
ti.codecs.forEach((codec) => {
|
983
|
+
if (primaryCodecMime === undefined) {
|
984
|
+
primaryCodecMime = codec.mimeType;
|
985
|
+
}
|
986
|
+
});
|
987
|
+
if (primaryCodecMime && track.kind === Track.Kind.Video) {
|
988
|
+
const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
|
989
|
+
if (updatedCodec !== videoCodec) {
|
990
|
+
this.log.debug('falling back to server selected codec', {
|
991
|
+
...this.logContext,
|
992
|
+
...getLogContextFromTrack(track),
|
993
|
+
codec: updatedCodec,
|
994
|
+
});
|
995
|
+
opts.videoCodec = updatedCodec;
|
996
|
+
|
997
|
+
// recompute encodings since bitrates/etc could have changed
|
998
|
+
encodings = computeVideoEncodings(
|
999
|
+
track.source === Track.Source.ScreenShare,
|
1000
|
+
req.width,
|
1001
|
+
req.height,
|
1002
|
+
opts,
|
1003
|
+
);
|
1004
|
+
}
|
937
1005
|
}
|
1006
|
+
await negotiate();
|
938
1007
|
}
|
939
1008
|
|
940
1009
|
const publication = new LocalTrackPublication(track.kind, ti, track, {
|
@@ -945,56 +1014,12 @@ export default class LocalParticipant extends Participant {
|
|
945
1014
|
publication.options = opts;
|
946
1015
|
track.sid = ti.sid;
|
947
1016
|
|
948
|
-
if (!this.engine.pcManager) {
|
949
|
-
throw new UnexpectedConnectionState('pcManager is not ready');
|
950
|
-
}
|
951
1017
|
this.log.debug(`publishing ${track.kind} with encodings`, {
|
952
1018
|
...this.logContext,
|
953
1019
|
encodings,
|
954
1020
|
trackInfo: ti,
|
955
1021
|
});
|
956
1022
|
|
957
|
-
track.sender = await this.engine.createSender(track, opts, encodings);
|
958
|
-
|
959
|
-
if (track instanceof LocalVideoTrack) {
|
960
|
-
opts.degradationPreference ??= getDefaultDegradationPreference(track);
|
961
|
-
track.setDegradationPreference(opts.degradationPreference);
|
962
|
-
}
|
963
|
-
|
964
|
-
if (encodings) {
|
965
|
-
if (isFireFox() && track.kind === Track.Kind.Audio) {
|
966
|
-
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
967
|
-
livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
|
968
|
-
publish high quality audio track. But firefox always uses this value as the actual
|
969
|
-
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
|
970
|
-
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
971
|
-
fix the issue.
|
972
|
-
*/
|
973
|
-
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
974
|
-
for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
|
975
|
-
if (transceiver.sender === track.sender) {
|
976
|
-
trackTransceiver = transceiver;
|
977
|
-
break;
|
978
|
-
}
|
979
|
-
}
|
980
|
-
if (trackTransceiver) {
|
981
|
-
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
982
|
-
transceiver: trackTransceiver,
|
983
|
-
codec: 'opus',
|
984
|
-
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
|
985
|
-
});
|
986
|
-
}
|
987
|
-
} else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
|
988
|
-
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
989
|
-
cid: req.cid,
|
990
|
-
codec: track.codec,
|
991
|
-
maxbr: encodings[0].maxBitrate / 1000,
|
992
|
-
});
|
993
|
-
}
|
994
|
-
}
|
995
|
-
|
996
|
-
await this.engine.negotiate();
|
997
|
-
|
998
1023
|
if (track instanceof LocalVideoTrack) {
|
999
1024
|
track.startMonitor(this.engine.client);
|
1000
1025
|
} else if (track instanceof LocalAudioTrack) {
|
@@ -1081,15 +1106,19 @@ export default class LocalParticipant extends Participant {
|
|
1081
1106
|
throw new UnexpectedConnectionState('cannot publish track when not connected');
|
1082
1107
|
}
|
1083
1108
|
|
1084
|
-
const
|
1109
|
+
const negotiate = async () => {
|
1110
|
+
const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
|
1111
|
+
if (encodings) {
|
1112
|
+
transceiverInit.sendEncodings = encodings;
|
1113
|
+
}
|
1114
|
+
await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);
|
1085
1115
|
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1116
|
+
await this.engine.negotiate();
|
1117
|
+
};
|
1118
|
+
|
1119
|
+
const rets = await Promise.all([this.engine.addTrack(req), negotiate()]);
|
1120
|
+
const ti = rets[0];
|
1091
1121
|
|
1092
|
-
await this.engine.negotiate();
|
1093
1122
|
this.log.debug(`published ${videoCodec} for track ${track.sid}`, {
|
1094
1123
|
...this.logContext,
|
1095
1124
|
encodings,
|
@@ -1309,6 +1338,13 @@ export default class LocalParticipant extends Participant {
|
|
1309
1338
|
}
|
1310
1339
|
}
|
1311
1340
|
|
1341
|
+
/** @internal */
|
1342
|
+
setEnabledPublishCodecs(codecs: Codec[]) {
|
1343
|
+
this.enabledPublishVideoCodecs = codecs.filter(
|
1344
|
+
(c) => c.mime.split('/')[0].toLowerCase() === 'video',
|
1345
|
+
);
|
1346
|
+
}
|
1347
|
+
|
1312
1348
|
/** @internal */
|
1313
1349
|
updateInfo(info: ParticipantInfo): boolean {
|
1314
1350
|
if (info.sid !== this.sid) {
|
@@ -386,4 +386,5 @@ export type ParticipantEventCallbacks = {
|
|
386
386
|
status: TrackPublication.SubscriptionStatus,
|
387
387
|
) => void;
|
388
388
|
attributesChanged: (changedAttributes: Record<string, string>) => void;
|
389
|
+
localTrackSubscribed: (trackPublication: LocalTrackPublication) => void;
|
389
390
|
};
|
package/src/room/timers.ts
CHANGED
@@ -4,13 +4,22 @@
|
|
4
4
|
* that the timer fires on time.
|
5
5
|
*/
|
6
6
|
export default class CriticalTimers {
|
7
|
-
|
8
|
-
|
7
|
+
static setTimeout: (...args: Parameters<typeof setTimeout>) => ReturnType<typeof setTimeout> = (
|
8
|
+
...args: Parameters<typeof setTimeout>
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
10
|
+
) => setTimeout(...args);
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
+
static setInterval: (...args: Parameters<typeof setInterval>) => ReturnType<typeof setInterval> =
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
14
|
+
(...args: Parameters<typeof setInterval>) => setInterval(...args);
|
12
15
|
|
13
|
-
static clearTimeout
|
16
|
+
static clearTimeout: (
|
17
|
+
...args: Parameters<typeof clearTimeout>
|
18
|
+
) => ReturnType<typeof clearTimeout> = (...args: Parameters<typeof clearTimeout>) =>
|
19
|
+
clearTimeout(...args);
|
14
20
|
|
15
|
-
static clearInterval
|
21
|
+
static clearInterval: (
|
22
|
+
...args: Parameters<typeof clearInterval>
|
23
|
+
) => ReturnType<typeof clearInterval> = (...args: Parameters<typeof clearInterval>) =>
|
24
|
+
clearInterval(...args);
|
16
25
|
}
|
@@ -47,6 +47,66 @@ describe('videoLayersFromEncodings', () => {
|
|
47
47
|
expect(layers[2].height).toBe(720);
|
48
48
|
});
|
49
49
|
|
50
|
+
it('returns qualities starting from lowest for SVC', () => {
|
51
|
+
const layers = videoLayersFromEncodings(
|
52
|
+
1280,
|
53
|
+
720,
|
54
|
+
[
|
55
|
+
{
|
56
|
+
/** @ts-ignore */
|
57
|
+
scalabilityMode: 'L2T2',
|
58
|
+
},
|
59
|
+
],
|
60
|
+
true,
|
61
|
+
);
|
62
|
+
|
63
|
+
expect(layers).toHaveLength(2);
|
64
|
+
expect(layers[0].quality).toBe(VideoQuality.MEDIUM);
|
65
|
+
expect(layers[0].width).toBe(1280);
|
66
|
+
expect(layers[1].quality).toBe(VideoQuality.LOW);
|
67
|
+
expect(layers[1].width).toBe(640);
|
68
|
+
});
|
69
|
+
|
70
|
+
it('returns qualities starting from lowest for SVC (three layers)', () => {
|
71
|
+
const layers = videoLayersFromEncodings(
|
72
|
+
1280,
|
73
|
+
720,
|
74
|
+
[
|
75
|
+
{
|
76
|
+
/** @ts-ignore */
|
77
|
+
scalabilityMode: 'L3T3',
|
78
|
+
},
|
79
|
+
],
|
80
|
+
true,
|
81
|
+
);
|
82
|
+
|
83
|
+
expect(layers).toHaveLength(3);
|
84
|
+
expect(layers[0].quality).toBe(VideoQuality.HIGH);
|
85
|
+
expect(layers[0].width).toBe(1280);
|
86
|
+
expect(layers[1].quality).toBe(VideoQuality.MEDIUM);
|
87
|
+
expect(layers[1].width).toBe(640);
|
88
|
+
expect(layers[2].quality).toBe(VideoQuality.LOW);
|
89
|
+
expect(layers[2].width).toBe(320);
|
90
|
+
});
|
91
|
+
|
92
|
+
it('returns qualities starting from lowest for SVC (single layer)', () => {
|
93
|
+
const layers = videoLayersFromEncodings(
|
94
|
+
1280,
|
95
|
+
720,
|
96
|
+
[
|
97
|
+
{
|
98
|
+
/** @ts-ignore */
|
99
|
+
scalabilityMode: 'L1T2',
|
100
|
+
},
|
101
|
+
],
|
102
|
+
true,
|
103
|
+
);
|
104
|
+
|
105
|
+
expect(layers).toHaveLength(1);
|
106
|
+
expect(layers[0].quality).toBe(VideoQuality.LOW);
|
107
|
+
expect(layers[0].width).toBe(1280);
|
108
|
+
});
|
109
|
+
|
50
110
|
it('handles portrait', () => {
|
51
111
|
const layers = videoLayersFromEncodings(720, 1280, [
|
52
112
|
{
|
@@ -607,7 +607,7 @@ export function videoLayersFromEncodings(
|
|
607
607
|
for (let i = 0; i < sm.spatial; i += 1) {
|
608
608
|
layers.push(
|
609
609
|
new VideoLayer({
|
610
|
-
quality: VideoQuality.HIGH - i,
|
610
|
+
quality: Math.min(VideoQuality.HIGH, sm.spatial - 1) - i,
|
611
611
|
width: Math.ceil(width / resRatio ** i),
|
612
612
|
height: Math.ceil(height / resRatio ** i),
|
613
613
|
bitrate: encodings[0].maxBitrate
|
@@ -64,7 +64,7 @@ export interface TrackPublishDefaults {
|
|
64
64
|
simulcast?: boolean;
|
65
65
|
|
66
66
|
/**
|
67
|
-
* scalability mode for svc codecs, defaults to '
|
67
|
+
* scalability mode for svc codecs, defaults to 'L3T3_KEY'.
|
68
68
|
* for svc codecs, simulcast is disabled.
|
69
69
|
*/
|
70
70
|
scalabilityMode?: ScalabilityMode;
|
package/src/room/types.ts
CHANGED
package/src/room/utils.ts
CHANGED
@@ -532,8 +532,16 @@ export function toHttpUrl(url: string): string {
|
|
532
532
|
|
533
533
|
export function extractTranscriptionSegments(
|
534
534
|
transcription: TranscriptionModel,
|
535
|
+
firstReceivedTimesMap: Map<string, number>,
|
535
536
|
): TranscriptionSegment[] {
|
536
537
|
return transcription.segments.map(({ id, text, language, startTime, endTime, final }) => {
|
538
|
+
const firstReceivedTime = firstReceivedTimesMap.get(id) ?? Date.now();
|
539
|
+
const lastReceivedTime = Date.now();
|
540
|
+
if (final) {
|
541
|
+
firstReceivedTimesMap.delete(id);
|
542
|
+
} else {
|
543
|
+
firstReceivedTimesMap.set(id, firstReceivedTime);
|
544
|
+
}
|
537
545
|
return {
|
538
546
|
id,
|
539
547
|
text,
|
@@ -541,6 +549,8 @@ export function extractTranscriptionSegments(
|
|
541
549
|
endTime: Number.parseInt(endTime.toString()),
|
542
550
|
final,
|
543
551
|
language,
|
552
|
+
firstReceivedTime,
|
553
|
+
lastReceivedTime,
|
544
554
|
};
|
545
555
|
});
|
546
556
|
}
|