livekit-client 2.22.2 → 2.22.4

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 (245) hide show
  1. package/README.md +35 -0
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +488 -438
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +954 -301
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +10 -2
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/api/SignalClient.d.ts +4 -2
  15. package/dist/src/api/SignalClient.d.ts.map +1 -1
  16. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  17. package/dist/src/api/utils.d.ts +1 -0
  18. package/dist/src/api/utils.d.ts.map +1 -1
  19. package/dist/src/e2ee/E2eeManager.d.ts +26 -0
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/types.d.ts +15 -1
  22. package/dist/src/e2ee/types.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  25. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts.map +1 -0
  26. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -18
  27. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  28. package/dist/src/logger.d.ts +4 -0
  29. package/dist/src/logger.d.ts.map +1 -1
  30. package/dist/src/options.d.ts +14 -0
  31. package/dist/src/options.d.ts.map +1 -1
  32. package/dist/src/room/PCTransport.d.ts +78 -7
  33. package/dist/src/room/PCTransport.d.ts.map +1 -1
  34. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  35. package/dist/src/room/RTCEngine.d.ts +9 -0
  36. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  37. package/dist/src/room/Room.d.ts +1 -1
  38. package/dist/src/room/Room.d.ts.map +1 -1
  39. package/dist/src/room/defaults.d.ts.map +1 -1
  40. package/dist/src/room/errors.d.ts +18 -0
  41. package/dist/src/room/errors.d.ts.map +1 -1
  42. package/dist/src/room/events.d.ts +3 -2
  43. package/dist/src/room/events.d.ts.map +1 -1
  44. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  45. package/dist/src/room/participant/publishUtils.d.ts +16 -0
  46. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  47. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  48. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  49. package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
  50. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  51. package/dist/src/room/track/options.d.ts +1 -1
  52. package/dist/src/room/track/utils.d.ts +13 -0
  53. package/dist/src/room/track/utils.d.ts.map +1 -1
  54. package/dist/src/room/utils.d.ts +66 -0
  55. package/dist/src/room/utils.d.ts.map +1 -1
  56. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  57. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  58. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  59. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  60. package/dist/ts4.2/api/WebSocketStream.d.ts +33 -33
  61. package/dist/ts4.2/api/utils.d.ts +9 -8
  62. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  63. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  64. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  65. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  66. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  67. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  68. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  69. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  70. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  71. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  72. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -85
  73. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  74. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  75. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  76. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  77. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  78. package/dist/ts4.2/e2ee/types.d.ts +231 -217
  79. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  80. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  81. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  82. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -226
  83. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  84. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  85. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +28 -28
  86. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  87. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  88. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  89. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  90. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  91. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  92. package/dist/ts4.2/index.d.ts +76 -76
  93. package/dist/ts4.2/logger.d.ts +66 -62
  94. package/dist/ts4.2/options.d.ts +163 -149
  95. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  96. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  97. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  98. package/dist/ts4.2/room/PCTransport.d.ts +246 -175
  99. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  100. package/dist/ts4.2/room/RTCEngine.d.ts +269 -261
  101. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  102. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  103. package/dist/ts4.2/room/Room.d.ts +354 -365
  104. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  105. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -62
  106. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +94 -95
  107. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  108. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  109. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  110. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  111. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  112. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  113. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  114. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  115. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  116. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  117. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  118. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  119. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  120. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  121. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  122. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  123. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  124. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  125. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  126. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  127. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  128. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  129. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  130. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  131. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  132. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  133. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  134. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  135. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  136. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  137. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  138. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  139. package/dist/ts4.2/room/debounce.d.ts +11 -11
  140. package/dist/ts4.2/room/defaults.d.ts +9 -9
  141. package/dist/ts4.2/room/errors.d.ts +180 -162
  142. package/dist/ts4.2/room/events.d.ts +638 -637
  143. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +287 -287
  144. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  145. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  146. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  147. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -37
  148. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  149. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  150. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  151. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  152. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  153. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  154. package/dist/ts4.2/room/stats.d.ts +71 -71
  155. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  156. package/dist/ts4.2/room/timers.d.ts +12 -12
  157. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  158. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  159. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  160. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  161. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +39 -42
  162. package/dist/ts4.2/room/track/LocalTrack.d.ts +125 -125
  163. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  164. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +102 -93
  165. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +55 -55
  166. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  167. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  168. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  169. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  170. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  171. package/dist/ts4.2/room/track/create.d.ts +25 -25
  172. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  173. package/dist/ts4.2/room/track/options.d.ts +403 -403
  174. package/dist/ts4.2/room/track/processor/types.d.ts +31 -31
  175. package/dist/ts4.2/room/track/record.d.ts +13 -13
  176. package/dist/ts4.2/room/track/types.d.ts +28 -28
  177. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  178. package/dist/ts4.2/room/types.d.ts +127 -127
  179. package/dist/ts4.2/room/utils.d.ts +246 -180
  180. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  181. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  182. package/dist/ts4.2/test/mocks.d.ts +12 -12
  183. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  184. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  185. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  186. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  187. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  188. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  189. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  190. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  191. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  192. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  193. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  194. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  195. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  196. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  197. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  198. package/dist/ts4.2/version.d.ts +15 -15
  199. package/package.json +5 -4
  200. package/src/api/SignalClient.e2e.test.ts +1 -3
  201. package/src/api/SignalClient.test.ts +160 -10
  202. package/src/api/SignalClient.ts +75 -54
  203. package/src/api/WebSocketStream.ts +3 -8
  204. package/src/api/utils.ts +10 -0
  205. package/src/e2ee/E2eeManager.test.ts +196 -0
  206. package/src/e2ee/E2eeManager.ts +114 -19
  207. package/src/e2ee/types.ts +19 -1
  208. package/src/e2ee/worker/DataCryptor.ts +2 -1
  209. package/src/e2ee/worker/ErrorRateLimiter.test.ts +53 -0
  210. package/src/e2ee/worker/ErrorRateLimiter.ts +52 -0
  211. package/src/e2ee/worker/FrameCryptor.ts +20 -70
  212. package/src/e2ee/worker/e2ee.worker.ts +34 -11
  213. package/src/logger.ts +22 -0
  214. package/src/options.ts +16 -0
  215. package/src/room/PCTransport.test.ts +183 -7
  216. package/src/room/PCTransport.ts +197 -36
  217. package/src/room/PCTransportManager.ts +3 -1
  218. package/src/room/RTCEngine.test.ts +84 -0
  219. package/src/room/RTCEngine.ts +52 -12
  220. package/src/room/Room.test.ts +32 -6
  221. package/src/room/Room.ts +27 -7
  222. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +16 -20
  223. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -5
  224. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  225. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  226. package/src/room/defaults.ts +1 -0
  227. package/src/room/errors.test.ts +36 -0
  228. package/src/room/errors.ts +27 -0
  229. package/src/room/events.ts +2 -1
  230. package/src/room/participant/LocalParticipant.ts +30 -14
  231. package/src/room/participant/publishUtils.test.ts +133 -0
  232. package/src/room/participant/publishUtils.ts +54 -38
  233. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  234. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  235. package/src/room/track/LocalTrack.ts +18 -7
  236. package/src/room/track/LocalVideoTrack.ts +25 -8
  237. package/src/room/track/RemoteAudioTrack.ts +3 -3
  238. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  239. package/src/room/track/RemoteVideoTrack.ts +29 -3
  240. package/src/room/track/options.ts +1 -1
  241. package/src/room/track/utils.test.ts +201 -2
  242. package/src/room/track/utils.ts +88 -0
  243. package/src/room/utils.test.ts +211 -0
  244. package/src/room/utils.ts +129 -0
  245. package/src/test/compressionStreamStubs.ts +53 -0
