livekit-client 2.5.0 → 2.5.1
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/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
|
}
|