livekit-client 2.13.5 → 2.13.6

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 (41) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +11 -3
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +86 -76
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +5 -5
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
  12. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  13. package/dist/src/e2ee/types.d.ts +1 -0
  14. package/dist/src/e2ee/types.d.ts.map +1 -1
  15. package/dist/src/e2ee/worker/FrameCryptor.d.ts +2 -1
  16. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  17. package/dist/src/room/PCTransport.d.ts +3 -2
  18. package/dist/src/room/PCTransport.d.ts.map +1 -1
  19. package/dist/src/room/PCTransportManager.d.ts +3 -3
  20. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  21. package/dist/src/room/RTCEngine.d.ts +4 -0
  22. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  23. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  24. package/dist/ts4.2/src/api/SignalClient.d.ts +5 -5
  25. package/dist/ts4.2/src/e2ee/types.d.ts +1 -0
  26. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +2 -1
  27. package/dist/ts4.2/src/room/PCTransport.d.ts +3 -2
  28. package/dist/ts4.2/src/room/PCTransportManager.d.ts +3 -3
  29. package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -0
  30. package/package.json +8 -8
  31. package/src/api/SignalClient.ts +13 -14
  32. package/src/connectionHelper/checks/publishVideo.ts +1 -0
  33. package/src/e2ee/E2eeManager.ts +3 -0
  34. package/src/e2ee/types.ts +1 -0
  35. package/src/e2ee/worker/FrameCryptor.ts +15 -0
  36. package/src/e2ee/worker/e2ee.worker.ts +2 -0
  37. package/src/room/PCTransport.ts +30 -4
  38. package/src/room/PCTransportManager.ts +10 -7
  39. package/src/room/RTCEngine.ts +15 -7
  40. package/src/room/Room.ts +9 -9
  41. package/src/room/track/LocalVideoTrack.ts +14 -15
@@ -65,6 +65,7 @@ onmessage = (ev) => {
65
65
  data.readableStream,
66
66
  data.writableStream,
67
67
  data.trackId,
68
+ data.isReuse,
68
69
  data.codec,
69
70
  );
70
71
  break;
@@ -75,6 +76,7 @@ onmessage = (ev) => {
75
76
  data.readableStream,
76
77
  data.writableStream,
77
78
  data.trackId,
79
+ data.isReuse,
78
80
  data.codec,
79
81
  );
80
82
  break;
@@ -50,6 +50,8 @@ export default class PCTransport extends EventEmitter {
50
50
 
51
51
  private ddExtID = 0;
52
52
 
53
+ private latestOfferId: number = 0;
54
+
53
55
  pendingCandidates: RTCIceCandidateInit[] = [];
54
56
 
55
57
  restartingIce: boolean = false;
@@ -62,7 +64,7 @@ export default class PCTransport extends EventEmitter {
62
64
 
63
65
  remoteNackMids: string[] = [];
64
66
 
65
- onOffer?: (offer: RTCSessionDescriptionInit) => void;
67
+ onOffer?: (offer: RTCSessionDescriptionInit, offerId: number) => void;
66
68
 
67
69
  onIceCandidate?: (candidate: RTCIceCandidate) => void;
68
70
 
@@ -137,7 +139,20 @@ export default class PCTransport extends EventEmitter {
137
139
  this.pendingCandidates.push(candidate);
138
140
  }
139
141
 
140
- async setRemoteDescription(sd: RTCSessionDescriptionInit): Promise<void> {
142
+ async setRemoteDescription(sd: RTCSessionDescriptionInit, offerId: number): Promise<boolean> {
143
+ if (
144
+ sd.type === 'answer' &&
145
+ this.latestOfferId > 0 &&
146
+ offerId > 0 &&
147
+ offerId !== this.latestOfferId
148
+ ) {
149
+ this.log.warn('ignoring answer for old offer', {
150
+ ...this.logContext,
151
+ offerId,
152
+ latestOfferId: this.latestOfferId,
153
+ });
154
+ return false;
155
+ }
141
156
  let mungedSDP: string | undefined = undefined;
142
157
  if (sd.type === 'offer') {
143
158
  let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
@@ -218,6 +233,7 @@ export default class PCTransport extends EventEmitter {
218
233
  });
219
234
  }
220
235
  }
236
+ return true;
221
237
  }
222
238
 
223
239
  // debounced negotiate interface
@@ -235,6 +251,9 @@ export default class PCTransport extends EventEmitter {
235
251
  }, debounceInterval);
