livekit-client 0.16.6 → 0.17.2

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 (94) hide show
  1. package/dist/api/RequestQueue.js +6 -6
  2. package/dist/api/RequestQueue.js.map +1 -1
  3. package/dist/api/SignalClient.d.ts +3 -1
  4. package/dist/api/SignalClient.js +47 -5
  5. package/dist/api/SignalClient.js.map +1 -1
  6. package/dist/connect.js +1 -1
  7. package/dist/connect.js.map +1 -1
  8. package/dist/options.d.ts +7 -2
  9. package/dist/proto/livekit_models.d.ts +33 -0
  10. package/dist/proto/livekit_models.js +213 -3
  11. package/dist/proto/livekit_models.js.map +1 -1
  12. package/dist/proto/livekit_rtc.d.ts +15 -1
  13. package/dist/proto/livekit_rtc.js +128 -2
  14. package/dist/proto/livekit_rtc.js.map +1 -1
  15. package/dist/room/RTCEngine.d.ts +4 -3
  16. package/dist/room/RTCEngine.js +34 -13
  17. package/dist/room/RTCEngine.js.map +1 -1
  18. package/dist/room/Room.js +27 -12
  19. package/dist/room/Room.js.map +1 -1
  20. package/dist/room/events.d.ts +6 -1
  21. package/dist/room/events.js +6 -1
  22. package/dist/room/events.js.map +1 -1
  23. package/dist/room/participant/LocalParticipant.d.ts +3 -1
  24. package/dist/room/participant/LocalParticipant.js +24 -1
  25. package/dist/room/participant/LocalParticipant.js.map +1 -1
  26. package/dist/room/participant/RemoteParticipant.d.ts +2 -1
  27. package/dist/room/participant/RemoteParticipant.js +3 -3
  28. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  29. package/dist/room/participant/publishUtils.d.ts +6 -0
  30. package/dist/room/participant/publishUtils.js +65 -24
  31. package/dist/room/participant/publishUtils.js.map +1 -1
  32. package/dist/room/participant/publishUtils.test.js +35 -5
  33. package/dist/room/participant/publishUtils.test.js.map +1 -1
  34. package/dist/room/track/LocalAudioTrack.d.ts +2 -0
  35. package/dist/room/track/LocalAudioTrack.js +23 -0
  36. package/dist/room/track/LocalAudioTrack.js.map +1 -1
  37. package/dist/room/track/LocalTrack.d.ts +4 -0
  38. package/dist/room/track/LocalTrack.js +35 -0
  39. package/dist/room/track/LocalTrack.js.map +1 -1
  40. package/dist/room/track/LocalVideoTrack.d.ts +1 -0
  41. package/dist/room/track/LocalVideoTrack.js +13 -0
  42. package/dist/room/track/LocalVideoTrack.js.map +1 -1
  43. package/dist/room/track/RemoteTrack.d.ts +1 -0
  44. package/dist/room/track/RemoteTrack.js +2 -0
  45. package/dist/room/track/RemoteTrack.js.map +1 -1
  46. package/dist/room/track/RemoteVideoTrack.d.ts +4 -2
  47. package/dist/room/track/RemoteVideoTrack.js +28 -11
  48. package/dist/room/track/RemoteVideoTrack.js.map +1 -1
  49. package/dist/room/track/Track.d.ts +5 -1
  50. package/dist/room/track/Track.js +20 -1
  51. package/dist/room/track/Track.js.map +1 -1
  52. package/dist/room/track/create.js +1 -0
  53. package/dist/room/track/create.js.map +1 -1
  54. package/dist/room/track/defaults.js +2 -2
  55. package/dist/room/track/defaults.js.map +1 -1
  56. package/dist/room/track/options.d.ts +65 -15
  57. package/dist/room/track/options.js +38 -0
  58. package/dist/room/track/options.js.map +1 -1
  59. package/dist/room/track/types.d.ts +11 -0
  60. package/dist/room/track/utils.d.ts +10 -0
  61. package/dist/room/track/utils.js +46 -1
  62. package/dist/room/track/utils.js.map +1 -1
  63. package/dist/room/utils.d.ts +2 -0
  64. package/dist/room/utils.js +9 -1
  65. package/dist/room/utils.js.map +1 -1
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +2 -2
  69. package/src/api/RequestQueue.ts +7 -7
  70. package/src/api/SignalClient.ts +36 -6
  71. package/src/connect.ts +1 -1
  72. package/src/options.ts +12 -3
  73. package/src/proto/livekit_models.ts +249 -0
  74. package/src/proto/livekit_rtc.ts +155 -0
  75. package/src/room/RTCEngine.ts +39 -16
  76. package/src/room/Room.ts +27 -13
  77. package/src/room/events.ts +6 -1
  78. package/src/room/participant/LocalParticipant.ts +31 -4
  79. package/src/room/participant/RemoteParticipant.ts +4 -4
  80. package/src/room/participant/publishUtils.test.ts +46 -6
  81. package/src/room/participant/publishUtils.ts +72 -27
  82. package/src/room/track/LocalAudioTrack.ts +19 -1
  83. package/src/room/track/LocalTrack.ts +37 -0
  84. package/src/room/track/LocalVideoTrack.ts +9 -1
  85. package/src/room/track/RemoteTrack.ts +3 -0
  86. package/src/room/track/RemoteVideoTrack.ts +25 -10
  87. package/src/room/track/Track.ts +16 -2
  88. package/src/room/track/create.ts +1 -0
  89. package/src/room/track/defaults.ts +2 -2
  90. package/src/room/track/options.ts +55 -3
  91. package/src/room/track/types.ts +12 -0
  92. package/src/room/track/utils.ts +39 -0
  93. package/src/room/utils.ts +8 -0
  94. package/src/version.ts +1 -1
