livekit-client 2.22.3 → 2.23.0

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 (272) 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 +71 -8
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +1746 -547
  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 +2 -1
  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 +1 -0
  17. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  18. package/dist/src/api/utils.d.ts +1 -1
  19. package/dist/src/api/utils.d.ts.map +1 -1
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  22. package/dist/src/e2ee/worker/naluUtils.d.ts +12 -1
  23. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  24. package/dist/src/index.d.ts +2 -2
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/options.d.ts +14 -0
  27. package/dist/src/options.d.ts.map +1 -1
  28. package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -1
  29. package/dist/src/room/PCTransport.d.ts +100 -7
  30. package/dist/src/room/PCTransport.d.ts.map +1 -1
  31. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  32. package/dist/src/room/RTCEngine.d.ts +16 -0
  33. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  34. package/dist/src/room/Room.d.ts +54 -1
  35. package/dist/src/room/Room.d.ts.map +1 -1
  36. package/dist/src/room/data-channel/DataChannelManager.d.ts +9 -8
  37. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -1
  38. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
  39. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -1
  40. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -1
  41. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -1
  42. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  43. package/dist/src/room/defaults.d.ts.map +1 -1
  44. package/dist/src/room/errors.d.ts +18 -0
  45. package/dist/src/room/errors.d.ts.map +1 -1
  46. package/dist/src/room/events.d.ts +3 -2
  47. package/dist/src/room/events.d.ts.map +1 -1
  48. package/dist/src/room/participant/LocalParticipant.d.ts +4 -0
  49. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  50. package/dist/src/room/participant/Participant.d.ts +1 -1
  51. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  52. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  53. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  54. package/dist/src/room/rpc/utils.d.ts.map +1 -1
  55. package/dist/src/room/track/LocalAudioTrack.d.ts +24 -1
  56. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/LocalTrack.d.ts +18 -0
  58. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  59. package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
  60. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  61. package/dist/src/room/track/RemoteAudioTrack.d.ts +6 -0
  62. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  63. package/dist/src/room/track/processor/types.d.ts +11 -0
  64. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  65. package/dist/src/room/track/utils.d.ts +13 -0
  66. package/dist/src/room/track/utils.d.ts.map +1 -1
  67. package/dist/src/room/utils.d.ts +60 -1
  68. package/dist/src/room/utils.d.ts.map +1 -1
  69. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  70. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  71. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  72. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  73. package/dist/ts4.2/api/WebSocketStream.d.ts +34 -33
  74. package/dist/ts4.2/api/utils.d.ts +9 -9
  75. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  76. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  77. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  78. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  79. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  80. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  81. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  82. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  83. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  84. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  85. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -111
  86. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  87. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  88. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  89. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  90. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  91. package/dist/ts4.2/e2ee/types.d.ts +231 -231
  92. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  93. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  94. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -21
  95. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -209
  96. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  97. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  98. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +39 -28
  99. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  100. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  101. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  102. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  103. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  104. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  105. package/dist/ts4.2/index.d.ts +76 -76
  106. package/dist/ts4.2/logger.d.ts +66 -66
  107. package/dist/ts4.2/options.d.ts +163 -149
  108. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  109. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  110. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  111. package/dist/ts4.2/room/PCTransport.d.ts +268 -175
  112. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  113. package/dist/ts4.2/room/RTCEngine.d.ts +277 -262
  114. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  115. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  116. package/dist/ts4.2/room/Room.d.ts +407 -365
  117. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  118. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +63 -62
  119. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +107 -95
  120. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  121. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  122. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  123. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  124. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  125. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  126. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  127. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  128. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  129. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  130. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  131. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  132. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  133. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  134. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  135. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  136. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  137. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  138. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  139. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  140. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  141. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  142. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  143. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  144. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  145. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  146. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  147. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  148. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  149. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  150. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  151. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  152. package/dist/ts4.2/room/debounce.d.ts +11 -11
  153. package/dist/ts4.2/room/defaults.d.ts +9 -9
  154. package/dist/ts4.2/room/errors.d.ts +180 -162
  155. package/dist/ts4.2/room/events.d.ts +638 -637
  156. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +291 -287
  157. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  158. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  159. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  160. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -53
  161. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  162. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  163. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  164. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  165. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  166. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  167. package/dist/ts4.2/room/stats.d.ts +71 -71
  168. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  169. package/dist/ts4.2/room/timers.d.ts +12 -12
  170. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  171. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  172. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  173. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  174. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +62 -42
  175. package/dist/ts4.2/room/track/LocalTrack.d.ts +143 -125
  176. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  177. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +109 -100
  178. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +61 -55
  179. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  180. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  181. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  182. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  183. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  184. package/dist/ts4.2/room/track/create.d.ts +25 -25
  185. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  186. package/dist/ts4.2/room/track/options.d.ts +403 -403
  187. package/dist/ts4.2/room/track/processor/types.d.ts +42 -31
  188. package/dist/ts4.2/room/track/record.d.ts +13 -13
  189. package/dist/ts4.2/room/track/types.d.ts +28 -28
  190. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  191. package/dist/ts4.2/room/types.d.ts +127 -127
  192. package/dist/ts4.2/room/utils.d.ts +273 -214
  193. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  194. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  195. package/dist/ts4.2/test/mocks.d.ts +12 -12
  196. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  197. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  198. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  199. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  200. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  201. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  202. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  203. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  204. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  205. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  206. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  207. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  208. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  209. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  210. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  211. package/dist/ts4.2/version.d.ts +15 -15
  212. package/package.json +5 -4
  213. package/src/api/SignalClient.e2e.test.ts +1 -3
  214. package/src/api/SignalClient.test.ts +394 -10
  215. package/src/api/SignalClient.ts +132 -76
  216. package/src/api/WebSocketStream.test.ts +11 -0
  217. package/src/api/WebSocketStream.ts +22 -1
  218. package/src/api/utils.ts +1 -1
  219. package/src/e2ee/E2eeManager.test.ts +43 -0
  220. package/src/e2ee/E2eeManager.ts +6 -2
  221. package/src/e2ee/worker/FrameCryptor.test.ts +212 -90
  222. package/src/e2ee/worker/FrameCryptor.ts +21 -4
  223. package/src/e2ee/worker/naluUtils.test.ts +123 -0
  224. package/src/e2ee/worker/naluUtils.ts +57 -2
  225. package/src/index.ts +2 -0
  226. package/src/options.ts +16 -0
  227. package/src/room/DefaultReconnectPolicy.test.ts +58 -0
  228. package/src/room/DefaultReconnectPolicy.ts +14 -2
  229. package/src/room/PCTransport.test.ts +327 -9
  230. package/src/room/PCTransport.ts +339 -36
  231. package/src/room/PCTransportManager.ts +3 -1
  232. package/src/room/RTCEngine.test.ts +318 -2
  233. package/src/room/RTCEngine.ts +126 -12
  234. package/src/room/Room.test.ts +148 -6
  235. package/src/room/Room.ts +312 -80
  236. package/src/room/data-channel/DataChannelManager.test.ts +24 -7
  237. package/src/room/data-channel/DataChannelManager.ts +43 -38
  238. package/src/room/data-channel/FlowControlledDataChannel.test.ts +60 -18
  239. package/src/room/data-channel/FlowControlledDataChannel.ts +40 -22
  240. package/src/room/data-channel/LossyDataChannel.test.ts +38 -0
  241. package/src/room/data-channel/LossyDataChannel.ts +10 -1
  242. package/src/room/data-channel/ReliableDataChannel.test.ts +72 -0
  243. package/src/room/data-channel/ReliableDataChannel.ts +16 -5
  244. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +47 -20
  245. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +16 -5
  246. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  247. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  248. package/src/room/defaults.ts +1 -0
  249. package/src/room/errors.test.ts +36 -0
  250. package/src/room/errors.ts +27 -0
  251. package/src/room/events.ts +2 -1
  252. package/src/room/participant/LocalParticipant.test.ts +136 -1
  253. package/src/room/participant/LocalParticipant.ts +66 -17
  254. package/src/room/participant/Participant.ts +6 -3
  255. package/src/room/participant/publishUtils.ts +0 -19
  256. package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
  257. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  258. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  259. package/src/room/rpc/utils.test.ts +32 -0
  260. package/src/room/rpc/utils.ts +7 -0
  261. package/src/room/track/LocalAudioTrack.ts +101 -10
  262. package/src/room/track/LocalTrack.ts +87 -19
  263. package/src/room/track/LocalVideoTrack.ts +17 -3
  264. package/src/room/track/RemoteAudioTrack.ts +59 -14
  265. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  266. package/src/room/track/RemoteVideoTrack.ts +29 -3
  267. package/src/room/track/processor/types.ts +11 -0
  268. package/src/room/track/utils.test.ts +206 -2
  269. package/src/room/track/utils.ts +117 -5
  270. package/src/room/utils.test.ts +250 -0
  271. package/src/room/utils.ts +152 -19
  272. package/src/test/compressionStreamStubs.ts +53 -0
