livekit-client 1.6.1 → 1.6.3
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 +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;
|