@@ -3,6 +3,8 @@ import type TypedEventEmitter from 'typed-emitter';
3
3
  import { SignalClient, SignalOptions } from '../api/SignalClient';
4
4
  import log from '../logger';
5
5
  import {
6
+ ClientConfigSetting,
7
+ ClientConfiguration,
6
8
  DataPacket, DataPacket_Kind, SpeakerInfo, TrackInfo, UserPacket,
7
9
  } from '../proto/livekit_models';
8
10
  import {
@@ -14,7 +16,7 @@ import {
14
16
  import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors';
15
17
  import { EngineEvent } from './events';
16
18
  import PCTransport from './PCTransport';
17
- import { isFireFox, sleep } from './utils';
19
+ import { isFireFox, isWeb, sleep } from './utils';
18
20
 
19
21
  const lossyDataChannel = '_lossy';
20
22
  const reliableDataChannel = '_reliable';
@@ -70,7 +72,9 @@ export default class RTCEngine extends (
70
72
 
71
73
  private reconnectStart: number = 0;
72
74
 
73
- private fullReconnect: boolean = false;
75
+ private fullReconnectOnNext: boolean = false;
76
+
77
+ private clientConfiguration?: ClientConfiguration;
74
78
 
75
79
  private connectedServerAddr?: string;
76
80
 
@@ -96,6 +100,7 @@ export default class RTCEngine extends (
96
100
  if (!this.subscriberPrimary) {
97
101
  this.negotiate();
98
102
  }
103
+ this.clientConfiguration = joinResponse.clientConfiguration;
99
104
 
100
105
  return joinResponse;
101
106
  }
@@ -107,7 +112,10 @@ export default class RTCEngine extends (
107
112
  if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
108
113
  this.publisher.pc.getSenders().forEach((sender) => {
109
114
  try {
110
- this.publisher?.pc.removeTrack(sender);
115
+ // TODO: react-native-webrtc doesn't have removeTrack yet.
116
+ if (this.publisher?.pc.removeTrack) {
117
+ this.publisher?.pc.removeTrack(sender);
118
+ }
111
119
  } catch (e) {
112
120
  log.warn('could not removeTrack', e);
113
121
  }
@@ -166,6 +174,11 @@ export default class RTCEngine extends (
166
174
  this.rtcConfig.iceServers = rtcIceServers;
167
175
  }
168
176
 
177
+ // @ts-ignore
178
+ this.rtcConfig.sdpSemantics = 'unified-plan';
179
+ // @ts-ignore
180
+ this.rtcConfig.continualGatheringPolicy = 'gather_continually';
181
+
169
182
  this.publisher = new PCTransport(this.rtcConfig);
170
183
  this.subscriber = new PCTransport(this.rtcConfig);
171
184
 
@@ -216,10 +229,18 @@ export default class RTCEngine extends (
216
229
  }
217
230
  };
218
231
 
219
- this.subscriber.pc.ontrack = (ev: RTCTrackEvent) => {
220
- this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
221
- };
222
-
232
+ if (isWeb()) {
233
+ this.subscriber.pc.ontrack = (ev: RTCTrackEvent) => {
234
+ this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
235
+ };
236
+ } else {
237
+ // TODO: react-native-webrtc doesn't have ontrack yet, replace when ready.
238
+ // @ts-ignore
239
+ this.subscriber.pc.onaddstream = (ev: { stream: MediaStream }) => {
240
+ const track = ev.stream.getTracks()[0];
241
+ this.emit(EngineEvent.MediaTrackAdded, track, ev.stream);
242
+ };
243
+ }
223
244
  // data channels
224
245
  this.lossyDC = this.publisher.pc.createDataChannel(lossyDataChannel, {
225
246
  // will drop older packets that arrive
@@ -303,7 +324,7 @@ export default class RTCEngine extends (
303
324
 
304
325
  this.client.onLeave = (leave?: LeaveRequest) => {
305
326
  if (leave?.canReconnect) {
306
- this.fullReconnect = true;
327
+ this.fullReconnectOnNext = true;
307
328
  this.primaryPC = undefined;
308
329
  } else {
309
330
  this.emit(EngineEvent.Disconnected);
@@ -376,19 +397,19 @@ export default class RTCEngine extends (
376
397
  if (this.isClosed) {
377
398
  return;
378
399
  }
379
- if (isFireFox()) {
380
- // FF does not support DTLS restart.
381
- this.fullReconnect = true;
400
+ if (isFireFox() // TODO remove once clientConfiguration handles firefox case server side
401
+ || this.clientConfiguration?.resumeConnection === ClientConfigSetting.DISABLED) {
402
+ this.fullReconnectOnNext = true;
382
403
  }
383
404
 
384
405
  try {
385
- if (this.fullReconnect) {
406
+ if (this.fullReconnectOnNext) {
386
407
  await this.restartConnection();
387
408
  } else {
388
409
  await this.resumeConnection();
389
410
  }
390
411
  this.reconnectAttempts = 0;
391
- this.fullReconnect = false;
412
+ this.fullReconnectOnNext = false;
392
413
  } catch (e) {
393
414
  this.reconnectAttempts += 1;
394
415
  let recoverable = true;
@@ -398,7 +419,7 @@ export default class RTCEngine extends (
398
419
  recoverable = false;
399
420
  } else if (!(e instanceof SignalReconnectError)) {
400
421
  // cannot resume
401
- this.fullReconnect = true;
422
+ this.fullReconnectOnNext = true;
402
423
  }
403
424
 
404
425
  const duration = Date.now() - this.reconnectStart;
@@ -444,6 +465,7 @@ export default class RTCEngine extends (
444
465
  }
445
466
 
446
467
  await this.waitForPCConnected();
468
+ this.client.setReconnected();
447
469
 
448
470
  // reconnect success
449
471
  this.emit(EngineEvent.Restarted, joinResponse);
@@ -478,6 +500,7 @@ export default class RTCEngine extends (
478
500
  }
479
501
 
480
502
  await this.waitForPCConnected();
503
+ this.client.setReconnected();
481
504
 
482
505
  // resume success
483
506
  this.emit(EngineEvent.Resumed);
@@ -564,7 +587,7 @@ export default class RTCEngine extends (
564
587
  this.publisher.negotiate();
565
588
  }
566
589
 
567
- private dataChannelForKind(kind: DataPacket_Kind): RTCDataChannel | undefined {
590
+ dataChannelForKind(kind: DataPacket_Kind): RTCDataChannel | undefined {
568
591
  if (kind === DataPacket_Kind.LOSSY) {
569
592
  return this.lossyDC;
570
593
  } if (kind === DataPacket_Kind.RELIABLE) {
@@ -621,7 +644,7 @@ export type EngineEventCallbacks = {
621
644
  mediaTrackAdded: (
622
645
  track: MediaStreamTrack,
623
646
  streams: MediaStream,
624
- receiver: RTCRtpReceiver
647
+ receiver?: RTCRtpReceiver
625
648
  ) => void,
626
649
  activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void,
627
650
  dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void,
package/src/room/Room.ts CHANGED
@@ -28,8 +28,9 @@ import LocalTrackPublication from './track/LocalTrackPublication';
28
28
  import RemoteTrackPublication from './track/RemoteTrackPublication';
29
29
  import { Track } from './track/Track';
30
30
  import { TrackPublication } from './track/TrackPublication';
31
- import { RemoteTrack } from './track/types';
32
- import { unpackStreamId } from './utils';
31
+ import { AdaptiveStreamSettings, RemoteTrack } from './track/types';
32
+ import { getNewAudioContext } from './track/utils';
33
+ import { isWeb, unpackStreamId } from './utils';
33
34
 
34
35
  export enum RoomState {
35
36
  Disconnected = 'disconnected',
@@ -195,7 +196,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
195
196
 
196
197
  try {
197
198
  const joinResponse = await this.engine.join(url, token, opts);
198
- log.debug('connected to Livekit Server', joinResponse.serverVersion);
199
+ log.debug(`connected to Livekit Server version: ${joinResponse.serverVersion}, region: ${joinResponse.serverRegion}`);
199
200
 
200
201
  if (!joinResponse.serverVersion) {
201
202
  throw new UnsupportedServer('unknown server version');
@@ -270,8 +271,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
270
271
  clearTimeout(connectTimeout);
271
272
 
272
273
  // also hook unload event
273
- window.addEventListener('beforeunload', this.onBeforeUnload);
274
- navigator.mediaDevices.addEventListener('devicechange', this.handleDeviceChange);
274
+ if (isWeb()) {
275
+ window.addEventListener('beforeunload', this.onBeforeUnload);
276
+ navigator.mediaDevices.addEventListener('devicechange', this.handleDeviceChange);
277
+ }
275
278
 
276
279
  resolve(this);
277
280
  });
@@ -437,12 +440,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
437
440
  if (!trackId || trackId === '') trackId = mediaTrack.id;
438
441
 
439
442
  const participant = this.getOrCreateParticipant(participantId);
443
+ let adaptiveStreamSettings: AdaptiveStreamSettings | undefined;
444
+ if (this.options.adaptiveStream) {
445
+ if (typeof this.options.adaptiveStream === 'object') {
446
+ adaptiveStreamSettings = this.options.adaptiveStream;
447
+ } else {
448
+ adaptiveStreamSettings = {};
449
+ }
450
+ }
440
451
  participant.addSubscribedMediaTrack(
441
452
  mediaTrack,
442
453
  trackId,
443
454
  stream,
444
455
  receiver,
445
- this.options.adaptiveStream,
456
+ adaptiveStreamSettings,
446
457
  );
447
458
  }
448
459
 
@@ -458,6 +469,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
458
469
  };
459
470
 
460
471
  private handleRestarted = async (joinResponse: JoinResponse) => {
472
+ log.debug('reconnected to server region', joinResponse.serverRegion);
461
473
  this.state = RoomState.Connected;
462
474
  this.emit(RoomEvent.Reconnected);
463
475
  this.emit(RoomEvent.StateChanged, this.state);
@@ -511,8 +523,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
511
523
  this.audioContext.close();
512
524
  this.audioContext = undefined;
513
525
  }
514
- window.removeEventListener('beforeunload', this.onBeforeUnload);
515
- navigator.mediaDevices.removeEventListener('devicechange', this.handleDeviceChange);
526
+ if (isWeb()) {
527
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
528
+ navigator.mediaDevices.removeEventListener('devicechange', this.handleDeviceChange);
529
+ }
516
530
  this.state = RoomState.Disconnected;
517
531
  this.emit(RoomEvent.Disconnected);
518
532
  this.emit(RoomEvent.StateChanged, this.state);
@@ -522,7 +536,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
522
536
  // handle changes to participant state, and send events
523
537
  participantInfos.forEach((info) => {
524
538
  if (info.sid === this.localParticipant.sid
525
- || info.identity === this.localParticipant.identity) {
539
+ || info.identity === this.localParticipant.identity) {
526
540
  this.localParticipant.updateInfo(info);
527
541
  return;
528
542
  }
@@ -717,10 +731,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
717
731
  }
718
732
  // by using an AudioContext, it reduces lag on audio elements
719
733
  // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
720
- // @ts-ignore
721
- const AudioContext = window.AudioContext || window.webkitAudioContext;
722
- if (AudioContext) {
723
- this.audioContext = new AudioContext();
734
+ const ctx = getNewAudioContext();
735
+ if (ctx) {
736
+ this.audioContext = ctx;
724
737
  }
725
738
  }
726
739
 
@@ -826,6 +839,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
826
839
  participantTracks: [],
827
840
  },
828
841
  publishTracks: this.localParticipant.publishedTracksInfo(),
842
+ dataChannels: this.localParticipant.dataChannelsInfo(),
829
843
  });
830
844
  }
831
845
 
@@ -185,7 +185,7 @@ export enum RoomEvent {
185
185
  ConnectionQualityChanged = 'connectionQualityChanged',
186
186
 
187
187
  /**
188
- * StreamState indicates if a subscribed track has been paused by the SFU
188
+ * StreamState indicates if a subscribed (remote) track has been paused by the SFU
189
189
  * (typically this happens because of subscriber's bandwidth constraints)
190
190
  *
191
191
  * When bandwidth conditions allow, the track will be resumed automatically.
@@ -400,6 +400,11 @@ export enum TrackEvent {
400
400
  AudioPlaybackStarted = 'audioPlaybackStarted',
401
401
  /** @internal */
402
402
  AudioPlaybackFailed = 'audioPlaybackFailed',
403
+ /**
404
+ * @internal
405
+ * Only fires on LocalAudioTrack instances
406
+ */
407
+ AudioSilenceDetected = 'audioSilenceDetected',
403
408
  /** @internal */
404
409
  VisibilityChanged = 'visibilityChanged',
405
410
  /** @internal */
@@ -3,7 +3,9 @@ import { RoomOptions } from '../../options';
3
3
  import {
4
4
  DataPacket, DataPacket_Kind,
5
5
  } from '../../proto/livekit_models';
6
- import { AddTrackRequest, SubscribedQualityUpdate, TrackPublishedResponse } from '../../proto/livekit_rtc';
6
+ import {
7
+ AddTrackRequest, DataChannelInfo, SubscribedQualityUpdate, TrackPublishedResponse,
8
+ } from '../../proto/livekit_rtc';
7
9
  import {
8
10
  TrackInvalidError,
9
11
  UnexpectedConnectionState,
@@ -17,10 +19,12 @@ import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTr
17
19
  import {
18
20
  CreateLocalTracksOptions,
19
21
  ScreenShareCaptureOptions,
20
- TrackPublishOptions, VideoCodec, VideoPresets,
22
+ ScreenSharePresets,
23
+ TrackPublishOptions, VideoCodec,
21
24
  } from '../track/options';
22
25
  import { Track } from '../track/Track';
23
26
  import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
27
+ import { isFireFox } from '../utils';
24
28
  import Participant from './Participant';
25
29
  import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
26
30
  import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
@@ -256,6 +260,7 @@ export default class LocalParticipant extends Participant {
256
260
  } else if (track.kind === Track.Kind.Audio) {
257
261
  track.source = Track.Source.Microphone;
258
262
  }
263
+ track.mediaStream = stream;
259
264
  return track;
260
265
  });
261
266
  }
@@ -272,7 +277,7 @@ export default class LocalParticipant extends Participant {
272
277
  options = {};
273
278
  }
274
279
  if (options.resolution === undefined) {
275
- options.resolution = VideoPresets.fhd.resolution;
280
+ options.resolution = ScreenSharePresets.h1080fps15.resolution;
276
281
  }
277
282
 
278
283
  let videoConstraints: MediaTrackConstraints | boolean = true;
@@ -355,6 +360,12 @@ export default class LocalParticipant extends Participant {
355
360
  track.stopOnMute = true;
356
361
  }
357
362
 
363
+ if (track.source === Track.Source.ScreenShare && isFireFox()) {
364
+ // Firefox does not work well with simulcasted screen share
365
+ // we frequently get no data on layer 0 when enabled
366
+ opts.simulcast = false;
367
+ }
368
+
358
369
  // handle track actions
359
370
  track.on(TrackEvent.Muted, this.onTrackMuted);
360
371
  track.on(TrackEvent.Unmuted, this.onTrackUnmuted);
@@ -525,7 +536,7 @@ export default class LocalParticipant extends Participant {
525
536
  destination?: RemoteParticipant[] | string[]) {
526
537
  const dest: string[] = [];
527
538
  if (destination !== undefined) {
528
- destination.forEach((val : any) => {
539
+ destination.forEach((val: any) => {
529
540
  if (val instanceof RemoteParticipant) {
530
541
  dest.push(val.sid);
531
542
  } else {
@@ -681,4 +692,20 @@ export default class LocalParticipant extends Participant {
681
692
  });
682
693
  return infos;
683
694
  }
695
+
696
+ /** @internal */
697
+ dataChannelsInfo(): DataChannelInfo[] {
698
+ const infos: DataChannelInfo[] = [];
699
+ const getInfo = (dc: RTCDataChannel | undefined) => {
700
+ if (dc?.id !== undefined && dc.id !== null) {
701
+ infos.push({
702
+ label: dc.label,
703
+ id: dc.id,
704
+ });
705
+ }
706
+ };
707
+ getInfo(this.engine.dataChannelForKind(DataPacket_Kind.LOSSY));
708
+ getInfo(this.engine.dataChannelForKind(DataPacket_Kind.RELIABLE));
709
+ return infos;
710
+ }
684
711
  }
@@ -10,7 +10,7 @@ import RemoteAudioTrack from '../track/RemoteAudioTrack';
10
10
  import RemoteTrackPublication from '../track/RemoteTrackPublication';
11
11
  import RemoteVideoTrack from '../track/RemoteVideoTrack';
12
12
  import { Track } from '../track/Track';
13
- import { RemoteTrack } from '../track/types';
13
+ import { AdaptiveStreamSettings, RemoteTrack } from '../track/types';
14
14
  import Participant, { ParticipantEventCallbacks } from './Participant';
15
15
 
16
16
  export default class RemoteParticipant extends Participant {
@@ -82,7 +82,7 @@ export default class RemoteParticipant extends Participant {
82
82
  sid: Track.SID,
83
83
  mediaStream: MediaStream,
84
84
  receiver?: RTCRtpReceiver,
85
- adaptiveStream?: boolean,
85
+ adaptiveStreamSettings?: AdaptiveStreamSettings,
86
86
  triesLeft?: number,
87
87
  ) {
88
88
  // find the track publication
@@ -114,7 +114,7 @@ export default class RemoteParticipant extends Participant {
114
114
  if (triesLeft === undefined) triesLeft = 20;
115
115
  setTimeout(() => {
116
116
  this.addSubscribedMediaTrack(mediaTrack, sid, mediaStream,
117
- receiver, adaptiveStream, triesLeft! - 1);
117
+ receiver, adaptiveStreamSettings, triesLeft! - 1);
118
118
  }, 150);
119
119
  return;
120
120
  }
@@ -122,7 +122,7 @@ export default class RemoteParticipant extends Participant {
122
122
  const isVideo = mediaTrack.kind === 'video';
123
123
  let track: RemoteTrack;
124
124
  if (isVideo) {
125
- track = new RemoteVideoTrack(mediaTrack, sid, receiver, adaptiveStream);
125
+ track = new RemoteVideoTrack(mediaTrack, sid, receiver, adaptiveStreamSettings);
126
126
  } else {
127
127
  track = new RemoteAudioTrack(mediaTrack, sid, receiver);
128
128
  }
@@ -1,11 +1,15 @@
1
- import { VideoPresets, VideoPresets43 } from '../track/options';
2
1
  import {
2
+ ScreenSharePresets, VideoPreset, VideoPresets, VideoPresets43,
3
+ } from '../track/options';
4
+ import {
5
+ computeDefaultScreenShareSimulcastPresets,
3
6
  computeVideoEncodings,
4
7
  determineAppropriateEncoding,
5
8
  presets169,
6
9
  presets43,
7
10
  presetsForResolution,
8
11
  presetsScreenShare,
12
+ sortPresets,
9
13
  } from './publishUtils';
10
14
 
11
15
  describe('presetsForResolution', () => {
@@ -63,7 +67,7 @@ describe('computeVideoEncodings', () => {
63
67
 
64
68
  // ensure they are what we expect
65
69
  expect(encodings![0].rid).toBe('q');
66
- expect(encodings![0].maxBitrate).toBe(VideoPresets.qvga.encoding.maxBitrate);
70
+ expect(encodings![0].maxBitrate).toBe(VideoPresets.h180.encoding.maxBitrate);
67
71
  expect(encodings![0].scaleResolutionDownBy).toBe(3);
68
72
  expect(encodings![1].rid).toBe('h');
69
73
  expect(encodings![1].scaleResolutionDownBy).toBe(1.5);
@@ -77,7 +81,7 @@ describe('computeVideoEncodings', () => {
77
81
  expect(encodings).toHaveLength(3);
78
82
  expect(encodings![0].scaleResolutionDownBy).toBe(3);
79
83
  expect(encodings![1].scaleResolutionDownBy).toBe(1.5);
80
- expect(encodings![2].maxBitrate).toBe(VideoPresets.qhd.encoding.maxBitrate);
84
+ expect(encodings![2].maxBitrate).toBe(VideoPresets.h540.encoding.maxBitrate);
81
85
  });
82
86
 
83
87
  it('returns two encodings for lower-res simulcast', () => {
@@ -88,9 +92,9 @@ describe('computeVideoEncodings', () => {
88
92
 
89
93
  // ensure they are what we expect
90
94
  expect(encodings![0].rid).toBe('q');
91
- expect(encodings![0].maxBitrate).toBe(VideoPresets.qvga.encoding.maxBitrate);
95
+ expect(encodings![0].maxBitrate).toBe(VideoPresets.h180.encoding.maxBitrate);
92
96
  expect(encodings![1].rid).toBe('h');
93
- expect(encodings![1].maxBitrate).toBe(VideoPresets.vga.encoding.maxBitrate);
97
+ expect(encodings![1].maxBitrate).toBe(VideoPresets.h360.encoding.maxBitrate);
94
98
  });
95
99
 
96
100
  it('respects provided min resolution', () => {
@@ -99,7 +103,43 @@ describe('computeVideoEncodings', () => {
99
103
  });
100
104
  expect(encodings).toHaveLength(1);
101
105
  expect(encodings![0].rid).toBe('q');
102
- expect(encodings![0].maxBitrate).toBe(VideoPresets43.qvga.encoding.maxBitrate);
106
+ expect(encodings![0].maxBitrate).toBe(VideoPresets43.h120.encoding.maxBitrate);
103
107
  expect(encodings![0].scaleResolutionDownBy).toBe(1);
104
108
  });
105
109
  });
110
+
111
+ describe('customSimulcastLayers', () => {
112
+ it('sorts presets from lowest to highest', () => {
113
+ const sortedPresets = sortPresets(
114
+ [VideoPresets.h1440, VideoPresets.h360, VideoPresets.h1080, VideoPresets.h90],
115
+ ) as Array<VideoPreset>;
116
+ expect(sortPresets).not.toBeUndefined();
117
+ expect(sortedPresets[0]).toBe(VideoPresets.h90);
118
+ expect(sortedPresets[1]).toBe(VideoPresets.h360);
119
+ expect(sortedPresets[2]).toBe(VideoPresets.h1080);
120
+ expect(sortedPresets[3]).toBe(VideoPresets.h1440);
121
+ });
122
+ it('sorts presets from lowest to highest, even when dimensions are the same', () => {
123
+ const sortedPresets = sortPresets([
124
+ new VideoPreset(1920, 1080, 3_000_000, 20),
125
+ new VideoPreset(1920, 1080, 2_000_000, 15),
126
+ new VideoPreset(1920, 1080, 3_000_000, 15),
127
+ ]) as Array<VideoPreset>;
128
+ expect(sortPresets).not.toBeUndefined();
129
+ expect(sortedPresets[0].encoding.maxBitrate).toBe(2_000_000);
130
+ expect(sortedPresets[1].encoding.maxFramerate).toBe(15);
131
+ expect(sortedPresets[2].encoding.maxFramerate).toBe(20);
132
+ });
133
+ });
134
+
135
+ describe('screenShareSimulcastDefaults', () => {
136
+ it('computes appropriate bitrate from original preset', () => {
137
+ const defaultSimulcastLayers = computeDefaultScreenShareSimulcastPresets(
138
+ ScreenSharePresets.h720fps15,
139
+ );
140
+ expect(defaultSimulcastLayers[0].width).toBe(640);
141
+ expect(defaultSimulcastLayers[0].height).toBe(360);
142
+ expect(defaultSimulcastLayers[0].encoding.maxFramerate).toBe(3);
143
+ expect(defaultSimulcastLayers[0].encoding.maxBitrate).toBe(150_000);
144
+ });
145
+ });
@@ -26,32 +26,38 @@ export function mediaTrackToLocalTrack(
26
26
  }
27
27
 
28
28
  /* @internal */
29
- export const presets169 = [
30
- VideoPresets.qvga,
31
- VideoPresets.vga,
32
- VideoPresets.qhd,
33
- VideoPresets.hd,
34
- VideoPresets.fhd,
35
- ];
29
+ export const presets169 = Object.values(VideoPresets);
30
+
31
+ /* @internal */
32
+ export const presets43 = Object.values(VideoPresets43);
33
+
34
+ /* @internal */
35
+ export const presetsScreenShare = Object.values(ScreenSharePresets);
36
36
 
37
37
  /* @internal */
38
- export const presets43 = [
39
- VideoPresets43.qvga,
40
- VideoPresets43.vga,
41
- VideoPresets43.qhd,
42
- VideoPresets43.hd,
43
- VideoPresets43.fhd,
38
+ export const defaultSimulcastPresets169 = [
39
+ VideoPresets.h180,
40
+ VideoPresets.h360,
44
41
  ];
45
42
 
46
43
  /* @internal */
47
- export const presetsScreenShare = [
48
- ScreenSharePresets.vga,
49
- ScreenSharePresets.hd_8,
50
- ScreenSharePresets.hd_15,
51
- ScreenSharePresets.fhd_15,
52
- ScreenSharePresets.fhd_30,
44
+ export const defaultSimulcastPresets43 = [
45
+ VideoPresets43.h180,
46
+ VideoPresets43.h360,
53
47
  ];
54
48
 
49
+ /* @internal */
50
+ export const computeDefaultScreenShareSimulcastPresets = (fromPreset: VideoPreset) => {
51
+ const layers = [{ scaleResolutionDownBy: 2, fps: 3 }];
52
+ return layers.map((t) => new VideoPreset(
53
+ Math.floor(fromPreset.width / t.scaleResolutionDownBy),
54
+ Math.floor(fromPreset.height / t.scaleResolutionDownBy),
55
+ Math.max(150_000, Math.floor(fromPreset.encoding.maxBitrate
56
+ / (t.scaleResolutionDownBy ** 2 * ((fromPreset.encoding.maxFramerate ?? 30) / t.fps)))),
57
+ t.fps,
58
+ ));
59
+ };
60
+
55
61
  const videoRids = ['q', 'h', 'f'];
56
62
 
57
63
  /* @internal */
@@ -65,7 +71,7 @@ export function computeVideoEncodings(
65
71
  if (isScreenShare) {
66
72
  videoEncoding = options?.screenShareEncoding;
67
73
  }
68
- const useSimulcast = !isScreenShare && options?.simulcast;
74
+ const useSimulcast = options?.simulcast;
69
75
 
70
76
  if ((!videoEncoding && !useSimulcast) || !width || !height) {
71
77
  // when we aren't simulcasting, will need to return a single encoding without
@@ -82,16 +88,22 @@ export function computeVideoEncodings(
82
88
  if (!useSimulcast) {
83
89
  return [videoEncoding];
84
90
  }
85
-
86
- const presets = presetsForResolution(isScreenShare, width, height);
91
+ const original = new VideoPreset(
92
+ width, height, videoEncoding.maxBitrate, videoEncoding.maxFramerate,
93
+ );
94
+ let presets: Array<VideoPreset> = [];
95
+ if (isScreenShare) {
96
+ presets = sortPresets(options?.screenShareSimulcastLayers)
97
+ ?? defaultSimulcastLayers(isScreenShare, original);
98
+ } else {
99
+ presets = sortPresets(options?.videoSimulcastLayers)
100
+ ?? defaultSimulcastLayers(isScreenShare, original);
101
+ }
87
102
  let midPreset: VideoPreset | undefined;
88
103
  const lowPreset = presets[0];
89
104
  if (presets.length > 1) {
90
- [,midPreset] = presets;
105
+ [, midPreset] = presets;
91
106
  }
92
- const original = new VideoPreset(
93
- width, height, videoEncoding.maxBitrate, videoEncoding.maxFramerate,
94
- );
95
107
 
96
108
  // NOTE:
97
109
  // 1. Ordering of these encodings is important. Chrome seems
@@ -108,7 +120,7 @@ export function computeVideoEncodings(
108
120
  lowPreset, midPreset, original,
109
121
  ]);
110
122
  }
111
- if (size >= 500) {
123
+ if (size >= 480) {
112
124
  return encodingsFromPresets(width, height, [
113
125
  lowPreset, original,
114
126
  ]);
@@ -155,6 +167,21 @@ export function presetsForResolution(
155
167
  return presets43;
156
168
  }
157
169
 
170
+ /* @internal */
171
+ export function defaultSimulcastLayers(
172
+ isScreenShare: boolean, original: VideoPreset,
173
+ ): VideoPreset[] {
174
+ if (isScreenShare) {
175
+ return computeDefaultScreenShareSimulcastPresets(original);
176
+ }
177
+ const { width, height } = original;
178
+ const aspect = width > height ? width / height : height / width;
179
+ if (Math.abs(aspect - 16.0 / 9) < Math.abs(aspect - 4.0 / 3)) {
180
+ return defaultSimulcastPresets169;
181
+ }
182
+ return defaultSimulcastPresets43;
183
+ }
184
+
158
185
  // presets should be ordered by low, medium, high
159
186
  function encodingsFromPresets(
160
187
  width: number,
@@ -178,3 +205,21 @@ function encodingsFromPresets(
178
205
  });
179
206
  return encodings;
180
207
  }
208
+
209
+ /** @internal */
210
+ export function sortPresets(presets: Array<VideoPreset> | undefined) {
211
+ if (!presets) return;
212
+ return presets.sort((a, b) => {
213
+ const { encoding: aEnc } = a;
214
+ const { encoding: bEnc } = b;
215
+
216
+ if (aEnc.maxBitrate > bEnc.maxBitrate) {
217
+ return 1;
218
+ }
219
+ if (aEnc.maxBitrate < bEnc.maxBitrate) return -1;
220
+ if (aEnc.maxBitrate === bEnc.maxBitrate && aEnc.maxFramerate && bEnc.maxFramerate) {
221
+ return aEnc.maxFramerate > bEnc.maxFramerate ? 1 : -1;
222
+ }
223
+ return 0;
224
+ });
225
+ }