livekit-client 1.5.0 → 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.esm.mjs +2257 -5488
- 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/PCTransport.d.ts +7 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +10 -4
- 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 +5 -0
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +3 -2
- package/dist/src/room/participant/LocalParticipant.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 +1 -0
- 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/PCTransport.d.ts +7 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +10 -4
- package/dist/ts4.2/src/room/Room.d.ts +21 -4
- package/dist/ts4.2/src/room/events.d.ts +5 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +3 -2
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
- 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 +23 -23
- 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/PCTransport.ts +11 -1
- package/src/room/RTCEngine.ts +111 -49
- package/src/room/Room.ts +234 -63
- package/src/room/events.ts +5 -0
- package/src/room/participant/LocalParticipant.ts +46 -22
- package/src/room/participant/RemoteParticipant.ts +5 -5
- package/src/room/participant/publishUtils.ts +1 -1
- package/src/room/track/LocalAudioTrack.ts +1 -1
- package/src/room/track/LocalTrack.ts +20 -1
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/RemoteVideoTrack.ts +4 -0
- package/src/room/track/Track.ts +22 -5
- 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
|
|
@@ -223,14 +239,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
223
239
|
await fetch(`http${url.substring(2)}`, { method: 'HEAD' });
|
224
240
|
}
|
225
241
|
|
226
|
-
connect = (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
|
242
|
+
connect = async (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
|
243
|
+
// In case a disconnect called happened right before the connect call, make sure the disconnect is completed first by awaiting its lock
|
244
|
+
const unlockDisconnect = await this.disconnectLock.lock();
|
245
|
+
|
227
246
|
if (this.state === ConnectionState.Connected) {
|
228
247
|
// when the state is reconnecting or connected, this function returns immediately
|
229
248
|
log.info(`already connected to room ${this.name}`);
|
249
|
+
unlockDisconnect();
|
230
250
|
return Promise.resolve();
|
231
251
|
}
|
232
252
|
|
233
253
|
if (this.connectFuture) {
|
254
|
+
unlockDisconnect();
|
234
255
|
return this.connectFuture.promise;
|
235
256
|
}
|
236
257
|
|
@@ -240,6 +261,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
240
261
|
if (!this.abortController || this.abortController.signal.aborted) {
|
241
262
|
this.abortController = new AbortController();
|
242
263
|
}
|
264
|
+
// at this point the intention to connect has been signalled so we can allow cancelling of the connection via disconnect() again
|
265
|
+
unlockDisconnect();
|
243
266
|
|
244
267
|
if (this.state === ConnectionState.Reconnecting) {
|
245
268
|
log.info('Reconnection attempt replaced by new connection attempt');
|
@@ -303,18 +326,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
303
326
|
|
304
327
|
this.localParticipant.updateInfo(pi);
|
305
328
|
// 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
|
-
);
|
329
|
+
this.setupLocalParticipantEvents();
|
318
330
|
|
319
331
|
// populate remote participants, these should not trigger new events
|
320
332
|
joinResponse.otherParticipants.forEach((info) => {
|
@@ -342,7 +354,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
342
354
|
} catch (err) {
|
343
355
|
this.recreateEngine();
|
344
356
|
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
345
|
-
|
357
|
+
let errorMessage = '';
|
358
|
+
if (err instanceof Error) {
|
359
|
+
errorMessage = err.message;
|
360
|
+
log.debug(`error trying to establish signal connection`, { error: err });
|
361
|
+
}
|
362
|
+
reject(new ConnectionError(`could not establish signal connection: ${errorMessage}`));
|
346
363
|
return;
|
347
364
|
}
|
348
365
|
|
@@ -389,26 +406,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
389
406
|
* disconnects the room, emits [[RoomEvent.Disconnected]]
|
390
407
|
*/
|
391
408
|
disconnect = async (stopTracks = true) => {
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
409
|
+
const unlock = await this.disconnectLock.lock();
|
410
|
+
try {
|
411
|
+
if (this.state === ConnectionState.Disconnected) {
|
412
|
+
log.debug('already disconnected');
|
413
|
+
return;
|
414
|
+
}
|
415
|
+
log.info('disconnect from room', { identity: this.localParticipant.identity });
|
416
|
+
if (
|
417
|
+
this.state === ConnectionState.Connecting ||
|
418
|
+
this.state === ConnectionState.Reconnecting
|
419
|
+
) {
|
420
|
+
// try aborting pending connection attempt
|
421
|
+
log.warn('abort connection attempt');
|
422
|
+
this.abortController?.abort();
|
423
|
+
// in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
|
424
|
+
this.connectFuture?.reject?.(new ConnectionError('Client initiated disconnect'));
|
425
|
+
this.connectFuture = undefined;
|
426
|
+
}
|
427
|
+
// send leave
|
428
|
+
if (this.engine?.client.isConnected) {
|
429
|
+
await this.engine.client.sendLeave();
|
430
|
+
}
|
431
|
+
// close engine (also closes client)
|
432
|
+
if (this.engine) {
|
433
|
+
await this.engine.close();
|
434
|
+
}
|
435
|
+
this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
|
436
|
+
/* @ts-ignore */
|
437
|
+
this.engine = undefined;
|
438
|
+
} finally {
|
439
|
+
unlock();
|
408
440
|
}
|
409
|
-
this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
|
410
|
-
/* @ts-ignore */
|
411
|
-
this.engine = undefined;
|
412
441
|
};
|
413
442
|
|
414
443
|
/**
|
@@ -440,12 +469,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
440
469
|
/**
|
441
470
|
* @internal for testing
|
442
471
|
*/
|
443
|
-
simulateScenario(scenario: string) {
|
472
|
+
async simulateScenario(scenario: string) {
|
444
473
|
let postAction = () => {};
|
445
474
|
let req: SimulateScenario | undefined;
|
446
475
|
switch (scenario) {
|
447
476
|
case 'signal-reconnect':
|
448
|
-
this.engine.client.close();
|
477
|
+
await this.engine.client.close();
|
449
478
|
if (this.engine.client.onClose) {
|
450
479
|
this.engine.client.onClose('simulate disconnect');
|
451
480
|
}
|
@@ -508,8 +537,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
508
537
|
}
|
509
538
|
}
|
510
539
|
|
511
|
-
private onBeforeUnload = () => {
|
512
|
-
this.disconnect();
|
540
|
+
private onBeforeUnload = async () => {
|
541
|
+
await this.disconnect();
|
513
542
|
};
|
514
543
|
|
515
544
|
/**
|
@@ -520,7 +549,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
520
549
|
* - `getUserMedia`
|
521
550
|
*/
|
522
551
|
async startAudio() {
|
523
|
-
this.acquireAudioContext();
|
552
|
+
await this.acquireAudioContext();
|
524
553
|
|
525
554
|
const elements: Array<HTMLMediaElement> = [];
|
526
555
|
this.participants.forEach((p) => {
|
@@ -534,7 +563,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
534
563
|
});
|
535
564
|
|
536
565
|
try {
|
537
|
-
await Promise.all(
|
566
|
+
await Promise.all(
|
567
|
+
elements.map((e) => {
|
568
|
+
e.muted = false;
|
569
|
+
return e.play();
|
570
|
+
}),
|
571
|
+
);
|
538
572
|
this.handleAudioPlaybackStarted();
|
539
573
|
} catch (err) {
|
540
574
|
this.handleAudioPlaybackFailed(err);
|
@@ -550,7 +584,18 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
550
584
|
}
|
551
585
|
|
552
586
|
/**
|
553
|
-
*
|
587
|
+
* Returns the active audio output device used in this room.
|
588
|
+
*
|
589
|
+
* Note: to get the active `audioinput` or `videoinput` use [[LocalTrack.getDeviceId()]]
|
590
|
+
*
|
591
|
+
* @return the previously successfully set audio output device ID or an empty string if the default device is used.
|
592
|
+
*/
|
593
|
+
getActiveAudioOutputDevice(): string {
|
594
|
+
return this.options.audioOutput?.deviceId ?? '';
|
595
|
+
}
|
596
|
+
|
597
|
+
/**
|
598
|
+
* Switches all active devices used in this room to the given device.
|
554
599
|
*
|
555
600
|
* 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
601
|
*
|
@@ -592,16 +637,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
592
637
|
this.options.audioOutput ??= {};
|
593
638
|
const prevDeviceId = this.options.audioOutput.deviceId;
|
594
639
|
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
640
|
try {
|
604
|
-
await Promise.all(
|
641
|
+
await Promise.all(
|
642
|
+
Array.from(this.participants.values()).map((p) => p.setAudioOutput({ deviceId })),
|
643
|
+
);
|
605
644
|
} catch (e) {
|
606
645
|
this.options.audioOutput.deviceId = prevDeviceId;
|
607
646
|
throw e;
|
@@ -609,6 +648,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
609
648
|
}
|
610
649
|
}
|
611
650
|
|
651
|
+
private setupLocalParticipantEvents() {
|
652
|
+
this.localParticipant
|
653
|
+
.on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
654
|
+
.on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
655
|
+
.on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
656
|
+
.on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
657
|
+
.on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
658
|
+
.on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
659
|
+
.on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
660
|
+
.on(
|
661
|
+
ParticipantEvent.ParticipantPermissionsChanged,
|
662
|
+
this.onLocalParticipantPermissionsChanged,
|
663
|
+
);
|
664
|
+
}
|
665
|
+
|
612
666
|
private recreateEngine() {
|
613
667
|
this.engine?.close();
|
614
668
|
/* @ts-ignore */
|
@@ -773,7 +827,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
773
827
|
|
774
828
|
this.participants.clear();
|
775
829
|
this.activeSpeakers = [];
|
776
|
-
if (this.audioContext) {
|
830
|
+
if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
|
777
831
|
this.audioContext.close();
|
778
832
|
this.audioContext = undefined;
|
779
833
|
}
|
@@ -986,18 +1040,28 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
986
1040
|
});
|
987
1041
|
};
|
988
1042
|
|
989
|
-
private acquireAudioContext() {
|
990
|
-
if (
|
991
|
-
this.
|
1043
|
+
private async acquireAudioContext() {
|
1044
|
+
if (
|
1045
|
+
typeof this.options.expWebAudioMix !== 'boolean' &&
|
1046
|
+
this.options.expWebAudioMix.audioContext
|
1047
|
+
) {
|
1048
|
+
// override audio context with custom audio context if supplied by user
|
1049
|
+
this.audioContext = this.options.expWebAudioMix.audioContext;
|
1050
|
+
await this.audioContext.resume();
|
1051
|
+
} else {
|
1052
|
+
// by using an AudioContext, it reduces lag on audio elements
|
1053
|
+
// https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
|
1054
|
+
this.audioContext = getNewAudioContext() ?? undefined;
|
992
1055
|
}
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1056
|
+
|
1057
|
+
if (this.options.expWebAudioMix) {
|
1058
|
+
this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
|
1059
|
+
}
|
1060
|
+
|
1061
|
+
const newContextIsRunning = this.audioContext?.state === 'running';
|
1062
|
+
if (newContextIsRunning !== this.canPlaybackAudio) {
|
1063
|
+
this.audioEnabled = newContextIsRunning;
|
1064
|
+
this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
|
1001
1065
|
}
|
1002
1066
|
}
|
1003
1067
|
|
@@ -1212,6 +1276,113 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1212
1276
|
this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
|
1213
1277
|
};
|
1214
1278
|
|
1279
|
+
/**
|
1280
|
+
* Allows to populate a room with simulated participants.
|
1281
|
+
* No actual connection to a server will be established, all state is
|
1282
|
+
* @experimental
|
1283
|
+
*/
|
1284
|
+
simulateParticipants(options: SimulationOptions) {
|
1285
|
+
const publishOptions = {
|
1286
|
+
audio: true,
|
1287
|
+
video: true,
|
1288
|
+
...options.publish,
|
1289
|
+
};
|
1290
|
+
const participantOptions = {
|
1291
|
+
count: 9,
|
1292
|
+
audio: false,
|
1293
|
+
video: true,
|
1294
|
+
aspectRatios: [1.66, 1.7, 1.3],
|
1295
|
+
...options.participants,
|
1296
|
+
};
|
1297
|
+
this.handleDisconnect();
|
1298
|
+
this.name = 'simulated-room';
|
1299
|
+
|
1300
|
+
this.localParticipant.updateInfo(
|
1301
|
+
ParticipantInfo.fromPartial({
|
1302
|
+
identity: 'simulated-local',
|
1303
|
+
name: 'local-name',
|
1304
|
+
}),
|
1305
|
+
);
|
1306
|
+
this.setupLocalParticipantEvents();
|
1307
|
+
this.emit(RoomEvent.SignalConnected);
|
1308
|
+
this.emit(RoomEvent.Connected);
|
1309
|
+
this.setAndEmitConnectionState(ConnectionState.Connected);
|
1310
|
+
if (publishOptions.video) {
|
1311
|
+
const camPub = new LocalTrackPublication(
|
1312
|
+
Track.Kind.Video,
|
1313
|
+
TrackInfo.fromPartial({
|
1314
|
+
source: TrackSource.CAMERA,
|
1315
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1316
|
+
type: TrackType.AUDIO,
|
1317
|
+
name: 'video-dummy',
|
1318
|
+
}),
|
1319
|
+
new LocalVideoTrack(
|
1320
|
+
createDummyVideoStreamTrack(
|
1321
|
+
160 * participantOptions.aspectRatios[0] ?? 1,
|
1322
|
+
160,
|
1323
|
+
true,
|
1324
|
+
true,
|
1325
|
+
),
|
1326
|
+
),
|
1327
|
+
);
|
1328
|
+
// @ts-ignore
|
1329
|
+
this.localParticipant.addTrackPublication(camPub);
|
1330
|
+
this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, camPub);
|
1331
|
+
}
|
1332
|
+
if (publishOptions.audio) {
|
1333
|
+
const audioPub = new LocalTrackPublication(
|
1334
|
+
Track.Kind.Audio,
|
1335
|
+
TrackInfo.fromPartial({
|
1336
|
+
source: TrackSource.MICROPHONE,
|
1337
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1338
|
+
type: TrackType.AUDIO,
|
1339
|
+
}),
|
1340
|
+
new LocalAudioTrack(getEmptyAudioStreamTrack()),
|
1341
|
+
);
|
1342
|
+
// @ts-ignore
|
1343
|
+
this.localParticipant.addTrackPublication(audioPub);
|
1344
|
+
this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, audioPub);
|
1345
|
+
}
|
1346
|
+
|
1347
|
+
for (let i = 0; i < participantOptions.count - 1; i += 1) {
|
1348
|
+
let info: ParticipantInfo = ParticipantInfo.fromPartial({
|
1349
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1350
|
+
identity: `simulated-${i}`,
|
1351
|
+
state: ParticipantInfo_State.ACTIVE,
|
1352
|
+
tracks: [],
|
1353
|
+
joinedAt: Date.now(),
|
1354
|
+
});
|
1355
|
+
const p = this.getOrCreateParticipant(info.identity, info);
|
1356
|
+
if (participantOptions.video) {
|
1357
|
+
const dummyVideo = createDummyVideoStreamTrack(
|
1358
|
+
160 * participantOptions.aspectRatios[i % participantOptions.aspectRatios.length] ?? 1,
|
1359
|
+
160,
|
1360
|
+
false,
|
1361
|
+
true,
|
1362
|
+
);
|
1363
|
+
const videoTrack = TrackInfo.fromPartial({
|
1364
|
+
source: TrackSource.CAMERA,
|
1365
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1366
|
+
type: TrackType.AUDIO,
|
1367
|
+
});
|
1368
|
+
p.addSubscribedMediaTrack(dummyVideo, videoTrack.sid, new MediaStream([dummyVideo]));
|
1369
|
+
info.tracks = [...info.tracks, videoTrack];
|
1370
|
+
}
|
1371
|
+
if (participantOptions.audio) {
|
1372
|
+
const dummyTrack = getEmptyAudioStreamTrack();
|
1373
|
+
const audioTrack = TrackInfo.fromPartial({
|
1374
|
+
source: TrackSource.MICROPHONE,
|
1375
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1376
|
+
type: TrackType.AUDIO,
|
1377
|
+
});
|
1378
|
+
p.addSubscribedMediaTrack(dummyTrack, audioTrack.sid, new MediaStream([dummyTrack]));
|
1379
|
+
info.tracks = [...info.tracks, audioTrack];
|
1380
|
+
}
|
1381
|
+
|
1382
|
+
p.updateInfo(info);
|
1383
|
+
}
|
1384
|
+
}
|
1385
|
+
|
1215
1386
|
// /** @internal */
|
1216
1387
|
emit<E extends keyof RoomEventCallbacks>(
|
1217
1388
|
event: E,
|
package/src/room/events.ts
CHANGED
@@ -418,6 +418,7 @@ export enum EngineEvent {
|
|
418
418
|
Restarting = 'restarting',
|
419
419
|
Restarted = 'restarted',
|
420
420
|
SignalResumed = 'signalResumed',
|
421
|
+
Closing = 'closing',
|
421
422
|
MediaTrackAdded = 'mediaTrackAdded',
|
422
423
|
ActiveSpeakersUpdate = 'activeSpeakersUpdate',
|
423
424
|
DataPacketReceived = 'dataPacketReceived',
|
@@ -427,6 +428,10 @@ export enum TrackEvent {
|
|
427
428
|
Message = 'message',
|
428
429
|
Muted = 'muted',
|
429
430
|
Unmuted = 'unmuted',
|
431
|
+
/**
|
432
|
+
* Only fires on LocalTracks
|
433
|
+
*/
|
434
|
+
Restarted = 'restarted',
|
430
435
|
Ended = 'ended',
|
431
436
|
Subscribed = 'subscribed',
|
432
437
|
Unsubscribed = 'unsubscribed',
|
@@ -262,7 +262,7 @@ export default class LocalParticipant extends Participant {
|
|
262
262
|
} else if (track && track.track) {
|
263
263
|
// screenshare cannot be muted, unpublish instead
|
264
264
|
if (source === Track.Source.ScreenShare) {
|
265
|
-
track = this.unpublishTrack(track.track);
|
265
|
+
track = await this.unpublishTrack(track.track);
|
266
266
|
const screenAudioTrack = this.getTrack(Track.Source.ScreenShareAudio);
|
267
267
|
if (screenAudioTrack && screenAudioTrack.track) {
|
268
268
|
this.unpublishTrack(screenAudioTrack.track);
|
@@ -528,13 +528,19 @@ export default class LocalParticipant extends Participant {
|
|
528
528
|
let encodings: RTCRtpEncodingParameters[] | undefined;
|
529
529
|
let simEncodings: RTCRtpEncodingParameters[] | undefined;
|
530
530
|
if (track.kind === Track.Kind.Video) {
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
531
|
+
let dims: Track.Dimensions = {
|
532
|
+
width: 0,
|
533
|
+
height: 0,
|
534
|
+
};
|
535
|
+
try {
|
536
|
+
dims = await track.waitForDimensions();
|
537
|
+
} catch (e) {
|
538
|
+
// log failure
|
539
|
+
log.error('could not determine track dimensions');
|
540
|
+
}
|
535
541
|
// width and height should be defined for video
|
536
|
-
req.width = width
|
537
|
-
req.height = height
|
542
|
+
req.width = dims.width;
|
543
|
+
req.height = dims.height;
|
538
544
|
// for svc codecs, disable simulcast and use vp8 for backup codec
|
539
545
|
if (track instanceof LocalVideoTrack) {
|
540
546
|
if (opts?.videoCodec === 'av1') {
|
@@ -565,8 +571,8 @@ export default class LocalParticipant extends Participant {
|
|
565
571
|
|
566
572
|
encodings = computeVideoEncodings(
|
567
573
|
track.source === Track.Source.ScreenShare,
|
568
|
-
width,
|
569
|
-
height,
|
574
|
+
dims.width,
|
575
|
+
dims.height,
|
570
576
|
opts,
|
571
577
|
);
|
572
578
|
req.layers = videoLayersFromEncodings(req.width, req.height, simEncodings ?? encodings);
|
@@ -694,10 +700,10 @@ export default class LocalParticipant extends Participant {
|
|
694
700
|
log.debug(`published ${videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
|
695
701
|
}
|
696
702
|
|
697
|
-
unpublishTrack(
|
703
|
+
async unpublishTrack(
|
698
704
|
track: LocalTrack | MediaStreamTrack,
|
699
705
|
stopOnUnpublish?: boolean,
|
700
|
-
): LocalTrackPublication | undefined {
|
706
|
+
): Promise<LocalTrackPublication | undefined> {
|
701
707
|
// look through all published tracks to find the right ones
|
702
708
|
const publication = this.getPublicationForTrack(track);
|
703
709
|
|
@@ -744,7 +750,7 @@ export default class LocalParticipant extends Participant {
|
|
744
750
|
} catch (e) {
|
745
751
|
log.warn('failed to unpublish track', { error: e, method: 'unpublishTrack' });
|
746
752
|
} finally {
|
747
|
-
this.engine.negotiate();
|
753
|
+
await this.engine.negotiate();
|
748
754
|
}
|
749
755
|
}
|
750
756
|
|
@@ -769,15 +775,33 @@ export default class LocalParticipant extends Participant {
|
|
769
775
|
return publication;
|
770
776
|
}
|
771
777
|
|
772
|
-
unpublishTracks(
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
+
async unpublishTracks(
|
779
|
+
tracks: LocalTrack[] | MediaStreamTrack[],
|
780
|
+
): Promise<LocalTrackPublication[]> {
|
781
|
+
const results = await Promise.all(tracks.map((track) => this.unpublishTrack(track)));
|
782
|
+
return results.filter(
|
783
|
+
(track) => track instanceof LocalTrackPublication,
|
784
|
+
) as LocalTrackPublication[];
|
785
|
+
}
|
786
|
+
|
787
|
+
async republishAllTracks(options?: TrackPublishOptions) {
|
788
|
+
const localPubs: LocalTrackPublication[] = [];
|
789
|
+
this.tracks.forEach((pub) => {
|
790
|
+
if (pub.track) {
|
791
|
+
if (options) {
|
792
|
+
pub.options = { ...pub.options, ...options };
|
793
|
+
}
|
794
|
+
localPubs.push(pub);
|
778
795
|
}
|
779
796
|
});
|
780
|
-
|
797
|
+
|
798
|
+
await Promise.all(
|
799
|
+
localPubs.map(async (pub) => {
|
800
|
+
const track = pub.track!;
|
801
|
+
await this.unpublishTrack(track, false);
|
802
|
+
await this.publishTrack(track, pub.options);
|
803
|
+
}),
|
804
|
+
);
|
781
805
|
}
|
782
806
|
|
783
807
|
/**
|
@@ -964,7 +988,7 @@ export default class LocalParticipant extends Participant {
|
|
964
988
|
});
|
965
989
|
this.unpublishTrack(track);
|
966
990
|
} else if (track.isUserProvided) {
|
967
|
-
await track.
|
991
|
+
await track.mute();
|
968
992
|
} else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
|
969
993
|
try {
|
970
994
|
if (isWeb()) {
|
@@ -993,8 +1017,8 @@ export default class LocalParticipant extends Participant {
|
|
993
1017
|
log.debug('track ended, attempting to use a different device');
|
994
1018
|
await track.restartTrack();
|
995
1019
|
} catch (e) {
|
996
|
-
log.warn(`could not restart track,
|
997
|
-
await track.
|
1020
|
+
log.warn(`could not restart track, muting instead`);
|
1021
|
+
await track.mute();
|
998
1022
|
}
|
999
1023
|
}
|
1000
1024
|
};
|
@@ -263,11 +263,6 @@ export default class RemoteParticipant extends Participant {
|
|
263
263
|
validTracks.set(ti.sid, publication);
|
264
264
|
});
|
265
265
|
|
266
|
-
// always emit events for new publications, Room will not forward them unless it's ready
|
267
|
-
newTracks.forEach((publication) => {
|
268
|
-
this.emit(ParticipantEvent.TrackPublished, publication);
|
269
|
-
});
|
270
|
-
|
271
266
|
// detect removed tracks
|
272
267
|
this.tracks.forEach((publication) => {
|
273
268
|
if (!validTracks.has(publication.trackSid)) {
|
@@ -278,6 +273,11 @@ export default class RemoteParticipant extends Participant {
|
|
278
273
|
this.unpublishTrack(publication.trackSid, true);
|
279
274
|
}
|
280
275
|
});
|
276
|
+
|
277
|
+
// always emit events for new publications, Room will not forward them unless it's ready
|
278
|
+
newTracks.forEach((publication) => {
|
279
|
+
this.emit(ParticipantEvent.TrackPublished, publication);
|
280
|
+
});
|
281
281
|
}
|
282
282
|
|
283
283
|
/** @internal */
|
@@ -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,
|
@@ -3,10 +3,12 @@ import log from '../../logger';
|
|
3
3
|
import DeviceManager from '../DeviceManager';
|
4
4
|
import { TrackInvalidError } from '../errors';
|
5
5
|
import { TrackEvent } from '../events';
|
6
|
-
import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isMobile } from '../utils';
|
6
|
+
import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isMobile, sleep } from '../utils';
|
7
7
|
import type { VideoCodec } from './options';
|
8
8
|
import { attachToElement, detachTrack, Track } from './Track';
|
9
9
|
|
10
|
+
const defaultDimensionsTimeout = 2 * 1000;
|
11
|
+
|
10
12
|
export default abstract class LocalTrack extends Track {
|
11
13
|
/** @internal */
|
12
14
|
sender?: RTCRtpSender;
|
@@ -72,6 +74,22 @@ export default abstract class LocalTrack extends Track {
|
|
72
74
|
return this.providedByUser;
|
73
75
|
}
|
74
76
|
|
77
|
+
async waitForDimensions(timeout = defaultDimensionsTimeout): Promise<Track.Dimensions> {
|
78
|
+
if (this.kind === Track.Kind.Audio) {
|
79
|
+
throw new Error('cannot get dimensions for audio tracks');
|
80
|
+
}
|
81
|
+
|
82
|
+
const started = Date.now();
|
83
|
+
while (Date.now() - started < timeout) {
|
84
|
+
const dims = this.dimensions;
|
85
|
+
if (dims) {
|
86
|
+
return dims;
|
87
|
+
}
|
88
|
+
await sleep(50);
|
89
|
+
}
|
90
|
+
throw new TrackInvalidError('unable to get track dimensions after timeout');
|
91
|
+
}
|
92
|
+
|
75
93
|
/**
|
76
94
|
* @returns DeviceID of the device that is currently being used for this track
|
77
95
|
*/
|
@@ -183,6 +201,7 @@ export default abstract class LocalTrack extends Track {
|
|
183
201
|
|
184
202
|
this.mediaStream = mediaStream;
|
185
203
|
this.constraints = constraints;
|
204
|
+
this.emit(TrackEvent.Restarted, this);
|
186
205
|
return this;
|
187
206
|
}
|
188
207
|
|