livekit-client 2.4.2 → 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 (80) hide show
  1. package/README.md +8 -4
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  4. package/dist/livekit-client.esm.mjs +374 -102
  5. package/dist/livekit-client.esm.mjs.map +1 -1
  6. package/dist/livekit-client.umd.js +1 -1
  7. package/dist/livekit-client.umd.js.map +1 -1
  8. package/dist/src/api/SignalClient.d.ts +3 -2
  9. package/dist/src/api/SignalClient.d.ts.map +1 -1
  10. package/dist/src/connectionHelper/checks/publishAudio.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
  12. package/dist/src/room/PCTransport.d.ts +1 -1
  13. package/dist/src/room/PCTransport.d.ts.map +1 -1
  14. package/dist/src/room/RTCEngine.d.ts +4 -3
  15. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  16. package/dist/src/room/Room.d.ts +5 -0
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/errors.d.ts +4 -3
  19. package/dist/src/room/errors.d.ts.map +1 -1
  20. package/dist/src/room/events.d.ts +12 -3
  21. package/dist/src/room/events.d.ts.map +1 -1
  22. package/dist/src/room/participant/LocalParticipant.d.ts +5 -2
  23. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  24. package/dist/src/room/participant/Participant.d.ts +1 -0
  25. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  26. package/dist/src/room/participant/RemoteParticipant.d.ts +1 -1
  27. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  28. package/dist/src/room/timers.d.ts +4 -4
  29. package/dist/src/room/timers.d.ts.map +1 -1
  30. package/dist/src/room/track/RemoteAudioTrack.d.ts +1 -1
  31. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  32. package/dist/src/room/track/RemoteTrack.d.ts +12 -2
  33. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -1
  35. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  36. package/dist/src/room/track/options.d.ts +1 -1
  37. package/dist/src/room/types.d.ts +2 -0
  38. package/dist/src/room/types.d.ts.map +1 -1
  39. package/dist/src/room/utils.d.ts +1 -1
  40. package/dist/src/room/utils.d.ts.map +1 -1
  41. package/dist/src/version.d.ts +1 -1
  42. package/dist/ts4.2/src/api/SignalClient.d.ts +3 -2
  43. package/dist/ts4.2/src/room/PCTransport.d.ts +1 -1
  44. package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -3
  45. package/dist/ts4.2/src/room/Room.d.ts +5 -0
  46. package/dist/ts4.2/src/room/errors.d.ts +4 -3
  47. package/dist/ts4.2/src/room/events.d.ts +12 -3
  48. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +5 -2
  49. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
  50. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +1 -1
  51. package/dist/ts4.2/src/room/timers.d.ts +4 -4
  52. package/dist/ts4.2/src/room/track/RemoteAudioTrack.d.ts +1 -1
  53. package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +12 -2
  54. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -1
  55. package/dist/ts4.2/src/room/track/options.d.ts +1 -1
  56. package/dist/ts4.2/src/room/types.d.ts +2 -0
  57. package/dist/ts4.2/src/room/utils.d.ts +1 -1
  58. package/dist/ts4.2/src/version.d.ts +1 -1
  59. package/package.json +10 -10
  60. package/src/api/SignalClient.ts +12 -6
  61. package/src/connectionHelper/checks/publishAudio.ts +4 -1
  62. package/src/connectionHelper/checks/publishVideo.ts +6 -3
  63. package/src/room/PCTransport.ts +4 -1
  64. package/src/room/RTCEngine.ts +11 -5
  65. package/src/room/Room.ts +42 -5
  66. package/src/room/errors.ts +7 -3
  67. package/src/room/events.ts +12 -1
  68. package/src/room/participant/LocalParticipant.ts +125 -84
  69. package/src/room/participant/Participant.ts +1 -0
  70. package/src/room/participant/RemoteParticipant.ts +1 -1
  71. package/src/room/timers.ts +15 -6
  72. package/src/room/track/LocalVideoTrack.test.ts +60 -0
  73. package/src/room/track/LocalVideoTrack.ts +1 -1
  74. package/src/room/track/RemoteAudioTrack.ts +1 -1
  75. package/src/room/track/RemoteTrack.ts +38 -2
  76. package/src/room/track/RemoteVideoTrack.ts +2 -2
  77. package/src/room/track/options.ts +1 -1
  78. package/src/room/types.ts +2 -0
  79. package/src/room/utils.ts +10 -0
  80. package/src/version.ts +1 -1
@@ -323,6 +323,11 @@ export enum RoomEvent {
323
323
  * args: (kind: MediaDeviceKind, deviceId: string)
324
324
  */
325
325
  ActiveDeviceChanged = 'activeDeviceChanged',
326
+
327
+ /**
328
+ * fired when the first remote participant has subscribed to the localParticipant's track
329
+ */
330
+ LocalTrackSubscribed = 'localTrackSubscribed',
326
331
  }
