livekit-client 1.7.0 → 1.8.0

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 (84) hide show
  1. package/README.md +20 -1
  2. package/dist/livekit-client.esm.mjs +2240 -1067
  3. package/dist/livekit-client.esm.mjs.map +1 -1
  4. package/dist/livekit-client.umd.js +1 -1
  5. package/dist/livekit-client.umd.js.map +1 -1
  6. package/dist/src/index.d.ts +3 -1
  7. package/dist/src/index.d.ts.map +1 -1
  8. package/dist/src/options.d.ts +5 -0
  9. package/dist/src/options.d.ts.map +1 -1
  10. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  11. package/dist/src/proto/livekit_models.d.ts +32 -0
  12. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  13. package/dist/src/proto/livekit_rtc.d.ts +315 -75
  14. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +9 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/ReconnectPolicy.d.ts +1 -0
  18. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
  19. package/dist/src/room/RegionUrlProvider.d.ts +14 -0
  20. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
  21. package/dist/src/room/Room.d.ts +6 -1
  22. package/dist/src/room/Room.d.ts.map +1 -1
  23. package/dist/src/room/defaults.d.ts.map +1 -1
  24. package/dist/src/room/errors.d.ts +2 -1
  25. package/dist/src/room/errors.d.ts.map +1 -1
  26. package/dist/src/room/events.d.ts +15 -2
  27. package/dist/src/room/events.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
  29. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalTrack.d.ts +3 -2
  31. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  34. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  35. package/dist/src/room/track/RemoteVideoTrack.d.ts +2 -1
  36. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  37. package/dist/src/room/track/Track.d.ts +3 -1
  38. package/dist/src/room/track/Track.d.ts.map +1 -1
  39. package/dist/src/room/track/utils.d.ts.map +1 -1
  40. package/dist/src/room/types.d.ts +4 -0
  41. package/dist/src/room/types.d.ts.map +1 -1
  42. package/dist/src/room/utils.d.ts +4 -0
  43. package/dist/src/room/utils.d.ts.map +1 -1
  44. package/dist/ts4.2/src/index.d.ts +3 -1
  45. package/dist/ts4.2/src/options.d.ts +5 -0
  46. package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
  47. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
  48. package/dist/ts4.2/src/room/RTCEngine.d.ts +9 -1
  49. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
  50. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
  51. package/dist/ts4.2/src/room/Room.d.ts +6 -1
  52. package/dist/ts4.2/src/room/errors.d.ts +2 -1
  53. package/dist/ts4.2/src/room/events.d.ts +15 -2
  54. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
  55. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
  56. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  57. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +2 -1
  58. package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
  59. package/dist/ts4.2/src/room/types.d.ts +4 -0
  60. package/dist/ts4.2/src/room/utils.d.ts +4 -0
  61. package/package.json +19 -19
  62. package/src/api/SignalClient.ts +4 -4
  63. package/src/index.ts +3 -0
  64. package/src/options.ts +6 -0
  65. package/src/proto/google/protobuf/timestamp.ts +15 -6
  66. package/src/proto/livekit_models.ts +903 -222
  67. package/src/proto/livekit_rtc.ts +1053 -279
  68. package/src/room/RTCEngine.ts +168 -56
  69. package/src/room/ReconnectPolicy.ts +2 -0
  70. package/src/room/RegionUrlProvider.ts +73 -0
  71. package/src/room/Room.ts +212 -133
  72. package/src/room/defaults.ts +1 -0
  73. package/src/room/errors.ts +1 -0
  74. package/src/room/events.ts +15 -0
  75. package/src/room/track/LocalAudioTrack.ts +14 -6
  76. package/src/room/track/LocalTrack.ts +22 -8
  77. package/src/room/track/LocalVideoTrack.ts +12 -6
  78. package/src/room/track/RemoteTrackPublication.ts +10 -4
  79. package/src/room/track/RemoteVideoTrack.test.ts +2 -0
  80. package/src/room/track/RemoteVideoTrack.ts +53 -9
  81. package/src/room/track/Track.ts +46 -31
  82. package/src/room/track/utils.ts +3 -2
  83. package/src/room/types.ts +6 -0
  84. package/src/room/utils.ts +53 -0