236
252
 
237
253
  async createAndSendOffer(options?: RTCOfferOptions) {
254
+ // increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
255
+ const offerId = this.latestOfferId + 1;
256
+ this.latestOfferId = offerId;
238
257
  if (this.onOffer === undefined) {
239
258
  return;
240
259
  }
@@ -317,9 +336,16 @@ export default class PCTransport extends EventEmitter {
317
336
  });
318
337
  }
319
338
  });
320
-
339
+ if (this.latestOfferId > offerId) {
340
+ this.log.warn('latestOfferId mismatch', {
341
+ ...this.logContext,
342
+ latestOfferId: this.latestOfferId,
343
+ offerId,
344
+ });
345
+ return;
346
+ }
321
347
  await this.setMungedSDP(offer, write(sdpParsed));
322
- this.onOffer(offer);
348
+ this.onOffer(offer, this.latestOfferId);
323
349
  }
324
350
 
325
351
  async createAndSetAnswer(): Promise<RTCSessionDescriptionInit> {
@@ -48,7 +48,7 @@ export class PCTransportManager {
48
48
 
49
49
  public onTrack?: (ev: RTCTrackEvent) => void;
50
50
 
51
- public onPublisherOffer?: (offer: RTCSessionDescriptionInit) => void;
51
+ public onPublisherOffer?: (offer: RTCSessionDescriptionInit, offerId: number) => void;
52
52
 
53
53
  private isPublisherConnectionRequired: boolean;
54
54
 
@@ -96,8 +96,8 @@ export class PCTransportManager {
96
96
  this.subscriber.onTrack = (ev) => {
97
97
  this.onTrack?.(ev);
98
98
  };
99
- this.publisher.onOffer = (offer) => {
100
- this.onPublisherOffer?.(offer);
99
+ this.publisher.onOffer = (offer, offerId) => {
100
+ this.onPublisherOffer?.(offer, offerId);
101
101
  };
102
102
 
103
103
  this.state = PCTransportState.NEW;
@@ -126,8 +126,8 @@ export class PCTransportManager {
126
126
  return this.publisher.createAndSendOffer(options);
127
127
  }
128
128
 
129
- setPublisherAnswer(sd: RTCSessionDescriptionInit) {
130
- return this.publisher.setRemoteDescription(sd);
129
+ setPublisherAnswer(sd: RTCSessionDescriptionInit, offerId: number) {
130
+ return this.publisher.setRemoteDescription(sd, offerId);
131
131
  }
132
132
 
133
133
  removeTrack(sender: RTCRtpSender) {
@@ -168,7 +168,7 @@ export class PCTransportManager {
168
168
  }
169
169
  }
170
170
 
171
- async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit) {
171
+ async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit, offerId: number) {
172
172
  this.log.debug('received server offer', {
173
173
  ...this.logContext,
174
174
  RTCSdpType: sd.type,
@@ -177,7 +177,10 @@ export class PCTransportManager {
177
177
  });
178
178
  const unlock = await this.remoteOfferLock.lock();
179
179
  try {
180
- await this.subscriber.setRemoteDescription(sd);
180
+ const success = await this.subscriber.setRemoteDescription(sd, offerId);
181
+ if (!success) {
182
+ return undefined;
183
+ }
181
184
 
182
185
  // answer the offer
183
186
  const answer = await this.subscriber.createAndSetAnswer();
@@ -111,6 +111,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
111
111
  */
112
112
  latestJoinResponse?: JoinResponse;
113
113
 
114
+ /**
115
+ * @internal
116
+ */
117
+ latestRemoteOfferId: number = 0;
118
+
114
119
  get isClosed() {
115
120
  return this._isClosed;
116
121
  }
@@ -420,8 +425,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
420
425
  this.client.sendIceCandidate(candidate, target);
421
426
  };
422
427
 
423
- this.pcManager.onPublisherOffer = (offer) => {
424
- this.client.sendOffer(offer);
428
+ this.pcManager.onPublisherOffer = (offer, offerId) => {
429
+ this.client.sendOffer(offer, offerId);
425
430
  };
426
431
 
427
432
  this.pcManager.onDataChannel = this.handleDataChannel;
@@ -476,12 +481,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
476
481
 
477
482
  private setupSignalClientCallbacks() {
478
483
  // configure signaling client
479
- this.client.onAnswer = async (sd) => {
484
+ this.client.onAnswer = async (sd, offerId) => {
480
485
  if (!this.pcManager) {
481
486
  return;
482
487
  }
483
488
  this.log.debug('received server answer', { ...this.logContext, RTCSdpType: sd.type });
484
- await this.pcManager.setPublisherAnswer(sd);
489
+ await this.pcManager.setPublisherAnswer(sd, offerId);
485
490
  };
486
491
 
487
492
  // add candidate on trickle
@@ -494,12 +499,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
494
499
  };
495
500
 
496
501
  // when server creates an offer for the client
497
- this.client.onOffer = async (sd) => {
502
+ this.client.onOffer = async (sd, offerId) => {
503
+ this.latestRemoteOfferId = offerId;
498
504
  if (!this.pcManager) {
499
505
  return;
500
506
  }
501
- const answer = await this.pcManager.createSubscriberAnswerFromOffer(sd);
502
- this.client.sendAnswer(answer);
507
+ const answer = await this.pcManager.createSubscriberAnswerFromOffer(sd, offerId);
508
+ if (answer) {
509
+ this.client.sendAnswer(answer, offerId);
510
+ }
503
511
  };
504
512
 
505
513
  this.client.onLocalTrackPublished = (res: TrackPublishedResponse) => {
package/src/room/Room.ts CHANGED
@@ -1301,10 +1301,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1301
1301
  */
1302
1302
  async switchActiveDevice(kind: MediaDeviceKind, deviceId: string, exact: boolean = true) {
1303
1303
  let success = true;
1304
- let needsUpdateWithoutTracks = false;
1304
+ let shouldTriggerImmediateDeviceChange = false;
1305
1305
  const deviceConstraint = exact ? { exact: deviceId } : deviceId;
1306
1306
  if (kind === 'audioinput') {
1307
- needsUpdateWithoutTracks = this.localParticipant.audioTrackPublications.size === 0;
1307
+ shouldTriggerImmediateDeviceChange = this.localParticipant.audioTrackPublications.size === 0;
1308
1308
  const prevDeviceId =
1309
1309
  this.getActiveDevice(kind) ?? this.options.audioCaptureDefaults!.deviceId;
1310
1310
  this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
@@ -1319,8 +1319,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1319
1319
  this.options.audioCaptureDefaults!.deviceId = prevDeviceId;
1320
1320
  throw e;
1321
1321
  }
1322
+ const isMuted = tracks.some((t) => t.track?.isMuted ?? false);
1323
+ if (success && isMuted) shouldTriggerImmediateDeviceChange = true;
1322
1324
  } else if (kind === 'videoinput') {
1323
- needsUpdateWithoutTracks = this.localParticipant.videoTrackPublications.size === 0;
1325
+ shouldTriggerImmediateDeviceChange = this.localParticipant.videoTrackPublications.size === 0;
1324
1326
  const prevDeviceId =
1325
1327
  this.getActiveDevice(kind) ?? this.options.videoCaptureDefaults!.deviceId;
1326
1328
  this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
@@ -1336,6 +1338,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1336
1338
  throw e;
1337
1339
  }
1338
1340
  } else if (kind === 'audiooutput') {
1341
+ shouldTriggerImmediateDeviceChange = true;
1339
1342
  if (
1340
1343
  (!supportsSetSinkId() && !this.options.webAudioMix) ||
1341
1344
  (this.options.webAudioMix && this.audioContext && !('setSinkId' in this.audioContext))
@@ -1367,12 +1370,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1367
1370
  throw e;
1368
1371
  }
1369
1372
  }
1370
- if (needsUpdateWithoutTracks || kind === 'audiooutput') {
1371
- // if there are not active tracks yet or we're switching audiooutput, we need to manually update the active device map here as changing audio output won't result in a track restart
1372
- this.localParticipant.activeDeviceMap.set(
1373
- kind,
1374
- (kind === 'audiooutput' && this.options.audioOutput?.deviceId) || deviceId,
1375
- );
1373
+
1374
+ if (shouldTriggerImmediateDeviceChange) {
1375
+ this.localParticipant.activeDeviceMap.set(kind, deviceId);
1376
1376
  this.emit(RoomEvent.ActiveDeviceChanged, kind, deviceId);
1377
1377
  }
1378
1378
 
@@ -7,12 +7,11 @@ import {
7
7
  } from '@livekit/protocol';
8
8
  import type { SignalClient } from '../../api/SignalClient';
9
9
  import type { StructuredLogger } from '../../logger';
10
- import { getBrowser } from '../../utils/browserParser';
11
10
  import { ScalabilityMode } from '../participant/publishUtils';
12
11
  import type { VideoSenderStats } from '../stats';
13
12
  import { computeBitrate, monitorFrequency } from '../stats';
14
13
  import type { LoggerOptions } from '../types';
15
- import { compareVersions, isFireFox, isMobile, isSVCCodec, isWeb } from '../utils';
14
+ import { isFireFox, isMobile, isSVCCodec, isWeb } from '../utils';
16
15
  import LocalTrack from './LocalTrack';
17
16
  import { Track, VideoQuality } from './Track';
18
17
  import type { VideoCaptureOptions, VideoCodec } from './options';
@@ -459,15 +458,18 @@ async function setPublishingLayersForSender(
459
458
  }
460
459
 
461
460
  let hasChanged = false;
462
- const browser = getBrowser();
463
- const closableSpatial =
464
- browser?.name === 'Chrome' && compareVersions(browser?.version, '133') > 0;
461
+
462
+ /* disable closable spatial layer as it has video blur / frozen issue with current server / client
463
+ 1. chrome 113: when switching to up layer with scalability Mode change, it will generate a
464
+ low resolution frame and recover very quickly, but noticable
465
+ 2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable */
466
+ const closableSpatial = false;
465
467
  /* @ts-ignore */
466
468
  if (closableSpatial && encodings[0].scalabilityMode) {
467
469
  // svc dynacast encodings
468
470
  const encoding = encodings[0];
469
471
  /* @ts-ignore */
470
- const mode = new ScalabilityMode(encoding.scalabilityMode);
472
+ // const mode = new ScalabilityMode(encoding.scalabilityMode);
471
473
  let maxQuality = ProtoVideoQuality.OFF;
472
474
  qualities.forEach((q) => {
473
475
  if (q.enabled && (maxQuality === ProtoVideoQuality.OFF || q.quality > maxQuality)) {
@@ -480,25 +482,22 @@ async function setPublishingLayersForSender(
480
482
  encoding.active = false;
481
483
  hasChanged = true;
482
484
  }
483
- } else if (!encoding.active || mode.spatial !== maxQuality + 1) {
485
+ } else if (!encoding.active /* || mode.spatial !== maxQuality + 1*/) {
484
486
  hasChanged = true;
485
487
  encoding.active = true;
486
- /* @ts-ignore */
487
- const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode);
488
+ /*
489
+ @ts-ignore
490
+ const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode)
488
491
  mode.spatial = maxQuality + 1;
489
492
  mode.suffix = originalMode.suffix;
490
493
  if (mode.spatial === 1) {
491
494
  // no suffix for L1Tx
492
495
  mode.suffix = undefined;
493
496
  }
494
- /* @ts-ignore */
497
+ @ts-ignore
495
498
  encoding.scalabilityMode = mode.toString();
496
499
  encoding.scaleResolutionDownBy = 2 ** (2 - maxQuality);
497
- if (senderEncodings[0].maxBitrate) {
498
- encoding.maxBitrate =
499
- senderEncodings[0].maxBitrate /
500
- (encoding.scaleResolutionDownBy * encoding.scaleResolutionDownBy);
501
- }
500
+ */
502
501
  }
503
502
  } else {
504
503
  if (isSVC) {