@@ -1,6 +1,7 @@
1
1
  // TODO code inspired by https://github.com/webrtc/samples/blob/gh-pages/src/content/insertable-streams/endtoend-encryption/js/worker.js
2
2
  import { EventEmitter } from 'events';
3
3
  import type TypedEventEmitter from 'typed-emitter';
4
+ import { getErrorDescription } from '../../api/utils';
4
5
  import {
5
6
  appendPacketTrailerToEncodedFrame,
6
7
  processPacketTrailer,
@@ -22,6 +23,7 @@ import type {
22
23
  RatchetResult,
23
24
  } from '../types';
24
25
  import { deriveKeys, isVideoFrame, needsRbspUnescaping, parseRbsp, writeRbsp } from '../utils';
26
+ import { ErrorRateLimiter } from './ErrorRateLimiter';
25
27
  import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
26
28
  import { processNALUsForEncryption } from './naluUtils';
27
29
  import { identifySifPayload } from './sifPayload';
@@ -111,18 +113,7 @@ export class FrameCryptor extends BaseFrameCryptor {
111
113
 
112
114
  private frameMetadataFrameId = 0;
113
115
 
114
- /**
115
- * Throttling mechanism for decryption errors to prevent memory leaks
116
- */
117
- private lastErrorTimestamp: Map<string, number> = new Map();
118
-
119
- private errorCounts: Map<string, number> = new Map();
120
-
121
- private readonly ERROR_THROTTLE_MS = 1000; // Emit error at most once per second
122
-
123
- private readonly MAX_ERRORS_PER_MINUTE = 5; // Maximum errors to emit per minute per key
124
-
125
- private readonly ERROR_WINDOW_MS = 60000; // 1 minute window
116
+ private errorLimiter = new ErrorRateLimiter();
126
117
 
127
118
  private undecryptedTrackTimeout?: ReturnType<typeof setTimeout>;
128
119
 
@@ -196,8 +187,7 @@ export class FrameCryptor extends BaseFrameCryptor {
196
187
  clearTimeout(this.undecryptedTrackTimeout);
197
188
  this.undecryptedTrackTimeout = undefined;
198
189
  this.participantIdentity = undefined;
199
- this.lastErrorTimestamp = new Map();
200
- this.errorCounts = new Map();
190
+ this.errorLimiter.reset();
201
191
  }
202
192
 
203
193
  isEnabled() {
@@ -421,64 +411,24 @@ export class FrameCryptor extends BaseFrameCryptor {
421
411
  this.sifTrailer = trailer;
422
412
  }
423
413
 
424
- /**
425
- * Checks if we should emit an error based on throttling rules to prevent memory leaks
426
- * @param errorKey - unique key identifying the error context
427
- * @returns true if the error should be emitted, false otherwise
428
- */
429
- private shouldEmitError(errorKey: string): boolean {
430
- const now = Date.now();
431
- const lastErrorTime = this.lastErrorTimestamp.get(errorKey) ?? 0;
432
- const errorCount = this.errorCounts.get(errorKey) ?? 0;
433
-
434
- // Reset count if we're in a new time window
435
- if (now - lastErrorTime > this.ERROR_WINDOW_MS) {
436
- this.errorCounts.set(errorKey, 0);
437
- this.lastErrorTimestamp.set(errorKey, now);
438
- return true;
439
- }
440
-
441
- // Check if we've exceeded the throttle time
442
- if (now - lastErrorTime < this.ERROR_THROTTLE_MS) {
443
- return false;
444
- }
445
-
446
- // Check if we've exceeded the max errors per window
447
- if (errorCount >= this.MAX_ERRORS_PER_MINUTE) {
448
- // Only log a warning once when hitting the limit
449
- if (errorCount === this.MAX_ERRORS_PER_MINUTE) {
450
- workerLogger.warn(`Suppressing further decryption errors for ${this.participantIdentity}`, {
451
- ...this.logContext,
452
- errorKey,
453
- });
454
- this.errorCounts.set(errorKey, errorCount + 1);
455
- }
456
- return false;
457
- }
458
-
459
- // Update tracking
460
- this.lastErrorTimestamp.set(errorKey, now);
461
- this.errorCounts.set(errorKey, errorCount + 1);
462
- return true;
463
- }
464
-
465
- /**
466
- * Emits a throttled error to prevent memory leaks from repeated decryption failures
467
- * @param error - the CryptorError to emit
468
- */
469
414
  private emitThrottledError(error: CryptorError) {
470
415
  const errorKey = `${this.participantIdentity}-${error.reason}-decrypt`;
416
+ const emit = this.errorLimiter.shouldEmit(errorKey, () => {
417
+ workerLogger.warn(`Suppressing further decryption errors for ${this.participantIdentity}`, {
418
+ ...this.logContext,
419
+ errorKey,
420
+ });
421
+ });
422
+ if (!emit) return;
471
423
 
472
- if (this.shouldEmitError(errorKey)) {
473
- const errorCount = this.errorCounts.get(errorKey) ?? 0;
474
- if (errorCount > 1) {
475
- workerLogger.debug(`Decryption error (${errorCount} occurrences in window)`, {
476
- ...this.logContext,
477
- reason: CryptorErrorReason[error.reason],
478
- });
479
- }
480
- this.emit(CryptorEvent.Error, error);
424
+ const count = this.errorLimiter.countFor(errorKey);
425
+ if (count > 1) {
426
+ workerLogger.debug(`Decryption error (${count} occurrences in window)`, {
427
+ ...this.logContext,
428
+ reason: CryptorErrorReason[error.reason],
429
+ });
481
430
  }
431
+ this.emit(CryptorEvent.Error, error);
482
432
  }
483
433
 
484
434
  /**
@@ -587,7 +537,7 @@ export class FrameCryptor extends BaseFrameCryptor {
587
537
  return controller.enqueue(encodedFrame);
588
538
  } catch (e: any) {
589
539
  // TODO: surface this to the app.
590
- workerLogger.error(e);
540
+ workerLogger.error(`error while encrypting`, { ...this.logContext, error: e });
591
541
  }
592
542
  } else {
593
543
  workerLogger.debug('failed to encrypt, emitting error', this.logContext);
@@ -858,7 +808,7 @@ export class FrameCryptor extends BaseFrameCryptor {
858
808
  }
859
809
  } else {
860
810
  throw new CryptorError(
861
- `Decryption failed: ${error.message}`,
811
+ `Decryption failed: ${getErrorDescription(error, 'decryption')}`,
862
812
  CryptorErrorReason.InvalidKey,
863
813
  this.participantIdentity,
864
814
  );
@@ -18,6 +18,7 @@ import type {
18
18
  ScriptTransformOptions,
19
19
  } from '../types';
20
20
  import { DataCryptor } from './DataCryptor';
21
+ import { ErrorRateLimiter } from './ErrorRateLimiter';
21
22
  import { FrameCryptor, encryptionEnabledMap } from './FrameCryptor';
22
23
  import { ParticipantKeyHandler } from './ParticipantKeyHandler';
23
24
 
@@ -36,8 +37,21 @@ let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS;
36
37
 
37
38
  let rtpMap: Map<number, VideoCodec> = new Map();
38
39
 
40
+ const dataDecryptErrorLimiter = new ErrorRateLimiter();
41
+
39
42
  workerLogger.setDefaultLevel('info');
40
43
 
44
+ // Forward worker log calls to the main thread so they reach any
45
+ // setLogExtension consumer installed there. The main-thread workerLogger
46
+ // re-emits them, which invokes both the console and the extension.
47
+ workerLogger.methodFactory = (methodName) => (msg, context) => {
48
+ postMessage({
49
+ kind: 'log',
50
+ data: { level: methodName, msg, context },
51
+ });
52
+ };
53
+ workerLogger.setLevel(workerLogger.getLevel());
54
+
41
55
  onmessage = (ev) => {
42
56
  messageQueue.run(async () => {
43
57
  const { kind, data }: E2EEWorkerMessage = ev.data;
@@ -45,7 +59,7 @@ onmessage = (ev) => {
45
59
  switch (kind) {
46
60
  case 'init':
47
61
  workerLogger.setLevel(data.loglevel);
48
- workerLogger.info('worker initialized');
62
+ workerLogger.info('e2ee worker initialized');
49
63
  keyProviderOptions = data.keyProviderOptions;
50
64
  useSharedKey = !!data.keyProviderOptions.sharedKey;
51
65
  // acknowledge init successful
@@ -55,6 +69,9 @@ onmessage = (ev) => {
55
69
  };
56
70
  postMessage(ackMsg);
57
71
  break;
72
+ case 'setLogLevel':
73
+ workerLogger.setLevel(data.level);
74
+ break;
58
75
  case 'enable':
59
76
  setEncryptionEnabled(data.enabled, data.participantIdentity);
60
77
  workerLogger.info(
@@ -96,11 +113,6 @@ onmessage = (ev) => {
96
113
  data.payload,
97
114
  getParticipantKeyHandler(data.participantIdentity),
98
115
  );
99
- console.log('encrypted payload', {
100
- original: data.payload,
101
- encrypted: encryptedPayload,
102
- iv,
103
- });
104
116
  postMessage({
105
117
  kind: 'encryptDataResponse',
106
118
  data: {
@@ -125,12 +137,23 @@ onmessage = (ev) => {
125
137
  data: { payload: decryptedPayload, uuid: data.uuid },
126
138
  } satisfies DecryptDataResponseMessage);
127
139
  } catch (error) {
128
- // Send error back to main thread with uuid so it can reject the corresponding promise
129
- workerLogger.error('DataCryptor decryption failed', {
130
- error,
131
- participantIdentity: data.participantIdentity,
132
- uuid: data.uuid,
140
+ // Send error back to main thread with uuid so it can reject the corresponding promise.
141
+ // The error response must always be posted so the awaiting future resolves; only the
142
+ // log is throttled to avoid flooding when a broken key keeps producing failures.
143
+ const errorKey = `${data.participantIdentity}-datadecrypt`;
144
+ const shouldLog = dataDecryptErrorLimiter.shouldEmit(errorKey, () => {
145
+ workerLogger.warn(
146
+ `Suppressing further data decryption errors for ${data.participantIdentity}`,
147
+ { errorKey },
148
+ );
133
149
  });
150
+ if (shouldLog) {
151
+ workerLogger.error('DataCryptor decryption failed', {
152
+ error,
153
+ participantIdentity: data.participantIdentity,
154
+ uuid: data.uuid,
155
+ });
156
+ }
134
157
  postMessage({
135
158
  kind: 'error',
136
159
  data: {
package/src/logger.ts CHANGED
@@ -131,3 +131,25 @@ export function setLogExtension(extension: LogExtension, logger?: StructuredLogg
131
131
  }
132
132
 
133
133
  export const workerLogger = log.getLogger(LoggerNames.E2EE) as StructuredLogger;
134
+
135
+ const workerLogLevelListeners = new Set<(level: LogLevel) => void>();
136
+
137
+ const originalWorkerSetLevel = workerLogger.setLevel.bind(workerLogger);
138
+ workerLogger.setLevel = ((level: log.LogLevelDesc, persist?: boolean) => {
139
+ originalWorkerSetLevel(level, persist);
140
+ const numeric = workerLogger.getLevel() as LogLevel;
141
+ workerLogLevelListeners.forEach((cb) => cb(numeric));
142
+ }) as typeof workerLogger.setLevel;
143
+
144
+ /** @internal Subscribe to workerLogger level changes (so E2EE workers can be kept in sync). */
145
+ export function onWorkerLogLevelChanged(cb: (level: LogLevel) => void): () => void {
146
+ workerLogLevelListeners.add(cb);
147
+ return () => {
148
+ workerLogLevelListeners.delete(cb);
149
+ };
150
+ }
151
+
152
+ /** @internal Test-only accessor: current number of workerLogger level listeners. */
153
+ export function getWorkerLogLevelListenerCount(): number {
154
+ return workerLogLevelListeners.size;
155
+ }
package/src/options.ts CHANGED
@@ -166,6 +166,22 @@ export interface InternalRoomConnectOptions {
166
166
  */
167
167
  rtcConfig?: RTCConfiguration;
168
168
 
169
+ /**
170
+ * when the server runs ICE lite, asks it to use a full ICE agent for this participant.
171
+ * this is a server-side setting that the server may ignore, defaults to false
172
+ */
173
+ disableIceLite: boolean;
174
+
175
+ /**
176
+ * whether the SFU may pause subscribed video tracks when this participant's downstream
177
+ * connection is congested, so that other tracks can continue to stream smoothly.
178
+ * paused tracks resume automatically when conditions allow, and `RoomEvent.TrackStreamStateChanged`
179
+ * fires on each transition.
180
+ * when left undefined, the server's default applies (the project setting on LiveKit Cloud,
181
+ * or `rtc.congestion_control.allow_pause` when self-hosting)
182
+ */
183
+ subscriberAllowPause?: boolean;
184
+
169
185
  /** specifies how often an initial join connection is allowed to retry (only applicable if server is not reachable) */
170
186
  maxRetries: number;
171
187
 
@@ -2,11 +2,15 @@ import { type MediaDescription, parse } from 'sdp-transform';
2
2
  import { describe, expect, it } from 'vitest';
3
3
  import {
4
4
  applyVideoStartBitrate,
5
+ computeConnectionStartBitrate,
6
+ computeTrackStartBitrate,
5
7
  conformBundledCodecFmtp,
6
8
  ensureAudioNackAndStereo,
7
9
  ensureVideoDDExtension,
8
10
  extractStereoAndNackAudioFromOffer,
11
+ findTrackCodecPayload,
9
12
  fmtpConfigHasParam,
13
+ isDuplicateAnswer,
10
14
  placeholderMidsFromTransceivers,
11
15
  } from './PCTransport';
12
16
  import { ddExtensionURI } from './utils';
@@ -61,9 +65,7 @@ a=recvonly
61
65
  a=rtpmap:49 H265/90000
62
66
  a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`;
63
67
 
64
- describe('video start bitrate', () => {
65
- it('applies the bitrate only to the section whose msid track ID matches the cid', () => {
66
- const { media } = parse(`v=0
68
+ const TWO_VIDEO_SECTIONS = `v=0
67
69
  o=- 0 0 IN IP4 127.0.0.1
68
70
  s=-
69
71
  t=0 0
@@ -79,15 +81,159 @@ c=IN IP4 0.0.0.0
79
81
  a=mid:1
80
82
  a=sendonly
81
83
  a=msid:PA_remote|camera camera-cid
82
- a=rtpmap:96 VP8/90000`);
84
+ a=rtpmap:96 VP8/90000`;
83
85
 
84
- for (const section of media) {
85
- applyVideoStartBitrate(section, 'camera-cid', 'VP8', 1_000);
86
- }
86
+ // The same bundle after the screen share is unpublished: `unpublishTrack` sets the
87
+ // transceiver to `inactive`, but the section keeps its `a=msid`, so it still matches the
88
+ // append-only trackBitrates entry. Only the camera is still sending.
89
+ const UNPUBLISHED_SCREEN_SHARE = `v=0
90
+ o=- 0 0 IN IP4 127.0.0.1
91
+ s=-
92
+ t=0 0
93
+ a=group:BUNDLE 0 1
94
+ m=video 9 UDP/TLS/RTP/SAVPF 96
95
+ c=IN IP4 0.0.0.0
96
+ a=mid:0
97
+ a=inactive
98
+ a=msid:PA_remote|camera other-track
99
+ a=rtpmap:96 VP8/90000
100
+ m=video 9 UDP/TLS/RTP/SAVPF 96
101
+ c=IN IP4 0.0.0.0
102
+ a=mid:1
103
+ a=sendonly
104
+ a=msid:PA_remote|camera camera-cid
105
+ a=rtpmap:96 VP8/90000`;
106
+
107
+ // The legacy `addTrack` fallback (no `addTransceiver` support) reuses a transceiver instead
108
+ // of creating a sendonly one, so the published camera lands on a `sendrecv` section. Mid 1
109
+ // omits the direction attribute entirely, which SDP also defaults to sendrecv. Both send.
110
+ const LEGACY_ADD_TRACK_SECTIONS = `v=0
111
+ o=- 0 0 IN IP4 127.0.0.1
112
+ s=-
113
+ t=0 0
114
+ a=group:BUNDLE 0 1
115
+ m=video 9 UDP/TLS/RTP/SAVPF 96
116
+ c=IN IP4 0.0.0.0
117
+ a=mid:0
118
+ a=sendrecv
119
+ a=msid:PA_remote|camera camera-cid
120
+ a=rtpmap:96 VP8/90000
121
+ m=video 9 UDP/TLS/RTP/SAVPF 96
122
+ c=IN IP4 0.0.0.0
123
+ a=mid:1
124
+ a=msid:PA_remote|camera other-track
125
+ a=rtpmap:96 VP8/90000`;
126
+
127
+ describe('video start bitrate', () => {
128
+ it('matches only the section whose msid track ID matches the cid', () => {
129
+ const { media } = parse(TWO_VIDEO_SECTIONS);
130
+
131
+ expect(findTrackCodecPayload(media[0], 'camera-cid', 'VP8')).toBeUndefined();
132
+ expect(findTrackCodecPayload(media[1], 'camera-cid', 'VP8')).toBe(96);
133
+ // Section belongs to the track but does not offer the codec.
134
+ expect(findTrackCodecPayload(media[1], 'camera-cid', 'AV1')).toBe(0);
135
+ });
136
+
137
+ it('applies the bitrate only to the section it is given', () => {
138
+ const { media } = parse(TWO_VIDEO_SECTIONS);
139
+
140
+ applyVideoStartBitrate(media[1], 96, 900);
87
141
 
88
142
  expect(fmtpOf(media, '0', 96)).toBeUndefined();
89
143
  expect(paramSet(fmtpOf(media, '1', 96)!)).toContain('x-google-start-bitrate=900');
90
144
  });
145
+
146
+ it('caps camera at 1 Mbps but leaves screen share uncapped', () => {
147
+ const camera = { cid: 'c', codec: 'VP8', maxbr: 3_000 };
148
+ const screenShare = { ...camera, isScreenShare: true };
149
+
150
+ expect(computeTrackStartBitrate(camera)).toBe(1_000);
151
+ expect(computeTrackStartBitrate(screenShare)).toBe(2_700);
152
+ });
153
+
154
+ it('gives no hint below the 300 kbps target floor', () => {
155
+ expect(computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 299 })).toBeUndefined();
156
+ expect(computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 300 })).toBe(270);
157
+ });
158
+
159
+ it('uses one connection-level value: the largest hint across video sections', () => {
160
+ const { media } = parse(TWO_VIDEO_SECTIONS);
161
+
162
+ const startBitrate = computeConnectionStartBitrate(media, [
163
+ { cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 },
164
+ { cid: 'other-track', codec: 'VP8', maxbr: 3_000, isScreenShare: true },
165
+ ]);
166
+
167
+ expect(startBitrate).toBe(2_700);
168
+ });
169
+
170
+ it('ignores registered tracks with no section in the current SDP', () => {
171
+ const { media } = parse(TWO_VIDEO_SECTIONS);
172
+
173
+ const startBitrate = computeConnectionStartBitrate(media, [
174
+ { cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 },
175
+ // Stale entry: trackBitrates is append-only and outlives an unpublish.
176
+ { cid: 'unpublished-cid', codec: 'VP8', maxbr: 8_000, isScreenShare: true },
177
+ ]);
178
+
179
+ expect(startBitrate).toBe(900);
180
+ });
181
+
182
+ it('ignores a section that stopped sending but kept its msid', () => {
183
+ const trackBitrates = [
184
+ { cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 },
185
+ { cid: 'other-track', codec: 'VP8', maxbr: 8_000, isScreenShare: true },
186
+ ];
187
+
188
+ // While both send, the uncapped screen share wins the connection-level max.
189
+ expect(computeConnectionStartBitrate(parse(TWO_VIDEO_SECTIONS).media, trackBitrates)).toBe(
190
+ 7_200,
191
+ );
192
+ // Once it is unpublished its entry is stale, so only the capped camera counts. Both
193
+ // directions a removed sender can land on are excluded: `inactive` from a sendonly
194
+ // transceiver, `recvonly` from the sendrecv one the addTrack fallback reuses.
195
+ expect(
196
+ computeConnectionStartBitrate(parse(UNPUBLISHED_SCREEN_SHARE).media, trackBitrates),
197
+ ).toBe(900);
198
+ expect(
199
+ computeConnectionStartBitrate(
200
+ parse(UNPUBLISHED_SCREEN_SHARE.replace('a=inactive', 'a=recvonly')).media,
201
+ trackBitrates,
202
+ ),
203
+ ).toBe(900);
204
+ });
205
+
206
+ it('gives no connection value when no section maps to a published track', () => {
207
+ const { media } = parse(TWO_VIDEO_SECTIONS);
208
+
209
+ expect(computeConnectionStartBitrate(media, [])).toBeUndefined();
210
+ });
211
+
212
+ it('counts sendrecv and direction-less sections, which still send local media', () => {
213
+ // The legacy addTrack fallback never produces `sendonly`, so a strict match on it would
214
+ // leave those clients with no hint at all.
215
+ const { media } = parse(LEGACY_ADD_TRACK_SECTIONS);
216
+
217
+ expect(
218
+ computeConnectionStartBitrate(media, [{ cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 }]),
219
+ ).toBe(900);
220
+ expect(
221
+ computeConnectionStartBitrate(media, [
222
+ { cid: 'other-track', codec: 'VP8', maxbr: 2_000, isScreenShare: true },
223
+ ]),
224
+ ).toBe(1_800);
225
+ });
226
+
227
+ it('leaves the hint unset when only non-sending sections match', () => {
228
+ // Nothing is published, so the one-shot hint must not be consumed on a dead section.
229
+ const { media } = parse(UNPUBLISHED_SCREEN_SHARE);
230
+
231
+ expect(
232
+ computeConnectionStartBitrate(media, [
233
+ { cid: 'other-track', codec: 'VP8', maxbr: 8_000, isScreenShare: true },
234
+ ]),
235
+ ).toBeUndefined();
236
+ });
91
237
  });
92
238
 
93
239
  describe('placeholderMidsFromTransceivers', () => {
@@ -390,3 +536,33 @@ a=extmap:3 ${ddExtensionURI}`);
390
536
  expect(ddOf(sdp.media, '1')).toBe(12);
391
537
  });
392
538
  });
