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
@@ -28,57 +28,232 @@ interface TrackBitrateInfo {
28
28
  * Why same for all codecs: Target bitrate already accounts for codec efficiency
29
29
  * (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
30
30
  * Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
31
+ * The cap is lowered further for connections that were slow to set up, see
32
+ * `computeStartBitrateCap`, or that the browser reports as slow, see
33
+ * `connectionEffectiveTypeStartBitrateCap`.
31
34
  */
32
35
  const startBitrateMultiplier = 0.9;
33
36
 
34
37
  /** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
35
38
  const maxStartBitrateKbps = 1000;
36
39
 
40
+ /**
41
+ * Minimum x-google-start-bitrate in kbps: libwebrtc's own default starting estimate. A target
42
+ * below this gets no hint, since seeding above the real capacity costs more than the ramp it
43
+ * saves, and a slow connection is seeded no higher than this.
44
+ */
45
+ const minTargetBitrateKbps = 300;
46
+
47
+ /**
48
+ * Connection setup times (ms) bounding the ramp in `computeStartBitrateCap`: at or below the
49
+ * first the hint is capped at `maxStartBitrateKbps`, at or above the second at
50
+ * `minTargetBitrateKbps`. Measured on shaped links (CLT-3380): healthy links set up in under
51
+ * 860 ms, a 1 Mbps link with 150 ms RTT took 1.1–1.7 s, a 500 kbps link ~2.3 s at the median,
52
+ * and a 300 kbps link never under 3 s. The first sits above the 1 Mbps link, which a lower seed
53
+ * only slows down, and the second above the 500 kbps link's median, so only links that cannot
54
+ * carry more land at the floor.
55
+ */
56
+ const setupTimeForMaxStartBitrateMs = 1500;
57
+ const setupTimeForMinStartBitrateMs = 3500;
58
+
59
+ /** Cap on the start bitrate hint, in kbps, when the browser reports a `3g` connection. */
60
+ const effectiveType3gStartBitrateKbps = 500;
61
+
62
+ /**
63
+ * The part of the Network Information API (`navigator.connection`) read here. It is Chromium
64
+ * only, so TypeScript's DOM typings do not declare it.
65
+ */
66
+ interface NetworkInformation {
67
+ readonly effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';
68
+ }
69
+
37
70
  const debounceInterval = 20;
38
71
 
39
72
  /**
40
- * Applies the configured start bitrate when this media section belongs to `cid`.
41
- * This SDP munging is used for a bitrate setting that cannot be applied through
42
- * `RTCRtpEncodingParameters`.
73
+ * Codec payload for `codec` in this media section, when the section carries `cid`.
43
74
  *
44
- * Returns `undefined` when the section does not belong to the track, `0` when
45
- * it does but does not offer the requested codec, and the codec payload when the
46
- * requested codec is present (whether the bitrate was added or already set).
75
+ * Returns `undefined` when the section does not belong to the track, `0` when it does but
76
+ * does not offer the requested codec, and the codec payload otherwise.
47
77
  *
48
78
  * @internal
49
79
  */
50
- export function applyVideoStartBitrate(
80
+ export function findTrackCodecPayload(
51
81
  media: MediaDescription,
52
82
  cid: string,
53
83
  codec: string,
54
- maxbr: number,
55
- isScreenShare = false,
56
84
  ): number | undefined {
57
85
  if (!media.msid?.includes(cid)) {
58
86
  return undefined;
59
87
  }
88
+ return media.rtp.find((rtp) => rtp.codec.toUpperCase() === codec.toUpperCase())?.payload ?? 0;
89
+ }
60
90
 
61
- const codecPayload =
62
- media.rtp.find((rtp) => rtp.codec.toUpperCase() === codec.toUpperCase())?.payload ?? 0;
63
- if (codecPayload === 0) {
64
- return 0;
91
+ /**
92
+ * Start bitrate hinted for a single track, or `undefined` when its target is too low to
93
+ * be worth seeding.
94
+ *
95
+ * 90% of the target leaves ~10% headroom for the estimator to settle. The same multiplier
96
+ * is used for every codec because the target already reflects the codec's efficiency.
97
+ * Camera is capped at 1 Mbps so the estimator does not open too aggressively on a
98
+ * high-bitrate track; screen share is exempt, because its content needs the bitrate
99
+ * immediately to stay legible.
100
+ *
101
+ * The cap is lowered for a connection that was slow to set up (`computeStartBitrateCap`), and
102
+ * for one the browser reports as slow (`connectionEffectiveTypeStartBitrateCap`), whichever is
103
+ * lower. Screen share is exempt from both, however slow the connection.
104
+ *
105
+ * @internal
106
+ */
107
+ export function computeTrackStartBitrate(
108
+ trackbr: TrackBitrateInfo,
109
+ connectionSetupTimeMs?: number,
110
+ ): number | undefined {
111
+ if (trackbr.maxbr < minTargetBitrateKbps) {
112
+ return undefined;
65
113
  }
114
+ const calculated = Math.min(Math.round(trackbr.maxbr * startBitrateMultiplier), trackbr.maxbr);
115
+ if (trackbr.isScreenShare) {
116
+ return calculated;
117
+ }
118
+ const cap = Math.min(
119
+ computeStartBitrateCap(connectionSetupTimeMs),
120
+ connectionEffectiveTypeStartBitrateCap(),
121
+ );
122
+ return Math.min(calculated, cap);
123
+ }
66
124
 
67
- // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
68
- // from starting too aggressively. Screen share is not capped since text/UI
69
- // clarity requires high bitrate from the start.
70
- // TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
71
- const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
72
- const startBitrate = isScreenShare
73
- ? calculatedStartBitrate
74
- : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
125
+ /**
126
+ * Cap on the start bitrate hint, in kbps, from the browser's effective connection type
127
+ * (`navigator.connection.effectiveType`, Chromium only): `slow-2g` and `2g` cap at
128
+ * `minTargetBitrateKbps`, `3g` at `effectiveType3gStartBitrateKbps`.
129
+ *
130
+ * It catches links that have low bandwidth but low latency, which set up quickly and so slip
131
+ * past the setup time ramp. `4g`, and browsers without the API, give no guidance and stay at the
132
+ * `maxStartBitrateKbps` ceiling, as `computeStartBitrateCap` does without a setup time. The
133
+ * connection RTT the API also reports is not used: it lags behind network changes, and the setup
134
+ * time already reflects latency.
135
+ */
136
+ function connectionEffectiveTypeStartBitrateCap(): number {
137
+ // A `typeof` guard, since `globalThis` is missing from some supported browsers (Chrome < 71,
138
+ // Firefox < 65, Safari < 12.1) and the build does not polyfill it.
139
+ const connection =
140
+ typeof navigator !== 'undefined'
141
+ ? (navigator as Navigator & { connection?: NetworkInformation }).connection
142
+ : undefined;
143
+ switch (connection?.effectiveType) {
144
+ case 'slow-2g':
145
+ case '2g':
146
+ return minTargetBitrateKbps;
147
+ case '3g':
148
+ return effectiveType3gStartBitrateKbps;
149
+ default:
150
+ return maxStartBitrateKbps;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Cap on the start bitrate hint, in kbps, from how long the connection took to set up.
156
+ *
157
+ * Connection setup time (signaling join plus ICE/DTLS) is the network signal every platform has
158
+ * before the first video offer, since libwebrtc cannot probe the path until a video sender
159
+ * exists. It grows with round-trip time and loss, which also mark the links where a 1 Mbps
160
+ * seed overshoots, so the cap ramps linearly from `maxStartBitrateKbps` at
161
+ * `setupTimeForMaxStartBitrateMs` down to `minTargetBitrateKbps` at
162
+ * `setupTimeForMinStartBitrateMs`. A video offer built while the connection is still being set
163
+ * up (publishing during connect) passes the time elapsed so far, a lower bound on the setup time.
164
+ * Without any timing the cap stays at the 1 Mbps ceiling.
165
+ */
166
+ function computeStartBitrateCap(connectionSetupTimeMs?: number): number {
167
+ if (connectionSetupTimeMs === undefined) {
168
+ return maxStartBitrateKbps;
169
+ }
170
+ const ramp = Math.min(
171
+ Math.max(
172
+ (connectionSetupTimeMs - setupTimeForMaxStartBitrateMs) /
173
+ (setupTimeForMinStartBitrateMs - setupTimeForMaxStartBitrateMs),
174
+ 0,
175
+ ),
176
+ 1,
177
+ );
178
+ return Math.round(maxStartBitrateKbps - ramp * (maxStartBitrateKbps - minTargetBitrateKbps));
179
+ }
180
+
181
+ /**
182
+ * The single start bitrate for this peer connection: the largest hint among the video
183
+ * m-sections of `media` that map to a published track.
184
+ *
185
+ * libwebrtc reads `x-google-start-bitrate` per m-section but applies it to the shared
186
+ * `Call` (`WebRtcVideoSendChannel::ApplyChangedParams` -> `SetSdpBitrateParameters`), where
187
+ * `RtpBitrateConfigurator` holds one config for the whole connection. Differing per-section
188
+ * values are therefore last-writer-wins, decided by m-section order, so every video section
189
+ * gets the same number instead.
190
+ *
191
+ * Only sections that can send local media are considered. `trackBitrates` is append-only and an
192
+ * unpublished section keeps its `a=msid`, so matching on msid alone would still pair a stale
193
+ * entry with its old section — letting an uncapped screen-share target seed a connection that
194
+ * now carries only a camera, or consuming the one-shot hint on a section that sends nothing.
195
+ *
196
+ * The direction is the discriminator, as an exclusion rather than a match: `recvonly` and
197
+ * `inactive` are the only directions that cannot carry local media, and they are exactly the
198
+ * two a dead section lands on — unpublish sets `inactive` (explicitly in
199
+ * `LocalParticipant.unpublishTrack`, and via `removeTrack`'s sendonly -> inactive transition for
200
+ * the simulcast senders), `removeTrack` on a `sendrecv` transceiver leaves `recvonly`, and the
201
+ * pre-populated placeholders are `recvonly`. Everything else sends: `sendonly` from the
202
+ * `addTransceiver` path, `sendrecv` from the legacy `addTrack` fallback (which reuses a
203
+ * transceiver rather than creating a sendonly one), and a section with no direction attribute,
204
+ * which SDP defaults to `sendrecv`.
205
+ *
206
+ * @internal
207
+ */
208
+ export function computeConnectionStartBitrate(
209
+ media: MediaDescription[],
210
+ trackBitrates: TrackBitrateInfo[],
211
+ connectionSetupTimeMs?: number,
212
+ ): number | undefined {
213
+ let connectionStartBitrate: number | undefined;
214
+ for (const m of media) {
215
+ if (m.type !== 'video' || m.direction === 'recvonly' || m.direction === 'inactive') {
216
+ continue;
217
+ }
218
+ for (const trackbr of trackBitrates) {
219
+ if (!trackbr.cid) {
220
+ continue;
221
+ }
222
+ const codecPayload = findTrackCodecPayload(m, trackbr.cid, trackbr.codec);
223
+ if (codecPayload === undefined) {
224
+ continue;
225
+ }
226
+ const startBitrate =
227
+ codecPayload > 0 ? computeTrackStartBitrate(trackbr, connectionSetupTimeMs) : undefined;
228
+ if (
229
+ startBitrate !== undefined &&
230
+ (connectionStartBitrate === undefined || startBitrate > connectionStartBitrate)
231
+ ) {
232
+ connectionStartBitrate = startBitrate;
233
+ }
234
+ break;
235
+ }
236
+ }
237
+ return connectionStartBitrate;
238
+ }
75
239
 
240
+ /**
241
+ * Declares `x-google-start-bitrate` on `codecPayload`'s fmtp. This SDP munging is used for
242
+ * a bitrate setting that cannot be applied through `RTCRtpEncodingParameters`.
243
+ *
244
+ * Returns whether the section now carries the hint.
245
+ *
246
+ * @internal
247
+ */
248
+ export function applyVideoStartBitrate(
249
+ media: MediaDescription,
250
+ codecPayload: number,
251
+ startBitrate: number,
252
+ ): boolean {
76
253
  const fmtp = media.fmtp.find((entry) => entry.payload === codecPayload);
77
254
  if (fmtp) {
78
- // If another track's fmtp already has a start bitrate, it cannot be
79
- // overridden here because the payload type is shared across the bundle.
80
- // This forces every track sharing that payload to use the initial track's
81
- // start bitrate.
255
+ // A payload type is shared across the bundle, so a value written for one section is
256
+ // already the connection-level one; leave it rather than rewrite it.
82
257
  if (!fmtp.config.includes('x-google-start-bitrate')) {
83
258
  fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
84
259
  }
@@ -90,7 +265,7 @@ export function applyVideoStartBitrate(
90
265
  });
91
266
  }
92
267
 
93
- return codecPayload;
268
+ return true;
94
269
  }
95
270
 
96
271
  export const PCEvents = {
@@ -127,6 +302,11 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
127
302
 
128
303
  latestOfferId: number = 0;
129
304
 
305
+ private answeredOfferId: number = 0;
306
+
307
+ /** offerId (0 if unnumbered) of the answer being applied right now */
308
+ private answerInFlight: number | undefined;
309
+
130
310
  latestAcknowledgedOfferId: number = 0;
131
311
 
132
312
  private offerLock: Mutex;
@@ -141,6 +321,29 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
141
321
 
142
322
  trackBitrates: TrackBitrateInfo[] = [];
143
323
 
324
+ /**
325
+ * Whether an offer carrying the connection-level `x-google-start-bitrate` has been
326
+ * accepted locally. The hint is written once per peer connection: libwebrtc retains
327
+ * `start_bitrate_bps` in `RtpBitrateConfigurator` and re-applies it on network route
328
+ * changes, so a later rewrite is at best a no-op and at worst restarts a converged
329
+ * bandwidth estimator. A new peer connection (full reconnect) seeds a new estimator.
330
+ */
331
+ private hasAppliedVideoStartBitrate = false;
332
+
333
+ /**
334
+ * How long this peer connection took to set up, in ms, recorded once the initial connect
335
+ * succeeds. Lowers the start bitrate hint for a slow connection; see `computeStartBitrateCap`.
336
+ */
337
+ private connectionSetupTimeMs?: number;
338
+
339
+ /**
340
+ * When the connection attempt that built this peer connection began (`performance.now()`).
341
+ * Until `connectionSetupTimeMs` is known, the time elapsed since then is used as a lower bound
342
+ * on the setup time, so a video published during connect is still seeded from it rather than
343
+ * waiting for the connection, which would delay the offer.
344
+ */
345
+ private connectionStartedAt?: number;
346
+
144
347
  remoteStereoMids: string[] = [];
145
348
 
146
349
  remoteNackMids: string[] = [];
@@ -250,6 +453,36 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
250
453
  });
251
454
  return false;
252
455
  }
456
+ if (sd.type === 'answer') {
457
+ if (
458
+ isDuplicateAnswer(
459
+ offerId,
460
+ this.answerInFlight,
461
+ this.answeredOfferId,
462
+ this._pc?.signalingState,
463
+ this.pendingInitialOffer !== undefined,
464
+ )
465
+ ) {
466
+ // a repeat of an answer already applied or being applied (e.g.
467
+ // duplicated by a proxy): there is no local offer left to answer
468
+ this.log.warn('ignoring duplicate answer', { offerId });
469
+ return false;
470
+ }
471
+ // claimed before any await, so a copy arriving mid-apply is caught too;
472
+ // released however the apply ends, so a genuine retry is still possible
473
+ this.answerInFlight = offerId;
474
+ }
475
+ try {
476
+ return await this.applyRemoteDescription(sd, offerId);
477
+ } finally {
478
+ if (sd.type === 'answer') this.answerInFlight = undefined;
479
+ }
480
+ }
481
+
482
+ private async applyRemoteDescription(
483
+ sd: RTCSessionDescriptionInit,
484
+ offerId: number,
485
+ ): Promise<boolean> {
253
486
  let mungedSDP: string | undefined = undefined;
254
487
  if (sd.type === 'offer') {
255
488
  let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
@@ -331,6 +564,8 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
331
564
  mungedSDP = write(sdpParsed);
332
565
  }
333
566
  await this.setMungedSDP(sd, mungedSDP, true);
567
+ // only a successfully applied answer makes later copies duplicates
568
+ if (sd.type === 'answer' && offerId > 0) this.answeredOfferId = offerId;
334
569
 
335
570
  if (this.pendingCandidates.length > 0) {
336
571
  this.iceLog.debug('flushing queued ICE candidates', {
@@ -453,6 +688,18 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
453
688
  this.log.debug('original offer', { sdp: offer.sdp });
454
689
 
455
690
  const sdpParsed = parse(offer.sdp ?? '');
691
+ // One value for every video m-section, written only on the first offer that carries
692
+ // local video: the hint is connection-level in libwebrtc, so differing per-section
693
+ // values would be last-writer-wins on m-section order. Offers before any video is
694
+ // published (data channel or audio only) find no target and leave the latch unset.
695
+ const connectionStartBitrate = this.hasAppliedVideoStartBitrate
696
+ ? undefined
697
+ : computeConnectionStartBitrate(
698
+ sdpParsed.media,
699
+ this.trackBitrates,
700
+ this.connectionSetupTimeEstimateMs(),
701
+ );
702
+ let appliedVideoStartBitrate = false;
456
703
  sdpParsed.media.forEach((media) => {
457
704
  ensureIPAddrMatchVersion(media);
458
705
  if (media.type === 'audio') {
@@ -463,19 +710,21 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
463
710
  return false;
464
711
  }
465
712
 
466
- const codecPayload = applyVideoStartBitrate(
467
- media,
468
- trackbr.cid,
469
- trackbr.codec,
470
- trackbr.maxbr,
471
- trackbr.isScreenShare,
472
- );
713
+ const codecPayload = findTrackCodecPayload(media, trackbr.cid, trackbr.codec);
473
714
  if (codecPayload === undefined) {
474
715
  return false;
475
716
  }
476
717
 
477
- if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
478
- this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
718
+ if (codecPayload > 0) {
719
+ if (connectionStartBitrate !== undefined) {
720
+ appliedVideoStartBitrate =
721
+ applyVideoStartBitrate(media, codecPayload, connectionStartBitrate) ||
722
+ appliedVideoStartBitrate;
723
+ }
724
+
725
+ if (isSVCCodec(trackbr.codec) && !isSafari()) {
726
+ this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
727
+ }
479
728
  }
480
729
 
481
730
  return true;
@@ -500,7 +749,17 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
500
749
  });
501
750
  return;
502
751
  }
503
- await this.setMungedSDP(offer, write(sdpParsed));
752
+ const mungedSdp = write(sdpParsed);
753
+ await this.setMungedSDP(offer, mungedSdp);
754
+ // setMungedSDP falls back to the unmunged SDP on rejection. Only consume the
755
+ // one-shot hint once the SDP carrying it has been accepted locally.
756
+ if (appliedVideoStartBitrate && offer.sdp === mungedSdp) {
757
+ this.hasAppliedVideoStartBitrate = true;
758
+ this.log.info('applied x-google-start-bitrate', {
759
+ startBitrateKbps: connectionStartBitrate,
760
+ connectionSetupTimeMs: this.connectionSetupTimeEstimateMs(),
761
+ });
762
+ }
504
763
  this.onOffer(offer, this.latestOfferId);
505
764
  } finally {
506
765
  unlock();
@@ -554,6 +813,30 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
554
813
  this.trackBitrates.push(info);
555
814
  }
556
815
 
816
+ /**
817
+ * Record how long this peer connection took to set up. Called once, when the initial connect
818
+ * succeeds; resumes and ICE restarts keep the estimator they have and never call this.
819
+ */
820
+ setConnectionSetupTime(ms: number) {
821
+ this.connectionSetupTimeMs = ms;
822
+ }
823
+
824
+ /** Record when the connection attempt that built this peer connection began. */
825
+ setConnectionStartedAt(startedAt: number) {
826
+ this.connectionStartedAt = startedAt;
827
+ }
828
+
829
+ /** The measured setup time, or while still connecting, the time elapsed so far. */
830
+ private connectionSetupTimeEstimateMs(): number | undefined {
831
+ if (this.connectionSetupTimeMs !== undefined) {
832
+ return this.connectionSetupTimeMs;
833
+ }
834
+ if (this.connectionStartedAt === undefined) {
835
+ return undefined;
836
+ }
837
+ return Math.round(performance.now() - this.connectionStartedAt);
838
+ }
839
+
557
840
  setConfiguration(rtcConfig: RTCConfiguration) {
558
841
  if (!this._pc) {
559
842
  throw new UnexpectedConnectionState('PC closed, cannot configure');
@@ -952,6 +1235,26 @@ export function conformBundledCodecFmtp(
952
1235
  }
953
1236
 
954
1237
  /** @internal */
1238
+ /**
1239
+ * Whether an incoming answer repeats one already applied or being applied.
1240
+ * Any answer for the offer whose answer is in flight is a duplicate, numbered
1241
+ * or not. With an offerId (current servers) an answer is also a duplicate if
1242
+ * that offer's answer was already applied. Without one, fall back to the
1243
+ * signaling state: in `stable` no local offer is pending, unless the initial
1244
+ * offer is still deferred (it is set locally only once its answer arrives).
1245
+ */
1246
+ export function isDuplicateAnswer(
1247
+ offerId: number,
1248
+ answerInFlight: number | undefined,
1249
+ answeredOfferId: number,
1250
+ signalingState: RTCSignalingState | undefined,
1251
+ hasPendingInitialOffer: boolean,
1252
+ ): boolean {
1253
+ if (answerInFlight === offerId) return true;
1254
+ if (offerId > 0) return offerId === answeredOfferId;
1255
+ return signalingState === 'stable' && !hasPendingInitialOffer;
1256
+ }
1257
+
955
1258
  export function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
956
1259
  stereoMids: string[];
957
1260
  nackMids: string[];
@@ -229,7 +229,9 @@ export class PCTransportManager {
229
229
  this.publisher.getConnectionState() !== 'connecting'
230
230
  ) {
231
231
  this.log.debug('negotiation required, start negotiating');
232
- this.publisher.negotiate();
232
+ this.publisher.negotiate().catch((err) => {
233
+ this.log.error(err);
234
+ });
233
235
  }
234
236
  await Promise.all(
235
237
  this.requiredTransports?.map((transport) =>