@@ -0,0 +1,58 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import DefaultReconnectPolicy from './DefaultReconnectPolicy';
3
+
4
+ const delayFor = (policy: DefaultReconnectPolicy, retryCount: number, random: number) => {
5
+ vi.spyOn(Math, 'random').mockReturnValue(random);
6
+ return policy.nextRetryDelayInMs({ retryCount, elapsedMs: 0 });
7
+ };
8
+
9
+ describe('DefaultReconnectPolicy', () => {
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ it('spreads the first retry over a window instead of retrying immediately', () => {
15
+ const policy = new DefaultReconnectPolicy();
16
+ expect(delayFor(policy, 0, 0)).toBe(0);
17
+ expect(delayFor(policy, 0, 0.5)).toBe(250);
18
+ expect(delayFor(policy, 0, 0.999)).toBeGreaterThan(450);
19
+ expect(delayFor(policy, 0, 0.999)).toBeLessThan(500);
20
+ });
21
+
22
+ it('scales the jitter of later retries with the delay', () => {
23
+ const policy = new DefaultReconnectPolicy();
24
+ expect(delayFor(policy, 1, 0)).toBe(150);
25
+ expect(delayFor(policy, 1, 0.5)).toBe(300);
26
+ expect(delayFor(policy, 2, 0)).toBe(600);
27
+ expect(delayFor(policy, 2, 0.5)).toBe(1200);
28
+ expect(delayFor(policy, 9, 0)).toBe(3500);
29
+ expect(delayFor(policy, 9, 0.999)).toBeLessThan(10500);
30
+ });
31
+
32
+ it('returns a different delay for each client on every retry', () => {
33
+ const policy = new DefaultReconnectPolicy();
34
+ for (let retryCount = 0; retryCount < 10; retryCount++) {
35
+ expect(delayFor(policy, retryCount, 0.1)).not.toBe(delayFor(policy, retryCount, 0.9));
36
+ }
37
+ });
38
+
39
+ it('returns null once the retries are exhausted', () => {
40
+ const policy = new DefaultReconnectPolicy([0, 100]);
41
+ expect(policy.nextRetryDelayInMs({ retryCount: 2, elapsedMs: 0 })).toBeNull();
42
+ expect(
43
+ new DefaultReconnectPolicy().nextRetryDelayInMs({ retryCount: 10, elapsedMs: 0 }),
44
+ ).toBeNull();
45
+ });
46
+
47
+ it('applies jitter to custom delays', () => {
48
+ const policy = new DefaultReconnectPolicy([1000]);
49
+ expect(delayFor(policy, 0, 0.5)).toBe(1000);
50
+ expect(delayFor(policy, 0, 0)).toBe(500);
51
+ });
52
+
53
+ it('keeps a zero delay at a later index immediate', () => {
54
+ const policy = new DefaultReconnectPolicy([100, 0]);
55
+ expect(delayFor(policy, 1, 0.5)).toBe(0);
56
+ expect(delayFor(policy, 1, 0.999)).toBe(0);
57
+ });
58
+ });
@@ -2,6 +2,12 @@ import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
2
2
 
