livekit-client 1.5.0 → 1.6.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.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
|
+
}
|