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.
Files changed (43) hide show
  1. package/dist/livekit-client.esm.mjs +321 -105
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/api/SignalClient.d.ts +3 -3
  6. package/dist/src/api/SignalClient.d.ts.map +1 -1
  7. package/dist/src/index.d.ts +2 -1
  8. package/dist/src/index.d.ts.map +1 -1
  9. package/dist/src/proto/livekit_models.d.ts +43 -1
  10. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  11. package/dist/src/proto/livekit_rtc.d.ts +473 -4
  12. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  13. package/dist/src/room/PCTransport.d.ts +1 -0
  14. package/dist/src/room/PCTransport.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +2 -0
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts +1 -1
  18. package/dist/src/room/Room.d.ts.map +1 -1
  19. package/dist/src/room/timers.d.ts +13 -0
  20. package/dist/src/room/timers.d.ts.map +1 -0
  21. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  22. package/dist/src/room/types.d.ts +1 -0
  23. package/dist/src/room/types.d.ts.map +1 -1
  24. package/dist/ts4.2/src/api/SignalClient.d.ts +3 -3
  25. package/dist/ts4.2/src/index.d.ts +2 -1
  26. package/dist/ts4.2/src/proto/livekit_models.d.ts +45 -1
  27. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +514 -3
  28. package/dist/ts4.2/src/room/PCTransport.d.ts +1 -0
  29. package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -0
  30. package/dist/ts4.2/src/room/Room.d.ts +1 -1
  31. package/dist/ts4.2/src/room/timers.d.ts +13 -0
  32. package/dist/ts4.2/src/room/types.d.ts +1 -0
  33. package/package.json +1 -1
  34. package/src/api/SignalClient.ts +28 -20
  35. package/src/index.ts +2 -0
  36. package/src/proto/livekit_models.ts +116 -1
  37. package/src/proto/livekit_rtc.ts +106 -2
  38. package/src/room/PCTransport.ts +22 -6
  39. package/src/room/RTCEngine.ts +56 -43
  40. package/src/room/Room.ts +19 -11
  41. package/src/room/timers.ts +16 -0
  42. package/src/room/track/RemoteVideoTrack.ts +2 -1
  43. package/src/room/types.ts +1 -0
@@ -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
- this.remoteStereoMids = extractStereoTracksFromOffer(sd);
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 (!media.rtcpFb.some((fb) => fb.payload === opusPayload && fb.type === 'nack')) {
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 extractStereoTracksFromOffer(offer: RTCSessionDescriptionInit): string[] {
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
  }
@@ -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 = { ...this.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
- if (this.reconnectTimeout) {
707
- clearTimeout(this.reconnectTimeout);
708
- }
709
- this.reconnectTimeout = setTimeout(() => this.attemptReconnect(signalEvents), delay);
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.reconnectAttempts = 0;
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 clearPendingReconnect() {
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
- if (this.reconnectTimeout) {
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
- createDummyVideoStreamTrack(
1321
- 160 * participantOptions.aspectRatios[0] ?? 1,
1322
- 160,
1323
- true,
1324
- true,
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(getEmptyAudioStreamTrack()),
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;
package/src/room/types.ts CHANGED
@@ -2,6 +2,7 @@ export type SimulationOptions = {
2
2
  publish?: {
3
3
  audio?: boolean;
4
4
  video?: boolean;
5
+ useRealTracks?: boolean;
5
6
  };
6
7
  participants?: {
7
8
  count?: number;