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.
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;