livekit-client 0.15.4 → 0.16.3

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 (73) hide show
  1. package/dist/api/RequestQueue.d.ts +12 -0
  2. package/dist/api/RequestQueue.js +61 -0
  3. package/dist/api/RequestQueue.js.map +1 -0
  4. package/dist/api/SignalClient.d.ts +7 -3
  5. package/dist/api/SignalClient.js +25 -4
  6. package/dist/api/SignalClient.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/options.d.ts +0 -10
  11. package/dist/proto/livekit_rtc.d.ts +15 -10
  12. package/dist/proto/livekit_rtc.js +36 -22
  13. package/dist/proto/livekit_rtc.js.map +1 -1
  14. package/dist/room/RTCEngine.d.ts +27 -6
  15. package/dist/room/RTCEngine.js +163 -46
  16. package/dist/room/RTCEngine.js.map +1 -1
  17. package/dist/room/Room.d.ts +50 -6
  18. package/dist/room/Room.js +128 -67
  19. package/dist/room/Room.js.map +1 -1
  20. package/dist/room/events.d.ts +13 -4
  21. package/dist/room/events.js +15 -6
  22. package/dist/room/events.js.map +1 -1
  23. package/dist/room/participant/LocalParticipant.d.ts +1 -2
  24. package/dist/room/participant/LocalParticipant.js +7 -8
  25. package/dist/room/participant/LocalParticipant.js.map +1 -1
  26. package/dist/room/participant/Participant.d.ts +30 -4
  27. package/dist/room/participant/Participant.js +2 -2
  28. package/dist/room/participant/Participant.js.map +1 -1
  29. package/dist/room/participant/RemoteParticipant.d.ts +3 -4
  30. package/dist/room/participant/RemoteParticipant.js +3 -0
  31. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  32. package/dist/room/track/LocalAudioTrack.js +8 -1
  33. package/dist/room/track/LocalAudioTrack.js.map +1 -1
  34. package/dist/room/track/LocalTrackPublication.d.ts +2 -0
  35. package/dist/room/track/LocalTrackPublication.js.map +1 -1
  36. package/dist/room/track/LocalVideoTrack.d.ts +1 -5
  37. package/dist/room/track/LocalVideoTrack.js +12 -117
  38. package/dist/room/track/LocalVideoTrack.js.map +1 -1
  39. package/dist/room/track/RemoteTrackPublication.d.ts +1 -1
  40. package/dist/room/track/RemoteTrackPublication.js +7 -1
  41. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  42. package/dist/room/track/RemoteVideoTrack.js +12 -7
  43. package/dist/room/track/RemoteVideoTrack.js.map +1 -1
  44. package/dist/room/track/Track.d.ts +16 -3
  45. package/dist/room/track/Track.js +30 -20
  46. package/dist/room/track/Track.js.map +1 -1
  47. package/dist/room/track/types.d.ts +4 -4
  48. package/dist/room/utils.d.ts +1 -0
  49. package/dist/room/utils.js +5 -21
  50. package/dist/room/utils.js.map +1 -1
  51. package/dist/version.d.ts +2 -2
  52. package/dist/version.js +2 -2
  53. package/package.json +3 -4
  54. package/src/api/RequestQueue.ts +53 -0
  55. package/src/api/SignalClient.ts +33 -5
  56. package/src/index.ts +1 -1
  57. package/src/options.ts +0 -12
  58. package/src/proto/livekit_rtc.ts +55 -41
  59. package/src/room/RTCEngine.ts +198 -53
  60. package/src/room/Room.ts +227 -96
  61. package/src/room/events.ts +15 -4
  62. package/src/room/participant/LocalParticipant.ts +6 -7
  63. package/src/room/participant/Participant.ts +39 -4
  64. package/src/room/participant/RemoteParticipant.ts +9 -4
  65. package/src/room/track/LocalAudioTrack.ts +8 -1
  66. package/src/room/track/LocalTrackPublication.ts +3 -0
  67. package/src/room/track/LocalVideoTrack.ts +11 -142
  68. package/src/room/track/RemoteTrackPublication.ts +8 -2
  69. package/src/room/track/RemoteVideoTrack.ts +14 -7
  70. package/src/room/track/Track.ts +46 -24
  71. package/src/room/track/types.ts +4 -4
  72. package/src/room/utils.ts +4 -16
  73. package/src/version.ts +2 -2
