livekit-client 1.0.0 → 1.0.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 (143) hide show
  1. package/dist/livekit-client.esm.mjs +913 -233
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/{api → src/api}/RequestQueue.d.ts +0 -0
  6. package/dist/src/api/RequestQueue.d.ts.map +1 -0
  7. package/dist/{api → src/api}/SignalClient.d.ts +1 -1
  8. package/dist/src/api/SignalClient.d.ts.map +1 -0
  9. package/dist/{index.d.ts → src/index.d.ts} +2 -2
  10. package/dist/src/index.d.ts.map +1 -0
  11. package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
  12. package/dist/src/logger.d.ts.map +1 -0
  13. package/dist/{options.d.ts → src/options.d.ts} +0 -0
  14. package/dist/src/options.d.ts.map +1 -0
  15. package/dist/{proto → src/proto}/google/protobuf/timestamp.d.ts +0 -0
  16. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -0
  17. package/dist/{proto → src/proto}/livekit_models.d.ts +80 -0
  18. package/dist/src/proto/livekit_models.d.ts.map +1 -0
  19. package/dist/{proto → src/proto}/livekit_rtc.d.ts +661 -0
  20. package/dist/src/proto/livekit_rtc.d.ts.map +1 -0
  21. package/dist/{room → src/room}/DeviceManager.d.ts +0 -0
  22. package/dist/src/room/DeviceManager.d.ts.map +1 -0
  23. package/dist/{room → src/room}/PCTransport.d.ts +0 -0
  24. package/dist/src/room/PCTransport.d.ts.map +1 -0
  25. package/dist/{room → src/room}/RTCEngine.d.ts +1 -0
  26. package/dist/src/room/RTCEngine.d.ts.map +1 -0
  27. package/dist/{room → src/room}/Room.d.ts +2 -0
  28. package/dist/src/room/Room.d.ts.map +1 -0
  29. package/dist/{room → src/room}/errors.d.ts +0 -0
  30. package/dist/src/room/errors.d.ts.map +1 -0
  31. package/dist/{room → src/room}/events.d.ts +5 -1
  32. package/dist/src/room/events.d.ts.map +1 -0
  33. package/dist/{room → src/room}/participant/LocalParticipant.d.ts +4 -1
  34. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -0
  35. package/dist/{room → src/room}/participant/Participant.d.ts +0 -0
  36. package/dist/src/room/participant/Participant.d.ts.map +1 -0
  37. package/dist/{room → src/room}/participant/ParticipantTrackPermission.d.ts +0 -0
  38. package/dist/src/room/participant/ParticipantTrackPermission.d.ts.map +1 -0
  39. package/dist/{room → src/room}/participant/RemoteParticipant.d.ts +0 -0
  40. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -0
  41. package/dist/{room → src/room}/participant/publishUtils.d.ts +0 -0
  42. package/dist/src/room/participant/publishUtils.d.ts.map +1 -0
  43. package/dist/{room → src/room}/stats.d.ts +1 -0
  44. package/dist/src/room/stats.d.ts.map +1 -0
  45. package/dist/{room → src/room}/track/LocalAudioTrack.d.ts +0 -0
  46. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -0
  47. package/dist/{room → src/room}/track/LocalTrack.d.ts +3 -0
  48. package/dist/src/room/track/LocalTrack.d.ts.map +1 -0
  49. package/dist/{room → src/room}/track/LocalTrackPublication.d.ts +0 -0
  50. package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -0
  51. package/dist/{room → src/room}/track/LocalVideoTrack.d.ts +17 -2
  52. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -0
  53. package/dist/{room → src/room}/track/RemoteAudioTrack.d.ts +0 -0
  54. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -0
  55. package/dist/{room → src/room}/track/RemoteTrack.d.ts +0 -1
  56. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -0
  57. package/dist/{room → src/room}/track/RemoteTrackPublication.d.ts +0 -0
  58. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -0
  59. package/dist/{room → src/room}/track/RemoteVideoTrack.d.ts +25 -1
  60. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -0
  61. package/dist/{room → src/room}/track/Track.d.ts +6 -1
  62. package/dist/src/room/track/Track.d.ts.map +1 -0
  63. package/dist/{room → src/room}/track/TrackPublication.d.ts +0 -0
  64. package/dist/src/room/track/TrackPublication.d.ts.map +1 -0
  65. package/dist/{room → src/room}/track/create.d.ts +0 -0
  66. package/dist/src/room/track/create.d.ts.map +1 -0
  67. package/dist/{room → src/room}/track/defaults.d.ts +0 -0
  68. package/dist/src/room/track/defaults.d.ts.map +1 -0
  69. package/dist/{room → src/room}/track/options.d.ts +2 -1
  70. package/dist/src/room/track/options.d.ts.map +1 -0
  71. package/dist/{room → src/room}/track/types.d.ts +5 -0
  72. package/dist/src/room/track/types.d.ts.map +1 -0
  73. package/dist/{room → src/room}/track/utils.d.ts +0 -0
  74. package/dist/src/room/track/utils.d.ts.map +1 -0
  75. package/dist/{room → src/room}/utils.d.ts +0 -0
  76. package/dist/src/room/utils.d.ts.map +1 -0
  77. package/dist/src/test/MockMediaStreamTrack.d.ts +26 -0
  78. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -0
  79. package/dist/{test → src/test}/mocks.d.ts +0 -0
  80. package/dist/src/test/mocks.d.ts.map +1 -0
  81. package/dist/src/version.d.ts +3 -0
  82. package/dist/src/version.d.ts.map +1 -0
  83. package/package.json +7 -3
  84. package/src/api/SignalClient.ts +2 -2
  85. package/src/index.ts +2 -1
  86. package/src/proto/livekit_models.ts +90 -0
  87. package/src/proto/livekit_rtc.ts +235 -1
  88. package/src/room/RTCEngine.ts +30 -2
  89. package/src/room/Room.ts +61 -16
  90. package/src/room/events.ts +5 -0
  91. package/src/room/participant/LocalParticipant.ts +104 -23
  92. package/src/room/participant/RemoteParticipant.ts +1 -0
  93. package/src/room/stats.ts +2 -0
  94. package/src/room/track/LocalAudioTrack.ts +4 -0
  95. package/src/room/track/LocalTrack.ts +12 -5
  96. package/src/room/track/LocalVideoTrack.ts +144 -56
  97. package/src/room/track/RemoteTrack.ts +0 -2
  98. package/src/room/track/RemoteVideoTrack.test.ts +149 -0
  99. package/src/room/track/RemoteVideoTrack.ts +118 -37
  100. package/src/room/track/Track.ts +23 -2
  101. package/src/room/track/options.ts +2 -1
  102. package/src/room/track/types.ts +5 -0
  103. package/src/test/MockMediaStreamTrack.ts +83 -0
  104. package/src/version.ts +4 -2
  105. package/dist/api/RequestQueue.d.ts.map +0 -1
  106. package/dist/api/SignalClient.d.ts.map +0 -1
  107. package/dist/index.d.ts.map +0 -1
  108. package/dist/logger.d.ts.map +0 -1
  109. package/dist/options.d.ts.map +0 -1
  110. package/dist/proto/google/protobuf/timestamp.d.ts.map +0 -1
  111. package/dist/proto/livekit_models.d.ts.map +0 -1
  112. package/dist/proto/livekit_rtc.d.ts.map +0 -1
  113. package/dist/room/DeviceManager.d.ts.map +0 -1
  114. package/dist/room/PCTransport.d.ts.map +0 -1
  115. package/dist/room/RTCEngine.d.ts.map +0 -1
  116. package/dist/room/Room.d.ts.map +0 -1
  117. package/dist/room/errors.d.ts.map +0 -1
  118. package/dist/room/events.d.ts.map +0 -1
  119. package/dist/room/participant/LocalParticipant.d.ts.map +0 -1
  120. package/dist/room/participant/Participant.d.ts.map +0 -1
  121. package/dist/room/participant/ParticipantTrackPermission.d.ts.map +0 -1
  122. package/dist/room/participant/RemoteParticipant.d.ts.map +0 -1
  123. package/dist/room/participant/publishUtils.d.ts.map +0 -1
  124. package/dist/room/stats.d.ts.map +0 -1
  125. package/dist/room/track/LocalAudioTrack.d.ts.map +0 -1
  126. package/dist/room/track/LocalTrack.d.ts.map +0 -1
  127. package/dist/room/track/LocalTrackPublication.d.ts.map +0 -1
  128. package/dist/room/track/LocalVideoTrack.d.ts.map +0 -1
  129. package/dist/room/track/RemoteAudioTrack.d.ts.map +0 -1
  130. package/dist/room/track/RemoteTrack.d.ts.map +0 -1
  131. package/dist/room/track/RemoteTrackPublication.d.ts.map +0 -1
  132. package/dist/room/track/RemoteVideoTrack.d.ts.map +0 -1
  133. package/dist/room/track/Track.d.ts.map +0 -1
  134. package/dist/room/track/TrackPublication.d.ts.map +0 -1
  135. package/dist/room/track/create.d.ts.map +0 -1
  136. package/dist/room/track/defaults.d.ts.map +0 -1
  137. package/dist/room/track/options.d.ts.map +0 -1
  138. package/dist/room/track/types.d.ts.map +0 -1
  139. package/dist/room/track/utils.d.ts.map +0 -1
  140. package/dist/room/utils.d.ts.map +0 -1
  141. package/dist/test/mocks.d.ts.map +0 -1
  142. package/dist/version.d.ts +0 -3
  143. package/dist/version.d.ts.map +0 -1