3
3
  const maxRetryDelay = 7000;
4
4
 
5
+ /**
6
+ * Upper bound of the random delay added to the first retry, whose base delay is 0. It keeps a
7
+ * brief network blip recovering quickly while spreading out clients that disconnect together.
8
+ */
9
+ const maxFirstRetryJitterInMs = 500;
10
+
5
11
  const DEFAULT_RETRY_DELAYS_IN_MS = [
6
12
  0,
7
13
  300,
@@ -26,9 +32,15 @@ class DefaultReconnectPolicy implements ReconnectPolicy {
26
32
  if (context.retryCount >= this._retryDelays.length) return null;
27
33
 
28
34
  const retryDelay = this._retryDelays[context.retryCount];
29
- if (context.retryCount <= 1) return retryDelay;
30
35
 
31
- return retryDelay + Math.random() * 1_000;
36
+ // The first retry has no base delay to scale, so it gets a fixed window instead. A zero at a
37
+ // later index is an explicit request for an immediate retry and passes through the formula as zero.
38
+ if (context.retryCount === 0 && retryDelay === 0) {
39
+ return Math.random() * maxFirstRetryJitterInMs;
40
+ }
41
+
42
+ // Jitter scales with the delay (+/-50%) so clients spread out further on later retries.
43
+ return Math.round(retryDelay * (0.5 + Math.random()));
32
44
  }
33
45
  }
