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