@@ -9,31 +9,14 @@ import { VideoCaptureOptions } from './options';
9
9
  import { Track } from './Track';
10
10
  import { constraintsForOptions } from './utils';
11
11
 
12
- // delay before attempting to upgrade
13
- const QUALITY_UPGRADE_DELAY = 60 * 1000;
14
-
15
- // avoid downgrading too quickly
16
- const QUALITY_DOWNGRADE_DELAY = 5 * 1000;
17
-
18
- const ridOrder = ['q', 'h', 'f'];
19
-
20
12
  export default class LocalVideoTrack extends LocalTrack {
21
13
  /* internal */
22
14
  signalClient?: SignalClient;
23
15
 
24
16
  private prevStats?: Map<string, VideoSenderStats>;
25
17
 
26
- // last time it had a change in quality
27
- private lastQualityChange?: number;
28
-
29
- // last time we made an explicit change
30
- private lastExplicitQualityChange?: number;
31
-
32
18
  private encodings?: RTCRtpEncodingParameters[];
33
19
 
34
- // layers that are being subscribed to, and that we should publish
35
- private activeQualities?: SubscribedQuality[];
36
-
37
20
  constructor(
38
21
  mediaTrack: MediaStreamTrack,
39
22
  constraints?: MediaTrackConstraints,
@@ -49,7 +32,7 @@ export default class LocalVideoTrack extends LocalTrack {
49
32
  }
50
33
 
51
34
  /* @internal */
52
- startMonitor(signalClient: SignalClient, disableLayerPause: boolean) {
35
+ startMonitor(signalClient: SignalClient) {
53
36
  this.signalClient = signalClient;
54
37
  // save original encodings
55
38
  const params = this.sender?.getParameters();
@@ -58,7 +41,7 @@ export default class LocalVideoTrack extends LocalTrack {
58
41
  }
59
42
 
60
43
  setTimeout(() => {
61
- this.monitorSender(disableLayerPause);
44
+ this.monitorSender();
62
45
  }, monitorFrequency);
63
46
  }
64
47
 
@@ -186,7 +169,6 @@ export default class LocalVideoTrack extends LocalTrack {
186
169
  return;
187
170
  }
188
171
 
189
- this.activeQualities = qualities;
190
172
  let hasChanged = false;
191
173
  encodings.forEach((encoding, idx) => {
192
174
  let rid = encoding.rid ?? '';
@@ -227,17 +209,20 @@ export default class LocalVideoTrack extends LocalTrack {
227
209
  }
228
210
  }
229
211
 
230
- private monitorSender = async (disableLayerPause: boolean) => {
212
+ private monitorSender = async () => {
231
213
  if (!this.sender) {
232
214
  this._currentBitrate = 0;
233
215
  return;
234
216
  }
235
- const stats = await this.getSenderStats();
236
- const statsMap = new Map<string, VideoSenderStats>(stats.map((s) => [s.rid, s]));
237
217
 
238
- if (!disableLayerPause && this.prevStats && this.isSimulcast) {
239
- this.checkAndUpdateSimulcast(statsMap);
218
+ let stats: VideoSenderStats[] | undefined;
219
+ try {
220
+ stats = await this.getSenderStats();
221
+ } catch (e) {
222
+ log.error('could not get audio sender stats', e);
223
+ return;
240
224
  }
225
+ const statsMap = new Map<string, VideoSenderStats>(stats.map((s) => [s.rid, s]));
241
226
 
242
227
  if (this.prevStats) {
243
228
  let totalBitrate = 0;
@@ -250,125 +235,9 @@ export default class LocalVideoTrack extends LocalTrack {
250
235
 
251
236
  this.prevStats = statsMap;
252
237
  setTimeout(() => {
253
- this.monitorSender(disableLayerPause);
238
+ this.monitorSender();
254
239
  }, monitorFrequency);
255
240
  };
256
-
257
- private checkAndUpdateSimulcast(statsMap: Map<string, VideoSenderStats>) {
258
- if (!this.sender || this.isMuted || !this.encodings) {
259
- return;
260
- }
261
-
262
- let bestEncoding: RTCRtpEncodingParameters | undefined;
263
- const { encodings } = this.sender.getParameters();
264
- encodings.forEach((encoding) => {
265
- // skip inactive encodings
266
- if (!encoding.active) return;
267
-
268
- if (bestEncoding === undefined) {
269
- bestEncoding = encoding;
270
- } else if (
271
- bestEncoding.rid
272
- && encoding.rid
273
- && ridOrder.indexOf(bestEncoding.rid) < ridOrder.indexOf(encoding.rid)
274
- ) {
275
- bestEncoding = encoding;
276
- } else if (
277
- bestEncoding.maxBitrate !== undefined
278
- && encoding.maxBitrate !== undefined
279
- && bestEncoding.maxBitrate < encoding.maxBitrate
280
- ) {
281
- bestEncoding = encoding;
282
- }
283
- });
284
-
285
- if (!bestEncoding) {
286
- return;
287
- }
288
- const rid: string = bestEncoding.rid ?? '';
289
- const sendStats = statsMap.get(rid);
290
- const lastStats = this.prevStats?.get(rid);
291
- if (!sendStats || !lastStats) {
292
- return;
293
- }
294
- const currentQuality = videoQualityForRid(rid);
295
-
296
- // adaptive simulcast algorithm notes (davidzhao)
297
- // Chrome (and other browsers) will automatically pause the highest layer
298
- // when it runs into bandwidth limitations. When that happens, it would not
299
- // be able to send any new frames between the two stats checks.
300
- //
301
- // We need to set that layer to inactive intentionally, because chrome tends
302
- // to flicker, meaning it will attempt to send that layer again shortly
303
- // afterwards, flip-flopping every few seconds. We want to avoid that.
304
- //
305
- // Note: even after bandwidth recovers, the flip-flopping behavior continues
306
- // this is possibly due to SFU-side PLI generation and imperfect bandwidth estimation
307
- if (sendStats.qualityLimitationResolutionChanges
308
- - lastStats.qualityLimitationResolutionChanges > 0) {
309
- this.lastQualityChange = new Date().getTime();
310
- }
311
-
312
- // log.debug('frameSent', sendStats.framesSent, 'lastSent', lastStats.framesSent,
313
- // 'elapsed', sendStats.timestamp - lastStats.timestamp);
314
- if (sendStats.framesSent - lastStats.framesSent > 0) {
315
- // frames have been sending ok, consider upgrading quality
316
- if (currentQuality === VideoQuality.HIGH || !this.lastQualityChange) return;
317
-
318
- const nextQuality = currentQuality + 1;
319
- if ((new Date()).getTime() - this.lastQualityChange < QUALITY_UPGRADE_DELAY) {
320
- return;
321
- }
322
-
323
- if (this.activeQualities
324
- && this.activeQualities.some((q) => q.quality === nextQuality && !q.enabled)
325
- ) {
326
- // quality has been disabled by the server, so we should skip
327
- return;
328
- }
329
-
330
- // we are already at the highest layer
331
- let bestQuality = VideoQuality.LOW;
332
- encodings.forEach((encoding) => {
333
- const quality = videoQualityForRid(encoding.rid ?? '');
334
- if (quality > bestQuality) {
335
- bestQuality = quality;
336
- }
337
- });
338
- if (nextQuality > bestQuality) {
339
- return;
340
- }
341
-
342
- log.debug('upgrading video quality to', nextQuality);
343
- this.setPublishingQuality(nextQuality);
344
- return;
345
- }
346
-
347
- // if best layer has not sent anything, do not downgrade till the
348
- // best layer starts sending something. It is possible that the
349
- // browser has not started some layer(s) due to cpu/bandwidth
350
- // constraints
351
- if (sendStats.framesSent === 0) return;
352
-
353
- // if we've upgraded or downgraded recently, give it a bit of time before
354
- // downgrading again
355
- if (this.lastExplicitQualityChange
356
- && ((new Date()).getTime() - this.lastExplicitQualityChange) < QUALITY_DOWNGRADE_DELAY) {
357
- return;
358
- }
359
-
360
- if (currentQuality === VideoQuality.UNRECOGNIZED) {
361
- return;
362
- }
363
-
364
- if (currentQuality === VideoQuality.LOW) {
365
- // already the lowest quality, nothing we can do
366
- return;
367
- }
368
-
369
- log.debug('downgrading video quality to', currentQuality - 1);
370
- this.setPublishingQuality(currentQuality - 1);
371
- }
372
241
  }
373
242
 
374
243
  export function videoQualityForRid(rid: string): VideoQuality {
@@ -35,7 +35,12 @@ export default class RemoteTrackPublication extends TrackPublication {
35
35
  const sub: UpdateSubscription = {
36
36
  trackSids: [this.trackSid],
37
37
  subscribe: this.subscribed,
38
- participantTracks: [],
38
+ participantTracks: [{
39
+ // sending an empty participant id since TrackPublication doesn't keep it
40
+ // this is filled in by the participant that receives this message
41
+ participantSid: '',
42
+ trackSids: [this.trackSid],
43
+ }],
39
44
  };
40
45
  this.emit(TrackEvent.UpdateSubscription, sub);
41
46
  }
@@ -172,7 +177,8 @@ export default class RemoteTrackPublication extends TrackPublication {
172
177
  this.emitTrackUpdate();
173
178
  };
174
179
 
175
- protected emitTrackUpdate() {
180
+ /* @internal */
181
+ emitTrackUpdate() {
176
182
  const settings: UpdateTrackSettings = UpdateTrackSettings.fromPartial({
177
183
  trackSids: [this.trackSid],
178
184
  disabled: this.disabled,
@@ -58,7 +58,11 @@ export default class RemoteVideoTrack extends RemoteTrack {
58
58
  super.attach(element);
59
59
  }
60
60
 
61
- if (this.adaptiveStream) {
61
+ // It's possible attach is called multiple times on an element. When that's
62
+ // the case, we'd want to avoid adding duplicate elementInfos
63
+ if (this.adaptiveStream
64
+ && this.elementInfos.find((info) => info.element === element) === undefined
65
+ ) {
62
66
  this.elementInfos.push({
63
67
  element,
64
68
  visible: true, // default visible
@@ -71,6 +75,11 @@ export default class RemoteVideoTrack extends RemoteTrack {
71
75
 
72
76
  getIntersectionObserver().observe(element);
73
77
  getResizeObserver().observe(element);
78
+
79
+ // trigger the first resize update cycle
80
+ // if the tab is backgrounded, the initial resize event does not fire until
81
+ // the tab comes into focus for the first time.
82
+ this.debouncedHandleResize();
74
83
  }
75
84
  return element;
76
85
  }
@@ -174,7 +183,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
174
183
  // delay hidden events
175
184
  setTimeout(() => {
176
185
  this.updateVisibility();
177
- }, Date.now() - lastVisibilityChange);
186
+ }, REACTION_DELAY);
178
187
  return;
179
188
  }
180
189
 
@@ -186,11 +195,9 @@ export default class RemoteVideoTrack extends RemoteTrack {
186
195
  let maxWidth = 0;
187
196
  let maxHeight = 0;
188
197
  for (const info of this.elementInfos) {
189
- if (info.visible) {
190
- if (info.element.clientWidth + info.element.clientHeight > maxWidth + maxHeight) {
191
- maxWidth = info.element.clientWidth;
192
- maxHeight = info.element.clientHeight;
193
- }
198
+ if (info.element.clientWidth + info.element.clientHeight > maxWidth + maxHeight) {
199
+ maxWidth = info.element.clientWidth;
200
+ maxHeight = info.element.clientHeight;
194
201
  }
195
202
  }
196
203
 
@@ -1,14 +1,15 @@
1
1
  import { EventEmitter } from 'events';
2
+ import type TypedEventEmitter from 'typed-emitter';
2
3
  import { TrackSource, TrackType } from '../../proto/livekit_models';
3
4
  import { StreamState as ProtoStreamState } from '../../proto/livekit_rtc';
4
5
  import { TrackEvent } from '../events';
5
- import { isFireFox } from '../utils';
6
+ import { isFireFox, isSafari } from '../utils';
6
7
 
7
8
  // keep old audio elements when detached, we would re-use them since on iOS
8
9
  // Safari tracks which audio elements have been "blessed" by the user.
9
10
  const recycledElements: Array<HTMLAudioElement> = [];
10
11
 
11
- export class Track extends EventEmitter {
12
+ export class Track extends (EventEmitter as new () => TypedEventEmitter<TrackEventCallbacks>) {
12
13
  kind: Track.Kind;
13
14
 
14
15
  mediaStreamTrack: MediaStreamTrack;
@@ -71,18 +72,14 @@ export class Track extends EventEmitter {
71
72
  }
72
73
  }
73
74
 
74
- if (element instanceof HTMLVideoElement) {
75
- element.playsInline = true;
76
- element.autoplay = true;
77
- }
78
-
79
- // already attached
80
- if (this.attachedElements.includes(element)) {
81
- return element;
75
+ if (!this.attachedElements.includes(element)) {
76
+ this.attachedElements.push(element);
82
77
  }
83
78
 
79
+ // even if we believe it's already attached to the element, it's possible
80
+ // the element's srcObject was set to something else out of band.
81
+ // we'll want to re-attach it in that case
84
82
  attachToElement(this.mediaStreamTrack, element);
85
- this.attachedElements.push(element);
86
83
 
87
84
  if (element instanceof HTMLAudioElement) {
88
85
  // manually play audio to detect audio playback status
@@ -168,28 +165,40 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
168
165
  mediaStream = element.srcObject;
169
166
  } else {
170
167
  mediaStream = new MediaStream();
171
- element.srcObject = mediaStream;
172
168
  }
173
169
 
174
- // remove existing tracks of same type from stream
170
+ // check if track matches existing track
175
171
  let existingTracks: MediaStreamTrack[];
176
172
  if (track.kind === 'audio') {
177
173
  existingTracks = mediaStream.getAudioTracks();
178
174
  } else {
179
175
  existingTracks = mediaStream.getVideoTracks();
180
176
  }
177
+ if (!existingTracks.includes(track)) {
178
+ existingTracks.forEach((et) => {
179
+ mediaStream.removeTrack(et);
180
+ });
181
+ mediaStream.addTrack(track);
182
+ }
181
183
 
182
- existingTracks.forEach((et) => {
183
- mediaStream.removeTrack(et);
184
- });
185
-
186
- mediaStream.addTrack(track);
187
- if (isFireFox()) {
188
- // sometimes firefox doesn't render local video on the first try.
189
- // It needs to be re-attached after a timeout.
190
- setTimeout(() => {
191
- element.srcObject = mediaStream;
192
- }, 1);
184
+ // avoid flicker
185
+ if (element.srcObject !== mediaStream) {
186
+ element.srcObject = mediaStream;
187
+ if ((isSafari() || isFireFox()) && element instanceof HTMLVideoElement) {
188
+ // Firefox also has a timing issue where video doesn't actually get attached unless
189
+ // performed out-of-band
190
+ // Safari 15 has a bug where in certain layouts, video element renders
191
+ // black until the page is resized or other changes take place.
192
+ // Resetting the src triggers it to render.
193
+ // https://developer.apple.com/forums/thread/690523
194
+ setTimeout(() => {
195
+ element.srcObject = mediaStream;
196
+ }, 0);
197
+ }
198
+ }
199
+ element.autoplay = true;
200
+ if (element instanceof HTMLVideoElement) {
201
+ element.playsInline = true;
193
202
  }
194
203
  }
195
204
 
@@ -299,3 +308,16 @@ export namespace Track {
299
308
  }
300
309
  }
301
310
  }
311
+
312
+ export type TrackEventCallbacks = {
313
+ message: () => void,
314
+ muted: (track?: any) => void,
315
+ unmuted: (track?: any) => void,
316
+ ended: (track?: any) => void,
317
+ updateSettings: () => void,
318
+ updateSubscription: () => void,
319
+ audioPlaybackStarted: () => void,
320
+ audioPlaybackFailed: (error: Error) => void,
321
+ visibilityChanged: (visible: boolean, track?: any) => void,
322
+ videoDimensionsChanged: (dimensions: Track.Dimensions, track?: any) => void,
323
+ };
@@ -1,7 +1,7 @@
1
- import LocalAudioTrack from './LocalAudioTrack';
2
- import LocalVideoTrack from './LocalVideoTrack';
3
- import RemoteAudioTrack from './RemoteAudioTrack';
4
- import RemoteVideoTrack from './RemoteVideoTrack';
1
+ import type LocalAudioTrack from './LocalAudioTrack';
2
+ import type LocalVideoTrack from './LocalVideoTrack';
3
+ import type RemoteAudioTrack from './RemoteAudioTrack';
4
+ import type RemoteVideoTrack from './RemoteVideoTrack';
5
5
 
6
6
  export type RemoteTrack = RemoteAudioTrack | RemoteVideoTrack;
7
7
  export type AudioTrack = RemoteAudioTrack | LocalAudioTrack;
package/src/room/utils.ts CHANGED
@@ -1,4 +1,3 @@
1
- import uaparser from 'ua-parser-js';
2
1
  import { ClientInfo, ClientInfo_SDK } from '../proto/livekit_models';
3
2
  import { protocolVersion, version } from '../version';
4
3
 
@@ -20,6 +19,10 @@ export function isFireFox(): boolean {
20
19
  return navigator.userAgent.indexOf('Firefox') !== -1;
21
20
  }
22
21
 
22
+ export function isSafari(): boolean {
23
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
24
+ }
25
+
23
26
  function roDispatchCallback(entries: ResizeObserverEntry[]) {
24
27
  for (const entry of entries) {
25
28
  (entry.target as ObservableMediaElement).handleResize(entry);
@@ -50,25 +53,10 @@ export interface ObservableMediaElement extends HTMLMediaElement {
50
53
  }
51
54
 
52
55
  export function getClientInfo(): ClientInfo {
53
- const ua = uaparser(navigator.userAgent);
54
56
  const info = ClientInfo.fromPartial({
55
57
  sdk: ClientInfo_SDK.JS,
56
58
  protocol: protocolVersion,
57
59
  version,
58
- os: ua.os.name,
59
- osVersion: ua.os.version,
60
- browser: ua.browser.name,
61
- browserVersion: ua.browser.version,
62
60
  });
63
-
64
- let model = '';
65
- if (ua.device.vendor) {
66
- model += ua.device.vendor;
67
- }
68
- if (ua.device.model) {
69
- if (model) model += ' ';
70
- model += ua.device.model;
71
- }
72
- if (model) info.deviceModel = model;
73
61
  return info;
74
62
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '0.15.4';
2
- export const protocolVersion = 5;
1
+ export const version = '0.16.3';
2
+ export const protocolVersion = 6;