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.
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
  }