livekit-client 2.13.7 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +13 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +188 -90
- 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/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +7 -0
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +2 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +18 -0
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +3 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrackPublication.d.ts +1 -0
- package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +1 -0
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +4 -0
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/e2ee/types.d.ts +7 -0
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +2 -1
- package/dist/ts4.2/src/room/PCTransport.d.ts +1 -0
- package/dist/ts4.2/src/room/events.d.ts +18 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/ts4.2/src/room/participant/Participant.d.ts +3 -0
- package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +1 -0
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/ts4.2/src/room/track/Track.d.ts +1 -0
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +1 -0
- package/dist/ts4.2/src/room/track/options.d.ts +4 -0
- package/dist/ts4.2/src/room/utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/e2ee/E2eeManager.ts +8 -3
- package/src/e2ee/types.ts +8 -0
- package/src/e2ee/worker/FrameCryptor.ts +15 -0
- package/src/e2ee/worker/e2ee.worker.ts +8 -5
- package/src/room/PCTransport.ts +88 -77
- package/src/room/events.ts +21 -0
- package/src/room/participant/LocalParticipant.ts +14 -2
- package/src/room/participant/Participant.ts +3 -0
- package/src/room/participant/publishUtils.ts +2 -2
- package/src/room/track/LocalTrackPublication.ts +9 -1
- package/src/room/track/LocalVideoTrack.ts +68 -1
- package/src/room/track/Track.ts +1 -0
- package/src/room/track/TrackPublication.ts +1 -0
- package/src/room/track/create.ts +2 -2
- package/src/room/track/options.ts +5 -0
- package/src/room/utils.ts +19 -3
@@ -34,7 +34,7 @@ export declare function isFireFox(): boolean;
|
|
34
34
|
export declare function isChromiumBased(): boolean;
|
35
35
|
export declare function isSafari(): boolean;
|
36
36
|
export declare function isSafariBased(): boolean;
|
37
|
-
export declare function
|
37
|
+
export declare function isSafari17Based(): boolean;
|
38
38
|
export declare function isSafariSvcApi(browser?: BrowserDetails): boolean;
|
39
39
|
export declare function isMobile(): boolean;
|
40
40
|
export declare function isE2EESimulcastSupported(): boolean | undefined;
|
package/package.json
CHANGED
package/src/e2ee/E2eeManager.ts
CHANGED
@@ -25,6 +25,7 @@ import type {
|
|
25
25
|
RTPVideoMapMessage,
|
26
26
|
RatchetRequestMessage,
|
27
27
|
RemoveTransformMessage,
|
28
|
+
ScriptTransformOptions,
|
28
29
|
SetKeyMessage,
|
29
30
|
SifTrailerMessage,
|
30
31
|
UpdateCodecMessage,
|
@@ -220,8 +221,9 @@ export class E2EEManager
|
|
220
221
|
this.room.localParticipant.identity,
|
221
222
|
);
|
222
223
|
});
|
223
|
-
|
224
|
-
|
224
|
+
|
225
|
+
room.localParticipant.on(ParticipantEvent.LocalSenderCreated, async (sender, track) => {
|
226
|
+
this.setupE2EESender(track, sender);
|
225
227
|
});
|
226
228
|
|
227
229
|
keyProvider
|
@@ -345,7 +347,7 @@ export class E2EEManager
|
|
345
347
|
}
|
346
348
|
|
347
349
|
if (isScriptTransformSupported()) {
|
348
|
-
const options = {
|
350
|
+
const options: ScriptTransformOptions = {
|
349
351
|
kind: 'decode',
|
350
352
|
participantIdentity,
|
351
353
|
trackId,
|
@@ -371,6 +373,7 @@ export class E2EEManager
|
|
371
373
|
let writable: WritableStream = receiver.writableStream;
|
372
374
|
// @ts-ignore
|
373
375
|
let readable: ReadableStream = receiver.readableStream;
|
376
|
+
|
374
377
|
if (!writable || !readable) {
|
375
378
|
// @ts-ignore
|
376
379
|
const receiverStreams = receiver.createEncodedStreams();
|
@@ -390,6 +393,7 @@ export class E2EEManager
|
|
390
393
|
trackId: trackId,
|
391
394
|
codec,
|
392
395
|
participantIdentity: participantIdentity,
|
396
|
+
isReuse: E2EE_FLAG in receiver,
|
393
397
|
},
|
394
398
|
};
|
395
399
|
this.worker.postMessage(msg, [readable, writable]);
|
@@ -435,6 +439,7 @@ export class E2EEManager
|
|
435
439
|
codec,
|
436
440
|
trackId,
|
437
441
|
participantIdentity: this.room.localParticipant.identity,
|
442
|
+
isReuse: false,
|
438
443
|
},
|
439
444
|
};
|
440
445
|
this.worker.postMessage(msg, [senderStreams.readable, senderStreams.writable]);
|
package/src/e2ee/types.ts
CHANGED
@@ -49,6 +49,7 @@ export interface EncodeMessage extends BaseMessage {
|
|
49
49
|
writableStream: WritableStream;
|
50
50
|
trackId: string;
|
51
51
|
codec?: VideoCodec;
|
52
|
+
isReuse: boolean;
|
52
53
|
};
|
53
54
|
}
|
54
55
|
|
@@ -162,3 +163,10 @@ export type DecodeRatchetOptions = {
|
|
162
163
|
/** ratcheted key to try */
|
163
164
|
encryptionKey?: CryptoKey;
|
164
165
|
};
|
166
|
+
|
167
|
+
export type ScriptTransformOptions = {
|
168
|
+
kind: 'decode' | 'encode';
|
169
|
+
participantIdentity: string;
|
170
|
+
trackId: string;
|
171
|
+
codec?: VideoCodec;
|
172
|
+
};
|
@@ -69,6 +69,8 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
69
69
|
|
70
70
|
private detectedCodec?: VideoCodec;
|
71
71
|
|
72
|
+
private isTransformActive: boolean = false;
|
73
|
+
|
72
74
|
constructor(opts: {
|
73
75
|
keys: ParticipantKeyHandler;
|
74
76
|
participantIdentity: string;
|
@@ -159,6 +161,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
159
161
|
readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
160
162
|
writable: WritableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
161
163
|
trackId: string,
|
164
|
+
isReuse: boolean,
|
162
165
|
codec?: VideoCodec,
|
163
166
|
) {
|
164
167
|
if (codec) {
|
@@ -173,11 +176,20 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
173
176
|
...this.logContext,
|
174
177
|
});
|
175
178
|
|
179
|
+
if (isReuse && this.isTransformActive) {
|
180
|
+
workerLogger.debug('reuse transform', {
|
181
|
+
...this.logContext,
|
182
|
+
});
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
|
176
186
|
const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
|
177
187
|
const transformStream = new TransformStream({
|
178
188
|
transform: transformFn.bind(this),
|
179
189
|
});
|
180
190
|
|
191
|
+
this.isTransformActive = true;
|
192
|
+
|
181
193
|
readable
|
182
194
|
.pipeThrough(transformStream)
|
183
195
|
.pipeTo(writable)
|
@@ -189,6 +201,9 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
189
201
|
? e
|
190
202
|
: new CryptorError(e.message, undefined, this.participantIdentity),
|
191
203
|
);
|
204
|
+
})
|
205
|
+
.finally(() => {
|
206
|
+
this.isTransformActive = false;
|
192
207
|
});
|
193
208
|
this.trackId = trackId;
|
194
209
|
}
|
@@ -12,6 +12,7 @@ import type {
|
|
12
12
|
RatchetMessage,
|
13
13
|
RatchetRequestMessage,
|
14
14
|
RatchetResult,
|
15
|
+
ScriptTransformOptions,
|
15
16
|
} from '../types';
|
16
17
|
import { FrameCryptor, encryptionEnabledMap } from './FrameCryptor';
|
17
18
|
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
@@ -65,6 +66,7 @@ onmessage = (ev) => {
|
|
65
66
|
data.readableStream,
|
66
67
|
data.writableStream,
|
67
68
|
data.trackId,
|
69
|
+
data.isReuse,
|
68
70
|
data.codec,
|
69
71
|
);
|
70
72
|
break;
|
@@ -75,6 +77,7 @@ onmessage = (ev) => {
|
|
75
77
|
data.readableStream,
|
76
78
|
data.writableStream,
|
77
79
|
data.trackId,
|
80
|
+
data.isReuse,
|
78
81
|
data.codec,
|
79
82
|
);
|
80
83
|
break;
|
@@ -259,14 +262,14 @@ if (self.RTCTransformEvent) {
|
|
259
262
|
workerLogger.debug('setup transform event');
|
260
263
|
// @ts-ignore
|
261
264
|
self.onrtctransform = (event: RTCTransformEvent) => {
|
262
|
-
// @ts-ignore
|
265
|
+
// @ts-ignore
|
263
266
|
const transformer = event.transformer;
|
264
267
|
workerLogger.debug('transformer', transformer);
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
+
|
269
|
+
const { kind, participantIdentity, trackId, codec } =
|
270
|
+
transformer.options as ScriptTransformOptions;
|
268
271
|
const cryptor = getTrackCryptor(participantIdentity, trackId);
|
269
272
|
workerLogger.debug('transform', { codec });
|
270
|
-
cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, codec);
|
273
|
+
cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, false, codec);
|
271
274
|
};
|
272
275
|
}
|
package/src/room/PCTransport.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
1
2
|
import { EventEmitter } from 'events';
|
2
3
|
import type { MediaDescription, SessionDescription } from 'sdp-transform';
|
3
4
|
import { parse, write } from 'sdp-transform';
|
@@ -52,6 +53,8 @@ export default class PCTransport extends EventEmitter {
|
|
52
53
|
|
53
54
|
private latestOfferId: number = 0;
|
54
55
|
|
56
|
+
private offerLock: Mutex;
|
57
|
+
|
55
58
|
pendingCandidates: RTCIceCandidateInit[] = [];
|
56
59
|
|
57
60
|
restartingIce: boolean = false;
|
@@ -86,6 +89,7 @@ export default class PCTransport extends EventEmitter {
|
|
86
89
|
this.loggerOptions = loggerOptions;
|
87
90
|
this.config = config;
|
88
91
|
this._pc = this.createPC();
|
92
|
+
this.offerLock = new Mutex();
|
89
93
|
}
|
90
94
|
|
91
95
|
private createPC() {
|
@@ -251,101 +255,108 @@ export default class PCTransport extends EventEmitter {
|
|
251
255
|
}, debounceInterval);
|
252
256
|
|
253
257
|
async createAndSendOffer(options?: RTCOfferOptions) {
|
254
|
-
|
255
|
-
const offerId = this.latestOfferId + 1;
|
256
|
-
this.latestOfferId = offerId;
|
257
|
-
if (this.onOffer === undefined) {
|
258
|
-
return;
|
259
|
-
}
|
258
|
+
const unlock = await this.offerLock.lock();
|
260
259
|
|
261
|
-
|
262
|
-
|
263
|
-
this.
|
264
|
-
|
260
|
+
try {
|
261
|
+
// increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
|
262
|
+
const offerId = this.latestOfferId + 1;
|
263
|
+
this.latestOfferId = offerId;
|
265
264
|
|
266
|
-
|
267
|
-
// we're waiting for the peer to accept our offer, so we'll just wait
|
268
|
-
// the only exception to this is when ICE restart is needed
|
269
|
-
const currentSD = this._pc.remoteDescription;
|
270
|
-
if (options?.iceRestart && currentSD) {
|
271
|
-
// TODO: handle when ICE restart is needed but we don't have a remote description
|
272
|
-
// the best thing to do is to recreate the peerconnection
|
273
|
-
await this._pc.setRemoteDescription(currentSD);
|
274
|
-
} else {
|
275
|
-
this.renegotiate = true;
|
265
|
+
if (this.onOffer === undefined) {
|
276
266
|
return;
|
277
267
|
}
|
278
|
-
} else if (!this._pc || this._pc.signalingState === 'closed') {
|
279
|
-
this.log.warn('could not createOffer with closed peer connection', this.logContext);
|
280
|
-
return;
|
281
|
-
}
|
282
268
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
269
|
+
if (options?.iceRestart) {
|
270
|
+
this.log.debug('restarting ICE', this.logContext);
|
271
|
+
this.restartingIce = true;
|
272
|
+
}
|
287
273
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
274
|
+
if (this._pc && this._pc.signalingState === 'have-local-offer') {
|
275
|
+
// we're waiting for the peer to accept our offer, so we'll just wait
|
276
|
+
// the only exception to this is when ICE restart is needed
|
277
|
+
const currentSD = this._pc.remoteDescription;
|
278
|
+
if (options?.iceRestart && currentSD) {
|
279
|
+
// TODO: handle when ICE restart is needed but we don't have a remote description
|
280
|
+
// the best thing to do is to recreate the peerconnection
|
281
|
+
await this._pc.setRemoteDescription(currentSD);
|
282
|
+
} else {
|
283
|
+
this.renegotiate = true;
|
284
|
+
return;
|
285
|
+
}
|
286
|
+
} else if (!this._pc || this._pc.signalingState === 'closed') {
|
287
|
+
this.log.warn('could not createOffer with closed peer connection', this.logContext);
|
288
|
+
return;
|
289
|
+
}
|
298
290
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
291
|
+
// actually negotiate
|
292
|
+
this.log.debug('starting to negotiate', this.logContext);
|
293
|
+
const offer = await this.pc.createOffer(options);
|
294
|
+
this.log.debug('original offer', { sdp: offer.sdp, ...this.logContext });
|
295
|
+
|
296
|
+
const sdpParsed = parse(offer.sdp ?? '');
|
297
|
+
sdpParsed.media.forEach((media) => {
|
298
|
+
ensureIPAddrMatchVersion(media);
|
299
|
+
if (media.type === 'audio') {
|
300
|
+
ensureAudioNackAndStereo(media, [], []);
|
301
|
+
} else if (media.type === 'video') {
|
302
|
+
this.trackBitrates.some((trackbr): boolean => {
|
303
|
+
if (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
|
304
|
+
return false;
|
304
305
|
}
|
305
|
-
return false;
|
306
|
-
});
|
307
306
|
|
308
|
-
|
309
|
-
|
310
|
-
|
307
|
+
let codecPayload = 0;
|
308
|
+
media.rtp.some((rtp): boolean => {
|
309
|
+
if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
|
310
|
+
codecPayload = rtp.payload;
|
311
|
+
return true;
|
312
|
+
}
|
313
|
+
return false;
|
314
|
+
});
|
311
315
|
|
312
|
-
|
313
|
-
|
314
|
-
|
316
|
+
if (codecPayload === 0) {
|
317
|
+
return true;
|
318
|
+
}
|
315
319
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
320
|
+
if (isSVCCodec(trackbr.codec)) {
|
321
|
+
this.ensureVideoDDExtensionForSVC(media, sdpParsed);
|
322
|
+
}
|
323
|
+
|
324
|
+
// TODO: av1 slow starting issue already fixed in chrome 124, clean this after some versions
|
325
|
+
// mung sdp for av1 bitrate setting that can't apply by sendEncoding
|
326
|
+
if (trackbr.codec !== 'av1') {
|
327
|
+
return true;
|
328
|
+
}
|
321
329
|
|
322
|
-
|
330
|
+
const startBitrate = Math.round(trackbr.maxbr * startBitrateForSVC);
|
323
331
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
332
|
+
for (const fmtp of media.fmtp) {
|
333
|
+
if (fmtp.payload === codecPayload) {
|
334
|
+
// if another track's fmtp already is set, we cannot override the bitrate
|
335
|
+
// this has the unfortunate consequence of being forced to use the
|
336
|
+
// initial track's bitrate for all tracks
|
337
|
+
if (!fmtp.config.includes('x-google-start-bitrate')) {
|
338
|
+
fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
|
339
|
+
}
|
340
|
+
break;
|
331
341
|
}
|
332
|
-
break;
|
333
342
|
}
|
334
|
-
|
335
|
-
|
343
|
+
return true;
|
344
|
+
});
|
345
|
+
}
|
346
|
+
});
|
347
|
+
if (this.latestOfferId > offerId) {
|
348
|
+
this.log.warn('latestOfferId mismatch', {
|
349
|
+
...this.logContext,
|
350
|
+
latestOfferId: this.latestOfferId,
|
351
|
+
offerId,
|
336
352
|
});
|
353
|
+
return;
|
337
354
|
}
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
latestOfferId: this.latestOfferId,
|
343
|
-
offerId,
|
344
|
-
});
|
345
|
-
return;
|
355
|
+
await this.setMungedSDP(offer, write(sdpParsed));
|
356
|
+
this.onOffer(offer, this.latestOfferId);
|
357
|
+
} finally {
|
358
|
+
unlock();
|
346
359
|
}
|
347
|
-
await this.setMungedSDP(offer, write(sdpParsed));
|
348
|
-
this.onOffer(offer, this.latestOfferId);
|
349
360
|
}
|
350
361
|
|
351
362
|
async createAndSetAnswer(): Promise<RTCSessionDescriptionInit> {
|
package/src/room/events.ts
CHANGED
@@ -429,6 +429,21 @@ export enum ParticipantEvent {
|
|
429
429
|
*/
|
430
430
|
LocalTrackUnpublished = 'localTrackUnpublished',
|
431
431
|
|
432
|
+
/**
|
433
|
+
* A local track has been constrained by cpu.
|
434
|
+
* This event is useful to know when to reduce the capture resolution of the track.
|
435
|
+
*
|
436
|
+
* This event is emitted on the local participant.
|
437
|
+
*
|
438
|
+
* args: ([[LocalVideoTrack]], [[LocalTrackPublication]])
|
439
|
+
*/
|
440
|
+
LocalTrackCpuConstrained = 'localTrackCpuConstrained',
|
441
|
+
|
442
|
+
/**
|
443
|
+
* @internal
|
444
|
+
*/
|
445
|
+
LocalSenderCreated = 'localSenderCreated',
|
446
|
+
|
432
447
|
/**
|
433
448
|
* Participant metadata is a simple way for app-specific state to be pushed to
|
434
449
|
* all users.
|
@@ -515,6 +530,11 @@ export enum ParticipantEvent {
|
|
515
530
|
*/
|
516
531
|
TrackSubscriptionStatusChanged = 'trackSubscriptionStatusChanged',
|
517
532
|
|
533
|
+
/**
|
534
|
+
* a local track has been constrained by cpu
|
535
|
+
*/
|
536
|
+
TrackCpuConstrained = 'trackCpuConstrained',
|
537
|
+
|
518
538
|
// fired only on LocalParticipant
|
519
539
|
/** @internal */
|
520
540
|
MediaDevicesError = 'mediaDevicesError',
|
@@ -599,6 +619,7 @@ export enum TrackEvent {
|
|
599
619
|
Ended = 'ended',
|
600
620
|
Subscribed = 'subscribed',
|
601
621
|
Unsubscribed = 'unsubscribed',
|
622
|
+
CpuConstrained = 'cpuConstrained',
|
602
623
|
/** @internal */
|
603
624
|
UpdateSettings = 'updateSettings',
|
604
625
|
/** @internal */
|
@@ -93,7 +93,7 @@ import {
|
|
93
93
|
isLocalTrack,
|
94
94
|
isLocalVideoTrack,
|
95
95
|
isSVCCodec,
|
96
|
-
|
96
|
+
isSafari17Based,
|
97
97
|
isVideoTrack,
|
98
98
|
isWeb,
|
99
99
|
numberToBigInt,
|
@@ -708,7 +708,7 @@ export default class LocalParticipant extends Participant {
|
|
708
708
|
throw new DeviceUnsupportedError('getDisplayMedia not supported');
|
709
709
|
}
|
710
710
|
|
711
|
-
if (options.resolution === undefined && !
|
711
|
+
if (options.resolution === undefined && !isSafari17Based()) {
|
712
712
|
// we need to constrain the dimensions, otherwise it could lead to low bitrate
|
713
713
|
// due to encoding a huge video. Encoding such large surfaces is really expensive
|
714
714
|
// unfortunately Safari 17 has a but and cannot be constrained by default
|
@@ -1169,6 +1169,7 @@ export default class LocalParticipant extends Participant {
|
|
1169
1169
|
}
|
1170
1170
|
|
1171
1171
|
track.sender = await this.engine.createSender(track, opts, encodings);
|
1172
|
+
this.emit(ParticipantEvent.LocalSenderCreated, track.sender, track);
|
1172
1173
|
|
1173
1174
|
if (isLocalVideoTrack(track)) {
|
1174
1175
|
opts.degradationPreference ??= getDefaultDegradationPreference(track);
|
@@ -1271,6 +1272,9 @@ export default class LocalParticipant extends Participant {
|
|
1271
1272
|
loggerName: this.roomOptions.loggerName,
|
1272
1273
|
loggerContextCb: () => this.logContext,
|
1273
1274
|
});
|
1275
|
+
publication.on(TrackEvent.CpuConstrained, (constrainedTrack) =>
|
1276
|
+
this.onTrackCpuConstrained(constrainedTrack, publication),
|
1277
|
+
);
|
1274
1278
|
// save options for when it needs to be republished again
|
1275
1279
|
publication.options = opts;
|
1276
1280
|
track.sid = ti.sid;
|
@@ -2329,6 +2333,14 @@ export default class LocalParticipant extends Participant {
|
|
2329
2333
|
this.engine.client.sendUpdateLocalAudioTrack(pub.trackSid, pub.getTrackFeatures());
|
2330
2334
|
};
|
2331
2335
|
|
2336
|
+
private onTrackCpuConstrained = (track: LocalVideoTrack, publication: LocalTrackPublication) => {
|
2337
|
+
this.log.debug('track cpu constrained', {
|
2338
|
+
...this.logContext,
|
2339
|
+
...getLogContextFromTrack(publication),
|
2340
|
+
});
|
2341
|
+
this.emit(ParticipantEvent.LocalTrackCpuConstrained, track, publication);
|
2342
|
+
};
|
2343
|
+
|
2332
2344
|
private handleSubscribedQualityUpdate = async (update: SubscribedQualityUpdate) => {
|
2333
2345
|
if (!this.roomOptions?.dynacast) {
|
2334
2346
|
return;
|
@@ -13,6 +13,7 @@ import type TypedEmitter from 'typed-emitter';
|
|
13
13
|
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
|
14
14
|
import { ParticipantEvent, TrackEvent } from '../events';
|
15
15
|
import type LocalTrackPublication from '../track/LocalTrackPublication';
|
16
|
+
import type LocalVideoTrack from '../track/LocalVideoTrack';
|
16
17
|
import type RemoteTrack from '../track/RemoteTrack';
|
17
18
|
import type RemoteTrackPublication from '../track/RemoteTrackPublication';
|
18
19
|
import { Track } from '../track/Track';
|
@@ -403,6 +404,8 @@ export type ParticipantEventCallbacks = {
|
|
403
404
|
trackUnmuted: (publication: TrackPublication) => void;
|
404
405
|
localTrackPublished: (publication: LocalTrackPublication) => void;
|
405
406
|
localTrackUnpublished: (publication: LocalTrackPublication) => void;
|
407
|
+
localTrackCpuConstrained: (track: LocalVideoTrack, publication: LocalTrackPublication) => void;
|
408
|
+
localSenderCreated: (sender: RTCRtpSender, track: Track) => void;
|
406
409
|
participantMetadataChanged: (prevMetadata: string | undefined, participant?: any) => void;
|
407
410
|
participantNameChanged: (name: string) => void;
|
408
411
|
dataReceived: (payload: Uint8Array, kind: DataPacket_Kind) => void;
|
@@ -18,7 +18,7 @@ import {
|
|
18
18
|
isFireFox,
|
19
19
|
isReactNative,
|
20
20
|
isSVCCodec,
|
21
|
-
|
21
|
+
isSafariBased,
|
22
22
|
isSafariSvcApi,
|
23
23
|
unwrapConstraint,
|
24
24
|
} from '../utils';
|
@@ -151,7 +151,7 @@ export function computeVideoEncodings(
|
|
151
151
|
// Announced here: https://groups.google.com/g/discuss-webrtc/c/-QQ3pxrl-fw?pli=1
|
152
152
|
const browser = getBrowser();
|
153
153
|
if (
|
154
|
-
|
154
|
+
isSafariBased() ||
|
155
155
|
// Even tho RN runs M114, it does not produce SVC layers when a single encoding
|
156
156
|
// is provided. So we'll use the legacy SVC specification for now.
|
157
157
|
// TODO: when we upstream libwebrtc, this will need additional verification
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { AudioTrackFeature, TrackInfo } from '@livekit/protocol';
|
2
2
|
import { TrackEvent } from '../events';
|
3
3
|
import type { LoggerOptions } from '../types';
|
4
|
-
import { isAudioTrack } from '../utils';
|
4
|
+
import { isAudioTrack, isVideoTrack } from '../utils';
|
5
5
|
import LocalAudioTrack from './LocalAudioTrack';
|
6
6
|
import type LocalTrack from './LocalTrack';
|
7
7
|
import type LocalVideoTrack from './LocalVideoTrack';
|
@@ -28,12 +28,14 @@ export default class LocalTrackPublication extends TrackPublication {
|
|
28
28
|
setTrack(track?: Track) {
|
29
29
|
if (this.track) {
|
30
30
|
this.track.off(TrackEvent.Ended, this.handleTrackEnded);
|
31
|
+
this.track.off(TrackEvent.CpuConstrained, this.handleCpuConstrained);
|
31
32
|
}
|
32
33
|
|
33
34
|
super.setTrack(track);
|
34
35
|
|
35
36
|
if (track) {
|
36
37
|
track.on(TrackEvent.Ended, this.handleTrackEnded);
|
38
|
+
track.on(TrackEvent.CpuConstrained, this.handleCpuConstrained);
|
37
39
|
}
|
38
40
|
}
|
39
41
|
|
@@ -116,4 +118,10 @@ export default class LocalTrackPublication extends TrackPublication {
|
|
116
118
|
handleTrackEnded = () => {
|
117
119
|
this.emit(TrackEvent.Ended);
|
118
120
|
};
|
121
|
+
|
122
|
+
private handleCpuConstrained = () => {
|
123
|
+
if (this.track && isVideoTrack(this.track)) {
|
124
|
+
this.emit(TrackEvent.CpuConstrained, this.track);
|
125
|
+
}
|
126
|
+
};
|
119
127
|
}
|
@@ -7,6 +7,7 @@ import {
|
|
7
7
|
} from '@livekit/protocol';
|
8
8
|
import type { SignalClient } from '../../api/SignalClient';
|
9
9
|
import type { StructuredLogger } from '../../logger';
|
10
|
+
import { TrackEvent } from '../events';
|
10
11
|
import { ScalabilityMode } from '../participant/publishUtils';
|
11
12
|
import type { VideoSenderStats } from '../stats';
|
12
13
|
import { computeBitrate, monitorFrequency } from '../stats';
|
@@ -56,6 +57,10 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
56
57
|
|
57
58
|
private degradationPreference: RTCDegradationPreference = 'balanced';
|
58
59
|
|
60
|
+
private isCpuConstrained: boolean = false;
|
61
|
+
|
62
|
+
private optimizeForPerformance: boolean = false;
|
63
|
+
|
59
64
|
get sender(): RTCRtpSender | undefined {
|
60
65
|
return this._sender;
|
61
66
|
}
|
@@ -251,6 +256,9 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
251
256
|
}
|
252
257
|
await this.restart(constraints);
|
253
258
|
|
259
|
+
// reset cpu constrained state after track is restarted
|
260
|
+
this.isCpuConstrained = false;
|
261
|
+
|
254
262
|
for await (const sc of this.simulcastCodecs.values()) {
|
255
263
|
if (sc.sender && sc.sender.transport?.state !== 'closed') {
|
256
264
|
sc.mediaStreamTrack = this.mediaStreamTrack.clone();
|
@@ -334,6 +342,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
334
342
|
// only enable simulcast codec for preference codec setted
|
335
343
|
if (!this.codec && codecs.length > 0) {
|
336
344
|
await this.setPublishingLayers(isSVCCodec(codecs[0].codec), codecs[0].qualities);
|
345
|
+
|
337
346
|
return [];
|
338
347
|
}
|
339
348
|
|
@@ -378,6 +387,13 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
378
387
|
* Sets layers that should be publishing
|
379
388
|
*/
|
380
389
|
async setPublishingLayers(isSvc: boolean, qualities: SubscribedQuality[]) {
|
390
|
+
if (this.optimizeForPerformance) {
|
391
|
+
this.log.info('skipping setPublishingLayers due to optimized publishing performance', {
|
392
|
+
...this.logContext,
|
393
|
+
qualities,
|
394
|
+
});
|
395
|
+
return;
|
396
|
+
}
|
381
397
|
this.log.debug('setting publishing layers', { ...this.logContext, qualities });
|
382
398
|
if (!this.sender || !this.encodings) {
|
383
399
|
return;
|
@@ -394,6 +410,49 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
394
410
|
);
|
395
411
|
}
|
396
412
|
|
413
|
+
/**
|
414
|
+
* Designed for lower powered devices, reduces video publishing quality and disables simulcast.
|
415
|
+
* @experimental
|
416
|
+
*/
|
417
|
+
async prioritizePerformance() {
|
418
|
+
if (!this.sender) {
|
419
|
+
throw new Error('sender not found');
|
420
|
+
}
|
421
|
+
|
422
|
+
const unlock = await this.senderLock.lock();
|
423
|
+
|
424
|
+
try {
|
425
|
+
this.optimizeForPerformance = true;
|
426
|
+
const params = this.sender.getParameters();
|
427
|
+
|
428
|
+
params.encodings = params.encodings.map((e, idx) => ({
|
429
|
+
...e,
|
430
|
+
active: idx === 0,
|
431
|
+
scaleResolutionDownBy: Math.max(
|
432
|
+
1,
|
433
|
+
Math.ceil((this.mediaStreamTrack.getSettings().height ?? 360) / 360),
|
434
|
+
),
|
435
|
+
scalabilityMode: idx === 0 && isSVCCodec(this.codec) ? 'L1T3' : undefined,
|
436
|
+
maxFramerate: idx === 0 ? 15 : 0,
|
437
|
+
maxBitrate: idx === 0 ? e.maxBitrate : 0,
|
438
|
+
}));
|
439
|
+
this.log.debug('setting performance optimised encodings', {
|
440
|
+
...this.logContext,
|
441
|
+
encodings: params.encodings,
|
442
|
+
});
|
443
|
+
this.encodings = params.encodings;
|
444
|
+
await this.sender.setParameters(params);
|
445
|
+
} catch (e) {
|
446
|
+
this.log.error('failed to set performance optimised encodings', {
|
447
|
+
...this.logContext,
|
448
|
+
error: e,
|
449
|
+
});
|
450
|
+
this.optimizeForPerformance = false;
|
451
|
+
} finally {
|
452
|
+
unlock();
|
453
|
+
}
|
454
|
+
}
|
455
|
+
|
397
456
|
protected monitorSender = async () => {
|
398
457
|
if (!this.sender) {
|
399
458
|
this._currentBitrate = 0;
|
@@ -404,11 +463,19 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
404
463
|
try {
|
405
464
|
stats = await this.getSenderStats();
|
406
465
|
} catch (e) {
|
407
|
-
this.log.error('could not get
|
466
|
+
this.log.error('could not get video sender stats', { ...this.logContext, error: e });
|
408
467
|
return;
|
409
468
|
}
|
410
469
|
const statsMap = new Map<string, VideoSenderStats>(stats.map((s) => [s.rid, s]));
|
411
470
|
|
471
|
+
const isCpuConstrained = stats.some((s) => s.qualityLimitationReason === 'cpu');
|
472
|
+
if (isCpuConstrained !== this.isCpuConstrained) {
|
473
|
+
this.isCpuConstrained = isCpuConstrained;
|
474
|
+
if (this.isCpuConstrained) {
|
475
|
+
this.emit(TrackEvent.CpuConstrained);
|
476
|
+
}
|
477
|
+
}
|
478
|
+
|
412
479
|
if (this.prevStats) {
|
413
480
|
let totalBitrate = 0;
|
414
481
|
statsMap.forEach((s, key) => {
|