@@ -1,14 +1,29 @@
1
1
  import { SignalClient } from '../../api/SignalClient';
2
2
  import log from '../../logger';
3
3
  import { VideoLayer, VideoQuality } from '../../proto/livekit_models';
4
- import { SubscribedQuality } from '../../proto/livekit_rtc';
4
+ import { SubscribedCodec, SubscribedQuality } from '../../proto/livekit_rtc';
5
5
  import { computeBitrate, monitorFrequency, VideoSenderStats } from '../stats';
6
- import { isFireFox, isMobile } from '../utils';
6
+ import { isFireFox, isMobile, isWeb } from '../utils';
7
7
  import LocalTrack from './LocalTrack';
8
- import { VideoCaptureOptions } from './options';
8
+ import { VideoCaptureOptions, VideoCodec } from './options';
9
9
  import { Track } from './Track';
10
10
  import { constraintsForOptions } from './utils';
11
11
 
12
+ export class SimulcastTrackInfo {
13
+ codec: VideoCodec;
14
+
15
+ mediaStreamTrack: MediaStreamTrack;
16
+
17
+ sender?: RTCRtpSender;
18
+
19
+ encodings?: RTCRtpEncodingParameters[];
20
+
21
+ constructor(codec: VideoCodec, mediaStreamTrack: MediaStreamTrack) {
22
+ this.codec = codec;
23
+ this.mediaStreamTrack = mediaStreamTrack;
24
+ }
25
+ }
26
+
12
27
  export default class LocalVideoTrack extends LocalTrack {
13
28
  /* internal */
14
29
  signalClient?: SignalClient;
@@ -17,6 +32,11 @@ export default class LocalVideoTrack extends LocalTrack {
17
32
 
18
33
  private encodings?: RTCRtpEncodingParameters[];
19
34
 
35
+ private simulcastCodecs: Map<VideoCodec, SimulcastTrackInfo> = new Map<
36
+ VideoCodec,
37
+ SimulcastTrackInfo
38
+ >();
39
+
20
40
  constructor(mediaTrack: MediaStreamTrack, constraints?: MediaTrackConstraints) {
21
41
  super(mediaTrack, Track.Kind.Video, constraints);
22
42
  }
@@ -31,7 +51,11 @@ export default class LocalVideoTrack extends LocalTrack {
31
51
  /* @internal */
32
52
  startMonitor(signalClient: SignalClient) {
33
53
  this.signalClient = signalClient;
54
+ if (!isWeb()) {
55
+ return;
56
+ }
34
57
  // save original encodings
58
+ // TODO : merge simulcast tracks stats
35
59
  const params = this.sender?.getParameters();
36
60
  if (params) {
37
61
  this.encodings = params.encodings;
@@ -45,6 +69,11 @@ export default class LocalVideoTrack extends LocalTrack {
45
69
  stop() {
46
70
  this.sender = undefined;
47
71
  this._mediaStreamTrack.getConstraints();
72
+ this.simulcastCodecs.forEach((trackInfo) => {
73
+ trackInfo.mediaStreamTrack.stop();
74
+ trackInfo.sender = undefined;
75
+ });
76
+ this.simulcastCodecs.clear();
48
77
  super.stop();
49
78
  }
50
79
 
@@ -89,7 +118,7 @@ export default class LocalVideoTrack extends LocalTrack {
89
118
  bytesSent: v.bytesSent,
90
119
  framesSent: v.framesSent,
91
120
  timestamp: v.timestamp,
92
- rid: v.rid ?? '',
121
+ rid: v.rid ?? v.id,
93
122
  retransmittedPacketsSent: v.retransmittedPacketsSent,
94
123
  qualityLimitationReason: v.qualityLimitationReason,
95
124
  qualityLimitationResolutionChanges: v.qualityLimitationResolutionChanges,
@@ -145,6 +174,57 @@ export default class LocalVideoTrack extends LocalTrack {
145
174
  await this.restart(constraints);
146
175
  }
147
176
 
177
+ addSimulcastTrack(codec: VideoCodec, encodings?: RTCRtpEncodingParameters[]): SimulcastTrackInfo {
178
+ if (this.simulcastCodecs.has(codec)) {
179
+ throw new Error(`${codec} already added`);
180
+ }
181
+ const simulcastCodecInfo: SimulcastTrackInfo = {
182
+ codec,
183
+ mediaStreamTrack: this.mediaStreamTrack.clone(),
184
+ sender: undefined,
185
+ encodings,
186
+ };
187
+ this.simulcastCodecs.set(codec, simulcastCodecInfo);
188
+ return simulcastCodecInfo;
189
+ }
190
+
191
+ setSimulcastTrackSender(codec: VideoCodec, sender: RTCRtpSender) {
192
+ const simulcastCodecInfo = this.simulcastCodecs.get(codec);
193
+ if (!simulcastCodecInfo) {
194
+ return;
195
+ }
196
+ simulcastCodecInfo.sender = sender;
197
+ }
198
+
199
+ /**
200
+ * @internal
201
+ * Sets codecs that should be publishing
202
+ */
203
+ async setPublishingCodecs(codecs: SubscribedCodec[]) {
204
+ log.debug('setting publishing codecs', codecs);
205
+
206
+ for await (const codec of codecs) {
207
+ if (this.codec === codec.codec) {
208
+ await this.setPublishingLayers(codec.qualities);
209
+ } else {
210
+ const simulcastCodecInfo = this.simulcastCodecs.get(codec.codec as VideoCodec);
211
+ log.debug(`try setPublishingCodec for ${codec.codec}`, simulcastCodecInfo);
212
+ if (!simulcastCodecInfo || !simulcastCodecInfo.sender) {
213
+ return;
214
+ }
215
+
216
+ if (simulcastCodecInfo.encodings) {
217
+ log.debug(`try setPublishingLayersForSender ${codec.codec}`);
218
+ await setPublishingLayersForSender(
219
+ simulcastCodecInfo.sender,
220
+ simulcastCodecInfo.encodings!,
221
+ codec.qualities,
222
+ );
223
+ }
224
+ }
225
+ }
226
+ }
227
+
148
228
  /**
149
229
  * @internal
150
230
  * Sets layers that should be publishing
@@ -154,59 +234,8 @@ export default class LocalVideoTrack extends LocalTrack {
154
234
  if (!this.sender || !this.encodings) {
155
235
  return;
156
236
  }
157
- const params = this.sender.getParameters();
158
- const { encodings } = params;
159
- if (!encodings) {
160
- return;
161
- }
162
-
163
- if (encodings.length !== this.encodings.length) {
164
- log.warn('cannot set publishing layers, encodings mismatch');
165
- return;
166
- }
167
237
 
168
- let hasChanged = false;
169
- encodings.forEach((encoding, idx) => {
170
- let rid = encoding.rid ?? '';
171
- if (rid === '') {
172
- rid = 'q';
173
- }
174
- const quality = videoQualityForRid(rid);
175
- const subscribedQuality = qualities.find((q) => q.quality === quality);
176
- if (!subscribedQuality) {
177
- return;
178
- }
179
- if (encoding.active !== subscribedQuality.enabled) {
180
- hasChanged = true;
181
- encoding.active = subscribedQuality.enabled;
182
- log.debug(
183
- `setting layer ${subscribedQuality.quality} to ${
184
- encoding.active ? 'enabled' : 'disabled'
185
- }`,
186
- );
187
-
188
- // FireFox does not support setting encoding.active to false, so we
189
- // have a workaround of lowering its bitrate and resolution to the min.
190
- if (isFireFox()) {
191
- if (subscribedQuality.enabled) {
192
- encoding.scaleResolutionDownBy = this.encodings![idx].scaleResolutionDownBy;
193
- encoding.maxBitrate = this.encodings![idx].maxBitrate;
194
- /* @ts-ignore */
195
- encoding.maxFrameRate = this.encodings![idx].maxFrameRate;
196
- } else {
197
- encoding.scaleResolutionDownBy = 4;
198
- encoding.maxBitrate = 10;
199
- /* @ts-ignore */
200
- encoding.maxFrameRate = 2;
201
- }
202
- }
203
- }
204
- });
205
-
206
- if (hasChanged) {
207
- params.encodings = encodings;
208
- await this.sender.setParameters(params);
209
- }
238
+ await setPublishingLayersForSender(this.sender, this.encodings, qualities);
210
239
  }
211
240
 
212
241
  private monitorSender = async () => {
@@ -248,6 +277,65 @@ export default class LocalVideoTrack extends LocalTrack {
248
277
  }
249
278
  }
250
279
 
280
+ async function setPublishingLayersForSender(
281
+ sender: RTCRtpSender,
282
+ senderEncodings: RTCRtpEncodingParameters[],
283
+ qualities: SubscribedQuality[],
284
+ ) {
285
+ log.debug('setPublishingLayersForSender', { sender, qualities, senderEncodings });
286
+ const params = sender.getParameters();
287
+ const { encodings } = params;
288
+ if (!encodings) {
289
+ return;
290
+ }
291
+
292
+ if (encodings.length !== senderEncodings.length) {
293
+ log.warn('cannot set publishing layers, encodings mismatch');
294
+ return;
295
+ }
296
+
297
+ let hasChanged = false;
298
+ encodings.forEach((encoding, idx) => {
299
+ let rid = encoding.rid ?? '';
300
+ if (rid === '') {
301
+ rid = 'q';
302
+ }
303
+ const quality = videoQualityForRid(rid);
304
+ const subscribedQuality = qualities.find((q) => q.quality === quality);
305
+ if (!subscribedQuality) {
306
+ return;
307
+ }
308
+ if (encoding.active !== subscribedQuality.enabled) {
309
+ hasChanged = true;
310
+ encoding.active = subscribedQuality.enabled;
311
+ log.debug(
312
+ `setting layer ${subscribedQuality.quality} to ${encoding.active ? 'enabled' : 'disabled'}`,
313
+ );
314
+
315
+ // FireFox does not support setting encoding.active to false, so we
316
+ // have a workaround of lowering its bitrate and resolution to the min.
317
+ if (isFireFox()) {
318
+ if (subscribedQuality.enabled) {
319
+ encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
320
+ encoding.maxBitrate = senderEncodings[idx].maxBitrate;
321
+ /* @ts-ignore */
322
+ encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
323
+ } else {
324
+ encoding.scaleResolutionDownBy = 4;
325
+ encoding.maxBitrate = 10;
326
+ /* @ts-ignore */
327
+ encoding.maxFrameRate = 2;
328
+ }
329
+ }
330
+ }
331
+ });
332
+
333
+ if (hasChanged) {
334
+ params.encodings = encodings;
335
+ await sender.setParameters(params);
336
+ }
337
+ }
338
+
251
339
  export function videoQualityForRid(rid: string): VideoQuality {
252
340
  switch (rid) {
253
341
  case 'f':
@@ -6,8 +6,6 @@ export default abstract class RemoteTrack extends Track {
6
6
  /** @internal */
7
7
  receiver?: RTCRtpReceiver;
8
8
 
9
- streamState: Track.StreamState = Track.StreamState.Active;
10
-
11
9
  constructor(
12
10
  mediaTrack: MediaStreamTrack,
13
11
  sid: string,
@@ -0,0 +1,149 @@
1
+ import { TrackEvent } from '../events';
2
+ import RemoteVideoTrack, { ElementInfo } from './RemoteVideoTrack';
3
+ import MockMediaStreamTrack from '../../test/MockMediaStreamTrack';
4
+ import { Track } from './Track';
5
+
6
+ jest.useFakeTimers();
7
+
8
+ describe('RemoteVideoTrack', () => {
9
+ let track: RemoteVideoTrack;
10
+
11
+ beforeEach(() => {
12
+ track = new RemoteVideoTrack(new MockMediaStreamTrack(), 'sid', undefined, {});
13
+ });
14
+ describe('element visibility', () => {
15
+ let events: boolean[] = [];
16
+
17
+ beforeEach(() => {
18
+ track.on(TrackEvent.VisibilityChanged, (visible) => {
19
+ events.push(visible);
20
+ });
21
+ });
22
+ afterEach(() => {
23
+ events = [];
24
+ });
25
+
26
+ it('emits a visibility event upon observing visible element', () => {
27
+ const elementInfo = new MockElementInfo();
28
+ elementInfo.visible = true;
29
+
30
+ track.observeElementInfo(elementInfo);
31
+
32
+ expect(events).toHaveLength(1);
33
+ expect(events[0]).toBeTruthy();
34
+ });
35
+
36
+ it('emits a visibility event upon element becoming visible', () => {
37
+ const elementInfo = new MockElementInfo();
38
+ track.observeElementInfo(elementInfo);
39
+
40
+ elementInfo.setVisible(true);
41
+
42
+ expect(events).toHaveLength(2);
43
+ expect(events[1]).toBeTruthy();
44
+ });
45
+
46
+ it('emits a visibility event upon removing only visible element', () => {
47
+ const elementInfo = new MockElementInfo();
48
+ elementInfo.visible = true;
49
+
50
+ track.observeElementInfo(elementInfo);
51
+ track.stopObservingElementInfo(elementInfo);
52
+
53
+ expect(events).toHaveLength(2);
54
+ expect(events[1]).toBeFalsy();
55
+ });
56
+ });
57
+
58
+ describe('element dimensions', () => {
59
+ let events: Track.Dimensions[] = [];
60
+
61
+ beforeEach(() => {
62
+ track.on(TrackEvent.VideoDimensionsChanged, (dimensions) => {
63
+ events.push(dimensions);
64
+ });
65
+ });
66
+
67
+ afterEach(() => {
68
+ events = [];
69
+ });
70
+
71
+ it('emits a dimensions event upon observing element', () => {
72
+ const elementInfo = new MockElementInfo();
73
+ elementInfo.setDimensions(100, 100);
74
+
75
+ track.observeElementInfo(elementInfo);
76
+ jest.runAllTimers();
77
+
78
+ expect(events).toHaveLength(1);
79
+ expect(events[0].width).toBe(100);
80
+ expect(events[0].height).toBe(100);
81
+ });
82
+
83
+ it('emits a dimensions event upon element resize', () => {
84
+ const elementInfo = new MockElementInfo();
85
+ elementInfo.setDimensions(100, 100);
86
+
87
+ track.observeElementInfo(elementInfo);
88
+ jest.runAllTimers();
89
+
90
+ elementInfo.setDimensions(200, 200);
91
+ jest.runAllTimers();
92
+
93
+ expect(events).toHaveLength(2);
94
+ expect(events[1].width).toBe(200);
95
+ expect(events[1].height).toBe(200);
96
+ });
97
+ });
98
+ });
99
+
100
+ class MockElementInfo implements ElementInfo {
101
+ element: object = {};
102
+
103
+ private _width = 0;
104
+
105
+ private _height = 0;
106
+
107
+ setDimensions(width: number, height: number) {
108
+ let shouldEmit = false;
109
+ if (this._width !== width) {
110
+ this._width = width;
111
+ shouldEmit = true;
112
+ }
113
+ if (this._height !== height) {
114
+ this._height = height;
115
+ shouldEmit = true;
116
+ }
117
+
118
+ if (shouldEmit) {
119
+ this.handleResize?.();
120
+ }
121
+ }
122
+
123
+ width(): number {
124
+ return this._width;
125
+ }
126
+
127
+ height(): number {
128
+ return this._height;
129
+ }
130
+
131
+ visible = false;
132
+
133
+ setVisible = (visible: boolean) => {
134
+ if (this.visible !== visible) {
135
+ this.visible = visible;
136
+ this.handleVisibilityChanged?.();
137
+ }
138
+ };
139
+
140
+ visibilityChangedAt = 0;
141
+
142
+ handleResize?: () => void;
143
+
144
+ handleVisibilityChanged?: () => void;
145
+
146
+ observe(): void {}
147
+
148
+ stopObserving(): void {}
149
+ }
@@ -1,12 +1,7 @@
1
1
  import { debounce } from 'ts-debounce';
2
2
  import { TrackEvent } from '../events';
3
3
  import { computeBitrate, monitorFrequency, VideoReceiverStats } from '../stats';
4
- import {
5
- getIntersectionObserver,
6
- getResizeObserver,
7
- isMobile,
8
- ObservableMediaElement,
9
- } from '../utils';
4
+ import { getIntersectionObserver, getResizeObserver, ObservableMediaElement } from '../utils';
10
5
  import RemoteTrack from './RemoteTrack';
11
6
  import { attachToElement, detachTrack, Track } from './Track';
12
7
  import { AdaptiveStreamSettings } from './types';
@@ -85,24 +80,51 @@ export default class RemoteVideoTrack extends RemoteTrack {
85
80
  this.adaptiveStreamSettings &&
86
81
  this.elementInfos.find((info) => info.element === element) === undefined
87
82
  ) {
88
- this.elementInfos.push({
89
- element,
90
- visible: true, // default visible
91
- });
92
-
93
- (element as ObservableMediaElement).handleResize = this.debouncedHandleResize;
94
- (element as ObservableMediaElement).handleVisibilityChanged = this.handleVisibilityChanged;
95
-
96
- getIntersectionObserver().observe(element);
97
- getResizeObserver().observe(element);
83
+ const elementInfo = new HTMLElementInfo(element);
84
+ this.observeElementInfo(elementInfo);
85
+ }
86
+ this.hasUsedAttach = true;
87
+ return element;
88
+ }
98
89
 
90
+ /**
91
+ * Observe an ElementInfo for changes when adaptive streaming.
92
+ * @param elementInfo
93
+ * @internal
94
+ */
95
+ observeElementInfo(elementInfo: ElementInfo) {
96
+ if (
97
+ this.adaptiveStreamSettings &&
98
+ this.elementInfos.find((info) => info === elementInfo) === undefined
99
+ ) {
100
+ elementInfo.handleResize = () => {
101
+ this.debouncedHandleResize();
102
+ };
103
+ elementInfo.handleVisibilityChanged = () => {
104
+ this.updateVisibility();
105
+ };
106
+ this.elementInfos.push(elementInfo);
107
+ elementInfo.observe();
99
108
  // trigger the first resize update cycle
100
109
  // if the tab is backgrounded, the initial resize event does not fire until
101
110
  // the tab comes into focus for the first time.
102
111
  this.debouncedHandleResize();
112
+ this.updateVisibility();
103
113
  }
104
- this.hasUsedAttach = true;
105
- return element;
114
+ }
115
+
116
+ /**
117
+ * Stop observing an ElementInfo for changes.
118
+ * @param elementInfo
119
+ * @internal
120
+ */
121
+ stopObservingElementInfo(elementInfo: ElementInfo) {
122
+ const stopElementInfos = this.elementInfos.filter((info) => info === elementInfo);
123
+ for (const info of stopElementInfos) {
124
+ info.stopObserving();
125
+ }
126
+ this.elementInfos = this.elementInfos.filter((info) => info !== elementInfo);
127
+ this.updateVisibility();
106
128
  }
107
129
 
108
130
  detach(): HTMLMediaElement[];
@@ -122,6 +144,11 @@ export default class RemoteVideoTrack extends RemoteTrack {
122
144
  return detachedElements;
123
145
  }
124
146
 
147
+ /** @internal */
148
+ getDecoderImplementation(): string | undefined {
149
+ return this.prevStats?.decoderImplementation;
150
+ }
151
+
125
152
  protected monitorReceiver = async () => {
126
153
  if (!this.receiver) {
127
154
  this._currentBitrate = 0;
@@ -163,6 +190,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
163
190
  jitter: v.jitter,
164
191
  timestamp: v.timestamp,
165
192
  bytesReceived: v.bytesReceived,
193
+ decoderImplementation: v.decoderImplementation,
166
194
  };
167
195
  }
168
196
  });
@@ -170,26 +198,16 @@ export default class RemoteVideoTrack extends RemoteTrack {
170
198
  }
171
199
 
172
200
  private stopObservingElement(element: HTMLMediaElement) {
173
- getIntersectionObserver()?.unobserve(element);
174
- getResizeObserver()?.unobserve(element);
201
+ const stopElementInfos = this.elementInfos.filter((info) => info.element === element);
202
+ for (const info of stopElementInfos) {
203
+ info.stopObserving();
204
+ }
175
205
  this.elementInfos = this.elementInfos.filter((info) => info.element !== element);
176
206
  }
177
207
 
178
- private handleVisibilityChanged = (entry: IntersectionObserverEntry) => {
179
- const { target, isIntersecting } = entry;
180
- const elementInfo = this.elementInfos.find((info) => info.element === target);
181
- if (elementInfo) {
182
- elementInfo.visible = isIntersecting;
183
- elementInfo.visibilityChangedAt = Date.now();
184
- }
185
- this.updateVisibility();
186
- };
187
-
188
208
  protected async handleAppVisibilityChanged() {
189
209
  await super.handleAppVisibilityChanged();
190
210
  if (!this.isAdaptiveStream) return;
191
- // on desktop don't pause when tab is backgrounded
192
- if (!isMobile()) return;
193
211
  this.updateVisibility();
194
212
  }
195
213
 
@@ -202,7 +220,12 @@ export default class RemoteVideoTrack extends RemoteTrack {
202
220
  (prev, info) => Math.max(prev, info.visibilityChangedAt || 0),
203
221
  0,
204
222
  );
205
- const isVisible = this.elementInfos.some((info) => info.visible) && !this.isInBackground;
223
+
224
+ const backgroundPause =
225
+ this.adaptiveStreamSettings?.pauseVideoInBackground ?? true // default to true
226
+ ? this.isInBackground
227
+ : false;
228
+ const isVisible = this.elementInfos.some((info) => info.visible) && !backgroundPause;
206
229
 
207
230
  if (this.lastVisible === isVisible) {
208
231
  return;
@@ -226,8 +249,8 @@ export default class RemoteVideoTrack extends RemoteTrack {
226
249
  for (const info of this.elementInfos) {
227
250
  const pixelDensity = this.adaptiveStreamSettings?.pixelDensity ?? 1;
228
251
  const pixelDensityValue = pixelDensity === 'screen' ? window.devicePixelRatio : pixelDensity;
229
- const currentElementWidth = info.element.clientWidth * pixelDensityValue;
230
- const currentElementHeight = info.element.clientHeight * pixelDensityValue;
252
+ const currentElementWidth = info.width() * pixelDensityValue;
253
+ const currentElementHeight = info.height() * pixelDensityValue;
231
254
  if (currentElementWidth + currentElementHeight > maxWidth + maxHeight) {
232
255
  maxWidth = currentElementWidth;
233
256
  maxHeight = currentElementHeight;
@@ -242,12 +265,70 @@ export default class RemoteVideoTrack extends RemoteTrack {
242
265
  width: maxWidth,
243
266
  height: maxHeight,
244
267
  };
268
+
245
269
  this.emit(TrackEvent.VideoDimensionsChanged, this.lastDimensions, this);
246
270
  }
247
271
  }
248
272
 
249
- interface ElementInfo {
273
+ export interface ElementInfo {
274
+ element: object;
275
+ width(): number;
276
+ height(): number;
277
+ visible: boolean;
278
+ visibilityChangedAt: number | undefined;
279
+
280
+ handleResize?: () => void;
281
+ handleVisibilityChanged?: () => void;
282
+ observe(): void;
283
+ stopObserving(): void;
284
+ }
285
+
286
+ class HTMLElementInfo implements ElementInfo {
250
287
  element: HTMLMediaElement;
288
+
251
289
  visible: boolean;
252
- visibilityChangedAt?: number;
290
+
291
+ visibilityChangedAt: number | undefined;
292
+
293
+ handleResize?: () => void;
294
+
295
+ handleVisibilityChanged?: () => void;
296
+
297
+ constructor(element: HTMLMediaElement, visible: boolean = false) {
298
+ this.element = element;
299
+ this.visible = visible;
300
+ this.visibilityChangedAt = 0;
301
+ }
302
+
303
+ width(): number {
304
+ return this.element.clientWidth;
305
+ }
306
+
307
+ height(): number {
308
+ return this.element.clientWidth;
309
+ }
310
+
311
+ observe() {
312
+ (this.element as ObservableMediaElement).handleResize = () => {
313
+ this.handleResize?.();
314
+ };
315
+ (this.element as ObservableMediaElement).handleVisibilityChanged = this.onVisibilityChanged;
316
+
317
+ getIntersectionObserver().observe(this.element);
318
+ getResizeObserver().observe(this.element);
319
+ }
320
+
321
+ private onVisibilityChanged = (entry: IntersectionObserverEntry) => {
322
+ const { target, isIntersecting } = entry;
323
+ if (target === this.element) {
324
+ this.visible = isIntersecting;
325
+ this.visibilityChangedAt = Date.now();
326
+ this.handleVisibilityChanged?.();
327
+ }
328
+ };
329
+
330
+ stopObserving() {
331
+ getIntersectionObserver()?.unobserve(this.element);
332
+ getResizeObserver()?.unobserve(this.element);
333
+ }
253
334
  }
@@ -5,6 +5,8 @@ import { StreamState as ProtoStreamState } from '../../proto/livekit_rtc';
5
5
  import { TrackEvent } from '../events';
6
6
  import { isFireFox, isSafari, isWeb } from '../utils';
7
7
 
8
+ const BACKGROUND_REACTION_DELAY = 5000;
9
+
8
10
  // keep old audio elements when detached, we would re-use them since on iOS
9
11
  // Safari tracks which audio elements have been "blessed" by the user.
10
12
  const recycledElements: Array<HTMLAudioElement> = [];
@@ -28,10 +30,17 @@ export class Track extends (EventEmitter as new () => TypedEventEmitter<TrackEve
28
30
  */
29
31
  mediaStream?: MediaStream;
30
32
 
33
+ /**
34
+ * indicates current state of stream
35
+ */
36
+ streamState: Track.StreamState = Track.StreamState.Active;
37
+
31
38
  protected _mediaStreamTrack: MediaStreamTrack;
32
39
 
33
40
  protected isInBackground: boolean;
34
41
 
42
+ private backgroundTimeout: ReturnType<typeof setTimeout> | undefined;
43
+
35
44
  protected _currentBitrate: number = 0;
36
45
 
37
46
  protected constructor(mediaTrack: MediaStreamTrack, kind: Track.Kind) {
@@ -179,8 +188,20 @@ export class Track extends (EventEmitter as new () => TypedEventEmitter<TrackEve
179
188
  }
180
189
  }
181
190
 
182
- appVisibilityChangedListener = () => {
183
- this.handleAppVisibilityChanged();
191
+ protected appVisibilityChangedListener = () => {
192
+ if (this.backgroundTimeout) {
193
+ clearTimeout(this.backgroundTimeout);
194
+ }
195
+ // delay app visibility update if it goes to hidden
196
+ // update immediately if it comes back to focus
197
+ if (document.visibilityState === 'hidden') {
198
+ this.backgroundTimeout = setTimeout(
199
+ () => this.handleAppVisibilityChanged(),
200
+ BACKGROUND_REACTION_DELAY,
201
+ );
202
+ } else {
203
+ this.handleAppVisibilityChanged();
204
+ }
184
205
  };
185
206
 
186
207
  protected async handleAppVisibilityChanged() {
@@ -12,7 +12,8 @@ export interface TrackPublishDefaults {
12
12
  screenShareEncoding?: VideoEncoding;
13
13
 
14
14
  /**
15
- * codec, defaults to vp8
15
+ * codec, defaults to vp8; for svc codecs, auto enable vp8
16
+ * as backup. (TBD)
16
17
  */
17
18
  videoCodec?: VideoCodec;
18
19