livekit-client 1.7.0 → 1.8.0

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