@@ -4,7 +4,7 @@ import { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc
4
4
  import { TrackEvent } from '../events';
5
5
  import type RemoteTrack from './RemoteTrack';
6
6
  import RemoteVideoTrack from './RemoteVideoTrack';
7
- import type { Track } from './Track';
7
+ import { Track } from './Track';
8
8
  import { TrackPublication } from './TrackPublication';
9
9
 
10
10
  export default class RemoteTrackPublication extends TrackPublication {
@@ -181,6 +181,7 @@ export default class RemoteTrackPublication extends TrackPublication {
181
181
  prevTrack.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
182
182
  prevTrack.off(TrackEvent.Ended, this.handleEnded);
183
183
  prevTrack.detach();
184
+ prevTrack.stopMonitor();
184
185
  this.emit(TrackEvent.Unsubscribed, prevTrack);
185
186
  }
186
187
  super.setTrack(track);
@@ -207,8 +208,13 @@ export default class RemoteTrackPublication extends TrackPublication {
207
208
  /** @internal */
208
209
  updateInfo(info: TrackInfo) {
209
210
  super.updateInfo(info);
211
+ const prevMetadataMuted = this.metadataMuted;
210
212
  this.metadataMuted = info.muted;
211
- this.track?.setMuted(info.muted);
213
+ if (this.track) {
214
+ this.track.setMuted(info.muted);
215
+ } else if (prevMetadataMuted !== info.muted) {
216
+ this.emit(info.muted ? TrackEvent.Muted : TrackEvent.Unmuted);
217
+ }
212
218
  }
213
219
 
214
220
  private emitSubscriptionUpdateIfChanged(previousStatus: TrackPublication.SubscriptionStatus) {
@@ -233,8 +239,8 @@ export default class RemoteTrackPublication extends TrackPublication {
233
239
  }
234
240
 
235
241
  private isManualOperationAllowed(): boolean {
236
- if (this.isAdaptiveStream) {
237
- log.warn('adaptive stream is enabled, cannot change track settings', {
242
+ if (this.kind === Track.Kind.Video && this.isAdaptiveStream) {
243
+ log.warn('adaptive stream is enabled, cannot change video track settings', {
238
244
  trackSid: this.trackSid,
239
245
  });
240
246
  return false;
@@ -130,6 +130,8 @@ class MockElementInfo implements ElementInfo {
130
130
 
131
131
  visible = false;
132
132
 
133
+ pictureInPicture = false;
134
+
133
135
  setVisible = (visible: boolean) => {
134
136
  if (this.visible !== visible) {
135
137
  this.visible = visible;
@@ -3,7 +3,13 @@ import log from '../../logger';
3
3
  import { TrackEvent } from '../events';
4
4
  import { computeBitrate, VideoReceiverStats } from '../stats';
5
5
  import CriticalTimers from '../timers';
6
- import { getIntersectionObserver, getResizeObserver, ObservableMediaElement } from '../utils';
6
+ import {
7
+ getDevicePixelRatio,
8
+ getIntersectionObserver,
9
+ getResizeObserver,
10
+ isWeb,
11
+ ObservableMediaElement,
12
+ } from '../utils';
7
13
  import RemoteTrack from './RemoteTrack';
8
14
  import { attachToElement, detachTrack, Track } from './Track';
9
15
  import type { AdaptiveStreamSettings } from './types';
@@ -21,7 +27,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
21
27
 
22
28
  private lastDimensions?: Track.Dimensions;
23
29
 
24
- private hasUsedAttach: boolean = false;
30
+ private isObserved: boolean = false;
25
31
 
26
32
  constructor(
27
33
  mediaTrack: MediaStreamTrack,
@@ -38,7 +44,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
38
44
  }
39
45
 
40
46
  get mediaStreamTrack() {
41
- if (this.isAdaptiveStream && !this.hasUsedAttach) {
47
+ if (this.isAdaptiveStream && !this.isObserved) {
42
48
  log.warn(
43
49
  'When using adaptiveStream, you need to use remoteVideoTrack.attach() to add the track to a HTMLVideoElement, otherwise your video tracks might never start',
44
50
  );
@@ -78,7 +84,6 @@ export default class RemoteVideoTrack extends RemoteTrack {
78
84
  const elementInfo = new HTMLElementInfo(element);
79
85
  this.observeElementInfo(elementInfo);
80
86
  }
81
- this.hasUsedAttach = true;
82
87
  return element;
83
88
  }
84
89
 
@@ -105,6 +110,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
105
110
  // the tab comes into focus for the first time.
106
111
  this.debouncedHandleResize();
107
112
  this.updateVisibility();
113
+ this.isObserved = true;
108
114
  } else {
109
115
  log.warn('visibility resize observer not triggered');
110
116
  }
@@ -223,7 +229,9 @@ export default class RemoteVideoTrack extends RemoteTrack {
223
229
  this.adaptiveStreamSettings?.pauseVideoInBackground ?? true // default to true
224
230
  ? this.isInBackground
225
231
  : false;
226
- const isVisible = this.elementInfos.some((info) => info.visible) && !backgroundPause;
232
+ const isPiPMode = this.elementInfos.some((info) => info.pictureInPicture);
233
+ const isVisible =
234
+ (this.elementInfos.some((info) => info.visible) && !backgroundPause) || isPiPMode;
227
235
 
228
236
  if (this.lastVisible === isVisible) {
229
237
  return;
@@ -246,7 +254,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
246
254
  let maxHeight = 0;
247
255
  for (const info of this.elementInfos) {
248
256
  const pixelDensity = this.adaptiveStreamSettings?.pixelDensity ?? 1;
249
- const pixelDensityValue = pixelDensity === 'screen' ? window.devicePixelRatio : pixelDensity;
257
+ const pixelDensityValue = pixelDensity === 'screen' ? getDevicePixelRatio() : pixelDensity;
250
258
  const currentElementWidth = info.width() * pixelDensityValue;
251
259
  const currentElementHeight = info.height() * pixelDensityValue;
252
260
  if (currentElementWidth + currentElementHeight > maxWidth + maxHeight) {
@@ -273,6 +281,7 @@ export interface ElementInfo {
273
281
  width(): number;
274
282
  height(): number;
275
283
  visible: boolean;
284
+ pictureInPicture: boolean;
276
285
  visibilityChangedAt: number | undefined;
277
286
 
278
287
  handleResize?: () => void;
@@ -284,7 +293,13 @@ export interface ElementInfo {
284
293
  class HTMLElementInfo implements ElementInfo {
285
294
  element: HTMLMediaElement;
286
295
 
287
- visible: boolean;
296
+ get visible(): boolean {
297
+ return this.isPiP || this.isIntersecting;
298
+ }
299
+
300
+ get pictureInPicture(): boolean {
301
+ return this.isPiP;
302
+ }
288
303
 
289
304
  visibilityChangedAt: number | undefined;
290
305
 
@@ -292,9 +307,14 @@ class HTMLElementInfo implements ElementInfo {
292
307
 
293
308
  handleVisibilityChanged?: () => void;
294
309
 
310
+ private isPiP: boolean;
311
+
312
+ private isIntersecting: boolean;
313
+
295
314
  constructor(element: HTMLMediaElement, visible?: boolean) {
296
315
  this.element = element;
297
- this.visible = visible ?? isElementInViewport(element);
316
+ this.isIntersecting = visible ?? isElementInViewport(element);
317
+ this.isPiP = isWeb() && document.pictureInPictureElement === element;
298
318
  this.visibilityChangedAt = 0;
299
319
  }
300
320
 
@@ -307,6 +327,10 @@ class HTMLElementInfo implements ElementInfo {
307
327
  }
308
328
 
309
329
  observe() {
330
+ // make sure we update the current visible state once we start to observe
331
+ this.isIntersecting = isElementInViewport(this.element);
332
+ this.isPiP = document.pictureInPictureElement === this.element;
333
+
310
334
  (this.element as ObservableMediaElement).handleResize = () => {
311
335
  this.handleResize?.();
312
336
  };
@@ -314,20 +338,40 @@ class HTMLElementInfo implements ElementInfo {
314
338
 
315
339
  getIntersectionObserver().observe(this.element);
316
340
  getResizeObserver().observe(this.element);
341
+ (this.element as HTMLVideoElement).addEventListener('enterpictureinpicture', this.onEnterPiP);
342
+ (this.element as HTMLVideoElement).addEventListener('leavepictureinpicture', this.onLeavePiP);
317
343
  }
318
344
 
319
345
  private onVisibilityChanged = (entry: IntersectionObserverEntry) => {
320
346
  const { target, isIntersecting } = entry;
321
347
  if (target === this.element) {
322
- this.visible = isIntersecting;
348
+ this.isIntersecting = isIntersecting;
323
349
  this.visibilityChangedAt = Date.now();
324
350
  this.handleVisibilityChanged?.();
325
351
  }
326
352
  };
327
353
 
354
+ private onEnterPiP = () => {
355
+ this.isPiP = true;
356
+ this.handleVisibilityChanged?.();
357
+ };
358
+
359
+ private onLeavePiP = () => {
360
+ this.isPiP = false;
361
+ this.handleVisibilityChanged?.();
362
+ };
363
+
328
364
  stopObserving() {
329
365
  getIntersectionObserver()?.unobserve(this.element);
330
366
  getResizeObserver()?.unobserve(this.element);
367
+ (this.element as HTMLVideoElement).removeEventListener(
368
+ 'enterpictureinpicture',
369
+ this.onEnterPiP,
370
+ );
371
+ (this.element as HTMLVideoElement).removeEventListener(
372
+ 'leavepictureinpicture',
373
+ this.onLeavePiP,
374
+ );
331
375
  }
332
376
  }
333
377
 
@@ -42,7 +42,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
42
42
 
43
43
  protected _mediaStreamID: string;
44
44
 
45
- protected isInBackground: boolean;
45
+ protected isInBackground: boolean = false;
46
46
 
47
47
  private backgroundTimeout: ReturnType<typeof setTimeout> | undefined;
48
48
 
@@ -57,12 +57,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
57
57
  this._mediaStreamTrack = mediaTrack;
58
58
  this._mediaStreamID = mediaTrack.id;
59
59
  this.source = Track.Source.Unknown;
60
- if (isWeb()) {
61
- this.isInBackground = document.visibilityState === 'hidden';
62
- document.addEventListener('visibilitychange', this.appVisibilityChangedListener);
63
- } else {
64
- this.isInBackground = false;
65
- }
66
60
  }
67
61
 
68
62
  /** current receive bits per second */
@@ -97,6 +91,9 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
97
91
  if (this.kind === Track.Kind.Video) {
98
92
  elementType = 'video';
99
93
  }
94
+ if (this.attachedElements.length === 0 && Track.Kind.Video) {
95
+ this.addAppVisibilityListener();
96
+ }
100
97
  if (!element) {
101
98
  if (elementType === 'audio') {
102
99
  recycledElements.forEach((e) => {
@@ -167,37 +164,40 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
167
164
  */
168
165
  detach(element: HTMLMediaElement): HTMLMediaElement;
169
166
  detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[] {
170
- // detach from a single element
171
- if (element) {
172
- detachTrack(this._mediaStreamTrack, element);
173
- const idx = this.attachedElements.indexOf(element);
174
- if (idx >= 0) {
175
- this.attachedElements.splice(idx, 1);
176
- this.recycleElement(element);
177
- this.emit(TrackEvent.ElementDetached, element);
167
+ try {
168
+ // detach from a single element
169
+ if (element) {
170
+ detachTrack(this._mediaStreamTrack, element);
171
+ const idx = this.attachedElements.indexOf(element);
172
+ if (idx >= 0) {
173
+ this.attachedElements.splice(idx, 1);
174
+ this.recycleElement(element);
175
+ this.emit(TrackEvent.ElementDetached, element);
176
+ }
177
+ return element;
178
178
  }
179
- return element;
180
- }
181
179
 
182
- const detached: HTMLMediaElement[] = [];
183
- this.attachedElements.forEach((elm) => {
184
- detachTrack(this._mediaStreamTrack, elm);
185
- detached.push(elm);
186
- this.recycleElement(elm);
187
- this.emit(TrackEvent.ElementDetached, elm);
188
- });
180
+ const detached: HTMLMediaElement[] = [];
181
+ this.attachedElements.forEach((elm) => {
182
+ detachTrack(this._mediaStreamTrack, elm);
183
+ detached.push(elm);
184
+ this.recycleElement(elm);
185
+ this.emit(TrackEvent.ElementDetached, elm);
186
+ });
189
187
 
190
- // remove all tracks
191
- this.attachedElements = [];
192
- return detached;
188
+ // remove all tracks
189
+ this.attachedElements = [];
190
+ return detached;
191
+ } finally {
192
+ if (this.attachedElements.length === 0) {
193
+ this.removeAppVisibilityListener();
194
+ }
195
+ }
193
196
  }
194
197
 
195
198
  stop() {
196
199
  this.stopMonitor();
197
200
  this._mediaStreamTrack.stop();
198
- if (isWeb()) {
199
- document.removeEventListener('visibilitychange', this.appVisibilityChangedListener);
200
- }
201
201
  }
202
202
 
203
203
  protected enable() {
@@ -212,7 +212,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
212
212
  abstract startMonitor(signalClient?: SignalClient): void;
213
213
 
214
214
  /* @internal */
215
- protected stopMonitor() {
215
+ stopMonitor() {
216
216
  if (this.monitorInterval) {
217
217
  clearInterval(this.monitorInterval);
218
218
  }
@@ -253,6 +253,21 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
253
253
  protected async handleAppVisibilityChanged() {
254
254
  this.isInBackground = document.visibilityState === 'hidden';
255
255
  }
256
+
257
+ protected addAppVisibilityListener() {
258
+ if (isWeb()) {
259
+ this.isInBackground = document.visibilityState === 'hidden';
260
+ document.addEventListener('visibilitychange', this.appVisibilityChangedListener);
261
+ } else {
262
+ this.isInBackground = false;
263
+ }
264
+ }
265
+
266
+ protected removeAppVisibilityListener() {
267
+ if (isWeb()) {
268
+ document.removeEventListener('visibilitychange', this.appVisibilityChangedListener);
269
+ }
270
+ }
256
271
  }
257
272
 
258
273
  /** @internal */
@@ -105,8 +105,9 @@ export async function detectSilence(track: AudioTrack, timeOffset = 200): Promis
105
105
  * @internal
106
106
  */
107
107
  export function getNewAudioContext(): AudioContext | void {
108
- // @ts-ignore
109
- const AudioContext = window.AudioContext || window.webkitAudioContext;
108
+ const AudioContext =
109
+ // @ts-ignore
110
+ typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext);
110
111
  if (AudioContext) {
111
112
  return new AudioContext({ latencyHint: 'interactive' });
112
113
  }
package/src/room/types.ts CHANGED
@@ -20,3 +20,9 @@ export type DataPublishOptions = {
20
20
  /** the topic under which the message gets published */
21
21
  topic?: string;
22
22
  };
23
+
24
+ export type LiveKitReactNativeInfo = {
25
+ // Corresponds to RN's PlatformOSType
26
+ platform: 'ios' | 'android' | 'windows' | 'macos' | 'web' | 'native';
27
+ devicePixelRatio: number;
28
+ };
package/src/room/utils.ts CHANGED
@@ -4,6 +4,7 @@ import { protocolVersion, version } from '../version';
4
4
  import type LocalAudioTrack from './track/LocalAudioTrack';
5
5
  import type RemoteAudioTrack from './track/RemoteAudioTrack';
6
6
  import { getNewAudioContext } from './track/utils';
7
+ import type { LiveKitReactNativeInfo } from './types';
7
8
 
8
9
  const separator = '|';
9
10
 
@@ -122,6 +123,54 @@ export function isWeb(): boolean {
122
123
  return typeof document !== 'undefined';
123
124
  }
124
125
 
126
+ export function isReactNative(): boolean {
127
+ // navigator.product is deprecated on browsers, but will be set appropriately for react-native.
128
+ return navigator.product == 'ReactNative';
129
+ }
130
+
131
+ export function isCloud(serverUrl: URL) {
132
+ return serverUrl.hostname.endsWith('.livekit.cloud');
133
+ }
134
+
135
+ function getLKReactNativeInfo(): LiveKitReactNativeInfo | undefined {
136
+ // global defined only for ReactNative.
137
+ // @ts-ignore
138
+ if (global && global.LiveKitReactNativeGlobal) {
139
+ // @ts-ignore
140
+ return global.LiveKitReactNativeGlobal as LiveKitReactNativeInfo;
141
+ }
142
+
143
+ return undefined;
144
+ }
145
+
146
+ export function getReactNativeOs(): string | undefined {
147
+ if (!isReactNative()) {
148
+ return undefined;
149
+ }
150
+
151
+ let info = getLKReactNativeInfo();
152
+ if (info) {
153
+ return info.platform;
154
+ }
155
+
156
+ return undefined;
157
+ }
158
+
159
+ export function getDevicePixelRatio(): number {
160
+ if (isWeb()) {
161
+ return window.devicePixelRatio;
162
+ }
163
+
164
+ if (isReactNative()) {
165
+ let info = getLKReactNativeInfo();
166
+ if (info) {
167
+ return info.devicePixelRatio;
168
+ }
169
+ }
170
+
171
+ return 1;
172
+ }
173
+
125
174
  export function compareVersions(v1: string, v2: string): number {
126
175
  const parts1 = v1.split('.');
127
176
  const parts2 = v2.split('.');
@@ -174,6 +223,10 @@ export function getClientInfo(): ClientInfo {
174
223
  protocol: protocolVersion,
175
224
  version,
176
225
  });
226
+
227
+ if (isReactNative()) {
228
+ info.os = getReactNativeOs() ?? '';
229
+ }
177
230
  return info;
178
231
  }
179
232