livekit-client 1.6.0 → 1.6.2
Sign up to get free protection for your applications and to get access to all the features.
- 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 */
|