34
46
 
@@ -1,12 +1,16 @@
1
1
  import { type MediaDescription, parse } from 'sdp-transform';
2
- import { describe, expect, it } from 'vitest';
3
- import {
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import PCTransport, {
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,301 @@ 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`;
85
+
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);
83
139
 
84
- for (const section of media) {
85
- applyVideoStartBitrate(section, 'camera-cid', 'VP8', 1_000);
86
- }
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('ramps the cap down with connection setup time', () => {
160
+ // A 3 Mbps camera target, so only the cap moves.
161
+ const camera = (ms: number) =>
162
+ computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 3_000 }, ms);
163
+
164
+ expect(camera(0)).toBe(1_000);
165
+ expect(camera(471)).toBe(1_000); // unshaped baseline
166
+ expect(camera(1_273)).toBe(1_000); // 1 Mbps link median
167
+ expect(camera(1_500)).toBe(1_000); // the fast anchor
168
+ expect(camera(1_724)).toBe(922); // 1 Mbps link slow attempt
169
+ expect(camera(2_334)).toBe(708); // 500 kbps link median
170
+ expect(camera(2_500)).toBe(650); // midpoint of the ramp
171
+ expect(camera(3_093)).toBe(442); // 300 kbps link fastest attempt
172
+ expect(camera(3_500)).toBe(300); // the slow anchor
173
+ expect(camera(18_131)).toBe(300); // anything slower stays at the floor
174
+ });
175
+
176
+ it('keeps the 1 Mbps ceiling without a setup time', () => {
177
+ expect(computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 3_000 }, undefined)).toBe(
178
+ 1_000,
179
+ );
180
+ });
181
+
182
+ it('still applies 90% of the target under the ramp, and hints at the floor', () => {
183
+ expect(computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 500 }, 2_500)).toBe(450);
184
+ expect(computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 300 }, 3_500)).toBe(270);
185
+ });
186
+
187
+ it('leaves screen share uncapped however slow the setup', () => {
188
+ const screenShare = (ms: number) =>
189
+ computeTrackStartBitrate({ cid: 's', codec: 'VP8', maxbr: 3_000, isScreenShare: true }, ms);
190
+
191
+ expect(screenShare(1_273)).toBe(2_700);
192
+ expect(screenShare(2_500)).toBe(2_700);
193
+ expect(screenShare(4_061)).toBe(2_700);
194
+ });
195
+
196
+ it('applies the setup time to the connection-level value', () => {
197
+ const { media } = parse(TWO_VIDEO_SECTIONS);
198
+ const camera = { cid: 'camera-cid', codec: 'VP8', maxbr: 3_000 };
199
+ const screenShare = { cid: 'other-track', codec: 'VP8', maxbr: 3_000, isScreenShare: true };
200
+
201
+ expect(computeConnectionStartBitrate(media, [camera], 1_000)).toBe(1_000);
202
+ expect(computeConnectionStartBitrate(media, [camera], 2_500)).toBe(650);
203
+ // The uncapped screen share still wins the connection-level max on a slow setup.
204
+ expect(computeConnectionStartBitrate(media, [camera, screenShare], 2_500)).toBe(2_700);
205
+ });
206
+
207
+ describe('browser effective connection type', () => {
208
+ const withEffectiveType = (effectiveType: string | undefined) =>
209
+ vi.stubGlobal('navigator', { connection: effectiveType ? { effectiveType } : undefined });
210
+
211
+ afterEach(() => {
212
+ vi.unstubAllGlobals();
213
+ });
214
+
215
+ // A 3 Mbps camera target, so only the cap moves.
216
+ const camera = (setupTimeMs?: number) =>
217
+ computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 3_000 }, setupTimeMs);
218
+
219
+ it('caps 2g and slow-2g at 300 kbps and 3g at 500 kbps', () => {
220
+ withEffectiveType('slow-2g');
221
+ expect(camera(500)).toBe(300);
222
+ withEffectiveType('2g');
223
+ expect(camera(500)).toBe(300);
224
+ withEffectiveType('3g');
225
+ expect(camera(500)).toBe(500);
226
+ });
227
+
228
+ it('gives no cap for 4g or without the API', () => {
229
+ withEffectiveType('4g');
230
+ expect(camera(500)).toBe(1_000);
231
+ withEffectiveType(undefined);
232
+ expect(camera(500)).toBe(1_000);
233
+ vi.stubGlobal('navigator', undefined);
234
+ expect(camera(500)).toBe(1_000);
235
+ });
236
+
237
+ it('takes the lower of the setup time cap and the effective type cap', () => {
238
+ withEffectiveType('3g');
239
+ expect(camera(2_500)).toBe(500); // setup time alone allows 650
240
+ expect(camera(3_093)).toBe(442); // setup time is lower
241
+ expect(camera()).toBe(500);
242
+ });
243
+
244
+ it('still applies 90% of a target below the cap', () => {
245
+ withEffectiveType('3g');
246
+ expect(computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 450 }, 500)).toBe(405);
247
+ });
248
+
249
+ it('leaves screen share uncapped', () => {
250
+ withEffectiveType('slow-2g');
251
+ expect(
252
+ computeTrackStartBitrate(
253
+ { cid: 's', codec: 'VP8', maxbr: 3_000, isScreenShare: true },
254
+ 500,
255
+ ),
256
+ ).toBe(2_700);
257
+ });
258
+ });
259
+
260
+ describe('connection setup time while still connecting', () => {
261
+ beforeEach(() => {
262
+ // The constructor creates its peer connection eagerly; only its event handlers are set.
263
+ vi.stubGlobal('RTCPeerConnection', class {});
264
+ });
265
+
266
+ afterEach(() => {
267
+ vi.restoreAllMocks();
268
+ vi.unstubAllGlobals();
269
+ });
270
+
271
+ const estimate = (transport: PCTransport) =>
272
+ (
273
+ transport as unknown as { connectionSetupTimeEstimateMs(): number | undefined }
274
+ ).connectionSetupTimeEstimateMs();
275
+
276
+ it('uses the time elapsed since the join started before the setup time is measured', () => {
277
+ const transport = new PCTransport();
278
+ vi.spyOn(performance, 'now').mockReturnValue(10_000);
279
+ transport.setConnectionStartedAt(7_500);
280
+
281
+ expect(estimate(transport)).toBe(2_500);
282
+ expect(
283
+ computeTrackStartBitrate({ cid: 'c', codec: 'VP8', maxbr: 3_000 }, estimate(transport)),
284
+ ).toBe(650);
285
+ });
286
+
287
+ it('prefers the measured setup time once known', () => {
288
+ const transport = new PCTransport();
289
+ vi.spyOn(performance, 'now').mockReturnValue(10_000);
290
+ transport.setConnectionStartedAt(7_500);
291
+ transport.setConnectionSetupTime(3_600);
292
+
293
+ expect(estimate(transport)).toBe(3_600);
294
+ });
295
+
296
+ it('has no estimate without any timing', () => {
297
+ expect(estimate(new PCTransport())).toBeUndefined();
298
+ });
299
+ });
300
+
301
+ it('uses one connection-level value: the largest hint across video sections', () => {
302
+ const { media } = parse(TWO_VIDEO_SECTIONS);
303
+
304
+ const startBitrate = computeConnectionStartBitrate(media, [
305
+ { cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 },
306
+ { cid: 'other-track', codec: 'VP8', maxbr: 3_000, isScreenShare: true },
307
+ ]);
308
+
309
+ expect(startBitrate).toBe(2_700);
310
+ });
311
+
312
+ it('ignores registered tracks with no section in the current SDP', () => {
313
+ const { media } = parse(TWO_VIDEO_SECTIONS);
314
+
315
+ const startBitrate = computeConnectionStartBitrate(media, [
316
+ { cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 },
317
+ // Stale entry: trackBitrates is append-only and outlives an unpublish.
318
+ { cid: 'unpublished-cid', codec: 'VP8', maxbr: 8_000, isScreenShare: true },
319
+ ]);
320
+
321
+ expect(startBitrate).toBe(900);
322
+ });
323
+
324
+ it('ignores a section that stopped sending but kept its msid', () => {
325
+ const trackBitrates = [
326
+ { cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 },
327
+ { cid: 'other-track', codec: 'VP8', maxbr: 8_000, isScreenShare: true },
328
+ ];
329
+
330
+ // While both send, the uncapped screen share wins the connection-level max.
331
+ expect(computeConnectionStartBitrate(parse(TWO_VIDEO_SECTIONS).media, trackBitrates)).toBe(
332
+ 7_200,
333
+ );
334
+ // Once it is unpublished its entry is stale, so only the capped camera counts. Both
335
+ // directions a removed sender can land on are excluded: `inactive` from a sendonly
336
+ // transceiver, `recvonly` from the sendrecv one the addTrack fallback reuses.
337
+ expect(
338
+ computeConnectionStartBitrate(parse(UNPUBLISHED_SCREEN_SHARE).media, trackBitrates),
339
+ ).toBe(900);
340
+ expect(
341
+ computeConnectionStartBitrate(
342
+ parse(UNPUBLISHED_SCREEN_SHARE.replace('a=inactive', 'a=recvonly')).media,
343
+ trackBitrates,
344
+ ),
345
+ ).toBe(900);
346
+ });
347
+
348
+ it('gives no connection value when no section maps to a published track', () => {
349
+ const { media } = parse(TWO_VIDEO_SECTIONS);
350
+
351
+ expect(computeConnectionStartBitrate(media, [])).toBeUndefined();
352
+ });
353
+
354
+ it('counts sendrecv and direction-less sections, which still send local media', () => {
355
+ // The legacy addTrack fallback never produces `sendonly`, so a strict match on it would
356
+ // leave those clients with no hint at all.
357
+ const { media } = parse(LEGACY_ADD_TRACK_SECTIONS);
358
+
359
+ expect(
360
+ computeConnectionStartBitrate(media, [{ cid: 'camera-cid', codec: 'VP8', maxbr: 1_000 }]),
361
+ ).toBe(900);
362
+ expect(
363
+ computeConnectionStartBitrate(media, [
364
+ { cid: 'other-track', codec: 'VP8', maxbr: 2_000, isScreenShare: true },
365
+ ]),
366
+ ).toBe(1_800);
367
+ });
368
+
369
+ it('leaves the hint unset when only non-sending sections match', () => {
370
+ // Nothing is published, so the one-shot hint must not be consumed on a dead section.
371
+ const { media } = parse(UNPUBLISHED_SCREEN_SHARE);
372
+
373
+ expect(
374
+ computeConnectionStartBitrate(media, [
375
+ { cid: 'other-track', codec: 'VP8', maxbr: 8_000, isScreenShare: true },
376
+ ]),
377
+ ).toBeUndefined();
378
+ });
91
379
  });
92
380
 
93
381
  describe('placeholderMidsFromTransceivers', () => {
@@ -390,3 +678,33 @@ a=extmap:3 ${ddExtensionURI}`);
390
678
  expect(ddOf(sdp.media, '1')).toBe(12);
391
679
  });
