livekit-client 1.6.0 → 1.6.2
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 +245 -109
- 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/room/PCTransport.d.ts +7 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +6 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -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/track/LocalTrack.d.ts +1 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +1 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/ts4.2/src/room/PCTransport.d.ts +7 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +6 -1
- package/dist/ts4.2/src/room/Room.d.ts +1 -1
- package/dist/ts4.2/src/room/events.d.ts +1 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +3 -2
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
- package/dist/ts4.2/src/room/types.d.ts +1 -0
- package/package.json +15 -15
- package/src/room/PCTransport.ts +11 -1
- package/src/room/RTCEngine.ts +76 -23
- package/src/room/Room.ts +42 -12
- package/src/room/events.ts +1 -0
- package/src/room/participant/LocalParticipant.ts +43 -19
- package/src/room/participant/RemoteParticipant.ts +5 -5
- package/src/room/track/LocalTrack.ts +19 -1
- package/src/room/track/Track.ts +21 -5
- package/src/room/types.ts +1 -0
package/src/room/Room.ts
CHANGED
@@ -239,14 +239,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
239
239
|
await fetch(`http${url.substring(2)}`, { method: 'HEAD' });
|
240
240
|
}
|
241
241
|
|
242
|
-
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
|
+
|
243
246
|
if (this.state === ConnectionState.Connected) {
|
244
247
|
// when the state is reconnecting or connected, this function returns immediately
|
245
248
|
log.info(`already connected to room ${this.name}`);
|
249
|
+
unlockDisconnect();
|
246
250
|
return Promise.resolve();
|
247
251
|
}
|
248
252
|
|
249
253
|
if (this.connectFuture) {
|
254
|
+
unlockDisconnect();
|
250
255
|
return this.connectFuture.promise;
|
251
256
|
}
|
252
257
|
|
@@ -256,6 +261,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
256
261
|
if (!this.abortController || this.abortController.signal.aborted) {
|
257
262
|
this.abortController = new AbortController();
|
258
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();
|
259
266
|
|
260
267
|
if (this.state === ConnectionState.Reconnecting) {
|
261
268
|
log.info('Reconnection attempt replaced by new connection attempt');
|
@@ -556,7 +563,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
556
563
|
});
|
557
564
|
|
558
565
|
try {
|
559
|
-
await Promise.all(
|
566
|
+
await Promise.all(
|
567
|
+
elements.map((e) => {
|
568
|
+
e.muted = false;
|
569
|
+
return e.play();
|
570
|
+
}),
|
571
|
+
);
|
560
572
|
this.handleAudioPlaybackStarted();
|
561
573
|
} catch (err) {
|
562
574
|
this.handleAudioPlaybackFailed(err);
|
@@ -1045,6 +1057,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1045
1057
|
if (this.options.expWebAudioMix) {
|
1046
1058
|
this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
|
1047
1059
|
}
|
1060
|
+
|
1061
|
+
const newContextIsRunning = this.audioContext?.state === 'running';
|
1062
|
+
if (newContextIsRunning !== this.canPlaybackAudio) {
|
1063
|
+
this.audioEnabled = newContextIsRunning;
|
1064
|
+
this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
|
1065
|
+
}
|
1048
1066
|
}
|
1049
1067
|
|
1050
1068
|
private createParticipant(id: string, info?: ParticipantInfo): RemoteParticipant {
|
@@ -1263,10 +1281,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1263
1281
|
* No actual connection to a server will be established, all state is
|
1264
1282
|
* @experimental
|
1265
1283
|
*/
|
1266
|
-
simulateParticipants(options: SimulationOptions) {
|
1284
|
+
async simulateParticipants(options: SimulationOptions) {
|
1267
1285
|
const publishOptions = {
|
1268
1286
|
audio: true,
|
1269
1287
|
video: true,
|
1288
|
+
useRealTracks: false,
|
1270
1289
|
...options.publish,
|
1271
1290
|
};
|
1272
1291
|
const participantOptions = {
|
@@ -1278,8 +1297,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1278
1297
|
};
|
1279
1298
|
this.handleDisconnect();
|
1280
1299
|
this.name = 'simulated-room';
|
1281
|
-
|
1282
|
-
this.localParticipant.
|
1300
|
+
|
1301
|
+
this.localParticipant.updateInfo(
|
1302
|
+
ParticipantInfo.fromPartial({
|
1303
|
+
identity: 'simulated-local',
|
1304
|
+
name: 'local-name',
|
1305
|
+
}),
|
1306
|
+
);
|
1283
1307
|
this.setupLocalParticipantEvents();
|
1284
1308
|
this.emit(RoomEvent.SignalConnected);
|
1285
1309
|
this.emit(RoomEvent.Connected);
|
@@ -1294,12 +1318,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1294
1318
|
name: 'video-dummy',
|
1295
1319
|
}),
|
1296
1320
|
new LocalVideoTrack(
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1321
|
+
publishOptions.useRealTracks
|
1322
|
+
? (await navigator.mediaDevices.getUserMedia({ video: true })).getVideoTracks()[0]
|
1323
|
+
: createDummyVideoStreamTrack(
|
1324
|
+
160 * participantOptions.aspectRatios[0] ?? 1,
|
1325
|
+
160,
|
1326
|
+
true,
|
1327
|
+
true,
|
1328
|
+
),
|
1303
1329
|
),
|
1304
1330
|
);
|
1305
1331
|
// @ts-ignore
|
@@ -1314,7 +1340,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1314
1340
|
sid: Math.floor(Math.random() * 10_000).toString(),
|
1315
1341
|
type: TrackType.AUDIO,
|
1316
1342
|
}),
|
1317
|
-
new LocalAudioTrack(
|
1343
|
+
new LocalAudioTrack(
|
1344
|
+
publishOptions.useRealTracks
|
1345
|
+
? (await navigator.mediaDevices.getUserMedia({ audio: true })).getAudioTracks()[0]
|
1346
|
+
: getEmptyAudioStreamTrack(),
|
1347
|
+
),
|
1318
1348
|
);
|
1319
1349
|
// @ts-ignore
|
1320
1350
|
this.localParticipant.addTrackPublication(audioPub);
|
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',
|
@@ -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
|
/**
|
@@ -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 */
|
@@ -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
|
*/
|
package/src/room/track/Track.ts
CHANGED
@@ -121,7 +121,9 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
|
|
121
121
|
// we'll want to re-attach it in that case
|
122
122
|
attachToElement(this._mediaStreamTrack, element);
|
123
123
|
|
124
|
-
|
124
|
+
// handle auto playback failures
|
125
|
+
const allMediaStreamTracks = (element.srcObject as MediaStream).getTracks();
|
126
|
+
if (allMediaStreamTracks.some((tr) => tr.kind === 'audio')) {
|
125
127
|
// manually play audio to detect audio playback status
|
126
128
|
element
|
127
129
|
.play()
|
@@ -130,6 +132,17 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
|
|
130
132
|
})
|
131
133
|
.catch((e) => {
|
132
134
|
this.emit(TrackEvent.AudioPlaybackFailed, e);
|
135
|
+
// If audio playback isn't allowed make sure we still play back the video
|
136
|
+
if (
|
137
|
+
element &&
|
138
|
+
allMediaStreamTracks.some((tr) => tr.kind === 'video') &&
|
139
|
+
e.name === 'NotAllowedError'
|
140
|
+
) {
|
141
|
+
element.muted = true;
|
142
|
+
element.play().catch(() => {
|
143
|
+
// catch for Safari, exceeded options at this point to automatically play the media element
|
144
|
+
});
|
145
|
+
}
|
133
146
|
});
|
134
147
|
}
|
135
148
|
|
@@ -259,6 +272,13 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
|
|
259
272
|
mediaStream.addTrack(track);
|
260
273
|
}
|
261
274
|
|
275
|
+
element.autoplay = true;
|
276
|
+
// In case there are no audio tracks present on the mediastream, we set the element as muted to ensure autoplay works
|
277
|
+
element.muted = mediaStream.getAudioTracks().length === 0;
|
278
|
+
if (element instanceof HTMLVideoElement) {
|
279
|
+
element.playsInline = true;
|
280
|
+
}
|
281
|
+
|
262
282
|
// avoid flicker
|
263
283
|
if (element.srcObject !== mediaStream) {
|
264
284
|
element.srcObject = mediaStream;
|
@@ -280,10 +300,6 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
|
|
280
300
|
}, 0);
|
281
301
|
}
|
282
302
|
}
|
283
|
-
element.autoplay = true;
|
284
|
-
if (element instanceof HTMLVideoElement) {
|
285
|
-
element.playsInline = true;
|
286
|
-
}
|
287
303
|
}
|
288
304
|
|
289
305
|
/** @internal */
|