livekit-client 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.esm.mjs +2031 -5393
- 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/api/SignalClient.d.ts +3 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/ConnectionCheck.d.ts +1 -1
- package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/Checker.d.ts +4 -4
- package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -3
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +4 -1
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts +4 -4
- package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +4 -4
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +4 -4
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +4 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +21 -4
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +4 -0
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +2 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +1 -1
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +3 -3
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/types.d.ts +3 -3
- package/dist/src/room/track/types.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +13 -0
- package/dist/src/room/types.d.ts.map +1 -0
- package/dist/src/room/utils.d.ts +44 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +3 -2
- package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +1 -1
- package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +4 -4
- package/dist/ts4.2/src/index.d.ts +3 -2
- package/dist/ts4.2/src/logger.d.ts +3 -3
- package/dist/ts4.2/src/options.d.ts +4 -1
- package/dist/ts4.2/src/proto/google/protobuf/timestamp.d.ts +4 -4
- package/dist/ts4.2/src/proto/livekit_models.d.ts +4 -4
- package/dist/ts4.2/src/proto/livekit_rtc.d.ts +4 -4
- package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -3
- package/dist/ts4.2/src/room/Room.d.ts +21 -4
- package/dist/ts4.2/src/room/events.d.ts +4 -0
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/src/room/track/Track.d.ts +2 -1
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +1 -1
- package/dist/ts4.2/src/room/track/options.d.ts +3 -3
- package/dist/ts4.2/src/room/track/types.d.ts +3 -3
- package/dist/ts4.2/src/room/types.d.ts +13 -0
- package/dist/ts4.2/src/room/utils.d.ts +44 -0
- package/package.json +21 -21
- package/src/api/SignalClient.ts +40 -16
- package/src/connectionHelper/checks/turn.ts +1 -1
- package/src/connectionHelper/checks/websocket.ts +1 -1
- package/src/index.ts +5 -0
- package/src/options.ts +5 -1
- package/src/room/RTCEngine.ts +35 -26
- package/src/room/Room.ts +209 -61
- package/src/room/events.ts +4 -0
- package/src/room/participant/LocalParticipant.ts +3 -3
- package/src/room/participant/publishUtils.ts +1 -1
- package/src/room/track/LocalAudioTrack.ts +1 -1
- package/src/room/track/LocalTrack.ts +1 -0
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/RemoteVideoTrack.ts +4 -0
- package/src/room/track/Track.ts +1 -0
- package/src/room/types.ts +12 -0
- package/src/room/utils.ts +150 -12
package/src/room/Room.ts
CHANGED
@@ -17,6 +17,9 @@ import {
|
|
17
17
|
Room as RoomModel,
|
18
18
|
ServerInfo,
|
19
19
|
SpeakerInfo,
|
20
|
+
TrackInfo,
|
21
|
+
TrackSource,
|
22
|
+
TrackType,
|
20
23
|
UserPacket,
|
21
24
|
} from '../proto/livekit_models';
|
22
25
|
import {
|
@@ -42,7 +45,7 @@ import type { ConnectionQuality } from './participant/Participant';
|
|
42
45
|
import RemoteParticipant from './participant/RemoteParticipant';
|
43
46
|
import RTCEngine from './RTCEngine';
|
44
47
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
45
|
-
import
|
48
|
+
import LocalTrackPublication from './track/LocalTrackPublication';
|
46
49
|
import LocalVideoTrack from './track/LocalVideoTrack';
|
47
50
|
import type RemoteTrack from './track/RemoteTrack';
|
48
51
|
import RemoteTrackPublication from './track/RemoteTrackPublication';
|
@@ -50,7 +53,16 @@ import { Track } from './track/Track';
|
|
50
53
|
import type { TrackPublication } from './track/TrackPublication';
|
51
54
|
import type { AdaptiveStreamSettings } from './track/types';
|
52
55
|
import { getNewAudioContext } from './track/utils';
|
53
|
-
import {
|
56
|
+
import type { SimulationOptions } from './types';
|
57
|
+
import {
|
58
|
+
Future,
|
59
|
+
createDummyVideoStreamTrack,
|
60
|
+
getEmptyAudioStreamTrack,
|
61
|
+
isWeb,
|
62
|
+
Mutex,
|
63
|
+
supportsSetSinkId,
|
64
|
+
unpackStreamId,
|
65
|
+
} from './utils';
|
54
66
|
|
55
67
|
export enum ConnectionState {
|
56
68
|
Disconnected = 'disconnected',
|
@@ -118,6 +130,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
118
130
|
/** future holding client initiated connection attempt */
|
119
131
|
private connectFuture?: Future<void>;
|
120
132
|
|
133
|
+
private disconnectLock: Mutex;
|
134
|
+
|
121
135
|
/**
|
122
136
|
* Creates a new Room, the primary construct for a LiveKit session.
|
123
137
|
* @param options
|
@@ -144,6 +158,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
144
158
|
|
145
159
|
this.maybeCreateEngine();
|
146
160
|
|
161
|
+
this.disconnectLock = new Mutex();
|
162
|
+
|
147
163
|
this.localParticipant = new LocalParticipant('', '', this.engine, this.options);
|
148
164
|
}
|
149
165
|
|
@@ -303,18 +319,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
303
319
|
|
304
320
|
this.localParticipant.updateInfo(pi);
|
305
321
|
// forward metadata changed for the local participant
|
306
|
-
this.
|
307
|
-
.on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
308
|
-
.on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
309
|
-
.on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
310
|
-
.on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
311
|
-
.on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
312
|
-
.on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
313
|
-
.on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
314
|
-
.on(
|
315
|
-
ParticipantEvent.ParticipantPermissionsChanged,
|
316
|
-
this.onLocalParticipantPermissionsChanged,
|
317
|
-
);
|
322
|
+
this.setupLocalParticipantEvents();
|
318
323
|
|
319
324
|
// populate remote participants, these should not trigger new events
|
320
325
|
joinResponse.otherParticipants.forEach((info) => {
|
@@ -342,7 +347,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
342
347
|
} catch (err) {
|
343
348
|
this.recreateEngine();
|
344
349
|
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
345
|
-
|
350
|
+
let errorMessage = '';
|
351
|
+
if (err instanceof Error) {
|
352
|
+
errorMessage = err.message;
|
353
|
+
log.debug(`error trying to establish signal connection`, { error: err });
|
354
|
+
}
|
355
|
+
reject(new ConnectionError(`could not establish signal connection: ${errorMessage}`));
|
346
356
|
return;
|
347
357
|
}
|
348
358
|
|
@@ -389,26 +399,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
389
399
|
* disconnects the room, emits [[RoomEvent.Disconnected]]
|
390
400
|
*/
|
391
401
|
disconnect = async (stopTracks = true) => {
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
402
|
+
const unlock = await this.disconnectLock.lock();
|
403
|
+
try {
|
404
|
+
if (this.state === ConnectionState.Disconnected) {
|
405
|
+
log.debug('already disconnected');
|
406
|
+
return;
|
407
|
+
}
|
408
|
+
log.info('disconnect from room', { identity: this.localParticipant.identity });
|
409
|
+
if (
|
410
|
+
this.state === ConnectionState.Connecting ||
|
411
|
+
this.state === ConnectionState.Reconnecting
|
412
|
+
) {
|
413
|
+
// try aborting pending connection attempt
|
414
|
+
log.warn('abort connection attempt');
|
415
|
+
this.abortController?.abort();
|
416
|
+
// in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
|
417
|
+
this.connectFuture?.reject?.(new ConnectionError('Client initiated disconnect'));
|
418
|
+
this.connectFuture = undefined;
|
419
|
+
}
|
420
|
+
// send leave
|
421
|
+
if (this.engine?.client.isConnected) {
|
422
|
+
await this.engine.client.sendLeave();
|
423
|
+
}
|
424
|
+
// close engine (also closes client)
|
425
|
+
if (this.engine) {
|
426
|
+
await this.engine.close();
|
427
|
+
}
|
428
|
+
this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
|
429
|
+
/* @ts-ignore */
|
430
|
+
this.engine = undefined;
|
431
|
+
} finally {
|
432
|
+
unlock();
|
408
433
|
}
|
409
|
-
this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
|
410
|
-
/* @ts-ignore */
|
411
|
-
this.engine = undefined;
|
412
434
|
};
|
413
435
|
|
414
436
|
/**
|
@@ -440,12 +462,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
440
462
|
/**
|
441
463
|
* @internal for testing
|
442
464
|
*/
|
443
|
-
simulateScenario(scenario: string) {
|
465
|
+
async simulateScenario(scenario: string) {
|
444
466
|
let postAction = () => {};
|
445
467
|
let req: SimulateScenario | undefined;
|
446
468
|
switch (scenario) {
|
447
469
|
case 'signal-reconnect':
|
448
|
-
this.engine.client.close();
|
470
|
+
await this.engine.client.close();
|
449
471
|
if (this.engine.client.onClose) {
|
450
472
|
this.engine.client.onClose('simulate disconnect');
|
451
473
|
}
|
@@ -508,8 +530,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
508
530
|
}
|
509
531
|
}
|
510
532
|
|
511
|
-
private onBeforeUnload = () => {
|
512
|
-
this.disconnect();
|
533
|
+
private onBeforeUnload = async () => {
|
534
|
+
await this.disconnect();
|
513
535
|
};
|
514
536
|
|
515
537
|
/**
|
@@ -520,7 +542,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
520
542
|
* - `getUserMedia`
|
521
543
|
*/
|
522
544
|
async startAudio() {
|
523
|
-
this.acquireAudioContext();
|
545
|
+
await this.acquireAudioContext();
|
524
546
|
|
525
547
|
const elements: Array<HTMLMediaElement> = [];
|
526
548
|
this.participants.forEach((p) => {
|
@@ -550,7 +572,18 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
550
572
|
}
|
551
573
|
|
552
574
|
/**
|
553
|
-
*
|
575
|
+
* Returns the active audio output device used in this room.
|
576
|
+
*
|
577
|
+
* Note: to get the active `audioinput` or `videoinput` use [[LocalTrack.getDeviceId()]]
|
578
|
+
*
|
579
|
+
* @return the previously successfully set audio output device ID or an empty string if the default device is used.
|
580
|
+
*/
|
581
|
+
getActiveAudioOutputDevice(): string {
|
582
|
+
return this.options.audioOutput?.deviceId ?? '';
|
583
|
+
}
|
584
|
+
|
585
|
+
/**
|
586
|
+
* Switches all active devices used in this room to the given device.
|
554
587
|
*
|
555
588
|
* Note: setting AudioOutput is not supported on some browsers. See [setSinkId](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility)
|
556
589
|
*
|
@@ -592,16 +625,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
592
625
|
this.options.audioOutput ??= {};
|
593
626
|
const prevDeviceId = this.options.audioOutput.deviceId;
|
594
627
|
this.options.audioOutput.deviceId = deviceId;
|
595
|
-
const promises: Promise<void>[] = [];
|
596
|
-
this.participants.forEach((p) => {
|
597
|
-
promises.push(
|
598
|
-
p.setAudioOutput({
|
599
|
-
deviceId,
|
600
|
-
}),
|
601
|
-
);
|
602
|
-
});
|
603
628
|
try {
|
604
|
-
await Promise.all(
|
629
|
+
await Promise.all(
|
630
|
+
Array.from(this.participants.values()).map((p) => p.setAudioOutput({ deviceId })),
|
631
|
+
);
|
605
632
|
} catch (e) {
|
606
633
|
this.options.audioOutput.deviceId = prevDeviceId;
|
607
634
|
throw e;
|
@@ -609,6 +636,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
609
636
|
}
|
610
637
|
}
|
611
638
|
|
639
|
+
private setupLocalParticipantEvents() {
|
640
|
+
this.localParticipant
|
641
|
+
.on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
642
|
+
.on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
643
|
+
.on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
644
|
+
.on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
645
|
+
.on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
646
|
+
.on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
647
|
+
.on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
648
|
+
.on(
|
649
|
+
ParticipantEvent.ParticipantPermissionsChanged,
|
650
|
+
this.onLocalParticipantPermissionsChanged,
|
651
|
+
);
|
652
|
+
}
|
653
|
+
|
612
654
|
private recreateEngine() {
|
613
655
|
this.engine?.close();
|
614
656
|
/* @ts-ignore */
|
@@ -773,7 +815,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
773
815
|
|
774
816
|
this.participants.clear();
|
775
817
|
this.activeSpeakers = [];
|
776
|
-
if (this.audioContext) {
|
818
|
+
if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
|
777
819
|
this.audioContext.close();
|
778
820
|
this.audioContext = undefined;
|
779
821
|
}
|
@@ -986,18 +1028,22 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
986
1028
|
});
|
987
1029
|
};
|
988
1030
|
|
989
|
-
private acquireAudioContext() {
|
990
|
-
if (
|
991
|
-
this.
|
1031
|
+
private async acquireAudioContext() {
|
1032
|
+
if (
|
1033
|
+
typeof this.options.expWebAudioMix !== 'boolean' &&
|
1034
|
+
this.options.expWebAudioMix.audioContext
|
1035
|
+
) {
|
1036
|
+
// override audio context with custom audio context if supplied by user
|
1037
|
+
this.audioContext = this.options.expWebAudioMix.audioContext;
|
1038
|
+
await this.audioContext.resume();
|
1039
|
+
} else {
|
1040
|
+
// by using an AudioContext, it reduces lag on audio elements
|
1041
|
+
// https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
|
1042
|
+
this.audioContext = getNewAudioContext() ?? undefined;
|
992
1043
|
}
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
if (ctx) {
|
997
|
-
this.audioContext = ctx;
|
998
|
-
if (this.options.expWebAudioMix) {
|
999
|
-
this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
|
1000
|
-
}
|
1044
|
+
|
1045
|
+
if (this.options.expWebAudioMix) {
|
1046
|
+
this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
|
1001
1047
|
}
|
1002
1048
|
}
|
1003
1049
|
|
@@ -1212,6 +1258,108 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1212
1258
|
this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
|
1213
1259
|
};
|
1214
1260
|
|
1261
|
+
/**
|
1262
|
+
* Allows to populate a room with simulated participants.
|
1263
|
+
* No actual connection to a server will be established, all state is
|
1264
|
+
* @experimental
|
1265
|
+
*/
|
1266
|
+
simulateParticipants(options: SimulationOptions) {
|
1267
|
+
const publishOptions = {
|
1268
|
+
audio: true,
|
1269
|
+
video: true,
|
1270
|
+
...options.publish,
|
1271
|
+
};
|
1272
|
+
const participantOptions = {
|
1273
|
+
count: 9,
|
1274
|
+
audio: false,
|
1275
|
+
video: true,
|
1276
|
+
aspectRatios: [1.66, 1.7, 1.3],
|
1277
|
+
...options.participants,
|
1278
|
+
};
|
1279
|
+
this.handleDisconnect();
|
1280
|
+
this.name = 'simulated-room';
|
1281
|
+
this.localParticipant.identity = 'simulated-local';
|
1282
|
+
this.localParticipant.name = 'simulated-local';
|
1283
|
+
this.setupLocalParticipantEvents();
|
1284
|
+
this.emit(RoomEvent.SignalConnected);
|
1285
|
+
this.emit(RoomEvent.Connected);
|
1286
|
+
this.setAndEmitConnectionState(ConnectionState.Connected);
|
1287
|
+
if (publishOptions.video) {
|
1288
|
+
const camPub = new LocalTrackPublication(
|
1289
|
+
Track.Kind.Video,
|
1290
|
+
TrackInfo.fromPartial({
|
1291
|
+
source: TrackSource.CAMERA,
|
1292
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1293
|
+
type: TrackType.AUDIO,
|
1294
|
+
name: 'video-dummy',
|
1295
|
+
}),
|
1296
|
+
new LocalVideoTrack(
|
1297
|
+
createDummyVideoStreamTrack(
|
1298
|
+
160 * participantOptions.aspectRatios[0] ?? 1,
|
1299
|
+
160,
|
1300
|
+
true,
|
1301
|
+
true,
|
1302
|
+
),
|
1303
|
+
),
|
1304
|
+
);
|
1305
|
+
// @ts-ignore
|
1306
|
+
this.localParticipant.addTrackPublication(camPub);
|
1307
|
+
this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, camPub);
|
1308
|
+
}
|
1309
|
+
if (publishOptions.audio) {
|
1310
|
+
const audioPub = new LocalTrackPublication(
|
1311
|
+
Track.Kind.Audio,
|
1312
|
+
TrackInfo.fromPartial({
|
1313
|
+
source: TrackSource.MICROPHONE,
|
1314
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1315
|
+
type: TrackType.AUDIO,
|
1316
|
+
}),
|
1317
|
+
new LocalAudioTrack(getEmptyAudioStreamTrack()),
|
1318
|
+
);
|
1319
|
+
// @ts-ignore
|
1320
|
+
this.localParticipant.addTrackPublication(audioPub);
|
1321
|
+
this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, audioPub);
|
1322
|
+
}
|
1323
|
+
|
1324
|
+
for (let i = 0; i < participantOptions.count - 1; i += 1) {
|
1325
|
+
let info: ParticipantInfo = ParticipantInfo.fromPartial({
|
1326
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1327
|
+
identity: `simulated-${i}`,
|
1328
|
+
state: ParticipantInfo_State.ACTIVE,
|
1329
|
+
tracks: [],
|
1330
|
+
joinedAt: Date.now(),
|
1331
|
+
});
|
1332
|
+
const p = this.getOrCreateParticipant(info.identity, info);
|
1333
|
+
if (participantOptions.video) {
|
1334
|
+
const dummyVideo = createDummyVideoStreamTrack(
|
1335
|
+
160 * participantOptions.aspectRatios[i % participantOptions.aspectRatios.length] ?? 1,
|
1336
|
+
160,
|
1337
|
+
false,
|
1338
|
+
true,
|
1339
|
+
);
|
1340
|
+
const videoTrack = TrackInfo.fromPartial({
|
1341
|
+
source: TrackSource.CAMERA,
|
1342
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1343
|
+
type: TrackType.AUDIO,
|
1344
|
+
});
|
1345
|
+
p.addSubscribedMediaTrack(dummyVideo, videoTrack.sid, new MediaStream([dummyVideo]));
|
1346
|
+
info.tracks = [...info.tracks, videoTrack];
|
1347
|
+
}
|
1348
|
+
if (participantOptions.audio) {
|
1349
|
+
const dummyTrack = getEmptyAudioStreamTrack();
|
1350
|
+
const audioTrack = TrackInfo.fromPartial({
|
1351
|
+
source: TrackSource.MICROPHONE,
|
1352
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1353
|
+
type: TrackType.AUDIO,
|
1354
|
+
});
|
1355
|
+
p.addSubscribedMediaTrack(dummyTrack, audioTrack.sid, new MediaStream([dummyTrack]));
|
1356
|
+
info.tracks = [...info.tracks, audioTrack];
|
1357
|
+
}
|
1358
|
+
|
1359
|
+
p.updateInfo(info);
|
1360
|
+
}
|
1361
|
+
}
|
1362
|
+
|
1215
1363
|
// /** @internal */
|
1216
1364
|
emit<E extends keyof RoomEventCallbacks>(
|
1217
1365
|
event: E,
|
package/src/room/events.ts
CHANGED
@@ -427,6 +427,10 @@ export enum TrackEvent {
|
|
427
427
|
Message = 'message',
|
428
428
|
Muted = 'muted',
|
429
429
|
Unmuted = 'unmuted',
|
430
|
+
/**
|
431
|
+
* Only fires on LocalTracks
|
432
|
+
*/
|
433
|
+
Restarted = 'restarted',
|
430
434
|
Ended = 'ended',
|
431
435
|
Subscribed = 'subscribed',
|
432
436
|
Unsubscribed = 'unsubscribed',
|
@@ -964,7 +964,7 @@ export default class LocalParticipant extends Participant {
|
|
964
964
|
});
|
965
965
|
this.unpublishTrack(track);
|
966
966
|
} else if (track.isUserProvided) {
|
967
|
-
await track.
|
967
|
+
await track.mute();
|
968
968
|
} else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
|
969
969
|
try {
|
970
970
|
if (isWeb()) {
|
@@ -993,8 +993,8 @@ export default class LocalParticipant extends Participant {
|
|
993
993
|
log.debug('track ended, attempting to use a different device');
|
994
994
|
await track.restartTrack();
|
995
995
|
} catch (e) {
|
996
|
-
log.warn(`could not restart track,
|
997
|
-
await track.
|
996
|
+
log.warn(`could not restart track, muting instead`);
|
997
|
+
await track.mute();
|
998
998
|
}
|
999
999
|
}
|
1000
1000
|
};
|
@@ -305,7 +305,7 @@ function encodingsFromPresets(
|
|
305
305
|
const rid = videoRids[idx];
|
306
306
|
encodings.push({
|
307
307
|
rid,
|
308
|
-
scaleResolutionDownBy: size / Math.min(preset.width, preset.height),
|
308
|
+
scaleResolutionDownBy: Math.max(1, size / Math.min(preset.width, preset.height)),
|
309
309
|
maxBitrate: preset.encoding.maxBitrate,
|
310
310
|
/* @ts-ignore */
|
311
311
|
maxFramerate: preset.encoding.maxFramerate,
|
@@ -118,6 +118,10 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
118
118
|
* @internal
|
119
119
|
*/
|
120
120
|
stopObservingElementInfo(elementInfo: ElementInfo) {
|
121
|
+
if (!this.isAdaptiveStream) {
|
122
|
+
log.warn('stopObservingElementInfo ignored');
|
123
|
+
return;
|
124
|
+
}
|
121
125
|
const stopElementInfos = this.elementInfos.filter((info) => info === elementInfo);
|
122
126
|
for (const info of stopElementInfos) {
|
123
127
|
info.stopObserving();
|
package/src/room/track/Track.ts
CHANGED
@@ -398,6 +398,7 @@ export type TrackEventCallbacks = {
|
|
398
398
|
message: () => void;
|
399
399
|
muted: (track?: any) => void;
|
400
400
|
unmuted: (track?: any) => void;
|
401
|
+
restarted: (track?: any) => void;
|
401
402
|
ended: (track?: any) => void;
|
402
403
|
updateSettings: () => void;
|
403
404
|
updateSubscription: () => void;
|
package/src/room/utils.ts
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
import UAParser from 'ua-parser-js';
|
2
2
|
import { ClientInfo, ClientInfo_SDK } from '../proto/livekit_models';
|
3
3
|
import { protocolVersion, version } from '../version';
|
4
|
+
import type LocalAudioTrack from './track/LocalAudioTrack';
|
5
|
+
import type RemoteAudioTrack from './track/RemoteAudioTrack';
|
6
|
+
import { getNewAudioContext } from './track/utils';
|
4
7
|
|
5
8
|
const separator = '|';
|
6
9
|
|
@@ -178,22 +181,41 @@ let emptyVideoStreamTrack: MediaStreamTrack | undefined;
|
|
178
181
|
|
179
182
|
export function getEmptyVideoStreamTrack() {
|
180
183
|
if (!emptyVideoStreamTrack) {
|
181
|
-
|
182
|
-
// the canvas size is set to 16, because electron apps seem to fail with smaller values
|
183
|
-
canvas.width = 16;
|
184
|
-
canvas.height = 16;
|
185
|
-
canvas.getContext('2d')?.fillRect(0, 0, canvas.width, canvas.height);
|
186
|
-
// @ts-ignore
|
187
|
-
const emptyStream = canvas.captureStream();
|
188
|
-
[emptyVideoStreamTrack] = emptyStream.getTracks();
|
189
|
-
if (!emptyVideoStreamTrack) {
|
190
|
-
throw Error('Could not get empty media stream video track');
|
191
|
-
}
|
192
|
-
emptyVideoStreamTrack.enabled = false;
|
184
|
+
emptyVideoStreamTrack = createDummyVideoStreamTrack();
|
193
185
|
}
|
194
186
|
return emptyVideoStreamTrack;
|
195
187
|
}
|
196
188
|
|
189
|
+
export function createDummyVideoStreamTrack(
|
190
|
+
width: number = 16,
|
191
|
+
height: number = 16,
|
192
|
+
enabled: boolean = false,
|
193
|
+
paintContent: boolean = false,
|
194
|
+
) {
|
195
|
+
const canvas = document.createElement('canvas');
|
196
|
+
// the canvas size is set to 16 by default, because electron apps seem to fail with smaller values
|
197
|
+
canvas.width = width;
|
198
|
+
canvas.height = height;
|
199
|
+
const ctx = canvas.getContext('2d');
|
200
|
+
ctx?.fillRect(0, 0, canvas.width, canvas.height);
|
201
|
+
if (paintContent && ctx) {
|
202
|
+
ctx.beginPath();
|
203
|
+
ctx.arc(width / 2, height / 2, 50, 0, Math.PI * 2, true);
|
204
|
+
ctx.closePath();
|
205
|
+
ctx.fillStyle = 'grey';
|
206
|
+
ctx.fill();
|
207
|
+
}
|
208
|
+
// @ts-ignore
|
209
|
+
const dummyStream = canvas.captureStream();
|
210
|
+
const [dummyTrack] = dummyStream.getTracks();
|
211
|
+
if (!dummyTrack) {
|
212
|
+
throw Error('Could not get empty media stream video track');
|
213
|
+
}
|
214
|
+
dummyTrack.enabled = enabled;
|
215
|
+
|
216
|
+
return dummyTrack;
|
217
|
+
}
|
218
|
+
|
197
219
|
let emptyAudioStreamTrack: MediaStreamTrack | undefined;
|
198
220
|
|
199
221
|
export function getEmptyAudioStreamTrack() {
|
@@ -236,3 +258,119 @@ export class Future<T> {
|
|
236
258
|
}).finally(() => this.onFinally?.());
|
237
259
|
}
|
238
260
|
}
|
261
|
+
|
262
|
+
export type AudioAnalyserOptions = {
|
263
|
+
/**
|
264
|
+
* If set to true, the analyser will use a cloned version of the underlying mediastreamtrack, which won't be impacted by muting the track.
|
265
|
+
* Useful for local tracks when implementing things like "seems like you're muted, but trying to speak".
|
266
|
+
* Defaults to false
|
267
|
+
*/
|
268
|
+
cloneTrack?: boolean;
|
269
|
+
/**
|
270
|
+
* see https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/fftSize
|
271
|
+
*/
|
272
|
+
fftSize?: number;
|
273
|
+
/**
|
274
|
+
* see https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/smoothingTimeConstant
|
275
|
+
*/
|
276
|
+
smoothingTimeConstant?: number;
|
277
|
+
/**
|
278
|
+
* see https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/minDecibels
|
279
|
+
*/
|
280
|
+
minDecibels?: number;
|
281
|
+
/**
|
282
|
+
* see https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/maxDecibels
|
283
|
+
*/
|
284
|
+
maxDecibels?: number;
|
285
|
+
};
|
286
|
+
|
287
|
+
/**
|
288
|
+
* Creates and returns an analyser web audio node that is attached to the provided track.
|
289
|
+
* Additionally returns a convenience method `calculateVolume` to perform instant volume readings on that track.
|
290
|
+
* Call the returned `cleanup` function to close the audioContext that has been created for the instance of this helper
|
291
|
+
*/
|
292
|
+
export function createAudioAnalyser(
|
293
|
+
track: LocalAudioTrack | RemoteAudioTrack,
|
294
|
+
options?: AudioAnalyserOptions,
|
295
|
+
) {
|
296
|
+
const opts = {
|
297
|
+
cloneTrack: false,
|
298
|
+
fftSize: 2048,
|
299
|
+
smoothingTimeConstant: 0.8,
|
300
|
+
minDecibels: -100,
|
301
|
+
maxDecibels: -80,
|
302
|
+
...options,
|
303
|
+
};
|
304
|
+
const audioContext = getNewAudioContext();
|
305
|
+
|
306
|
+
if (!audioContext) {
|
307
|
+
throw new Error('Audio Context not supported on this browser');
|
308
|
+
}
|
309
|
+
const streamTrack = opts.cloneTrack ? track.mediaStreamTrack.clone() : track.mediaStreamTrack;
|
310
|
+
const mediaStreamSource = audioContext.createMediaStreamSource(new MediaStream([streamTrack]));
|
311
|
+
const analyser = audioContext.createAnalyser();
|
312
|
+
analyser.minDecibels = opts.minDecibels;
|
313
|
+
analyser.maxDecibels = opts.maxDecibels;
|
314
|
+
analyser.fftSize = opts.fftSize;
|
315
|
+
analyser.smoothingTimeConstant = opts.smoothingTimeConstant;
|
316
|
+
|
317
|
+
mediaStreamSource.connect(analyser);
|
318
|
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
319
|
+
|
320
|
+
/**
|
321
|
+
* Calculates the current volume of the track in the range from 0 to 1
|
322
|
+
*/
|
323
|
+
const calculateVolume = () => {
|
324
|
+
analyser.getByteFrequencyData(dataArray);
|
325
|
+
let sum = 0;
|
326
|
+
for (const amplitude of dataArray) {
|
327
|
+
sum += Math.pow(amplitude / 255, 2);
|
328
|
+
}
|
329
|
+
const volume = Math.sqrt(sum / dataArray.length);
|
330
|
+
return volume;
|
331
|
+
};
|
332
|
+
|
333
|
+
const cleanup = () => {
|
334
|
+
audioContext.close();
|
335
|
+
if (opts.cloneTrack) {
|
336
|
+
streamTrack.stop();
|
337
|
+
}
|
338
|
+
};
|
339
|
+
|
340
|
+
return { calculateVolume, analyser, cleanup };
|
341
|
+
}
|
342
|
+
|
343
|
+
export class Mutex {
|
344
|
+
private _locking: Promise<void>;
|
345
|
+
|
346
|
+
private _locks: number;
|
347
|
+
|
348
|
+
constructor() {
|
349
|
+
this._locking = Promise.resolve();
|
350
|
+
this._locks = 0;
|
351
|
+
}
|
352
|
+
|
353
|
+
isLocked() {
|
354
|
+
return this._locks > 0;
|
355
|
+
}
|
356
|
+
|
357
|
+
lock() {
|
358
|
+
this._locks += 1;
|
359
|
+
|
360
|
+
let unlockNext: () => void;
|
361
|
+
|
362
|
+
const willLock = new Promise<void>(
|
363
|
+
(resolve) =>
|
364
|
+
(unlockNext = () => {
|
365
|
+
this._locks -= 1;
|
366
|
+
resolve();
|
367
|
+
}),
|
368
|
+
);
|
369
|
+
|
370
|
+
const willUnlock = this._locking.then(() => unlockNext);
|
371
|
+
|
372
|
+
this._locking = this._locking.then(() => willLock);
|
373
|
+
|
374
|
+
return willUnlock;
|
375
|
+
}
|
376
|
+
}
|