392
680
  });
681
+
682
+ describe('isDuplicateAnswer', () => {
683
+ it('drops an answer while the same offer is still being answered', () => {
684
+ expect(isDuplicateAnswer(3, 3, 0, 'have-local-offer', false)).toBe(true);
685
+ });
686
+
687
+ it('drops a repeat of an unnumbered answer while the first is still applying', () => {
688
+ expect(isDuplicateAnswer(0, 0, 0, 'have-local-offer', false)).toBe(true);
689
+ });
690
+
691
+ it('drops an answer for an offer whose answer was already applied', () => {
692
+ expect(isDuplicateAnswer(3, undefined, 3, 'stable', false)).toBe(true);
693
+ });
694
+
695
+ it('accepts a retry after a failed apply: nothing in flight, nothing applied', () => {
696
+ expect(isDuplicateAnswer(3, undefined, 2, 'have-local-offer', false)).toBe(false);
697
+ });
698
+
699
+ it('accepts the answer for a new offer', () => {
700
+ expect(isDuplicateAnswer(4, undefined, 3, 'have-local-offer', false)).toBe(false);
701
+ });
702
+
703
+ it('without an offerId, drops an answer in stable', () => {
704
+ expect(isDuplicateAnswer(0, undefined, 0, 'stable', false)).toBe(true);
705
+ });
706
+
707
+ it('without an offerId, accepts the answer to a deferred initial offer', () => {
708
+ expect(isDuplicateAnswer(0, undefined, 0, 'stable', true)).toBe(false);
709
+ });
710
+ });