539
+
540
+ describe('isDuplicateAnswer', () => {
541
+ it('drops an answer while the same offer is still being answered', () => {
542
+ expect(isDuplicateAnswer(3, 3, 0, 'have-local-offer', false)).toBe(true);
543
+ });
544
+
545
+ it('drops a repeat of an unnumbered answer while the first is still applying', () => {
546
+ expect(isDuplicateAnswer(0, 0, 0, 'have-local-offer', false)).toBe(true);
547
+ });
548
+
549
+ it('drops an answer for an offer whose answer was already applied', () => {
550
+ expect(isDuplicateAnswer(3, undefined, 3, 'stable', false)).toBe(true);
551
+ });
552
+
553
+ it('accepts a retry after a failed apply: nothing in flight, nothing applied', () => {
554
+ expect(isDuplicateAnswer(3, undefined, 2, 'have-local-offer', false)).toBe(false);
555
+ });
556
+
557
+ it('accepts the answer for a new offer', () => {
558
+ expect(isDuplicateAnswer(4, undefined, 3, 'have-local-offer', false)).toBe(false);
559
+ });
560
+
561
+ it('without an offerId, drops an answer in stable', () => {
562
+ expect(isDuplicateAnswer(0, undefined, 0, 'stable', false)).toBe(true);
563
+ });
564
+
565
+ it('without an offerId, accepts the answer to a deferred initial offer', () => {
566
+ expect(isDuplicateAnswer(0, undefined, 0, 'stable', true)).toBe(false);
567
+ });
568
+ });