327
332
 
328
333
  export enum ParticipantEvent {
@@ -509,6 +514,11 @@ export enum ParticipantEvent {
509
514
  * When a participant's attributes changed, this event will be emitted with the changed attributes
510
515
  */
511
516
  AttributesChanged = 'attributesChanged',
517
+
518
+ /**
519
+ * fired on local participant only, when the first remote participant has subscribed to the track specified in the payload
520
+ */
521
+ LocalTrackSubscribed = 'localTrackSubscribed',
512
522
  }
513
523
 
514
524
  /** @internal */
@@ -538,8 +548,9 @@ export enum EngineEvent {
538
548
  RemoteMute = 'remoteMute',
539
549
  SubscribedQualityUpdate = 'subscribedQualityUpdate',
540
550
  LocalTrackUnpublished = 'localTrackUnpublished',
551
+ LocalTrackSubscribed = 'localTrackSubscribed',
541
552
  Offline = 'offline',
542
- SignalRequestError = 'signalRequestError',
553
+ SignalRequestResponse = 'signalRequestResponse',
543
554
  }
544
555
 
545
556
  export enum TrackEvent {
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  AddTrackRequest,
3
+ Codec,
3
4
  DataPacket,
4
5
  DataPacket_Kind,
5
6
  Encryption_Type,
6
- ErrorResponse,
7
7
  ParticipantInfo,
8
8
  ParticipantPermission,
9
+ RequestResponse,
10
+ RequestResponse_Reason,
9
11
  SimulcastCodec,
10
12
  SubscribedQualityUpdate,
13
+ TrackInfo,
11
14
  TrackUnpublishedResponse,
12
15
  UserPacket,
13
16
  } from '@livekit/protocol';
@@ -109,6 +112,8 @@ export default class LocalParticipant extends Participant {
109
112
  }
110
113
  >;
111
114
 
115
+ private enabledPublishVideoCodecs: Codec[] = [];
116
+
112
117
  /** @internal */
113
118
  constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
114
119
  super(sid, identity, undefined, undefined, {
@@ -177,7 +182,7 @@ export default class LocalParticipant extends Participant {
177
182
  .on(EngineEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
178
183
  .on(EngineEvent.SubscribedQualityUpdate, this.handleSubscribedQualityUpdate)
179
184
  .on(EngineEvent.Disconnected, this.handleDisconnected)
180
- .on(EngineEvent.SignalRequestError, this.handleSignalRequestError);
185
+ .on(EngineEvent.SignalRequestResponse, this.handleSignalRequestResponse);
181
186
  }
182
187
 
183
188
  private handleReconnecting = () => {
@@ -200,11 +205,13 @@ export default class LocalParticipant extends Participant {
200
205
  }
201
206
  };
202
207
 
203
- private handleSignalRequestError = (error: ErrorResponse) => {
204
- const { requestId, reason, message } = error;
205
- const failedRequest = this.pendingSignalRequests.get(requestId);
206
- if (failedRequest) {
207
- failedRequest.reject(new SignalRequestError(message, reason));
208
+ private handleSignalRequestResponse = (response: RequestResponse) => {
209
+ const { requestId, reason, message } = response;
210
+ const targetRequest = this.pendingSignalRequests.get(requestId);
211
+ if (targetRequest) {
212
+ if (reason !== RequestResponse_Reason.OK) {
213
+ targetRequest.reject(new SignalRequestError(message, reason));
214
+ }
208
215
  this.pendingSignalRequests.delete(requestId);
209
216
  }
210
217
  };
@@ -278,7 +285,9 @@ export default class LocalParticipant extends Participant {
278
285
  }
279
286
  await sleep(50);
280
287
  }
281
- reject(new SignalRequestError('Request to update local metadata timed out'));
288
+ reject(
289
+ new SignalRequestError('Request to update local metadata timed out', 'TimeoutError'),
290
+ );
282
291
  } catch (e: any) {
283
292
  if (e instanceof Error) reject(e);
284
293
  }
@@ -770,6 +779,17 @@ export default class LocalParticipant extends Participant {
770
779
  if (opts.videoCodec === undefined) {
771
780
  opts.videoCodec = defaultVideoCodec;
772
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
+
773
793
  const videoCodec = opts.videoCodec;
774
794
 
775
795
  // handle track actions
@@ -903,33 +923,87 @@ export default class LocalParticipant extends Participant {
903
923
  throw new UnexpectedConnectionState('cannot publish track when not connected');
904
924
  }
905
925
 
906
- const ti = await this.engine.addTrack(req);
907
- // server might not support the codec the client has requested, in that case, fallback
908
- // to a supported codec
909
- let primaryCodecMime: string | undefined;
910
- ti.codecs.forEach((codec) => {
911
- if (primaryCodecMime === undefined) {
912
- primaryCodecMime = codec.mimeType;
926
+ const negotiate = async () => {
927
+ if (!this.engine.pcManager) {
928
+ throw new UnexpectedConnectionState('pcManager is not ready');
913
929
  }
914
- });
915
- if (primaryCodecMime && track.kind === Track.Kind.Video) {
916
- const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
917
- if (updatedCodec !== videoCodec) {
918
- this.log.debug('falling back to server selected codec', {
919
- ...this.logContext,
920
- ...getLogContextFromTrack(track),
921
- codec: updatedCodec,
922
- });
923
- opts.videoCodec = updatedCodec;
924
-
925
- // recompute encodings since bitrates/etc could have changed
926
- encodings = computeVideoEncodings(
927
- track.source === Track.Source.ScreenShare,
928
- req.width,
929
- req.height,
930
- opts,
931
- );
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
+ }
932
1005
  }
1006
+ await negotiate();
933
1007
  }
934
1008
 
935
1009
  const publication = new LocalTrackPublication(track.kind, ti, track, {
@@ -940,56 +1014,12 @@ export default class LocalParticipant extends Participant {
940
1014
  publication.options = opts;
941
1015
  track.sid = ti.sid;
942
1016
 
943
- if (!this.engine.pcManager) {
944
- throw new UnexpectedConnectionState('pcManager is not ready');
945
- }
946
1017
  this.log.debug(`publishing ${track.kind} with encodings`, {
947
1018
  ...this.logContext,
948
1019
  encodings,
949
1020
  trackInfo: ti,
950
1021
  });
951
1022
 
952
- track.sender = await this.engine.createSender(track, opts, encodings);
953
-
954
- if (track instanceof LocalVideoTrack) {
955
- opts.degradationPreference ??= getDefaultDegradationPreference(track);
956
- track.setDegradationPreference(opts.degradationPreference);
957
- }
958
-
959
- if (encodings) {
960
- if (isFireFox() && track.kind === Track.Kind.Audio) {
961
- /* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
962
- livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
963
- publish high quality audio track. But firefox always uses this value as the actual
964
- bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
965
- So the client need to modify maxaverragebitrates in answer sdp to user provided value to
966
- fix the issue.
967
- */
968
- let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
969
- for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
970
- if (transceiver.sender === track.sender) {
971
- trackTransceiver = transceiver;
972
- break;
973
- }
974
- }
975
- if (trackTransceiver) {
976
- this.engine.pcManager.publisher.setTrackCodecBitrate({
977
- transceiver: trackTransceiver,
978
- codec: 'opus',
979
- maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
980
- });
981
- }
982
- } else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
983
- this.engine.pcManager.publisher.setTrackCodecBitrate({
984
- cid: req.cid,
985
- codec: track.codec,
986
- maxbr: encodings[0].maxBitrate / 1000,
987
- });
988
- }
989
- }
990
-
991
- await this.engine.negotiate();
992
-
993
1023
  if (track instanceof LocalVideoTrack) {
994
1024
  track.startMonitor(this.engine.client);
995
1025
  } else if (track instanceof LocalAudioTrack) {
@@ -1076,15 +1106,19 @@ export default class LocalParticipant extends Participant {
1076
1106
  throw new UnexpectedConnectionState('cannot publish track when not connected');
1077
1107
  }
1078
1108
 
1079
- 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);
1080
1115
 
1081
- const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
1082
- if (encodings) {
1083
- transceiverInit.sendEncodings = encodings;
1084
- }
1085
- 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];
1086
1121
 
1087
- await this.engine.negotiate();
1088
1122
  this.log.debug(`published ${videoCodec} for track ${track.sid}`, {
1089
1123
  ...this.logContext,
1090
1124
  encodings,
@@ -1304,6 +1338,13 @@ export default class LocalParticipant extends Participant {
1304
1338
  }
1305
1339
  }
1306
1340
 
1341
+ /** @internal */
1342
+ setEnabledPublishCodecs(codecs: Codec[]) {
1343
+ this.enabledPublishVideoCodecs = codecs.filter(
1344
+ (c) => c.mime.split('/')[0].toLowerCase() === 'video',
1345
+ );
1346
+ }
1347
+
1307
1348
  /** @internal */
1308
1349
  updateInfo(info: ParticipantInfo): boolean {
1309
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
  };
@@ -164,7 +164,7 @@ export default class RemoteParticipant extends Participant {
164
164
  mediaTrack: MediaStreamTrack,
165
165
  sid: Track.SID,
166
166
  mediaStream: MediaStream,
167
- receiver?: RTCRtpReceiver,
167
+ receiver: RTCRtpReceiver,
168
168
  adaptiveStreamSettings?: AdaptiveStreamSettings,
169
169
  triesLeft?: number,
170
170
  ) {
@@ -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
@@ -25,7 +25,7 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
25
25
  constructor(
26
26
  mediaTrack: MediaStreamTrack,
27
27
  sid: string,
28
- receiver?: RTCRtpReceiver,
28
+ receiver: RTCRtpReceiver,
29
29
  audioContext?: AudioContext,
30
30
  audioOutput?: AudioOutputOptions,
31
31
  loggerOptions?: LoggerOptions,
@@ -8,13 +8,13 @@ export default abstract class RemoteTrack<
8
8
  TrackKind extends Track.Kind = Track.Kind,
9
9
  > extends Track<TrackKind> {
10
10
  /** @internal */
11
- receiver?: RTCRtpReceiver;
11
+ receiver: RTCRtpReceiver | undefined;
12
12
 
13
13
  constructor(
14
14
  mediaTrack: MediaStreamTrack,
15
15
  sid: string,
16
16
  kind: TrackKind,
17
- receiver?: RTCRtpReceiver,
17
+ receiver: RTCRtpReceiver,
18
18
  loggerOptions?: LoggerOptions,
19
19
  ) {
20
20
  super(mediaTrack, kind, loggerOptions);
@@ -39,6 +39,9 @@ export default abstract class RemoteTrack<
39
39
  const onRemoveTrack = (event: MediaStreamTrackEvent) => {
40
40
  if (event.track === this._mediaStreamTrack) {
41
41
  stream.removeEventListener('removetrack', onRemoveTrack);
42
+ if (this.receiver && 'playoutDelayHint' in this.receiver) {
43
+ this.receiver.playoutDelayHint = undefined;
44
+ }
42
45
  this.receiver = undefined;
43
46
  this._currentBitrate = 0;
44
47
  this.emit(TrackEvent.Ended, this);
@@ -73,6 +76,39 @@ export default abstract class RemoteTrack<
73
76
  return statsReport;
74
77
  }
75
78
 
79
+ /**
80
+ * Allows to set a playout delay (in seconds) for this track.
81
+ * A higher value allows for more buffering of the track in the browser
82
+ * and will result in a delay of media being played back of `delayInSeconds`
83
+ */
84
+ setPlayoutDelay(delayInSeconds: number): void {
85
+ if (this.receiver) {
86
+ if ('playoutDelayHint' in this.receiver) {
87
+ this.receiver.playoutDelayHint = delayInSeconds;
88
+ } else {
89
+ this.log.warn('Playout delay not supported in this browser');
90
+ }
91
+ } else {
92
+ this.log.warn('Cannot set playout delay, track already ended');
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Returns the current playout delay (in seconds) of this track.
98
+ */
99
+ getPlayoutDelay(): number {
100
+ if (this.receiver) {
101
+ if ('playoutDelayHint' in this.receiver) {
102
+ return this.receiver.playoutDelayHint as number;
103
+ } else {
104
+ this.log.warn('Playout delay not supported in this browser');
105
+ }
106
+ } else {
107
+ this.log.warn('Cannot get playout delay, track already ended');
108
+ }
109
+ return 0;
110
+ }
111
+
76
112
  /* @internal */
77
113
  startMonitor() {
78
114
  if (!this.monitorInterval) {
@@ -26,7 +26,7 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
26
26
  constructor(
27
27
  mediaTrack: MediaStreamTrack,
28
28
  sid: string,
29
- receiver?: RTCRtpReceiver,
29
+ receiver: RTCRtpReceiver,
30
30
  adaptiveStreamSettings?: AdaptiveStreamSettings,
31
31
  loggerOptions?: LoggerOptions,
32
32
  ) {
@@ -226,7 +226,7 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
226
226
  );
227
227
 
228
228
  const backgroundPause =
229
- this.adaptiveStreamSettings?.pauseVideoInBackground ?? true // default to true
229
+ (this.adaptiveStreamSettings?.pauseVideoInBackground ?? true) // default to true
230
230
  ? this.isInBackground
231
231
  : false;
232
232
  const isPiPMode = this.elementInfos.some((info) => info.pictureInPicture);
@@ -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
  }
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { version as v } from '../package.json';
2
2
 
3
3
  export const version = v;
4
- export const protocolVersion = 13;
4
+ export const protocolVersion = 15;