livekit-client 1.6.1 → 1.6.3
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.esm.mjs +321 -105
- 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 -3
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +43 -1
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +473 -4
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +2 -0
- 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/timers.d.ts +13 -0
- package/dist/src/room/timers.d.ts.map +1 -0
- package/dist/src/room/track/RemoteVideoTrack.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/api/SignalClient.d.ts +3 -3
- package/dist/ts4.2/src/index.d.ts +2 -1
- package/dist/ts4.2/src/proto/livekit_models.d.ts +45 -1
- package/dist/ts4.2/src/proto/livekit_rtc.d.ts +514 -3
- package/dist/ts4.2/src/room/PCTransport.d.ts +1 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -0
- package/dist/ts4.2/src/room/Room.d.ts +1 -1
- package/dist/ts4.2/src/room/timers.d.ts +13 -0
- package/dist/ts4.2/src/room/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/api/SignalClient.ts +28 -20
- package/src/index.ts +2 -0
- package/src/proto/livekit_models.ts +116 -1
- package/src/proto/livekit_rtc.ts +106 -2
- package/src/room/PCTransport.ts +22 -6
- package/src/room/RTCEngine.ts +56 -43
- package/src/room/Room.ts +19 -11
- package/src/room/timers.ts +16 -0
- package/src/room/track/RemoteVideoTrack.ts +2 -1
- package/src/room/types.ts +1 -0
package/src/room/PCTransport.ts
CHANGED
@@ -30,6 +30,8 @@ export default class PCTransport extends EventEmitter {
|
|
30
30
|
|
31
31
|
remoteStereoMids: string[] = [];
|
32
32
|
|
33
|
+
remoteNackMids: string[] = [];
|
34
|
+
|
33
35
|
onOffer?: (offer: RTCSessionDescriptionInit) => void;
|
34
36
|
|
35
37
|
constructor(config?: RTCConfiguration) {
|
@@ -50,7 +52,9 @@ export default class PCTransport extends EventEmitter {
|
|
50
52
|
|
51
53
|
async setRemoteDescription(sd: RTCSessionDescriptionInit): Promise<void> {
|
52
54
|
if (sd.type === 'offer') {
|
53
|
-
|
55
|
+
let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
|
56
|
+
this.remoteStereoMids = stereoMids;
|
57
|
+
this.remoteNackMids = nackMids;
|
54
58
|
}
|
55
59
|
await this.pc.setRemoteDescription(sd);
|
56
60
|
|
@@ -116,7 +120,7 @@ export default class PCTransport extends EventEmitter {
|
|
116
120
|
const sdpParsed = parse(offer.sdp ?? '');
|
117
121
|
sdpParsed.media.forEach((media) => {
|
118
122
|
if (media.type === 'audio') {
|
119
|
-
ensureAudioNackAndStereo(media, []);
|
123
|
+
ensureAudioNackAndStereo(media, [], []);
|
120
124
|
} else if (media.type === 'video') {
|
121
125
|
// mung sdp for codec bitrate setting that can't apply by sendEncoding
|
122
126
|
this.trackBitrates.some((trackbr): boolean => {
|
@@ -169,7 +173,7 @@ export default class PCTransport extends EventEmitter {
|
|
169
173
|
const sdpParsed = parse(answer.sdp ?? '');
|
170
174
|
sdpParsed.media.forEach((media) => {
|
171
175
|
if (media.type === 'audio') {
|
172
|
-
ensureAudioNackAndStereo(media, this.remoteStereoMids);
|
176
|
+
ensureAudioNackAndStereo(media, this.remoteStereoMids, this.remoteNackMids);
|
173
177
|
}
|
174
178
|
});
|
175
179
|
await this.setMungedLocalDescription(answer, write(sdpParsed));
|
@@ -226,6 +230,7 @@ function ensureAudioNackAndStereo(
|
|
226
230
|
payloads?: string | undefined;
|
227
231
|
} & MediaDescription,
|
228
232
|
stereoMids: string[],
|
233
|
+
nackMids: string[],
|
229
234
|
) {
|
230
235
|
// found opus codec to add nack fb
|
231
236
|
let opusPayload = 0;
|
@@ -243,7 +248,10 @@ function ensureAudioNackAndStereo(
|
|
243
248
|
media.rtcpFb = [];
|
244
249
|
}
|
245
250
|
|
246
|
-
if (
|
251
|
+
if (
|
252
|
+
nackMids.includes(media.mid!) &&
|
253
|
+
!media.rtcpFb.some((fb) => fb.payload === opusPayload && fb.type === 'nack')
|
254
|
+
) {
|
247
255
|
media.rtcpFb.push({
|
248
256
|
payload: opusPayload,
|
249
257
|
type: 'nack',
|
@@ -264,8 +272,12 @@ function ensureAudioNackAndStereo(
|
|
264
272
|
}
|
265
273
|
}
|
266
274
|
|
267
|
-
function
|
275
|
+
function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
276
|
+
stereoMids: string[];
|
277
|
+
nackMids: string[];
|
278
|
+
} {
|
268
279
|
const stereoMids: string[] = [];
|
280
|
+
const nackMids: string[] = [];
|
269
281
|
const sdpParsed = parse(offer.sdp ?? '');
|
270
282
|
let opusPayload = 0;
|
271
283
|
sdpParsed.media.forEach((media) => {
|
@@ -278,6 +290,10 @@ function extractStereoTracksFromOffer(offer: RTCSessionDescriptionInit): string[
|
|
278
290
|
return false;
|
279
291
|
});
|
280
292
|
|
293
|
+
if (media.rtcpFb?.some((fb) => fb.payload === opusPayload && fb.type === 'nack')) {
|
294
|
+
nackMids.push(media.mid!);
|
295
|
+
}
|
296
|
+
|
281
297
|
media.fmtp.some((fmtp): boolean => {
|
282
298
|
if (fmtp.payload === opusPayload) {
|
283
299
|
if (fmtp.config.includes('sprop-stereo=1')) {
|
@@ -289,5 +305,5 @@ function extractStereoTracksFromOffer(offer: RTCSessionDescriptionInit): string[
|
|
289
305
|
});
|
290
306
|
}
|
291
307
|
});
|
292
|
-
return stereoMids;
|
308
|
+
return { stereoMids, nackMids };
|
293
309
|
}
|
package/src/room/RTCEngine.ts
CHANGED
@@ -17,6 +17,7 @@ import {
|
|
17
17
|
AddTrackRequest,
|
18
18
|
JoinResponse,
|
19
19
|
LeaveRequest,
|
20
|
+
ReconnectResponse,
|
20
21
|
SignalTarget,
|
21
22
|
TrackPublishedResponse,
|
22
23
|
} from '../proto/livekit_rtc';
|
@@ -31,6 +32,7 @@ import {
|
|
31
32
|
import { EngineEvent } from './events';
|
32
33
|
import PCTransport, { PCEvents } from './PCTransport';
|
33
34
|
import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
|
35
|
+
import CriticalTimers from './timers';
|
34
36
|
import type LocalTrack from './track/LocalTrack';
|
35
37
|
import type LocalVideoTrack from './track/LocalVideoTrack';
|
36
38
|
import type { SimulcastTrackInfo } from './track/LocalVideoTrack';
|
@@ -279,35 +281,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
279
281
|
|
280
282
|
this.participantSid = joinResponse.participant?.sid;
|
281
283
|
|
282
|
-
const rtcConfig =
|
283
|
-
|
284
|
-
// update ICE servers before creating PeerConnection
|
285
|
-
if (joinResponse.iceServers && !rtcConfig.iceServers) {
|
286
|
-
const rtcIceServers: RTCIceServer[] = [];
|
287
|
-
joinResponse.iceServers.forEach((iceServer) => {
|
288
|
-
const rtcIceServer: RTCIceServer = {
|
289
|
-
urls: iceServer.urls,
|
290
|
-
};
|
291
|
-
if (iceServer.username) rtcIceServer.username = iceServer.username;
|
292
|
-
if (iceServer.credential) {
|
293
|
-
rtcIceServer.credential = iceServer.credential;
|
294
|
-
}
|
295
|
-
rtcIceServers.push(rtcIceServer);
|
296
|
-
});
|
297
|
-
rtcConfig.iceServers = rtcIceServers;
|
298
|
-
}
|
299
|
-
|
300
|
-
if (
|
301
|
-
joinResponse.clientConfiguration &&
|
302
|
-
joinResponse.clientConfiguration.forceRelay === ClientConfigSetting.ENABLED
|
303
|
-
) {
|
304
|
-
rtcConfig.iceTransportPolicy = 'relay';
|
305
|
-
}
|
306
|
-
|
307
|
-
// @ts-ignore
|
308
|
-
rtcConfig.sdpSemantics = 'unified-plan';
|
309
|
-
// @ts-ignore
|
310
|
-
rtcConfig.continualGatheringPolicy = 'gather_continually';
|
284
|
+
const rtcConfig = this.makeRTCConfiguration(joinResponse);
|
311
285
|
|
312
286
|
this.publisher = new PCTransport(rtcConfig);
|
313
287
|
this.subscriber = new PCTransport(rtcConfig);
|
@@ -448,6 +422,40 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
448
422
|
};
|
449
423
|
}
|
450
424
|
|
425
|
+
private makeRTCConfiguration(serverResponse: JoinResponse | ReconnectResponse): RTCConfiguration {
|
426
|
+
const rtcConfig = { ...this.rtcConfig };
|
427
|
+
|
428
|
+
// update ICE servers before creating PeerConnection
|
429
|
+
if (serverResponse.iceServers && !rtcConfig.iceServers) {
|
430
|
+
const rtcIceServers: RTCIceServer[] = [];
|
431
|
+
serverResponse.iceServers.forEach((iceServer) => {
|
432
|
+
const rtcIceServer: RTCIceServer = {
|
433
|
+
urls: iceServer.urls,
|
434
|
+
};
|
435
|
+
if (iceServer.username) rtcIceServer.username = iceServer.username;
|
436
|
+
if (iceServer.credential) {
|
437
|
+
rtcIceServer.credential = iceServer.credential;
|
438
|
+
}
|
439
|
+
rtcIceServers.push(rtcIceServer);
|
440
|
+
});
|
441
|
+
rtcConfig.iceServers = rtcIceServers;
|
442
|
+
}
|
443
|
+
|
444
|
+
if (
|
445
|
+
serverResponse.clientConfiguration &&
|
446
|
+
serverResponse.clientConfiguration.forceRelay === ClientConfigSetting.ENABLED
|
447
|
+
) {
|
448
|
+
rtcConfig.iceTransportPolicy = 'relay';
|
449
|
+
}
|
450
|
+
|
451
|
+
// @ts-ignore
|
452
|
+
rtcConfig.sdpSemantics = 'unified-plan';
|
453
|
+
// @ts-ignore
|
454
|
+
rtcConfig.continualGatheringPolicy = 'gather_continually';
|
455
|
+
|
456
|
+
return rtcConfig;
|
457
|
+
}
|
458
|
+
|
451
459
|
private createDataChannels() {
|
452
460
|
if (!this.publisher) {
|
453
461
|
return;
|
@@ -703,10 +711,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
703
711
|
|
704
712
|
log.debug(`reconnecting in ${delay}ms`);
|
705
713
|
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
714
|
+
this.clearReconnectTimeout();
|
715
|
+
this.reconnectTimeout = CriticalTimers.setTimeout(
|
716
|
+
() => this.attemptReconnect(signalEvents),
|
717
|
+
delay,
|
718
|
+
);
|
710
719
|
};
|
711
720
|
|
712
721
|
private async attemptReconnect(signalEvents: boolean = false) {
|
@@ -733,11 +742,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
733
742
|
} else {
|
734
743
|
await this.resumeConnection(signalEvents);
|
735
744
|
}
|
736
|
-
this.
|
745
|
+
this.clearPendingReconnect();
|
737
746
|
this.fullReconnectOnNext = false;
|
738
|
-
if (this.reconnectTimeout) {
|
739
|
-
clearTimeout(this.reconnectTimeout);
|
740
|
-
}
|
741
747
|
} catch (e) {
|
742
748
|
this.reconnectAttempts += 1;
|
743
749
|
let reconnectRequired = false;
|
@@ -841,7 +847,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
841
847
|
}
|
842
848
|
|
843
849
|
try {
|
844
|
-
await this.client.reconnect(this.url, this.token, this.participantSid);
|
850
|
+
const res = await this.client.reconnect(this.url, this.token, this.participantSid);
|
851
|
+
if (res) {
|
852
|
+
const rtcConfig = this.makeRTCConfiguration(res);
|
853
|
+
this.publisher.pc.setConfiguration(rtcConfig);
|
854
|
+
this.subscriber.pc.setConfiguration(rtcConfig);
|
855
|
+
}
|
845
856
|
} catch (e) {
|
846
857
|
let message = '';
|
847
858
|
if (e instanceof Error) {
|
@@ -1034,19 +1045,21 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1034
1045
|
}
|
1035
1046
|
}
|
1036
1047
|
|
1037
|
-
private
|
1048
|
+
private clearReconnectTimeout() {
|
1038
1049
|
if (this.reconnectTimeout) {
|
1039
|
-
clearTimeout(this.reconnectTimeout);
|
1050
|
+
CriticalTimers.clearTimeout(this.reconnectTimeout);
|
1040
1051
|
}
|
1052
|
+
}
|
1053
|
+
|
1054
|
+
private clearPendingReconnect() {
|
1055
|
+
this.clearReconnectTimeout();
|
1041
1056
|
this.reconnectAttempts = 0;
|
1042
1057
|
}
|
1043
1058
|
|
1044
1059
|
private handleBrowserOnLine = () => {
|
1045
1060
|
// in case the engine is currently reconnecting, attempt a reconnect immediately after the browser state has changed to 'onLine'
|
1046
1061
|
if (this.client.isReconnecting) {
|
1047
|
-
|
1048
|
-
clearTimeout(this.reconnectTimeout);
|
1049
|
-
}
|
1062
|
+
this.clearReconnectTimeout();
|
1050
1063
|
this.attemptReconnect(true);
|
1051
1064
|
}
|
1052
1065
|
};
|
package/src/room/Room.ts
CHANGED
@@ -44,6 +44,7 @@ import type Participant from './participant/Participant';
|
|
44
44
|
import type { ConnectionQuality } from './participant/Participant';
|
45
45
|
import RemoteParticipant from './participant/RemoteParticipant';
|
46
46
|
import RTCEngine from './RTCEngine';
|
47
|
+
import CriticalTimers from './timers';
|
47
48
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
48
49
|
import LocalTrackPublication from './track/LocalTrackPublication';
|
49
50
|
import LocalVideoTrack from './track/LocalVideoTrack';
|
@@ -364,7 +365,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
364
365
|
}
|
365
366
|
|
366
367
|
// don't return until ICE connected
|
367
|
-
const connectTimeout = setTimeout(() => {
|
368
|
+
const connectTimeout = CriticalTimers.setTimeout(() => {
|
368
369
|
// timeout
|
369
370
|
this.recreateEngine();
|
370
371
|
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
@@ -372,7 +373,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
372
373
|
}, this.connOptions.peerConnectionTimeout);
|
373
374
|
const abortHandler = () => {
|
374
375
|
log.warn('closing engine');
|
375
|
-
clearTimeout(connectTimeout);
|
376
|
+
CriticalTimers.clearTimeout(connectTimeout);
|
376
377
|
this.recreateEngine();
|
377
378
|
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
378
379
|
reject(new ConnectionError('room connection has been cancelled'));
|
@@ -383,7 +384,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
383
384
|
this.abortController?.signal.addEventListener('abort', abortHandler);
|
384
385
|
|
385
386
|
this.engine.once(EngineEvent.Connected, () => {
|
386
|
-
clearTimeout(connectTimeout);
|
387
|
+
CriticalTimers.clearTimeout(connectTimeout);
|
387
388
|
this.abortController?.signal.removeEventListener('abort', abortHandler);
|
388
389
|
// also hook unload event
|
389
390
|
if (isWeb()) {
|
@@ -1281,10 +1282,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1281
1282
|
* No actual connection to a server will be established, all state is
|
1282
1283
|
* @experimental
|
1283
1284
|
*/
|
1284
|
-
simulateParticipants(options: SimulationOptions) {
|
1285
|
+
async simulateParticipants(options: SimulationOptions) {
|
1285
1286
|
const publishOptions = {
|
1286
1287
|
audio: true,
|
1287
1288
|
video: true,
|
1289
|
+
useRealTracks: false,
|
1288
1290
|
...options.publish,
|
1289
1291
|
};
|
1290
1292
|
const participantOptions = {
|
@@ -1317,12 +1319,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1317
1319
|
name: 'video-dummy',
|
1318
1320
|
}),
|
1319
1321
|
new LocalVideoTrack(
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1322
|
+
publishOptions.useRealTracks
|
1323
|
+
? (await navigator.mediaDevices.getUserMedia({ video: true })).getVideoTracks()[0]
|
1324
|
+
: createDummyVideoStreamTrack(
|
1325
|
+
160 * participantOptions.aspectRatios[0] ?? 1,
|
1326
|
+
160,
|
1327
|
+
true,
|
1328
|
+
true,
|
1329
|
+
),
|
1326
1330
|
),
|
1327
1331
|
);
|
1328
1332
|
// @ts-ignore
|
@@ -1337,7 +1341,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1337
1341
|
sid: Math.floor(Math.random() * 10_000).toString(),
|
1338
1342
|
type: TrackType.AUDIO,
|
1339
1343
|
}),
|
1340
|
-
new LocalAudioTrack(
|
1344
|
+
new LocalAudioTrack(
|
1345
|
+
publishOptions.useRealTracks
|
1346
|
+
? (await navigator.mediaDevices.getUserMedia({ audio: true })).getAudioTracks()[0]
|
1347
|
+
: getEmptyAudioStreamTrack(),
|
1348
|
+
),
|
1341
1349
|
);
|
1342
1350
|
// @ts-ignore
|
1343
1351
|
this.localParticipant.addTrackPublication(audioPub);
|
@@ -0,0 +1,16 @@
|
|
1
|
+
/**
|
2
|
+
* Timers that can be overridden with platform specific implementations
|
3
|
+
* that ensure that they are fired. These should be used when it is critical
|
4
|
+
* that the timer fires on time.
|
5
|
+
*/
|
6
|
+
export default class CriticalTimers {
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
8
|
+
static setTimeout = (...args: Parameters<typeof setTimeout>) => setTimeout(...args);
|
9
|
+
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
11
|
+
static setInterval = (...args: Parameters<typeof setInterval>) => setInterval(...args);
|
12
|
+
|
13
|
+
static clearTimeout = (...args: Parameters<typeof clearTimeout>) => clearTimeout(...args);
|
14
|
+
|
15
|
+
static clearInterval = (...args: Parameters<typeof clearInterval>) => clearInterval(...args);
|
16
|
+
}
|
@@ -2,6 +2,7 @@ import { debounce } from 'ts-debounce';
|
|
2
2
|
import log from '../../logger';
|
3
3
|
import { TrackEvent } from '../events';
|
4
4
|
import { computeBitrate, VideoReceiverStats } from '../stats';
|
5
|
+
import CriticalTimers from '../timers';
|
5
6
|
import { getIntersectionObserver, getResizeObserver, ObservableMediaElement } from '../utils';
|
6
7
|
import RemoteTrack from './RemoteTrack';
|
7
8
|
import { attachToElement, detachTrack, Track } from './Track';
|
@@ -233,7 +234,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
|
|
233
234
|
|
234
235
|
if (!isVisible && Date.now() - lastVisibilityChange < REACTION_DELAY) {
|
235
236
|
// delay hidden events
|
236
|
-
setTimeout(() => {
|
237
|
+
CriticalTimers.setTimeout(() => {
|
237
238
|
this.updateVisibility();
|
238
239
|
}, REACTION_DELAY);
|
239
240
|
return;
|