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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.22.2",
3
+ "version": "2.22.4",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -50,7 +50,7 @@
50
50
  "license": "Apache-2.0",
51
51
  "dependencies": {
52
52
  "@livekit/mutex": "1.1.1",
53
- "@livekit/protocol": "1.50.4",
53
+ "@livekit/protocol": "1.52.1",
54
54
  "events": "^3.3.0",
55
55
  "jose": "^6.1.0",
56
56
  "loglevel": "^1.9.2",
@@ -85,9 +85,10 @@
85
85
  "@types/ua-parser-js": "0.7.39",
86
86
  "@typescript-eslint/eslint-plugin": "8.64.0",
87
87
  "@typescript-eslint/parser": "8.64.0",
88
+ "@typescript/native": "npm:typescript@^7.0.2",
88
89
  "@vitest/browser": "^4.1.10",
89
90
  "@vitest/browser-playwright": "^4.1.10",
90
- "downlevel-dts": "^0.11.0",
91
+ "@nbilyk/downlevel-dts": "^0.15.2",
91
92
  "eslint": "10.7.0",
92
93
  "eslint-config-airbnb-extended": "^2.3.2",
93
94
  "eslint-config-prettier": "10.1.8",
@@ -113,7 +114,7 @@
113
114
  "tsx": "^4.21.0",
114
115
  "typedoc": "0.28.20",
115
116
  "typedoc-plugin-no-inherit": "1.6.1",
116
- "typescript": "5.9.3",
117
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
117
118
  "typescript-eslint": "^8.47.0",
118
119
  "vite": "8.1.5",
119
120
  "vitest": "^4.1.10"
@@ -174,9 +174,7 @@ describe.skipIf(!!unavailable)('SignalClient e2e', () => {
174
174
 
175
175
  it('rejects a reconnect when a leave arrives as the first message', async () => {
176
176
  // Reconnecting into a room that sends leave-first exercises the client's
177
- // first-message validation while in RECONNECTING (path-independent: it
178
- // doesn't rely on the mock detecting reconnect, which v1 hides inside the
179
- // gzipped join_request the mock ignores).
177
+ // first-message validation while in RECONNECTING.
180
178
  await join('happy');
181
179
  const token = await createToken({ signal: 'leave_first_message' });
182
180
  const err = await client.reconnect(serverUrl, token, 'RM_session').then(
@@ -206,6 +206,152 @@ describe('SignalClient.connect', () => {
206
206
  ClientInfo_Capability.CAP_PACKET_TRAILER,
207
207
  ]);
208
208
  });
209
+
210
+ it('does not request full ICE by default', async () => {
211
+ const joinResponse = createJoinResponse();
212
+ const signalResponse = createSignalResponse('join', joinResponse);
213
+ const mockReadable = createMockReadableStream([signalResponse]);
214
+ const mockConnection = createMockConnection(mockReadable);
215
+ let capturedUrl = '';
216
+
217
+ mockWebSocketStream({
218
+ connection: mockConnection,
219
+ onUrl: (url) => {
220
+ capturedUrl = url;
221
+ },
222
+ });
223
+
224
+ await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
225
+
226
+ const joinRequest = await decodeJoinRequestFromUrl(capturedUrl);
227
+ expect(joinRequest.connectionSettings?.disableIceLite).toBe(false);
228
+ });
229
+
230
+ it('requests full ICE in the join request when disableIceLite is set', async () => {
231
+ const joinResponse = createJoinResponse();
232
+ const signalResponse = createSignalResponse('join', joinResponse);
233
+ const mockReadable = createMockReadableStream([signalResponse]);
234
+ const mockConnection = createMockConnection(mockReadable);
235
+ let capturedUrl = '';
236
+
237
+ mockWebSocketStream({
238
+ connection: mockConnection,
239
+ onUrl: (url) => {
240
+ capturedUrl = url;
241
+ },
242
+ });
243
+
244
+ await signalClient.join('wss://test.livekit.io', 'test-token', {
245
+ ...defaultOptions,
246
+ disableIceLite: true,
247
+ });
248
+
249
+ const joinRequest = await decodeJoinRequestFromUrl(capturedUrl);
250
+ expect(joinRequest.connectionSettings?.disableIceLite).toBe(true);
251
+ });
252
+
253
+ it('requests full ICE on the v0 path when disableIceLite is set', async () => {
254
+ const joinResponse = createJoinResponse();
255
+ const signalResponse = createSignalResponse('join', joinResponse);
256
+ const mockReadable = createMockReadableStream([signalResponse]);
257
+ const mockConnection = createMockConnection(mockReadable);
258
+ let capturedUrl = '';
259
+
260
+ mockWebSocketStream({
261
+ connection: mockConnection,
262
+ onUrl: (url) => {
263
+ capturedUrl = url;
264
+ },
265
+ });
266
+
267
+ await signalClient.join(
268
+ 'wss://test.livekit.io',
269
+ 'test-token',
270
+ { ...defaultOptions, disableIceLite: true },
271
+ undefined,
272
+ true,
273
+ );
274
+
275
+ expect(new URL(capturedUrl).searchParams.get('disable_ice_lite')).toBe('1');
276
+ });
277
+
278
+ it('leaves subscriberAllowPause unset by default so the server default applies', async () => {
279
+ const joinResponse = createJoinResponse();
280
+ const signalResponse = createSignalResponse('join', joinResponse);
281
+ const mockReadable = createMockReadableStream([signalResponse]);
282
+ const mockConnection = createMockConnection(mockReadable);
283
+ let capturedUrl = '';
284
+
285
+ mockWebSocketStream({
286
+ connection: mockConnection,
287
+ onUrl: (url) => {
288
+ capturedUrl = url;
289
+ },
290
+ });
291
+
292
+ await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
293
+
294
+ const joinRequest = await decodeJoinRequestFromUrl(capturedUrl);
295
+ expect(joinRequest.connectionSettings?.subscriberAllowPause).toBeUndefined();
296
+ });
297
+
298
+ it.each([true, false])(
299
+ 'sets subscriberAllowPause=%s in the join request',
300
+ async (subscriberAllowPause) => {
301
+ const joinResponse = createJoinResponse();
302
+ const signalResponse = createSignalResponse('join', joinResponse);
303
+ const mockReadable = createMockReadableStream([signalResponse]);
304
+ const mockConnection = createMockConnection(mockReadable);
305
+ let capturedUrl = '';
306
+
307
+ mockWebSocketStream({
308
+ connection: mockConnection,
309
+ onUrl: (url) => {
310
+ capturedUrl = url;
311
+ },
312
+ });
313
+
314
+ await signalClient.join('wss://test.livekit.io', 'test-token', {
315
+ ...defaultOptions,
316
+ subscriberAllowPause,
317
+ });
318
+
319
+ const joinRequest = await decodeJoinRequestFromUrl(capturedUrl);
320
+ expect(joinRequest.connectionSettings?.subscriberAllowPause).toBe(subscriberAllowPause);
321
+ },
322
+ );
323
+
324
+ it.each([
325
+ [undefined, null],
326
+ [true, '1'],
327
+ [false, '0'],
328
+ ])(
329
+ 'sets subscriber_allow_pause on the v0 path when subscriberAllowPause=%s',
330
+ async (subscriberAllowPause, expected) => {
331
+ const joinResponse = createJoinResponse();
332
+ const signalResponse = createSignalResponse('join', joinResponse);
333
+ const mockReadable = createMockReadableStream([signalResponse]);
334
+ const mockConnection = createMockConnection(mockReadable);
335
+ let capturedUrl = '';
336
+
337
+ mockWebSocketStream({
338
+ connection: mockConnection,
339
+ onUrl: (url) => {
340
+ capturedUrl = url;
341
+ },
342
+ });
343
+
344
+ await signalClient.join(
345
+ 'wss://test.livekit.io',
346
+ 'test-token',
347
+ { ...defaultOptions, subscriberAllowPause },
348
+ undefined,
349
+ true,
350
+ );
351
+
352
+ expect(new URL(capturedUrl).searchParams.get('subscriber_allow_pause')).toBe(expected);
353
+ },
354
+ );
209
355
  });
210
356
 
211
357
  describe('Happy Path - Reconnect', () => {
@@ -236,7 +382,7 @@ describe('SignalClient.connect', () => {
236
382
  expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
237
383
  });
238
384
 
239
- it('should handle reconnect with non-reconnect message (edge case)', async () => {
385
+ it('drops stray messages during reconnect and waits for the reconnect response', async () => {
240
386
  // First, initial connection
241
387
  const joinResponse = createJoinResponse();
242
388
  const joinSignalResponse = createSignalResponse('join', joinResponse);
@@ -247,19 +393,24 @@ describe('SignalClient.connect', () => {
247
393
 
248
394
  await signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions);
249
395
 
250
- // Setup reconnect with non-reconnect message (e.g., participant update)
396
+ // Server sends a stray update first, then the actual reconnect response.
397
+ // The stray should be dropped with a warning; the reconnect response should resolve.
251
398
  const updateSignalResponse = createSignalResponse('update', { participants: [] });
252
- const reconnectMockReadable = createMockReadableStream([updateSignalResponse]);
399
+ const reconnectResponse = new ReconnectResponse({ iceServers: [] });
400
+ const reconnectSignalResponse = createSignalResponse('reconnect', reconnectResponse);
401
+ const reconnectMockReadable = createMockReadableStream([
402
+ updateSignalResponse,
403
+ reconnectSignalResponse,
404
+ ]);
253
405
  const reconnectMockConnection = createMockConnection(reconnectMockReadable);
254
406
 
255
407
  mockWebSocketStream({ connection: reconnectMockConnection });
256
408
 
257
409
  const result = await signalClient.reconnect('wss://test.livekit.io', 'test-token', 'sid-123');
258
410
 
259
- // This is an edge case: reconnect resolves with undefined when non-reconnect message is received
260
- expect(result).toBeUndefined();
411
+ expect(result).toEqual(reconnectResponse);
261
412
  expect(signalClient.currentState).toBe(SignalConnectionState.CONNECTED);
262
- }, 1000);
413
+ });
263
414
  });
264
415
 
265
416
  describe('Failure Case - Timeout', () => {
@@ -1026,7 +1177,7 @@ describe('SignalClient.validateFirstMessage', () => {
1026
1177
  }
1027
1178
  });
1028
1179
 
1029
- it('should accept non-reconnect message during reconnecting state', async () => {
1180
+ it('should reject non-reconnect message during reconnecting state', async () => {
1030
1181
  // First establish a connection
1031
1182
  const joinResponse = createJoinResponse();
1032
1183
  const joinSignalResponse = createSignalResponse('join', joinResponse);
@@ -1044,9 +1195,8 @@ describe('SignalClient.validateFirstMessage', () => {
1044
1195
  const validateMethod = (signalClient as any).validateFirstMessage;
1045
1196
  if (validateMethod) {
1046
1197
  const result = validateMethod.call(signalClient, updateSignalResponse, true);
1047
- expect(result.isValid).toBe(true);
1048
- expect(result.response).toBeUndefined();
1049
- expect(result.shouldProcessFirstMessage).toBe(true);
1198
+ expect(result.isValid).toBe(false);
1199
+ expect(result.error).toBeInstanceOf(ConnectionError);
1050
1200
  }
1051
1201
  });
1052
1202
 
@@ -92,6 +92,8 @@ interface ConnectOpts extends SignalOptions {
92
92
  export interface SignalOptions {
93
93
  autoSubscribe: boolean;
94
94
  adaptiveStream?: boolean;
95
+ disableIceLite?: boolean;
96
+ subscriberAllowPause?: boolean;
95
97
  clientInfoCapabilities?: ClientInfo_Capability[];
96
98
  maxRetries: number;
97
99
  e2eeEnabled: boolean;
@@ -581,38 +583,61 @@ export class SignalClient {
581
583
  this.streamWriter = connection.writable.getWriter();
582
584
 
583
585
  // wsTimeout only guarded the upgrade; guard the first-message read with
584
- // its own timeout so a silent server can't hang join() forever.
585
- let firstMessage: ReadableStreamReadResult<string | ArrayBuffer>;
586
- let firstMessageTimeout: ReturnType<typeof setTimeout> | undefined;
586
+ // its own deadline so a silent server can't hang join() forever.
587
+ // During reconnect, drop any stray messages that arrive before the
588
+ // ReconnectResponse so we do not declare the channel reconnected on the
589
+ // wrong signal.
590
+ let firstSignalResponse: SignalResponse;
587
591
  try {
588
- firstMessage = await Promise.race([
589
- signalReader.read(),
590
- new Promise<never>((_, rejectRead) => {
591
- firstMessageTimeout = setTimeout(() => {
592
- rejectRead(
593
- ConnectionError.timeout(
594
- 'signal connection timed out while waiting for the first message',
595
- ),
596
- );
597
- }, JOIN_RESPONSE_TIMEOUT);
598
- }),
599
- ]);
592
+ const deadline = Date.now() + JOIN_RESPONSE_TIMEOUT;
593
+ // eslint-disable-next-line no-constant-condition
594
+ while (true) {
595
+ const remaining = deadline - Date.now();
596
+ if (remaining <= 0) {
597
+ throw ConnectionError.timeout(
598
+ 'signal connection timed out while waiting for the first message',
599
+ );
600
+ }
601
+ let readTimeout: ReturnType<typeof setTimeout> | undefined;
602
+ const readResult = await Promise.race([
603
+ signalReader.read(),
604
+ new Promise<never>((_, rejectRead) => {
605
+ readTimeout = setTimeout(() => {
606
+ rejectRead(
607
+ ConnectionError.timeout(
608
+ 'signal connection timed out while waiting for the first message',
609
+ ),
610
+ );
611
+ }, remaining);
612
+ }),
613
+ ]);
614
+ clearTimeout(readTimeout);
615
+ if (!readResult.value) {
616
+ throw ConnectionError.internal('no message received as first message');
617
+ }
618
+ const parsed = parseSignalResponse(readResult.value);
619
+ if (
620
+ this.lifecycleState === 'reconnecting' &&
621
+ parsed.message?.case !== 'reconnect' &&
622
+ parsed.message?.case !== 'leave'
623
+ ) {
624
+ this.log.warn('dropping signal message while awaiting reconnect response', {
625
+ messageCase: parsed.message?.case,
626
+ });
627
+ continue;
628
+ }
629
+ firstSignalResponse = parsed;
630
+ break;
631
+ }
600
632
  } catch (e) {
601
- // No first message in time: release the reader and tear down the ws
633
+ // No usable first message in time: release the reader and tear down the ws
602
634
  // so we surface the timeout instead of leaking an open connection.
603
635
  signalReader.releaseLock();
604
636
  reject(e);
605
637
  this.close();
606
638
  return;
607
- } finally {
608
- clearTimeout(firstMessageTimeout);
609
639
  }
610
640
  signalReader.releaseLock();
611
- if (!firstMessage.value) {
612
- throw ConnectionError.internal('no message received as first message');
613
- }
614
-
615
- const firstSignalResponse = parseSignalResponse(firstMessage.value);
616
641
 
617
642
  // Validate the first message
618
643
  const validation = this.validateFirstMessage(
@@ -643,11 +668,7 @@ export class SignalClient {
643
668
  }
644
669
  }
645
670
 
646
- // Handle successful connection
647
- const firstMessageToProcess = validation.shouldProcessFirstMessage
648
- ? firstSignalResponse
649
- : undefined;
650
- this.handleSignalConnected(connection, wsTimeout, attemptId, firstMessageToProcess);
671
+ this.handleSignalConnected(connection, wsTimeout, attemptId);
651
672
  resolve(validation.response);
652
673
  } catch (e) {
653
674
  reject(e);
@@ -660,13 +681,7 @@ export class SignalClient {
660
681
  });
661
682
  }
662
683
 
663
- async startReadingLoop(
664
- signalReader: ReadableStreamDefaultReader<string | ArrayBuffer>,
665
- firstMessage?: SignalResponse,
666
- ) {
667
- if (firstMessage) {
668
- this.handleSignalResponse(firstMessage);
669
- }
684
+ async startReadingLoop(signalReader: ReadableStreamDefaultReader<string | ArrayBuffer>) {
670
685
  const attemptId = this.attemptId;
671
686
  while (true) {
672
687
  if (this.signalLatency) {
@@ -1099,7 +1114,11 @@ export class SignalClient {
1099
1114
  * Handles a transport we lost without asking to. The client goes to `offline` rather than
1100
1115
  * `closed`: whether this session gets resumed, restarted or given up on is the engine's call.
1101
1116
  */
1102
- private async handleOnClose(reason: string, attemptId: number = this.attemptId) {
1117
+ private async handleOnClose(
1118
+ reason: string,
1119
+ attemptId: number = this.attemptId,
1120
+ simulateReconnectDelay?: number,
1121
+ ) {
1103
1122
  const onCloseCallback = this.onClose;
1104
1123
  if (!this.sendLifecycleInput({ type: 'transportFailed', attemptId, reason })) {
1105
1124
  // a close we caused ourselves, or one from a transport that has since been replaced: logged
@@ -1114,6 +1133,10 @@ export class SignalClient {
1114
1133
  }
1115
1134
  await this.teardownTransport(reason);
1116
1135
  this.log.info(`websocket connection closed: ${reason}`, { reason });
1136
+ if (simulateReconnectDelay) {
1137
+ this.log.info(`waiting for ${simulateReconnectDelay}ms until reconnecting signal`);
1138
+ await sleep(simulateReconnectDelay);
1139
+ }
1117
1140
  if (onCloseCallback) {
1118
1141
  onCloseCallback(reason);
1119
1142
  }
@@ -1184,7 +1207,6 @@ export class SignalClient {
1184
1207
  connection: WebSocketConnection,
1185
1208
  timeoutHandle: ReturnType<typeof setTimeout>,
1186
1209
  attemptId: number,
1187
- firstMessage?: SignalResponse,
1188
1210
  ) {
1189
1211
  clearTimeout(timeoutHandle);
1190
1212
  const established = this.sendLifecycleInput(
@@ -1205,7 +1227,7 @@ export class SignalClient {
1205
1227
  }
1206
1228
  this.log.info('signal connected');
1207
1229
  this.startPingInterval();
1208
- this.startReadingLoop(connection.readable.getReader(), firstMessage);
1230
+ this.startReadingLoop(connection.readable.getReader());
1209
1231
  }
1210
1232
 
1211
1233
  /**
@@ -1222,7 +1244,6 @@ export class SignalClient {
1222
1244
  isValid: boolean;
1223
1245
  response?: JoinResponse | ReconnectResponse;
1224
1246
  error?: ConnectionError;
1225
- shouldProcessFirstMessage?: boolean;
1226
1247
  } {
1227
1248
  if (firstSignalResponse.message?.case === 'join') {
1228
1249
  return {
@@ -1231,22 +1252,12 @@ export class SignalClient {
1231
1252
  };
1232
1253
  } else if (
1233
1254
  this.lifecycleState === 'reconnecting' &&
1234
- firstSignalResponse.message?.case !== 'leave'
1255
+ firstSignalResponse.message?.case === 'reconnect'
1235
1256
  ) {
1236
- if (firstSignalResponse.message?.case === 'reconnect') {
1237
- return {
1238
- isValid: true,
1239
- response: firstSignalResponse.message.value,
1240
- };
1241
- } else {
1242
- // in reconnecting, any message received means signal reconnected and we still need to process it
1243
- this.log.debug('declaring signal reconnected without reconnect response received');
1244
- return {
1245
- isValid: true,
1246
- response: undefined,
1247
- shouldProcessFirstMessage: true,
1248
- };
1249
- }
1257
+ return {
1258
+ isValid: true,
1259
+ response: firstSignalResponse.message.value,
1260
+ };
1250
1261
  } else if (this.isEstablishingConnection && firstSignalResponse.message?.case === 'leave') {
1251
1262
  return {
1252
1263
  isValid: false,
@@ -1394,6 +1405,14 @@ function createConnectionParams(
1394
1405
  params.set('adaptive_stream', '1');
1395
1406
  }
1396
1407
 
1408
+ if (opts.disableIceLite) {
1409
+ params.set('disable_ice_lite', '1');
1410
+ }
1411
+
1412
+ if (typeof opts.subscriberAllowPause !== 'undefined') {
1413
+ params.set('subscriber_allow_pause', opts.subscriberAllowPause ? '1' : '0');
1414
+ }
1415
+
1397
1416
  if (opts.reconnectReason) {
1398
1417
  params.set('reconnect_reason', opts.reconnectReason.toString());
1399
1418
  }
@@ -1421,6 +1440,8 @@ async function createJoinRequestConnectionParams(
1421
1440
  connectionSettings: new ConnectionSettings({
1422
1441
  autoSubscribe: !!opts.autoSubscribe,
1423
1442
  adaptiveStream: !!opts.adaptiveStream,
1443
+ disableIceLite: !!opts.disableIceLite,
1444
+ subscriberAllowPause: opts.subscriberAllowPause,
1424
1445
  }),
1425
1446
  reconnect: !!opts.reconnect,
1426
1447
  participantSid: opts.sid ? opts.sid : undefined,
@@ -2,6 +2,7 @@
2
2
  import { ConnectionError } from '../room/errors';
3
3
  import { sleep } from '../room/utils';
4
4
  import TypedPromise from '../utils/TypedPromise';
5
+ import { getErrorDescription } from './utils';
5
6
 
6
7
  export interface WebSocketConnection<T extends ArrayBuffer | string = ArrayBuffer | string> {
7
8
  readable: ReadableStream<T>;
@@ -68,15 +69,9 @@ export class WebSocketStream<T extends ArrayBuffer | string = ArrayBuffer | stri
68
69
  start(controller) {
69
70
  ws.onmessage = ({ data }) => controller.enqueue(data);
70
71
  ws.onerror = (e) =>
71
- controller.error(
72
- ConnectionError.websocket(
73
- e instanceof Error
74
- ? `${e.name}: ${e.message}`
75
- : `Encountered unknown websocket error: ${String(e)}`,
76
- ),
77
- );
72
+ controller.error(ConnectionError.websocket(getErrorDescription(e, 'websocket')));
78
73
  ws.onclose = (ev) => {
79
- if (ev.wasClean) {
74
+ if (ev.wasClean || ev.code === 1000) {
80
75
  controller.close();
81
76
  } else {
82
77
  controller.error(
package/src/api/utils.ts CHANGED
@@ -63,3 +63,13 @@ export function getAbortReasonAsString(
63
63
  return 'toString' in reason ? reason.toString() : defaultMessage;
64
64
  }
65
65
  }
66
+
67
+ export function getErrorDescription(error: unknown, errorCategory: string): string {
68
+ if (error instanceof Error) {
69
+ if (error.name && error.message) {
70
+ return `${error.name}: ${error.message}`;
71
+ }
72
+ return error.name;
73
+ }
74
+ return `Encountered unknown ${errorCategory} error: ${String(error)}`;
75
+ }