livekit-client 2.4.2 → 2.5.1

Sign up to get free protection for your applications and to get access to all the features.
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;