livekit-client 0.18.6 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/livekit-client.esm.mjs +1034 -438
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/{api → src/api}/RequestQueue.d.ts +0 -0
- package/dist/src/api/RequestQueue.d.ts.map +1 -0
- package/dist/{api → src/api}/SignalClient.d.ts +3 -3
- package/dist/src/api/SignalClient.d.ts.map +1 -0
- package/dist/{index.d.ts → src/index.d.ts} +3 -4
- package/dist/src/index.d.ts.map +1 -0
- package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/options.d.ts +61 -0
- package/dist/src/options.d.ts.map +1 -0
- package/dist/{proto → src/proto}/google/protobuf/timestamp.d.ts +0 -0
- package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -0
- package/dist/{proto → src/proto}/livekit_models.d.ts +80 -0
- package/dist/src/proto/livekit_models.d.ts.map +1 -0
- package/dist/{proto → src/proto}/livekit_rtc.d.ts +661 -0
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -0
- package/dist/{room → src/room}/DeviceManager.d.ts +0 -0
- package/dist/src/room/DeviceManager.d.ts.map +1 -0
- package/dist/{room → src/room}/PCTransport.d.ts +0 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -0
- package/dist/{room → src/room}/RTCEngine.d.ts +4 -2
- package/dist/src/room/RTCEngine.d.ts.map +1 -0
- package/dist/{room → src/room}/Room.d.ts +13 -7
- package/dist/src/room/Room.d.ts.map +1 -0
- package/dist/{room → src/room}/errors.d.ts +0 -0
- package/dist/src/room/errors.d.ts.map +1 -0
- package/dist/{room → src/room}/events.d.ts +11 -13
- package/dist/src/room/events.d.ts.map +1 -0
- package/dist/{room → src/room}/participant/LocalParticipant.d.ts +4 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -0
- package/dist/{room → src/room}/participant/Participant.d.ts +0 -4
- package/dist/src/room/participant/Participant.d.ts.map +1 -0
- package/dist/{room → src/room}/participant/ParticipantTrackPermission.d.ts +0 -0
- package/dist/src/room/participant/ParticipantTrackPermission.d.ts.map +1 -0
- package/dist/{room → src/room}/participant/RemoteParticipant.d.ts +3 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -0
- package/dist/{room → src/room}/participant/publishUtils.d.ts +0 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -0
- package/dist/{room → src/room}/stats.d.ts +1 -0
- package/dist/src/room/stats.d.ts.map +1 -0
- package/dist/{room → src/room}/track/LocalAudioTrack.d.ts +0 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -0
- package/dist/{room → src/room}/track/LocalTrack.d.ts +3 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -0
- package/dist/{room → src/room}/track/LocalTrackPublication.d.ts +0 -0
- package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -0
- package/dist/{room → src/room}/track/LocalVideoTrack.d.ts +17 -2
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -0
- package/dist/{room → src/room}/track/RemoteAudioTrack.d.ts +0 -0
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -0
- package/dist/{room → src/room}/track/RemoteTrack.d.ts +0 -1
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -0
- package/dist/{room → src/room}/track/RemoteTrackPublication.d.ts +0 -0
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -0
- package/dist/{room → src/room}/track/RemoteVideoTrack.d.ts +25 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -0
- package/dist/{room → src/room}/track/Track.d.ts +6 -1
- package/dist/src/room/track/Track.d.ts.map +1 -0
- package/dist/{room → src/room}/track/TrackPublication.d.ts +0 -0
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -0
- package/dist/{room → src/room}/track/create.d.ts +0 -0
- package/dist/src/room/track/create.d.ts.map +1 -0
- package/dist/{room → src/room}/track/defaults.d.ts +0 -0
- package/dist/src/room/track/defaults.d.ts.map +1 -0
- package/dist/{room → src/room}/track/options.d.ts +2 -31
- package/dist/src/room/track/options.d.ts.map +1 -0
- package/dist/{room → src/room}/track/types.d.ts +5 -0
- package/dist/src/room/track/types.d.ts.map +1 -0
- package/dist/{room → src/room}/track/utils.d.ts +0 -0
- package/dist/src/room/track/utils.d.ts.map +1 -0
- package/dist/{room → src/room}/utils.d.ts +0 -0
- package/dist/src/room/utils.d.ts.map +1 -0
- package/dist/src/test/MockMediaStreamTrack.d.ts +26 -0
- package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -0
- package/dist/{test → src/test}/mocks.d.ts +0 -0
- package/dist/src/test/mocks.d.ts.map +1 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.d.ts.map +1 -0
- package/package.json +6 -2
- package/src/api/SignalClient.ts +34 -9
- package/src/index.ts +4 -3
- package/src/options.ts +0 -82
- package/src/proto/livekit_models.ts +90 -0
- package/src/proto/livekit_rtc.ts +235 -1
- package/src/room/DeviceManager.ts +4 -1
- package/src/room/RTCEngine.ts +46 -9
- package/src/room/Room.ts +122 -53
- package/src/room/events.ts +12 -14
- package/src/room/participant/LocalParticipant.ts +108 -23
- package/src/room/participant/Participant.ts +0 -5
- package/src/room/participant/RemoteParticipant.ts +17 -5
- package/src/room/participant/publishUtils.test.ts +2 -2
- package/src/room/stats.ts +2 -0
- package/src/room/track/LocalAudioTrack.ts +4 -0
- package/src/room/track/LocalTrack.ts +12 -5
- package/src/room/track/LocalVideoTrack.ts +144 -56
- package/src/room/track/RemoteTrack.ts +0 -2
- package/src/room/track/RemoteVideoTrack.test.ts +149 -0
- package/src/room/track/RemoteVideoTrack.ts +118 -37
- package/src/room/track/Track.ts +23 -2
- package/src/room/track/create.ts +1 -1
- package/src/room/track/options.ts +2 -31
- package/src/room/track/types.ts +5 -0
- package/src/room/track/utils.test.ts +6 -6
- package/src/test/MockMediaStreamTrack.ts +83 -0
- package/src/version.ts +4 -2
- package/dist/api/RequestQueue.d.ts.map +0 -1
- package/dist/api/SignalClient.d.ts.map +0 -1
- package/dist/connect.d.ts +0 -24
- package/dist/connect.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/options.d.ts +0 -128
- package/dist/options.d.ts.map +0 -1
- package/dist/proto/google/protobuf/timestamp.d.ts.map +0 -1
- package/dist/proto/livekit_models.d.ts.map +0 -1
- package/dist/proto/livekit_rtc.d.ts.map +0 -1
- package/dist/room/DeviceManager.d.ts.map +0 -1
- package/dist/room/PCTransport.d.ts.map +0 -1
- package/dist/room/RTCEngine.d.ts.map +0 -1
- package/dist/room/Room.d.ts.map +0 -1
- package/dist/room/errors.d.ts.map +0 -1
- package/dist/room/events.d.ts.map +0 -1
- package/dist/room/participant/LocalParticipant.d.ts.map +0 -1
- package/dist/room/participant/Participant.d.ts.map +0 -1
- package/dist/room/participant/ParticipantTrackPermission.d.ts.map +0 -1
- package/dist/room/participant/RemoteParticipant.d.ts.map +0 -1
- package/dist/room/participant/publishUtils.d.ts.map +0 -1
- package/dist/room/stats.d.ts.map +0 -1
- package/dist/room/track/LocalAudioTrack.d.ts.map +0 -1
- package/dist/room/track/LocalTrack.d.ts.map +0 -1
- package/dist/room/track/LocalTrackPublication.d.ts.map +0 -1
- package/dist/room/track/LocalVideoTrack.d.ts.map +0 -1
- package/dist/room/track/RemoteAudioTrack.d.ts.map +0 -1
- package/dist/room/track/RemoteTrack.d.ts.map +0 -1
- package/dist/room/track/RemoteTrackPublication.d.ts.map +0 -1
- package/dist/room/track/RemoteVideoTrack.d.ts.map +0 -1
- package/dist/room/track/Track.d.ts.map +0 -1
- package/dist/room/track/TrackPublication.d.ts.map +0 -1
- package/dist/room/track/create.d.ts.map +0 -1
- package/dist/room/track/defaults.d.ts.map +0 -1
- package/dist/room/track/options.d.ts.map +0 -1
- package/dist/room/track/types.d.ts.map +0 -1
- package/dist/room/track/utils.d.ts.map +0 -1
- package/dist/room/utils.d.ts.map +0 -1
- package/dist/test/mocks.d.ts.map +0 -1
- package/dist/version.d.ts +0 -3
- package/dist/version.d.ts.map +0 -1
- package/src/connect.ts +0 -98
@@ -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
|
-
|
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':
|
@@ -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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
105
|
-
|
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
|
-
|
174
|
-
|
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
|
-
|
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.
|
230
|
-
const currentElementHeight = info.
|
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
|
-
|
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
|
}
|
package/src/room/track/Track.ts
CHANGED
@@ -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.
|
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() {
|
package/src/room/track/create.ts
CHANGED
@@ -89,7 +89,7 @@ export async function createLocalScreenTracks(
|
|
89
89
|
options = {};
|
90
90
|
}
|
91
91
|
if (options.resolution === undefined) {
|
92
|
-
options.resolution = VideoPresets.
|
92
|
+
options.resolution = VideoPresets.h1080.resolution;
|
93
93
|
}
|
94
94
|
|
95
95
|
let videoConstraints: MediaTrackConstraints | boolean = true;
|