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.
Files changed (42) hide show
  1. package/README.md +4 -0
  2. package/dist/livekit-client.esm.mjs +268 -71
  3. package/dist/livekit-client.esm.mjs.map +1 -1
  4. package/dist/livekit-client.umd.js +1 -1
  5. package/dist/livekit-client.umd.js.map +1 -1
  6. package/dist/src/room/PCTransport.d.ts.map +1 -1
  7. package/dist/src/room/Room.d.ts +5 -0
  8. package/dist/src/room/Room.d.ts.map +1 -1
  9. package/dist/src/room/events.d.ts +10 -2
  10. package/dist/src/room/events.d.ts.map +1 -1
  11. package/dist/src/room/participant/LocalParticipant.d.ts +4 -1
  12. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  13. package/dist/src/room/participant/Participant.d.ts +1 -0
  14. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  15. package/dist/src/room/timers.d.ts +4 -4
  16. package/dist/src/room/timers.d.ts.map +1 -1
  17. package/dist/src/room/track/options.d.ts +1 -1
  18. package/dist/src/room/types.d.ts +2 -0
  19. package/dist/src/room/types.d.ts.map +1 -1
  20. package/dist/src/room/utils.d.ts +1 -1
  21. package/dist/src/room/utils.d.ts.map +1 -1
  22. package/dist/ts4.2/src/room/Room.d.ts +5 -0
  23. package/dist/ts4.2/src/room/events.d.ts +10 -2
  24. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +4 -1
  25. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
  26. package/dist/ts4.2/src/room/timers.d.ts +4 -4
  27. package/dist/ts4.2/src/room/track/options.d.ts +1 -1
  28. package/dist/ts4.2/src/room/types.d.ts +2 -0
  29. package/dist/ts4.2/src/room/utils.d.ts +1 -1
  30. package/package.json +2 -2
  31. package/src/room/PCTransport.ts +3 -1
  32. package/src/room/RTCEngine.ts +1 -1
  33. package/src/room/Room.ts +28 -1
  34. package/src/room/events.ts +10 -0
  35. package/src/room/participant/LocalParticipant.ts +112 -76
  36. package/src/room/participant/Participant.ts +1 -0
  37. package/src/room/timers.ts +15 -6
  38. package/src/room/track/LocalVideoTrack.test.ts +60 -0
  39. package/src/room/track/LocalVideoTrack.ts +1 -1
  40. package/src/room/track/options.ts +1 -1
  41. package/src/room/types.ts +2 -0
  42. 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 ti = await this.engine.addTrack(req);
912
- // server might not support the codec the client has requested, in that case, fallback
913
- // to a supported codec
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
- if (primaryCodecMime && track.kind === Track.Kind.Video) {
921
- const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
922
- if (updatedCodec !== videoCodec) {
923
- this.log.debug('falling back to server selected codec', {
924
- ...this.logContext,
925
- ...getLogContextFromTrack(track),
926
- codec: updatedCodec,
927
- });
928
- opts.videoCodec = updatedCodec;
929
-
930
- // recompute encodings since bitrates/etc could have changed
931
- encodings = computeVideoEncodings(
932
- track.source === Track.Source.ScreenShare,
933
- req.width,
934
- req.height,
935
- opts,
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 ti = await this.engine.addTrack(req);
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
- const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
1087
- if (encodings) {
1088
- transceiverInit.sendEncodings = encodings;
1089
- }
1090
- await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);
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
  };
@@ -4,13 +4,22 @@
4
4
  * that the timer fires on time.
5
5
  */
6
6
  export default class CriticalTimers {
7
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
8
- static setTimeout = (...args: Parameters<typeof setTimeout>) => setTimeout(...args);
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
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
11
- static setInterval = (...args: Parameters<typeof setInterval>) => setInterval(...args);
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 = (...args: Parameters<typeof clearTimeout>) => clearTimeout(...args);
16
+ static clearTimeout: (
17
+ ...args: Parameters<typeof clearTimeout>
18
+ ) => ReturnType<typeof clearTimeout> = (...args: Parameters<typeof clearTimeout>) =>
19
+ clearTimeout(...args);
14
20
 
15
- static clearInterval = (...args: Parameters<typeof clearInterval>) => clearInterval(...args);
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 'L3T3'.
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
@@ -65,4 +65,6 @@ export interface TranscriptionSegment {
65
65
  startTime: number;
66
66
  endTime: number;
67
67
  final: boolean;
68
+ firstReceivedTime: number;
69
+ lastReceivedTime: number;
68
70
  }
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
  }